Skip to content

Commit 4789ad3

Browse files
✨ 'Filter' component
feat/filters
1 parent 2b7c6e7 commit 4789ad3

File tree

6 files changed

+330
-0
lines changed

6 files changed

+330
-0
lines changed

src/pages/Filter/Filter.tsx

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
import { Search } from 'lucide-react';
2+
import { useMemo, useState } from 'react';
3+
4+
import { LoadingScreen } from '@/components';
5+
import { SupplyPriority } from '@/service/supply/types';
6+
import { Separator } from '@/components/ui/separator';
7+
import { Input } from '@/components/ui/input';
8+
import Select from 'react-select';
9+
import { useSupplyCategories } from '@/hooks';
10+
import { ISupplyCategory } from '@/hooks/useSupplyCategories/types';
11+
import { useSupplies } from '@/hooks/useSupplies';
12+
import { group } from '@/lib/utils';
13+
import { Button } from '@/components/ui/button';
14+
import { useFormik } from 'formik';
15+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
16+
import { IUseShelterSearchParams } from '@/hooks/useShelters/types';
17+
import { IComplexSelectData, IComplexSelectGroupedData } from './types';
18+
19+
const Filter = (props: any) => {
20+
const priorityOptions = [
21+
{
22+
label: 'Necessita urgente',
23+
value: `${SupplyPriority.Urgent}`,
24+
},
25+
{
26+
label: 'Precisa',
27+
value: `${SupplyPriority.Needing}`,
28+
},
29+
{
30+
label: 'Disponível para doação',
31+
value: `${SupplyPriority.Remaining}`,
32+
},
33+
];
34+
const [supplyOptions, setSupplyOptions] = useState<IComplexSelectGroupedData[]>([]);
35+
const { data: supplyCategories, loading } = useSupplyCategories();
36+
const result = useSupplies();
37+
const {
38+
handleSubmit,
39+
values,
40+
setFieldValue,
41+
} = useFormik<IUseShelterSearchParams>({
42+
initialValues: {
43+
search: props.filters.search,
44+
priority: props.filters.priority,
45+
supplyCategories: props.filters.supplyCategories,
46+
supplies: props.filters.supplies,
47+
filterAvailableShelter: props.filters.filterAvailableShelter,
48+
filterUnavailableShelter: props.filters.filterUnavailableShelter,
49+
waitingShelterAvailability: props.filters.waitingShelterAvailability
50+
},
51+
enableReinitialize: true,
52+
onSubmit: async (values) => {
53+
const params = {
54+
search: values.search ,
55+
priority: values.priority,
56+
supplyCategories: values.supplyCategories,
57+
supplies: values.supplies,
58+
filterAvailableShelter: values.filterAvailableShelter,
59+
filterUnavailableShelter: values.filterUnavailableShelter,
60+
waitingShelterAvailability: values.waitingShelterAvailability,
61+
};
62+
63+
props.handleSearch(params);
64+
}
65+
});
66+
67+
const initSupplyOptions = useMemo(() => {
68+
const grouped = group(result.data ?? [], 'supplyCategory.name');
69+
/// set suply itens from backend data
70+
setSupplyOptions(Object.entries(grouped).map(([key, values]) => ({
71+
label: key,
72+
options: values.map((v) => {
73+
return {
74+
label: v.name,
75+
value: v.id
76+
}
77+
}),
78+
})));
79+
80+
/// init default options array with the already selected supplies
81+
const defaultOptions = props?.filters?.supplies.length > 0 ?
82+
Object.entries(grouped).reduce((filtered: IComplexSelectData[], option) => {
83+
const [, values] = option;
84+
for(const value of values) {
85+
for(const suply of props.filters.supplies) {
86+
if (value.id === suply) {
87+
filtered.push({
88+
label: value.name,
89+
value: value.id
90+
})
91+
}
92+
}
93+
}
94+
95+
return filtered;
96+
}, []) : [];
97+
98+
return defaultOptions;
99+
}, [result.data, props?.filters?.supplies]);
100+
101+
const handleSupplyCategoriesSelected = (supplyCategoriesSelected: readonly IComplexSelectData[]) => {
102+
console.log('init')
103+
const grouped = group(result.data ?? [], 'supplyCategory.name');
104+
105+
const supplyOptionsFiltered = Object.entries(grouped).reduce((filtered: IComplexSelectGroupedData[], option) => {
106+
const [key, values] = option;
107+
108+
const found = supplyCategoriesSelected.some(element => {
109+
return element.label === key;
110+
});
111+
112+
if (found) {
113+
filtered.push({
114+
label: key,
115+
options: values.map((v) => {
116+
return {
117+
label: v.name,
118+
value: v.id
119+
}
120+
}),
121+
})
122+
}
123+
124+
return filtered;
125+
}, []);
126+
127+
setFieldValue('supplyCategories', supplyCategoriesSelected.map(el => el.value));
128+
setSupplyOptions(supplyOptionsFiltered);
129+
}
130+
131+
const handleSupplySelected = (supplySelected: readonly IComplexSelectData[]) => {
132+
setFieldValue('supplies', supplySelected.map(el => el.value));
133+
}
134+
135+
const handlePriorityOptionSelected = (priorityOptionSelected: IComplexSelectData) => {
136+
setFieldValue('priority', parseInt(priorityOptionSelected.value));
137+
}
138+
139+
if (loading || result.loading) return <LoadingScreen />;
140+
141+
return (
142+
<Dialog open={props.isModalOpen} onOpenChange={props.closeModal}>
143+
<DialogContent className="rounded-md">
144+
<DialogHeader className='pad-10'>
145+
<DialogTitle className="text-base font-medium">Faça sua busca:</DialogTitle>
146+
</DialogHeader>
147+
<form onSubmit={handleSubmit}>
148+
<div className="pl-4 pr-4 pb-4 flex flex-col max-w-5xl w-full items-start h-full">
149+
<div className="flex flex-col gap-2 w-full my-4">
150+
<div className="relative">
151+
<Input
152+
placeholder="Buscar por abrigo ou endereço"
153+
className="h-12 text-md font-medium text-zinc-600 pl-10 pr-4"
154+
onChange={(ev) => {
155+
setFieldValue('search', ev.target.value);
156+
}}
157+
/>
158+
<div className="absolute inset-y-0 left-0 flex items-center pl-3">
159+
<Search name="search" size="20" className="stroke-zinc-300" />
160+
</div>
161+
</div>
162+
</div>
163+
<Separator className="mt-2" />
164+
<div className="flex flex-col gap-2 w-full my-4">
165+
<p className="text-muted-foreground text-sm md:text-lg font-medium">
166+
Busca avançada
167+
</p>
168+
<p className="text-muted-foreground text-sm md:text-lg font-medium">
169+
Você pode buscar pelo item que os abrigos precisam urgentemente de doação ou por itens que os abrigos tem disponibilidade para doar.
170+
</p>
171+
<div className="flex flex-col gap-1 w-full">
172+
<label className="text-muted-foreground text-sm md:text-lg font-medium">Status do item no abrigo</label>
173+
<Select
174+
defaultValue={ props.filters.priority ? priorityOptions.filter(el => props.filters.priority === parseInt(el.value))
175+
.map(element => {
176+
return {
177+
'label': element.label,
178+
'value': element.value
179+
}
180+
}) :
181+
[]}
182+
name="colors"
183+
placeholder={<div>Selecione</div>}
184+
options={priorityOptions as any}
185+
className="basic-select"
186+
classNamePrefix="select"
187+
onChange={(priorityOptionsSelected: any) => handlePriorityOptionSelected(priorityOptionsSelected)}
188+
/>
189+
</div>
190+
<div className="flex flex-col gap-1 w-full">
191+
<label className="text-muted-foreground text-sm md:text-lg font-medium">Categoria</label>
192+
<Select
193+
defaultValue={ props.filters.supplyCategories ?
194+
supplyCategories.filter(el => props.filters.supplyCategories.some((suplyCategoryId: string) => suplyCategoryId === el.id)).map(element => {
195+
return {
196+
'label': element.name,
197+
'value': element.id
198+
}
199+
}) :
200+
[]}
201+
isMulti
202+
placeholder={<div>Selecione</div>}
203+
name="colors"
204+
options={supplyCategories.map((element: ISupplyCategory) => {
205+
return {
206+
'label': element.name,
207+
'value': element.id
208+
}
209+
}) as any}
210+
className="basic-multi-select"
211+
classNamePrefix="select"
212+
onChange={(supplyCategoriesSelected) => handleSupplyCategoriesSelected(supplyCategoriesSelected)}
213+
/>
214+
</div>
215+
<div className="flex flex-col w-full">
216+
<label className="text-muted-foreground text-sm md:text-lg font-medium">Itens</label>
217+
<Select
218+
defaultValue={initSupplyOptions}
219+
placeholder={<div>Selecione</div>}
220+
isMulti
221+
options={supplyOptions}
222+
onChange={(suppliesSelected) => handleSupplySelected(suppliesSelected)}
223+
/>
224+
</div>
225+
</div>
226+
<Separator className="mt-2" />
227+
<div className="flex flex-col gap-2 w-full my-4">
228+
<p className="text-muted-foreground text-sm md:text-lg font-medium">
229+
Status do abrigo
230+
</p>
231+
<div>
232+
<label className="flex items-center mb-4">
233+
<input
234+
name="filterAvailableShelter"
235+
type="checkbox"
236+
className="mr-2 w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
237+
onChange={() => setFieldValue('filterAvailableShelter', !values.filterAvailableShelter)}
238+
defaultChecked={values.filterAvailableShelter}
239+
/>
240+
Abrigo Disponivel
241+
</label>
242+
</div>
243+
<div>
244+
<label className="flex items-center mb-4">
245+
<input
246+
name="filterUnavailableShelter"
247+
type="checkbox"
248+
className="mr-2 w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
249+
onChange={() => setFieldValue('filterUnavailableShelter', !values.filterUnavailableShelter)}
250+
defaultChecked={values.filterUnavailableShelter}
251+
/>
252+
Abrigo Indisponível
253+
</label>
254+
</div>
255+
<div>
256+
<label className="flex items-center mb-4">
257+
<input
258+
name="waitingShelterAvailability"
259+
type="checkbox"
260+
className="mr-2 w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
261+
onChange={() => setFieldValue('waitingShelterAvailability', !values.waitingShelterAvailability)}
262+
defaultChecked={values.waitingShelterAvailability}
263+
/>
264+
Aguardando disponibilidade
265+
</label>
266+
</div>
267+
</div>
268+
269+
<div className="flex flex-1 flex-col justify-end md:justify-start w-full py-6">
270+
<Button
271+
type="submit"
272+
className="flex gap-2 text-white font-medium text-lg bg-blue-500 hover:bg-blue-600 w-full"
273+
>
274+
Filtrar resultados
275+
</Button>
276+
</div>
277+
</div>
278+
</form>
279+
</DialogContent>
280+
</Dialog>
281+
);
282+
};
283+
284+
export { Filter };

