Feature: Add RSVP cut-off date functionality
This commit is contained in:
@@ -387,6 +387,7 @@ async function initializeDatabase() {
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
needed_items TEXT,
|
||||
wallpaper TEXT,
|
||||
rsvp_cutoff_date TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
@@ -54,6 +54,7 @@ interface Event {
|
||||
slug: string;
|
||||
needed_items?: string[] | string;
|
||||
wallpaper?: string;
|
||||
rsvp_cutoff_date?: string;
|
||||
}
|
||||
|
||||
const EventAdmin: React.FC = () => {
|
||||
@@ -85,7 +86,8 @@ const EventAdmin: React.FC = () => {
|
||||
const [updateForm, setUpdateForm] = useState({
|
||||
description: '',
|
||||
location: '',
|
||||
date: ''
|
||||
date: '',
|
||||
rsvp_cutoff_date: ''
|
||||
});
|
||||
|
||||
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 () => {
|
||||
if (!event) return;
|
||||
|
||||
@@ -377,14 +391,16 @@ const EventAdmin: React.FC = () => {
|
||||
...event,
|
||||
description: updateForm.description,
|
||||
location: updateForm.location,
|
||||
date: updateForm.date
|
||||
date: updateForm.date,
|
||||
rsvp_cutoff_date: updateForm.rsvp_cutoff_date
|
||||
});
|
||||
|
||||
setEvent(prev => prev ? {
|
||||
...prev,
|
||||
description: updateForm.description,
|
||||
location: updateForm.location,
|
||||
date: updateForm.date
|
||||
date: updateForm.date,
|
||||
rsvp_cutoff_date: updateForm.rsvp_cutoff_date
|
||||
} : null);
|
||||
|
||||
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) {
|
||||
return (
|
||||
<Container maxWidth="lg">
|
||||
@@ -487,6 +492,11 @@ const EventAdmin: React.FC = () => {
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
<strong>Date:</strong> {new Date(event.date).toLocaleString()}
|
||||
</Typography>
|
||||
{event.rsvp_cutoff_date && (
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
<strong>RSVP cut-off date:</strong> {new Date(event.rsvp_cutoff_date).toLocaleString()}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Add items status section */}
|
||||
@@ -837,6 +847,16 @@ const EventAdmin: React.FC = () => {
|
||||
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>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
@@ -42,6 +42,7 @@ interface FormData {
|
||||
date: string;
|
||||
location: string;
|
||||
needed_items: string[];
|
||||
rsvp_cutoff_date: string;
|
||||
}
|
||||
|
||||
const EventForm: React.FC = () => {
|
||||
@@ -52,6 +53,7 @@ const EventForm: React.FC = () => {
|
||||
date: '',
|
||||
location: '',
|
||||
needed_items: [],
|
||||
rsvp_cutoff_date: '',
|
||||
});
|
||||
const [wallpaper, setWallpaper] = useState<File | null>(null);
|
||||
const [currentItem, setCurrentItem] = useState('');
|
||||
@@ -184,6 +186,18 @@ const EventForm: React.FC = () => {
|
||||
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
|
||||
fullWidth
|
||||
label="Location"
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Grid,
|
||||
CardActions,
|
||||
Container,
|
||||
Chip,
|
||||
} from '@mui/material';
|
||||
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
@@ -21,6 +22,7 @@ interface Event {
|
||||
date: string;
|
||||
location: string;
|
||||
slug: string;
|
||||
rsvp_cutoff_date?: string;
|
||||
}
|
||||
|
||||
const EventList: React.FC = () => {
|
||||
@@ -40,14 +42,28 @@ const EventList: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleEventClick = (event: Event) => {
|
||||
navigate(`/rsvp/events/${event.slug}`);
|
||||
const isEventOpen = (event: Event) => {
|
||||
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}`);
|
||||
};
|
||||
|
||||
const handleViewClick = (event: Event, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/view/events/${event.slug}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Container maxWidth="md" sx={{ textAlign: 'center', mb: 8 }}>
|
||||
@@ -74,75 +90,67 @@ const EventList: React.FC = () => {
|
||||
</Button>
|
||||
</Container>
|
||||
|
||||
{events.length > 0 && (
|
||||
<Box sx={{ mt: 6 }}>
|
||||
<Typography variant="h4" component="h2" sx={{ mb: 4 }}>
|
||||
Current Events
|
||||
</Typography>
|
||||
<Grid container spacing={3}>
|
||||
{events.map((event) => (
|
||||
<Grid item xs={12} key={event.id}>
|
||||
<Card
|
||||
onClick={() => handleEventClick(event)}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
boxShadow: 6,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography variant="h5" component="h2">
|
||||
<Container maxWidth="lg">
|
||||
<Grid container spacing={4}>
|
||||
{events.map((event) => (
|
||||
<Grid item xs={12} sm={6} md={4} key={event.id}>
|
||||
<Card
|
||||
sx={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
cursor: isEventOpen(event) ? 'pointer' : 'default',
|
||||
opacity: isEventOpen(event) ? 1 : 0.7,
|
||||
}}
|
||||
onClick={() => handleEventClick(event)}
|
||||
>
|
||||
<CardContent sx={{ flexGrow: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<Typography variant="h5" component="h2" sx={{ flexGrow: 1 }}>
|
||||
{event.title}
|
||||
</Typography>
|
||||
<Typography color="textSecondary" gutterBottom>
|
||||
{new Date(event.date).toLocaleString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
})} at {event.location}
|
||||
<Chip
|
||||
label={isEventOpen(event) ? "Open" : "Closed"}
|
||||
color={isEventOpen(event) ? "success" : "error"}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
{event.description}
|
||||
</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 variant="body2" component="p">
|
||||
{event.description}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/view/events/${event.slug}`);
|
||||
}}
|
||||
color="primary"
|
||||
aria-label="view rsvps"
|
||||
variant="outlined"
|
||||
startIcon={<VisibilityIcon />}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
View RSVPs
|
||||
</Button>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAdminClick(event);
|
||||
}}
|
||||
color="primary"
|
||||
aria-label="manage rsvps"
|
||||
variant="outlined"
|
||||
startIcon={<AdminPanelSettingsIcon />}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
Manage RSVPs
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
)}
|
||||
)}
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<AdminPanelSettingsIcon />}
|
||||
onClick={(e) => handleAdminClick(event, e)}
|
||||
>
|
||||
Manage
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<VisibilityIcon />}
|
||||
onClick={(e) => handleViewClick(event, e)}
|
||||
>
|
||||
View RSVPs
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -56,6 +56,15 @@ const RSVPForm: React.FC = () => {
|
||||
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
|
||||
let items: string[] = [];
|
||||
if (eventResponse.data.needed_items) {
|
||||
@@ -291,6 +300,12 @@ const RSVPForm: React.FC = () => {
|
||||
</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 }}>
|
||||
<TextField
|
||||
label="Name"
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface Event {
|
||||
created_at: string;
|
||||
needed_items: string[];
|
||||
wallpaper?: string;
|
||||
rsvp_cutoff_date?: string;
|
||||
}
|
||||
|
||||
export interface Rsvp {
|
||||
|
||||
Reference in New Issue
Block a user