Feature: Add RSVP cut-off date functionality

This commit is contained in:
Starstrike
2025-05-01 13:44:29 -04:00
parent 3ac630bcc2
commit e1d23eeb32
6 changed files with 142 additions and 83 deletions

View File

@@ -387,6 +387,7 @@ async function initializeDatabase() {
slug TEXT NOT NULL UNIQUE, slug TEXT NOT NULL UNIQUE,
needed_items TEXT, needed_items TEXT,
wallpaper TEXT, wallpaper TEXT,
rsvp_cutoff_date TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) )
`); `);

View File

@@ -54,6 +54,7 @@ interface Event {
slug: string; slug: string;
needed_items?: string[] | string; needed_items?: string[] | string;
wallpaper?: string; wallpaper?: string;
rsvp_cutoff_date?: string;
} }
const EventAdmin: React.FC = () => { const EventAdmin: React.FC = () => {
@@ -85,7 +86,8 @@ const EventAdmin: React.FC = () => {
const [updateForm, setUpdateForm] = useState({ const [updateForm, setUpdateForm] = useState({
description: '', description: '',
location: '', location: '',
date: '' date: '',
rsvp_cutoff_date: ''
}); });
useEffect(() => { useEffect(() => {
@@ -369,6 +371,18 @@ const EventAdmin: React.FC = () => {
} }
}; };
const handleUpdateInfoClick = () => {
if (!event) return;
setUpdateForm({
description: event.description,
location: event.location,
date: event.date.slice(0, 16), // Format date for datetime-local input
rsvp_cutoff_date: event.rsvp_cutoff_date ? event.rsvp_cutoff_date.slice(0, 16) : ''
});
setUpdateInfoDialogOpen(true);
};
const handleUpdateInfoSubmit = async () => { const handleUpdateInfoSubmit = async () => {
if (!event) return; if (!event) return;
@@ -377,14 +391,16 @@ const EventAdmin: React.FC = () => {
...event, ...event,
description: updateForm.description, description: updateForm.description,
location: updateForm.location, location: updateForm.location,
date: updateForm.date date: updateForm.date,
rsvp_cutoff_date: updateForm.rsvp_cutoff_date
}); });
setEvent(prev => prev ? { setEvent(prev => prev ? {
...prev, ...prev,
description: updateForm.description, description: updateForm.description,
location: updateForm.location, location: updateForm.location,
date: updateForm.date date: updateForm.date,
rsvp_cutoff_date: updateForm.rsvp_cutoff_date
} : null); } : null);
setUpdateInfoDialogOpen(false); setUpdateInfoDialogOpen(false);
@@ -393,17 +409,6 @@ const EventAdmin: React.FC = () => {
} }
}; };
const handleUpdateInfoClick = () => {
if (!event) return;
setUpdateForm({
description: event.description,
location: event.location,
date: event.date.slice(0, 16) // Format date for datetime-local input
});
setUpdateInfoDialogOpen(true);
};
if (loading) { if (loading) {
return ( return (
<Container maxWidth="lg"> <Container maxWidth="lg">
@@ -487,6 +492,11 @@ const EventAdmin: React.FC = () => {
<Typography variant="subtitle1" gutterBottom> <Typography variant="subtitle1" gutterBottom>
<strong>Date:</strong> {new Date(event.date).toLocaleString()} <strong>Date:</strong> {new Date(event.date).toLocaleString()}
</Typography> </Typography>
{event.rsvp_cutoff_date && (
<Typography variant="subtitle1" gutterBottom>
<strong>RSVP cut-off date:</strong> {new Date(event.rsvp_cutoff_date).toLocaleString()}
</Typography>
)}
</Box> </Box>
{/* Add items status section */} {/* Add items status section */}
@@ -837,6 +847,16 @@ const EventAdmin: React.FC = () => {
shrink: true, shrink: true,
}} }}
/> />
<TextField
label="RSVP Cut-off Date"
type="datetime-local"
value={updateForm.rsvp_cutoff_date}
onChange={(e) => setUpdateForm(prev => ({ ...prev, rsvp_cutoff_date: e.target.value }))}
fullWidth
InputLabelProps={{
shrink: true,
}}
/>
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>

View File

@@ -42,6 +42,7 @@ interface FormData {
date: string; date: string;
location: string; location: string;
needed_items: string[]; needed_items: string[];
rsvp_cutoff_date: string;
} }
const EventForm: React.FC = () => { const EventForm: React.FC = () => {
@@ -52,6 +53,7 @@ const EventForm: React.FC = () => {
date: '', date: '',
location: '', location: '',
needed_items: [], needed_items: [],
rsvp_cutoff_date: '',
}); });
const [wallpaper, setWallpaper] = useState<File | null>(null); const [wallpaper, setWallpaper] = useState<File | null>(null);
const [currentItem, setCurrentItem] = useState(''); const [currentItem, setCurrentItem] = useState('');
@@ -184,6 +186,18 @@ const EventForm: React.FC = () => {
shrink: true, shrink: true,
}} }}
/> />
<DarkTextField
fullWidth
label="RSVP Cut-off Date"
name="rsvp_cutoff_date"
type="datetime-local"
value={formData.rsvp_cutoff_date}
onChange={handleChange}
variant="outlined"
InputLabelProps={{
shrink: true,
}}
/>
<DarkTextField <DarkTextField
fullWidth fullWidth
label="Location" label="Location"

