Feature: Add RSVP cut-off date functionality
This commit is contained in:
@@ -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
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user