src/pages/Filter/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Filter } from './Filter';
2+
3+
export { Filter };

src/pages/Filter/types.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export interface ISupplyOptionsSelect {
2+
id: string;
3+
name: string;
4+
supplyCategory: {
5+
id: string;
6+
name: string;
7+
};
8+
createdAt: Date;
9+
updatedAt: Date | null;
10+
}
11+
12+
export interface IComplexSelectGroupedData {
13+
label: string;
14+
options: IComplexSelectData[]
15+
}
16+
17+
export interface IComplexSelectData {
18+
label: string;
19+
value: string;
20+
}
21+
22+
export interface IAdvancedFilterProps {
23+
24+
}

src/pages/Home/Home.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Input } from '@/components/ui/input';
66
import { useShelters, useThrottle } from '@/hooks';
77
import { Button } from '@/components/ui/button';
88
import { SessionContext } from '@/contexts';
9+
import { Filter } from '../Filter';
910
import { IUseShelterSearchParams } from '@/hooks/useShelters/types';
1011

1112
const alertDescription =
@@ -79,6 +80,7 @@ const Home = () => {
7980

8081
return (
8182
<div className="flex flex-col h-screen items-center">
83+
{isModalOpen && <Filter handleSearch={handleSearch} isModalOpen={isModalOpen} closeModal={closeModal} filters={shelters.filters} />}
8284
<Header
8385
title="SOS Rio Grande do Sul"
8486
endAdornment={
@@ -138,6 +140,20 @@ const Home = () => {
138140
<Search name="search" size="20" className="stroke-zinc-300" />
139141
</div>
140142
</div>
143+
<div className="flex flex-row">
144+
<Button variant="ghost" size="sm" className="flex gap-2 items-center" onClick={() => setOpenModal(true)}>
145+
<ListFilter className="h-5 w-5" />
146+
<h1 className="font-semibold text-[16px] text-blue-500">
147+
Filtros
148+
</h1>
149+
</Button>
150+
<Button variant="ghost" size="sm" className="flex gap-2 items-center" onClick={() => clearSearch()}>
151+
<CircleAlert className="h-5 w-5" />
152+
<h1 className="font-semibold text-[16px] text-blue-500">
153+
Limpar Filtros
154+
</h1>
155+
</Button>
156+
</div>
141157
<main className="flex flex-col gap-4">
142158
{loading ? (
143159
<Loader className="justify-self-center self-center w-5 h-5 animate-spin" />

src/pages/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { EditShelterSupply } from './EditShelterSupply';
66
import { CreateSupply } from './CreateSupply';
77
import { CreateShelter } from './CreateShelter';
88
import { UpdateShelter } from './UpdateShelter';
9+
import { Filter } from './Filter';
910

1011
export {
1112
SignIn,
@@ -15,4 +16,5 @@ export {
1516
CreateSupply,
1617
CreateShelter,
1718
UpdateShelter,
19+
Filter
1820
};

tailwind.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,6 @@ module.exports = {
7979
},
8080
},
8181
},
82+
// eslint-disable-next-line no-undef
8283
plugins: [require('tailwindcss-animate')],
8384
};

0 commit comments

Comments
 (0)