From 032c12865b2f0d45378e92c7e0732dd87392f1fa Mon Sep 17 00:00:00 2001 From: Mary Caserio <114102750+marycaserio@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:53:17 -0600 Subject: [PATCH] refactor: donot events alerts (#28) Please include a summary of the changes and the related issue. ## Type of change - [x] Bug fix - [x] New feature - [ ] Code refactor - [ ] Breaking change - [ ] Style update - [ ] Documentation update ## Checklist - [x] I have performed a self-review of my code - [ ] Single file within 200 lines of codes - [ ] Comments has added - [x] My changes generate no new warnings --------- Co-authored-by: Elara Liu <40444637+ZL-Asica@users.noreply.github.com> --- src/components/Alerts/DonorAlerts.tsx | 158 ++++++++++++------ src/components/Home/DonorDashboard/index.tsx | 35 +--- .../NeedList/NeedDialog.tsx | 1 + src/components/Schedule/EventsCalendar.tsx | 7 +- src/components/common/ProtectedRoute.tsx | 35 +++- src/components/common/SearchBar.tsx | 80 ++++++--- src/hooks/useNeeds.ts | 19 ++- src/stores/userStore.ts | 2 + src/types/user.d.ts | 1 + src/utils/eventsUtils.ts | 1 + 10 files changed, 232 insertions(+), 107 deletions(-) diff --git a/src/components/Alerts/DonorAlerts.tsx b/src/components/Alerts/DonorAlerts.tsx index 8b49dce..408943b 100644 --- a/src/components/Alerts/DonorAlerts.tsx +++ b/src/components/Alerts/DonorAlerts.tsx @@ -1,70 +1,124 @@ import { Box, - Paper, Typography, List, ListItem, - ListItemText, + Card, + CardHeader, + Button, } from '@mui/material'; import { lighten, useTheme } from '@mui/material/styles'; +import { useNavigate } from 'react-router-dom'; -// interface DonorAlertsProps {} +import { useOrganizationStore } from '@/stores'; +import { useSavedOrgs } from '@/hooks'; const DonorAlerts = () => { - const alerts = [ - { - id: 1, - title: 'Food Shortage Alert', - message: 'Urgent need for canned food in Downtown area.', - category: 'Food', - }, - { - id: 2, - title: 'Volunteers Needed', - message: 'Animal Shelter needs volunteers this weekend.', - category: 'Volunteer', - }, - { - id: 3, - title: 'Funding Goal Reached', - message: 'Youth Theater Company has reached its funding goal!', - category: 'Funding', - }, - ]; - const theme = useTheme(); + const navigate = useNavigate(); + + const { savedOrgs } = useSavedOrgs(); + const savedOrgUids = new Set(savedOrgs.map((org) => org.uid)); + + const organizationProfiles = useOrganizationStore( + (state) => state.organizationProfiles + ); + + // Filtered organizations based on savedOrgUids + const filteredOrganizations = organizationProfiles.filter((org) => + savedOrgUids.has(org.uid) + ); + + // Get recently added needs + const recentNeeds = []; + for (const org of filteredOrganizations) { + if (org.needs) { + for (const need of org.needs) { + const createdAt = new Date(need.createdAt); + const now = new Date(); + const timeDiffMs = now.getTime() - createdAt.getTime(); + const hoursAgo = Math.floor(timeDiffMs / (1000 * 60 * 60)); + const name = org.name; + + if (hoursAgo <= 48) { + recentNeeds.push({ + need, + hoursAgo, + name, + }); + } + } + } + } + + const handleViewItem = (orgName: string) => { + navigate(`/?search=${encodeURIComponent(orgName)}`); + }; return ( - -

Alerts

- + + Alerts + + - {alerts.length > 0 ? ( - - {alerts.map((alert) => ( - - + {recentNeeds.length > 0 ? ( + + {recentNeeds.map((needData, index) => ( + + + + {needData.need.itemName} + + } + subheader={ + + Added {needData.hoursAgo} hours ago by {needData.name} + + } + action={ + + } /> - - ))} - - ) : ( - - No alerts match your search. - - )} - + + + ))} + + ) : ( + + No recent needs found. + + )}
); }; diff --git a/src/components/Home/DonorDashboard/index.tsx b/src/components/Home/DonorDashboard/index.tsx index 658f048..3e2985a 100644 --- a/src/components/Home/DonorDashboard/index.tsx +++ b/src/components/Home/DonorDashboard/index.tsx @@ -1,40 +1,22 @@ import { Box, Typography } from '@mui/material'; import { filter, lowerCase, some } from 'es-toolkit/compat'; -import { useEffect, useState } from 'react'; -import { toast } from 'sonner'; +import { useState } from 'react'; import OrganizationCard from './OrganizationCard'; import { useOrganizationStore } from '@/stores'; -import { SearchBar, LoadingCircle } from '@/components/common'; +import SearchBar from '@/components/common/SearchBar'; const DonorDashboard = () => { const [searchQuery, setSearchQuery] = useState(''); - const { - organizationProfiles, - fetchProfiles, - subscribeToProfiles, - loading, - error, - } = useOrganizationStore(); - - useEffect(() => { - fetchProfiles(); - const unsubscribe = subscribeToProfiles; - - return () => unsubscribe && unsubscribe(); - }, [fetchProfiles, subscribeToProfiles]); - - if (loading) return ; - if (error) { - toast.error(error); - return

{error}

; - } + const organizationProfiles = useOrganizationStore( + (state) => state.organizationProfiles + ); // Filtered organizations based on search query const filteredOrganizations = filter(organizationProfiles, (org) => { - if (org.name === '' || !org.name) return false; + if (!org.name) return false; const searchTerm = lowerCase(searchQuery); return ( lowerCase(org.name).includes(searchTerm) || @@ -45,10 +27,7 @@ const DonorDashboard = () => { return organizationProfiles.length > 0 ? (
- + {filteredOrganizations.map((org) => ( { key={day.toString()} overlap='circular' badgeContent={isSelected ? '•' : undefined} - color='primary' + sx={{ + '& .MuiBadge-badge': { + fontSize: '30px', + color: (theme) => theme.palette.primary.main, + }, + }} > { ); } + if (user.role !== 'organization') { + const orgLoading = useOrganizationStore((state) => state.loading); + const subscribeToProfiles = useOrganizationStore( + (state) => state.subscribeToProfiles + ); + const error = useOrganizationStore((state) => state.error); + + useEffect(() => { + const unsubscribe = subscribeToProfiles; + + return () => unsubscribe && unsubscribe(); + }, [subscribeToProfiles]); + + if (orgLoading) return ; + + if (error) { + toast.error(error); + return ( + + {error} + + ); + } + } + return element; }; diff --git a/src/components/common/SearchBar.tsx b/src/components/common/SearchBar.tsx index db76a90..1f07e2d 100644 --- a/src/components/common/SearchBar.tsx +++ b/src/components/common/SearchBar.tsx @@ -1,30 +1,64 @@ +import type { ChangeEvent } from 'react'; +import { useCallback, useMemo, useEffect, useState } from 'react'; import { TextField, Box, Button } from '@mui/material'; +import { useSearchParams } from 'react-router-dom'; +import { debounce } from 'es-toolkit/compat'; + +const sanitizeQuery = (query: string): string => { + return query.replace(/[^\s\w]/gi, '').trim(); +}; interface SearchBarProps { - searchQuery: string; - setSearchQuery: (query: string) => void; + onSearchChange: (query: string) => void; } -const SearchBar: React.FC = ({ - searchQuery, - setSearchQuery, -}) => ( - - setSearchQuery(e.target.value)} - /> - - -); +const SearchBar = ({ onSearchChange }: SearchBarProps) => { + const [searchParams, setSearchParams] = useSearchParams(); + const [query, setQuery] = useState(searchParams.get('search') || ''); + + const debouncedUpdate = useMemo( + () => + debounce((value: string) => { + setSearchParams({ search: value || '' }, { replace: true }); + onSearchChange(value); + }, 300), + [setSearchParams, onSearchChange] + ); + + const handleInputChange = useCallback( + (e: ChangeEvent) => { + const sanitizedQuery = sanitizeQuery(e.target.value); + setQuery(sanitizedQuery); + debouncedUpdate(sanitizedQuery); + }, + [debouncedUpdate] + ); + + // Initialize the search query from URL on mount + useEffect(() => { + const initialQuery = searchParams.get('search') || ''; + setQuery(initialQuery); + onSearchChange(initialQuery); + }, [searchParams, onSearchChange]); + + return ( + + + + + ); +}; export default SearchBar; diff --git a/src/hooks/useNeeds.ts b/src/hooks/useNeeds.ts index 5a5d816..c3c9979 100644 --- a/src/hooks/useNeeds.ts +++ b/src/hooks/useNeeds.ts @@ -25,6 +25,7 @@ const sanitizeNeeds = (needs: Supply[]): Supply[] => status: need.status, pickup: need.pickup, loanable: need.loanable, + createdAt: need.createdAt || new Date().toISOString(), ...(need.loanable ? { returnDate: need.returnDate } : {}), })); @@ -39,6 +40,8 @@ const useNeeds = (): UseNeedsHook => { loanable = false, returnDate?: string ): Promise => { + const createdAt = new Date().toISOString(); + const newNeed: Supply = { itemName, quantityNeeded, @@ -46,15 +49,24 @@ const useNeeds = (): UseNeedsHook => { providedBy: [], status: false, pickup, + createdAt, loanable, ...(loanable && returnDate ? { returnDate } : {}), }; try { + console.log('Before sanitizeNeeds:', [ + ...(organization.needs || []), + newNeed, + ]); const updatedNeeds = sanitizeNeeds([ ...(organization.needs || []), newNeed, ]); + console.log('After sanitizeNeeds:', [ + ...(organization.needs || []), + newNeed, + ]); await updateProfile({ needs: updatedNeeds }); console.log(`Successfully added new need: ${itemName}`); } catch (error_) { @@ -82,7 +94,12 @@ const useNeeds = (): UseNeedsHook => { } const updatedNeeds = [...organization.needs]; - updatedNeeds[needIndex] = { ...updatedNeeds[needIndex], ...updates }; + updatedNeeds[needIndex] = { + ...updatedNeeds[needIndex], + ...updates, + // Set status to false if quantityNeeded is greater than 0 + status: updates.quantityNeeded ? false : updatedNeeds[needIndex].status, + }; const sanitizedNeeds = sanitizeNeeds(updatedNeeds); await updateProfile({ needs: sanitizedNeeds }); diff --git a/src/stores/userStore.ts b/src/stores/userStore.ts index d17ad82..869c796 100644 --- a/src/stores/userStore.ts +++ b/src/stores/userStore.ts @@ -37,6 +37,8 @@ const useUserStore = create()( } } else { set({ user: undefined }); + const eventStore = useEventStore.getState(); + eventStore.setEvents([]); } set({ loading: false }); } diff --git a/src/types/user.d.ts b/src/types/user.d.ts index 1120a2e..57c049a 100644 --- a/src/types/user.d.ts +++ b/src/types/user.d.ts @@ -18,6 +18,7 @@ interface Supply { status: boolean; pickup: boolean; loanable: boolean; + createdAt: string; returnDate?: string; } diff --git a/src/utils/eventsUtils.ts b/src/utils/eventsUtils.ts index a44abbf..2fd5387 100644 --- a/src/utils/eventsUtils.ts +++ b/src/utils/eventsUtils.ts @@ -54,6 +54,7 @@ const createDonationEvent = ( pickup: method === 'pick-up', loanable: supply?.loanable || false, returnDate: supply?.loanable ? supply.returnDate || '' : '', + createdAt: new Date().toISOString(), }; }), });