View File

@@ -9,6 +9,7 @@ import {
Grid, Grid,
CardActions, CardActions,
Container, Container,
Chip,
} from '@mui/material'; } from '@mui/material';
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'; import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
import VisibilityIcon from '@mui/icons-material/Visibility'; import VisibilityIcon from '@mui/icons-material/Visibility';
@@ -21,6 +22,7 @@ interface Event {
date: string; date: string;
location: string; location: string;
slug: string; slug: string;
rsvp_cutoff_date?: string;
} }
const EventList: React.FC = () => { const EventList: React.FC = () => {
@@ -40,14 +42,28 @@ const EventList: React.FC = () => {
} }
}; };
const handleEventClick = (event: Event) => { const isEventOpen = (event: Event) => {
navigate(`/rsvp/events/${event.slug}`); if (!event.rsvp_cutoff_date) return true;
const cutoffDate = new Date(event.rsvp_cutoff_date);
return new Date() < cutoffDate;
}; };
const handleAdminClick = (event: Event) => { const handleEventClick = (event: Event) => {
if (isEventOpen(event)) {
navigate(`/rsvp/events/${event.slug}`);
}
};
const handleAdminClick = (event: Event, e: React.MouseEvent) => {
e.stopPropagation();
navigate(`/admin/events/${event.slug}`); navigate(`/admin/events/${event.slug}`);
}; };
const handleViewClick = (event: Event, e: React.MouseEvent) => {
e.stopPropagation();
navigate(`/view/events/${event.slug}`);
};
return ( return (
<Box> <Box>
<Container maxWidth="md" sx={{ textAlign: 'center', mb: 8 }}> <Container maxWidth="md" sx={{ textAlign: 'center', mb: 8 }}>
@@ -74,75 +90,67 @@ const EventList: React.FC = () => {
</Button> </Button>
</Container> </Container>
{events.length > 0 && ( <Container maxWidth="lg">
<Box sx={{ mt: 6 }}> <Grid container spacing={4}>
<Typography variant="h4" component="h2" sx={{ mb: 4 }}> {events.map((event) => (
Current Events <Grid item xs={12} sm={6} md={4} key={event.id}>
</Typography> <Card
<Grid container spacing={3}> sx={{
{events.map((event) => ( height: '100%',
<Grid item xs={12} key={event.id}> display: 'flex',
<Card flexDirection: 'column',
onClick={() => handleEventClick(event)} cursor: isEventOpen(event) ? 'pointer' : 'default',
sx={{ opacity: isEventOpen(event) ? 1 : 0.7,
cursor: 'pointer', }}
'&:hover': { onClick={() => handleEventClick(event)}
boxShadow: 6, >
} <CardContent sx={{ flexGrow: 1 }}>
}} <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
> <Typography variant="h5" component="h2" sx={{ flexGrow: 1 }}>
<CardContent>
<Typography variant="h5" component="h2">
{event.title} {event.title}
</Typography> </Typography>
<Typography color="textSecondary" gutterBottom> <Chip
{new Date(event.date).toLocaleString(undefined, { label={isEventOpen(event) ? "Open" : "Closed"}
year: 'numeric', color={isEventOpen(event) ? "success" : "error"}
month: 'numeric', size="small"
day: 'numeric', />
hour: 'numeric', </Box>
minute: '2-digit', <Typography variant="body2" color="text.secondary" paragraph>
hour12: true {event.description}
})} at {event.location} </Typography>
<Typography variant="body2" color="text.secondary">
<strong>Date:</strong> {new Date(event.date).toLocaleString()}
</Typography>
<Typography variant="body2" color="text.secondary">
<strong>Location:</strong> {event.location}
</Typography>
{event.rsvp_cutoff_date && (
<Typography variant="body2" color="text.secondary">
<strong>RSVP cut-off date:</strong> {new Date(event.rsvp_cutoff_date).toLocaleString()}
</Typography> </Typography>
<Typography variant="body2" component="p"> )}
{event.description} </CardContent>
</Typography> <CardActions>
</CardContent> <Button
<CardActions> size="small"
<Button startIcon={<AdminPanelSettingsIcon />}
onClick={(e) => { onClick={(e) => handleAdminClick(event, e)}
e.stopPropagation(); >
navigate(`/view/events/${event.slug}`); Manage
}} </Button>
color="primary" <Button
aria-label="view rsvps" size="small"
variant="outlined" startIcon={<VisibilityIcon />}
startIcon={<VisibilityIcon />} onClick={(e) => handleViewClick(event, e)}
sx={{ ml: 1 }} >
> View RSVPs
View RSVPs </Button>
</Button> </CardActions>
<Button </Card>
onClick={(e) => { </Grid>
e.stopPropagation(); ))}
handleAdminClick(event); </Grid>
}} </Container>
color="primary"
aria-label="manage rsvps"
variant="outlined"
startIcon={<AdminPanelSettingsIcon />}
sx={{ ml: 1 }}
>
Manage RSVPs
</Button>
</CardActions>
</Card>
</Grid>
))}
</Grid>
</Box>
)}
</Box> </Box>
); );
}; };

