Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filtering for needs, descriptions and location of organization #32

Merged
merged 6 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 73 additions & 23 deletions src/components/Home/DonorDashboard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,93 @@
import { Box, Typography } from '@mui/material';
import { filter, lowerCase, some } from 'es-toolkit/compat';
import { useState } from 'react';
import { useState, useMemo } from 'react';

import OrganizationCard from './OrganizationCard';

import { useOrganizationStore } from '@/stores';

import { SearchBar } from '@/components/common';
import { SearchBar, Filters } from '@/components/common';

const DonorDashboard = () => {
const [searchQuery, setSearchQuery] = useState('');
const [needsQuery, setNeedsQuery] = useState('');
const [descriptionQuery, setDescriptionQuery] = useState('');
const [locationQuery, setLocationQuery] = useState('');

const organizationProfiles = useOrganizationStore(
(state) => state.organizationProfiles
);

// Filtered organizations based on search query
const filteredOrganizations = filter(organizationProfiles, (org) => {
if (!org.name) return false;
const searchTerm = lowerCase(searchQuery);
return (
lowerCase(org.name).includes(searchTerm) ||
lowerCase(org.location).includes(searchTerm) ||
some(org.needs, (need) => lowerCase(need).includes(searchTerm))
);
});
const filteredOrganizations = useMemo(() => {
return organizationProfiles.filter((org) => {
const matchesSearch = searchQuery
? org.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
org.location.toLowerCase().includes(searchQuery.toLowerCase()) ||
(org.needs || []).some((need) =>
need.itemName.toLowerCase().includes(searchQuery.toLowerCase())
)
: true;

const matchesNeeds = needsQuery
? (org.needs || []).some((need) =>
need.itemName.toLowerCase().includes(needsQuery.toLowerCase())
)
: true;

const matchesDescription = descriptionQuery
? org.description
?.toLowerCase()
.includes(descriptionQuery.toLowerCase())
: true;

const matchesLocation = locationQuery
? org.location.toLowerCase().includes(locationQuery.toLowerCase())
: true;

return (
matchesSearch && matchesNeeds && matchesDescription && matchesLocation
);
});
}, [
organizationProfiles,
searchQuery,
needsQuery,
descriptionQuery,
locationQuery,
]);

return organizationProfiles.length > 0 ? (
<div>
<SearchBar onSearchChange={setSearchQuery} />
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
{filteredOrganizations.map((org) => (
<OrganizationCard
organization={org}
key={org.uid}
/>
))}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<SearchBar onSearchChange={setSearchQuery} />
<Filters
needsQuery={needsQuery}
setNeedsQuery={setNeedsQuery}
descriptionQuery={descriptionQuery}
setDescriptionQuery={setDescriptionQuery}
locationQuery={locationQuery}
setLocationQuery={setLocationQuery}
/>
</Box>
</div>

{filteredOrganizations.map((org) => (
<OrganizationCard
organization={org}
key={org.uid}
/>
))}
</Box>
) : (
<Typography
variant='body1'
Expand Down
147 changes: 147 additions & 0 deletions src/components/common/Filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { useState } from 'react';
import {
Box,
Button,
Popover,
FormControl,
InputLabel,
Select,
MenuItem,
TextField,
} from '@mui/material';
import FilterListIcon from '@mui/icons-material/FilterList';

import { useOrganizationStore } from '@/stores';

interface FiltersProps {
needsQuery: string;
setNeedsQuery: (query: string) => void;
descriptionQuery: string;
setDescriptionQuery: (query: string) => void;
locationQuery: string;
setLocationQuery: (query: string) => void;
}

const Filters = ({
needsQuery,
setNeedsQuery,
descriptionQuery,
setDescriptionQuery,
locationQuery,
setLocationQuery,
}: FiltersProps) => {
const organizationProfiles = useOrganizationStore(
(state) => state.organizationProfiles
);

const [anchorElement, setAnchorElement] = useState<null | HTMLElement>(null);

const handleClose = () => {
setAnchorElement(null);
};

const open = Boolean(anchorElement);
const id = open ? 'filters-popover' : undefined;

const needsOptions = [
...new Set(
organizationProfiles.flatMap(
(org) => org.needs?.flatMap((need) => need.itemName) || []
)
),
].filter(Boolean);

return (
<Box>
<Button
variant='contained'
onClick={(event_) => setAnchorElement(event_.currentTarget)}
startIcon={<FilterListIcon />}
sx={{ marginTop: 2 }}
>
Filters
</Button>
<Popover
id={id}
open={open}
anchorEl={anchorElement}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<Box
sx={{
p: 2,
display: 'flex',
flexDirection: 'column',
gap: 2,
width: '300px',
}}
>
{/* Needs dropdown */}
<FormControl fullWidth>
<InputLabel id='needs-filter-label'>Filter by Needs</InputLabel>
<Select
labelId='needs-filter-label'
value={needsQuery}
onChange={(event_) => setNeedsQuery(event_.target.value)}
MenuProps={{
anchorOrigin: {
vertical: 'bottom',
horizontal: 'left',
},
transformOrigin: {
vertical: 'top',
horizontal: 'left',
},
PaperProps: {
style: {
maxHeight: 'calc(80vh - 96px)',
overflowY: 'auto',
},
},
disablePortal: false,
}}
>
<MenuItem value=''>All</MenuItem>
{needsOptions.map((need, index) => (
<MenuItem
key={index}
value={String(need)}
>
{String(need)}
</MenuItem>
))}
</Select>
</FormControl>

{/* Description text field */}
<TextField
label='Filter by Description'
variant='outlined'
fullWidth
value={descriptionQuery}
onChange={(event_) => setDescriptionQuery(event_.target.value)}
/>

{/* Location text field */}
<TextField
label='Filter by Location'
variant='outlined'
fullWidth
value={locationQuery}
onChange={(event_) => setLocationQuery(event_.target.value)}
/>
</Box>
</Popover>
</Box>
);
};

export default Filters;
26 changes: 9 additions & 17 deletions src/components/common/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ChangeEvent } from 'react';
import { useCallback, useMemo, useEffect, useState } from 'react';
import { TextField, Box, Button } from '@mui/material';
import { TextField } from '@mui/material';
import { useSearchParams } from 'react-router-dom';
import { debounce } from 'es-toolkit/compat';

Expand Down Expand Up @@ -42,22 +42,14 @@ const SearchBar = ({ onSearchChange }: SearchBarProps) => {
}, [searchParams, onSearchChange]);

return (
<Box
display='flex'
alignItems='center'
mt={3}
mx={2}
>
<TextField
label='Search organizations...'
variant='outlined'
fullWidth
sx={{ marginRight: 1 }}
value={query}
onChange={handleInputChange}
/>
<Button variant='contained'>Recommend</Button>
</Box>
<TextField
label='Search organizations...'
variant='outlined'
fullWidth
sx={{ mx: 1, mt: 3, mr: 2 }}
value={query}
onChange={handleInputChange}
/>
);
};

Expand Down
1 change: 1 addition & 0 deletions src/components/common/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { default as ProtectedRoute } from './ProtectedRoute';
export { default as CustomDialog } from './CustomDialog';
export { default as RoleSelectionModal } from './RoleSelectionModal';
export { default as Layout } from './Layout';
export { default as Filters } from './Filters';
Loading