diff --git a/src/components/CardAboutShelter/CardAboutShelter.tsx b/src/components/CardAboutShelter/CardAboutShelter.tsx index 704314cf..cff1ea22 100644 --- a/src/components/CardAboutShelter/CardAboutShelter.tsx +++ b/src/components/CardAboutShelter/CardAboutShelter.tsx @@ -5,11 +5,14 @@ import { PawPrint, Landmark, Smartphone, + Building, + MapPinned, } from 'lucide-react'; import { Card } from '../ui/card'; import { ICardAboutShelter } from './types'; import { InfoRow } from './components'; +import { checkAndFormatAddress } from './utils'; const CardAboutShelter = (props: ICardAboutShelter) => { const { shelter } = props; @@ -17,12 +20,19 @@ const CardAboutShelter = (props: ICardAboutShelter) => { const check = (v?: string | number | boolean | null) => { return v !== undefined && v !== null; }; + const formatAddress = checkAndFormatAddress(shelter, false); return (
Sobre o abrigo
- } label={shelter.address} /> + } label={formatAddress} /> + {Boolean(shelter.city) && ( + } label="Cidade:" value={shelter.city} /> + )} + {Boolean(shelter.zipCode) && ( + } label="CEP:" value={shelter.zipCode} /> + )} } label={ diff --git a/src/components/CardAboutShelter/components/InfoRow/InfoRow.tsx b/src/components/CardAboutShelter/components/InfoRow/InfoRow.tsx index 64877393..da5007a2 100644 --- a/src/components/CardAboutShelter/components/InfoRow/InfoRow.tsx +++ b/src/components/CardAboutShelter/components/InfoRow/InfoRow.tsx @@ -4,12 +4,21 @@ import { IInfoRowProps } from './types'; const InfoRow = React.forwardRef( (props, ref) => { - const { icon, label, value, clipboardButton = false, className = '', ...rest } = props; + const { + icon, + label, + value, + clipboardButton = false, + className = '', + ...rest + } = props; const isLink = value?.startsWith('http'); const ValueComp = !value ? ( ) : isLink ? ( - {value} @@ -37,7 +46,7 @@ const InfoRow = React.forwardRef( {ValueComp} - {clipboardButton && ( + {clipboardButton && value && (
navigator.clipboard.writeText(value)} @@ -46,7 +55,6 @@ const InfoRow = React.forwardRef(
)}
-
); diff --git a/src/components/CardAboutShelter/index.ts b/src/components/CardAboutShelter/index.ts index 9cc91886..e171498b 100644 --- a/src/components/CardAboutShelter/index.ts +++ b/src/components/CardAboutShelter/index.ts @@ -1,3 +1,4 @@ -import { CardAboutShelter } from "./CardAboutShelter"; +import { CardAboutShelter } from './CardAboutShelter'; +import { checkAndFormatAddress } from './utils'; -export { CardAboutShelter }; +export { CardAboutShelter, checkAndFormatAddress }; diff --git a/src/components/CardAboutShelter/utils.ts b/src/components/CardAboutShelter/utils.ts new file mode 100644 index 00000000..65643266 --- /dev/null +++ b/src/components/CardAboutShelter/utils.ts @@ -0,0 +1,18 @@ +import { IUseSheltersData } from '@/hooks/useShelters/types'; +import { ICardAboutShelter } from './types'; + +const formatShelterAddressFields = ( + shelter: ICardAboutShelter['shelter'] | IUseSheltersData +) => + [shelter.street, shelter.streetNumber, shelter.neighbourhood] + .filter(Boolean) + .join(', '); + +export const checkAndFormatAddress = ( + shelter: ICardAboutShelter['shelter'] | IUseSheltersData, + showCity = true +) => + shelter.address ?? + `${formatShelterAddressFields(shelter)}${ + showCity ? ` - ${shelter.city}` : '' + }`; diff --git a/src/components/SearchInput/SearchInput.tsx b/src/components/SearchInput/SearchInput.tsx index bec6d6db..225ddb87 100644 --- a/src/components/SearchInput/SearchInput.tsx +++ b/src/components/SearchInput/SearchInput.tsx @@ -7,13 +7,19 @@ import { cn } from '@/lib/utils'; const SearchInput = React.forwardRef( (props, ref) => { - const { value, onChange, className, ...rest } = props; + const { + value, + onChange, + className, + placeholder = 'Buscar por abrigo ou endereço', + ...rest + } = props; return (
diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 7a1eb6b3..527baadf 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -3,8 +3,11 @@ import { useFetch } from './useFetch'; import { usePaginatedQuery } from './usePaginatedQuery'; import { useThrottle } from './useThrottle'; import { useShelter } from './useShelter'; +import { useShelterCities } from './useShelterCities'; +import { useDebouncedValue } from './useDebouncedValue'; import { useSupplyCategories } from './useSupplyCategories'; import { useSupplies } from './useSupplies'; +import { useViaCep } from './useViaCep'; import { usePartners } from './usePartners'; export { @@ -13,7 +16,10 @@ export { usePaginatedQuery, useThrottle, useShelter, + useShelterCities, + useDebouncedValue, useSupplyCategories, useSupplies, + useViaCep, usePartners, }; diff --git a/src/hooks/useDebouncedValue/index.ts b/src/hooks/useDebouncedValue/index.ts new file mode 100644 index 00000000..eeb87dab --- /dev/null +++ b/src/hooks/useDebouncedValue/index.ts @@ -0,0 +1,3 @@ +import { useDebouncedValue } from './useDebouncedValue'; + +export { useDebouncedValue }; diff --git a/src/hooks/useDebouncedValue/useDebouncedValue.ts b/src/hooks/useDebouncedValue/useDebouncedValue.ts new file mode 100644 index 00000000..932152cb --- /dev/null +++ b/src/hooks/useDebouncedValue/useDebouncedValue.ts @@ -0,0 +1,27 @@ +import { useState, useEffect } from 'react'; + +export const useDebouncedValue = (value: string, delay: number): string => { + const [debouncedValue, setDebouncedValue] = useState(value); + const [currentValue, setCurrentValue] = useState(value); + + useEffect(() => { + setCurrentValue(value); + }, [value]); + + useEffect(() => { + let timeoutId: NodeJS.Timeout | null = null; + + if (currentValue !== debouncedValue) { + if (timeoutId) clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + setDebouncedValue(currentValue); + }, delay); + } + + return () => { + if (timeoutId) clearTimeout(timeoutId); + }; + }, [currentValue, debouncedValue, delay]); + + return debouncedValue; +}; diff --git a/src/hooks/useFetch/useFetch.tsx b/src/hooks/useFetch/useFetch.tsx index de6728c6..121f5795 100644 --- a/src/hooks/useFetch/useFetch.tsx +++ b/src/hooks/useFetch/useFetch.tsx @@ -5,7 +5,7 @@ import { api } from '@/api'; import { IServerResponse } from '@/types'; import { IUseFetchOptions } from './types'; -function useFetch(path: string, options: IUseFetchOptions = {}) { +function useFetch(path?: string, options: IUseFetchOptions = {}) { const { cache, initialValue } = options; const [loading, setLoading] = useState(true); const [data, setData] = useState(initialValue || ({} as T)); @@ -15,10 +15,15 @@ function useFetch(path: string, options: IUseFetchOptions = {}) { const headers = config?.headers ?? {}; if (cache) headers['x-app-cache'] = 'true'; setLoading(true); - api - .get>(path, { ...config, headers }) - .then(({ data }) => setData(data.data)) - .finally(() => setLoading(false)); + + if (path) { + api + .get>(path, { ...config, headers }) + .then(({ data }) => setData(data.data ?? (data as T))) + .finally(() => setLoading(false)); + } else { + setLoading(false); + } }, [cache, path] ); diff --git a/src/hooks/usePaginatedQuery/paths.ts b/src/hooks/usePaginatedQuery/paths.ts index d78cd81a..5c1d2cc5 100644 --- a/src/hooks/usePaginatedQuery/paths.ts +++ b/src/hooks/usePaginatedQuery/paths.ts @@ -1,5 +1,6 @@ export enum PaginatedQueryPath { Shelters = '/shelters', + ShelterCities = '/shelters/cities', SupplyCategories = '/supply-categories', Supplies = '/supplies', } diff --git a/src/hooks/useShelter/types.ts b/src/hooks/useShelter/types.ts index 7edf22d6..1d8b1608 100644 --- a/src/hooks/useShelter/types.ts +++ b/src/hooks/useShelter/types.ts @@ -1,6 +1,11 @@ export interface IUseShelterData { id: string; name: string; + street?: string; + neighbourhood?: string; + city?: string; + streetNumber?: string | null; + zipCode?: string; address: string; pix?: string | null; shelteredPeople?: number | null; diff --git a/src/hooks/useShelterCities/index.ts b/src/hooks/useShelterCities/index.ts new file mode 100644 index 00000000..729553c8 --- /dev/null +++ b/src/hooks/useShelterCities/index.ts @@ -0,0 +1,3 @@ +import { useShelterCities } from './useShelterCities'; + +export { useShelterCities }; diff --git a/src/hooks/useShelterCities/types.ts b/src/hooks/useShelterCities/types.ts new file mode 100644 index 00000000..0b12d0e3 --- /dev/null +++ b/src/hooks/useShelterCities/types.ts @@ -0,0 +1,4 @@ +export interface IShelterCitiesData { + city: string; + sheltersCount: string; +} diff --git a/src/hooks/useShelterCities/useShelterCities.ts b/src/hooks/useShelterCities/useShelterCities.ts new file mode 100644 index 00000000..29c3ce5b --- /dev/null +++ b/src/hooks/useShelterCities/useShelterCities.ts @@ -0,0 +1,9 @@ +import { useFetch } from '../useFetch'; +import { PaginatedQueryPath } from '../usePaginatedQuery/paths'; +import { IShelterCitiesData } from './types'; + +export const useShelterCities = () => { + return useFetch(PaginatedQueryPath.ShelterCities, { + cache: true, + }); +}; diff --git a/src/hooks/useShelters/types.ts b/src/hooks/useShelters/types.ts index 42eeceba..5c897078 100644 --- a/src/hooks/useShelters/types.ts +++ b/src/hooks/useShelters/types.ts @@ -3,6 +3,11 @@ import { ShelterTagType } from '@/pages/Home/components/ShelterListItem/types'; export interface IUseSheltersData { id: string; name: string; + street?: string; + neighbourhood?: string; + city?: string; + streetNumber?: string | null; + zipCode?: string; address: string; pix?: string | null; shelteredPeople?: number | null; diff --git a/src/hooks/useViaCep/index.ts b/src/hooks/useViaCep/index.ts new file mode 100644 index 00000000..902a66bd --- /dev/null +++ b/src/hooks/useViaCep/index.ts @@ -0,0 +1,3 @@ +import { useViaCep } from './useViaCep'; + +export { useViaCep }; diff --git a/src/hooks/useViaCep/types.ts b/src/hooks/useViaCep/types.ts new file mode 100644 index 00000000..569845b4 --- /dev/null +++ b/src/hooks/useViaCep/types.ts @@ -0,0 +1,12 @@ +export interface IViaCepData { + cep: string; + logradouro: string; + complemento: string; + bairro: string; + localidade: string; + uf: string; + ibge: string; + gia: string; + dd: string; + siafi: string; +} diff --git a/src/hooks/useViaCep/useViaCep.ts b/src/hooks/useViaCep/useViaCep.ts new file mode 100644 index 00000000..1550f5b5 --- /dev/null +++ b/src/hooks/useViaCep/useViaCep.ts @@ -0,0 +1,10 @@ +import { useFetch } from '..'; +import { IViaCepData } from './types'; + +export const useViaCep = (cep: string | undefined) => { + const createdPath = + !cep || cep.length < 8 + ? undefined + : `https://viacep.com.br/ws/${cep}/json/`; + return useFetch(createdPath); +}; diff --git a/src/pages/CreateShelter/CreateShelter.tsx b/src/pages/CreateShelter/CreateShelter.tsx index 58c3906d..a7578a02 100644 --- a/src/pages/CreateShelter/CreateShelter.tsx +++ b/src/pages/CreateShelter/CreateShelter.tsx @@ -1,7 +1,9 @@ -import { ChevronLeft } from 'lucide-react'; +import { useEffect } from 'react'; +import { ChevronLeft, Loader } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { useFormik } from 'formik'; import * as Yup from 'yup'; +import ReactSelect from 'react-select'; import { Select, @@ -10,6 +12,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; + import { Header, TextField } from '@/components'; import { Button } from '@/components/ui/button'; import { ICreateShelter } from '@/service/shelter/types'; @@ -17,6 +20,9 @@ import { toast } from '@/components/ui/use-toast'; import { ShelterServices } from '@/service'; import { withAuth } from '@/hocs'; import { clearCache } from '@/api/cache'; +import { hardCodedRsCities } from './hardcodedCities'; +import { useDebouncedValue, useViaCep } from '@/hooks'; +import { cn } from '@/lib/utils'; const CreateShelterComponent = () => { const navigate = useNavigate(); @@ -28,10 +34,15 @@ const CreateShelterComponent = () => { setFieldValue, handleSubmit, values, + setErrors, } = useFormik({ initialValues: { name: '', - address: '', + street: '', + neighbourhood: '', + city: '', + streetNumber: null, + zipCode: '', shelteredPeople: 0, capacity: 0, verified: false, @@ -45,14 +56,19 @@ const CreateShelterComponent = () => { validateOnMount: false, validationSchema: Yup.object().shape({ name: Yup.string().required('Este campo deve ser preenchido'), - address: Yup.string().required('Este campo deve ser preenchido'), shelteredPeople: Yup.number() .min(0, 'O valor mínimo para este campo é 0') .nullable(), capacity: Yup.number() .min(1, 'O valor mínimo para este campo é 1') .nullable(), - petFriendly: Yup.bool().nullable(), + street: Yup.string().required('Este campo deve ser preenchido'), + neighbourhood: Yup.string().required('Este campo deve ser preenchido'), + city: Yup.string().required('Este campo deve ser preenchido'), + streetNumber: Yup.string() + .min(0, 'O valor mínimo para este campo é 1') + .required('Este campo deve ser preenchido'), + zipCode: Yup.string().required('Este campo deve ser preenchido'), contact: Yup.string().nullable(), pix: Yup.string().nullable(), }), @@ -73,6 +89,19 @@ const CreateShelterComponent = () => { } }, }); + const debouncedZipcode = useDebouncedValue(values?.zipCode ?? '', 500); + + const { data: cepData, loading: isLoadingZipCodeData } = + useViaCep(debouncedZipcode); + + useEffect(() => { + if (!cepData) return; + + if (cepData.logradouro) setFieldValue('street', cepData.logradouro); + if (cepData.bairro) setFieldValue('neighbourhood', cepData.bairro); + if (cepData.localidade) setFieldValue('city', cepData.localidade); + setErrors({}); + }, [cepData, setFieldValue, setErrors]); return (
@@ -103,11 +132,58 @@ const CreateShelterComponent = () => { helperText={errors.name} /> + {Boolean(isLoadingZipCodeData) && ( + + )} + + + +
+ + ({ + value: item, + label: item, + }))} + onChange={(v) => { + setFieldValue('city', v?.value); + }} + className={cn('w-full', { + 'border-[1px] border-red-600 rounded-md': errors.city, + })} + /> + {errors.city && ( +

{errors.city}

+ )} +
{ @@ -119,12 +120,28 @@ const Home = () => { loading={loading} count={shelters.count} data={shelters.results} + filterData={filterData} onFetchMoreData={handleFetchMore} searchValue={filterData.search} onSearchValueChange={(v) => { setFilterData((prev) => ({ ...prev, search: v })); setSearch(v); }} + onCitiesChange={(v) => { + setFilterData((prev) => ({ ...prev, cities: v })); + const searchQuery = qs.stringify( + { ...filterData, cities: v }, + { + skipNulls: true, + } + ); + setSearchParams(searchQuery); + refresh({ + params: { + search: searchQuery, + }, + }); + }} hasMoreItems={hasMore} onOpenModal={() => setOpenModal(true)} onClearSearch={clearSearch} diff --git a/src/pages/Home/components/Filter/CitiesFilter.tsx b/src/pages/Home/components/Filter/CitiesFilter.tsx new file mode 100644 index 00000000..c4b8d821 --- /dev/null +++ b/src/pages/Home/components/Filter/CitiesFilter.tsx @@ -0,0 +1,48 @@ +import Select from 'react-select'; + +import { useShelterCities } from '@/hooks'; +import { ISelectField } from './types'; + +interface CitiesFilterInterface { + cities: S; + setCities: (cities: S) => void; +} + +export const CitiesFilter = ({ + cities = [], + setCities, +}: CitiesFilterInterface) => { + const { data, loading } = useShelterCities(); + + if (loading) return null; + + const values: ISelectField[] = cities.map((item) => ({ + label: item, + value: item, + })); + const options: ISelectField[] = data.map((item) => ({ + label: `(${item.sheltersCount}) ${item.city}`, + value: item.city, + })); + + return ( +
+
+

Cidades

+

+ Selecione uma ou mais cidades para pesquisar. +

+ + ({ label, value: +priority } as any) - )} - onChange={(v) => { - const newValue = { - ...v, - value: v ? +v.value : SupplyPriority.Urgent, - }; - setFieldValue('priority', newValue); - }} - /> -
-
- - ({ - label: el.name, - value: el.id, - }))} - onChange={(v) => setFieldValue('supplies', v)} + +
+
+ + setFieldValue('search', ev.target.value ?? '') + } />
-
- -
-

- Status do abrigo -

-
-
- + +
-
+ diff --git a/src/pages/Home/components/Filter/types.ts b/src/pages/Home/components/Filter/types.ts index 07c8bc08..356af68e 100644 --- a/src/pages/Home/components/Filter/types.ts +++ b/src/pages/Home/components/Filter/types.ts @@ -13,6 +13,7 @@ export interface IFilterFormProps { supplyCategoryIds: string[]; supplyIds: string[]; shelterStatus: ShelterAvailabilityStatus[]; + cities: string[]; } export interface IFilterFormikProps { @@ -21,6 +22,7 @@ export interface IFilterFormikProps { supplyCategories: ISelectField[]; supplies: ISelectField[]; shelterStatus: ISelectField[]; + cities: string[]; } export interface IFilterProps { diff --git a/src/pages/Home/components/ShelterListView/ShelterListView.tsx b/src/pages/Home/components/ShelterListView/ShelterListView.tsx index 16bbd84f..8f8c80f4 100644 --- a/src/pages/Home/components/ShelterListView/ShelterListView.tsx +++ b/src/pages/Home/components/ShelterListView/ShelterListView.tsx @@ -1,5 +1,5 @@ import React, { Fragment } from 'react'; -import { CircleAlert, ListFilter } from 'lucide-react'; +import { CircleAlert, ListFilter, X } from 'lucide-react'; import { Alert, @@ -22,10 +22,12 @@ const ShelterListView = React.forwardRef( searchValue = '', hasMoreItems = false, onSearchValueChange, + onCitiesChange, onFetchMoreData, className = '', onOpenModal, onClearSearch, + filterData, ...rest } = props; @@ -50,6 +52,25 @@ const ShelterListView = React.forwardRef( : undefined } /> +
+ {filterData.cities?.map((item) => { + return ( + <> +
+ onCitiesChange?.( + filterData.cities.filter((it) => it !== item) + ) + } + > + {item} +
+ + ); + })} +
{loading ? ( - + ) : data.length === 0 ? ( ) : ( {data.map((s, idx) => ( - + ))} {hasMoreItems ? (