View File

@@ -56,6 +56,15 @@ const RSVPForm: React.FC = () => {
axios.get(`/api/events/${slug}/rsvps`) axios.get(`/api/events/${slug}/rsvps`)
]); ]);
// Check if event is closed for RSVPs
if (eventResponse.data.rsvp_cutoff_date) {
const cutoffDate = new Date(eventResponse.data.rsvp_cutoff_date);
if (new Date() > cutoffDate) {
navigate(`/view/events/${slug}`);
return;
}
}
// Process needed items // Process needed items
let items: string[] = []; let items: string[] = [];
if (eventResponse.data.needed_items) { if (eventResponse.data.needed_items) {
@@ -291,6 +300,12 @@ const RSVPForm: React.FC = () => {
</Typography> </Typography>
)} )}
{event?.rsvp_cutoff_date && (
<Typography variant="body2" color="text.secondary" align="center" sx={{ mb: 2 }}>
<strong>Note:</strong> RSVPs will close on {new Date(event.rsvp_cutoff_date).toLocaleString()}
</Typography>
)}
<Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField <TextField
label="Name" label="Name"

View File

@@ -8,6 +8,7 @@ export interface Event {
created_at: string; created_at: string;
needed_items: string[]; needed_items: string[];
wallpaper?: string; wallpaper?: string;
rsvp_cutoff_date?: string;
} }
export interface Rsvp { export interface Rsvp {