Skip to content

Commit

Permalink
Merge branch 'main' into donorEmail
Browse files Browse the repository at this point in the history
  • Loading branch information
ZL-Asica authored Dec 6, 2024
2 parents 97cf1fb + 032c128 commit 3bac6ac
Show file tree
Hide file tree
Showing 10 changed files with 232 additions and 107 deletions.
158 changes: 106 additions & 52 deletions src/components/Alerts/DonorAlerts.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Box>
<h1 style={{ marginLeft: '20px' }}>Alerts</h1>
<Paper
sx={{
mt: 2,
mx: 2,
backgroundColor: lighten(theme.palette.primary.light, 0.8),
}}
<Box sx={{ px: 2 }}>
<Typography
variant='h4'
sx={{ mb: 1, ml: 2 }}
>
Alerts
</Typography>
<Typography
variant='subtitle1'
color='text.secondary'
sx={{ mb: 3, ml: 2 }}
>
{alerts.length > 0 ? (
<List>
{alerts.map((alert) => (
<ListItem key={alert.id}>
<ListItemText
primary={alert.title}
secondary={`${alert.message} (Category: ${alert.category})`}
Needs recently added by your saved organizations
</Typography>
{recentNeeds.length > 0 ? (
<List sx={{ mt: 2 }}>
{recentNeeds.map((needData, index) => (
<ListItem key={index}>
<Card
sx={{
width: '100%',
backgroundColor: lighten(theme.palette.primary.light, 0.8),
boxShadow: theme.shadows[1],
}}
>
<CardHeader
title={
<Typography variant='h6'>
{needData.need.itemName}
</Typography>
}
subheader={
<Typography
variant='body2'
color='text.secondary'
>
Added {needData.hoursAgo} hours ago by {needData.name}
</Typography>
}
action={
<Button
variant='text'
onClick={() => handleViewItem(needData.name)}
sx={{ textTransform: 'none' }}
>
View Item
</Button>
}
/>
</ListItem>
))}
</List>
) : (
<Typography
variant='h2'
color='text.secondary'
align='center'
>
No alerts match your search.
</Typography>
)}
</Paper>
</Card>
</ListItem>
))}
</List>
) : (
<Typography
variant='body1'
color='text.secondary'
align='center'
sx={{ mt: 5 }}
>
No recent needs found.
</Typography>
)}
</Box>
);
};
Expand Down
35 changes: 7 additions & 28 deletions src/components/Home/DonorDashboard/index.tsx
Original file line number Diff line number Diff line change
@@ -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 <LoadingCircle />;
if (error) {
toast.error(error);
return <p>{error}</p>;
}
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) ||
Expand All @@ -45,10 +27,7 @@ const DonorDashboard = () => {

return organizationProfiles.length > 0 ? (
<div>
<SearchBar
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
<SearchBar onSearchChange={setSearchQuery} />
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
{filteredOrganizations.map((org) => (
<OrganizationCard
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const DEFAULT_NEED: Supply = {
pickup: false,
returnDate: undefined,
status: false,
createdAt: new Date().toISOString(),
loanable: false,
};

Expand Down
7 changes: 6 additions & 1 deletion src/components/Schedule/EventsCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ const EventsCalendar = ({ events, setSelectedDate }: EventsCalendarProps) => {
key={day.toString()}
overlap='circular'
badgeContent={isSelected ? '•' : undefined}
color='primary'
sx={{
'& .MuiBadge-badge': {
fontSize: '30px',
color: (theme) => theme.palette.primary.main,
},
}}
>
<PickersDay
{...other}
Expand Down
35 changes: 33 additions & 2 deletions src/components/common/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Typography } from '@mui/material';
import type { ReactElement } from 'react';
import { useEffect, type ReactElement } from 'react';
import { toast } from 'sonner';

import { useUserStore } from '@/stores';
import { useOrganizationStore, useUserStore } from '@/stores';

import LoadingCircle from '@/components/common/LoadingCircle';

Expand All @@ -28,6 +29,36 @@ const ProtectedRoute = ({ element }: ProtectedRouteProps) => {
);
}

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 <LoadingCircle />;

if (error) {
toast.error(error);
return (
<Typography
variant='body1'
color='error'
align='center'
sx={{ mt: 4 }}
>
{error}
</Typography>
);
}
}

return element;
};

Expand Down
80 changes: 57 additions & 23 deletions src/components/common/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -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<SearchBarProps> = ({
searchQuery,
setSearchQuery,
}) => (
<Box
display='flex'
alignItems='center'
mt={3}
mx={2}
>
<TextField
label='Search organizations...'
variant='outlined'
fullWidth
sx={{ marginRight: 1 }}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<Button variant='contained'>Recommend</Button>
</Box>
);
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<HTMLInputElement>) => {
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 (
<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>
);
};

export default SearchBar;
Loading

0 comments on commit 3bac6ac

Please sign in to comment.