From 08164560688eec97832c3bbf741f43ad5658ee71 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 29 Apr 2025 14:08:56 -0400 Subject: [PATCH] Add admin functionality and improve UI styling --- frontend/src/App.tsx | 2 + frontend/src/components/EventAdmin.tsx | 184 +++++++++++++++++++++++++ frontend/src/components/EventForm.tsx | 31 +++-- frontend/src/components/EventList.tsx | 19 +++ frontend/src/components/RSVPForm.tsx | 12 +- 5 files changed, 237 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/EventAdmin.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8a1b9e2..a1b32a6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,7 @@ import { Container } from '@mui/material'; import EventList from './components/EventList'; import EventForm from './components/EventForm'; import RSVPForm from './components/RSVPForm'; +import EventAdmin from './components/EventAdmin'; import './App.css'; const darkTheme = createTheme({ @@ -41,6 +42,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> diff --git a/frontend/src/components/EventAdmin.tsx b/frontend/src/components/EventAdmin.tsx new file mode 100644 index 0000000..be3ca7c --- /dev/null +++ b/frontend/src/components/EventAdmin.tsx @@ -0,0 +1,184 @@ +import React, { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { + Box, + Paper, + Typography, + Button, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + IconButton, + Container, + Dialog, + DialogTitle, + DialogContent, + DialogActions, +} from '@mui/material'; +import DeleteIcon from '@mui/icons-material/Delete'; +import EditIcon from '@mui/icons-material/Edit'; +import axios from 'axios'; + +interface RSVP { + id: number; + name: string; + attending: string; + bringing_guests: string; + guest_count: number; + guest_names: string; + items_bringing: string; +} + +interface Event { + id: number; + title: string; + description: string; + date: string; + location: string; + slug: string; +} + +const EventAdmin: React.FC = () => { + const { slug } = useParams<{ slug: string }>(); + const navigate = useNavigate(); + const [event, setEvent] = useState(null); + const [rsvps, setRsvps] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [rsvpToDelete, setRsvpToDelete] = useState(null); + + useEffect(() => { + fetchEventAndRsvps(); + }, [slug]); + + const fetchEventAndRsvps = async () => { + try { + const [eventResponse, rsvpsResponse] = await Promise.all([ + axios.get(`/api/events/${slug}`), + axios.get(`/api/events/${slug}/rsvps`) + ]); + setEvent(eventResponse.data); + setRsvps(rsvpsResponse.data); + setLoading(false); + } catch (error) { + setError('Failed to load event data'); + setLoading(false); + } + }; + + const handleDeleteRsvp = async (rsvp: RSVP) => { + setRsvpToDelete(rsvp); + setDeleteDialogOpen(true); + }; + + const confirmDelete = async () => { + if (!rsvpToDelete) return; + + try { + await axios.delete(`/api/events/${slug}/rsvps/${rsvpToDelete.id}`); + setRsvps(rsvps.filter(r => r.id !== rsvpToDelete.id)); + setDeleteDialogOpen(false); + setRsvpToDelete(null); + } catch (error) { + setError('Failed to delete RSVP'); + } + }; + + if (loading) { + return ( + + Loading... + + ); + } + + if (error || !event) { + return ( + + {error || 'Event not found'} + + ); + } + + return ( + + + + + {event.title} - Admin + + + + + + RSVPs ({rsvps.length}) + + + + + + + Name + Attending + Guests + Items Bringing + Actions + + + + {rsvps.map((rsvp) => ( + + {rsvp.name} + {rsvp.attending} + + {rsvp.bringing_guests === 'yes' ? ( + `${rsvp.guest_count} - ${rsvp.guest_names}` + ) : ( + 'No' + )} + + {rsvp.items_bringing} + + handleDeleteRsvp(rsvp)} + > + + + + + ))} + +
+
+
+ + setDeleteDialogOpen(false)} + > + Delete RSVP + + + Are you sure you want to delete {rsvpToDelete?.name}'s RSVP? + + + + + + + +
+ ); +}; + +export default EventAdmin; \ No newline at end of file diff --git a/frontend/src/components/EventForm.tsx b/frontend/src/components/EventForm.tsx index f177371..d16df32 100644 --- a/frontend/src/components/EventForm.tsx +++ b/frontend/src/components/EventForm.tsx @@ -6,6 +6,7 @@ import { TextField, Typography, Container, + Paper, } from '@mui/material'; import axios from 'axios'; @@ -17,6 +18,7 @@ const EventForm: React.FC = () => { date: '', location: '', }); + const [error, setError] = useState(null); const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; @@ -28,28 +30,37 @@ const EventForm: React.FC = () => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + setError(null); try { await axios.post('/api/events', formData); navigate('/'); } catch (error) { + setError('Failed to create event. Please try again.'); console.error('Error creating event:', error); } }; return ( - - + + Create New Event -
+ + {error && ( + + {error} + + )} + + { name="description" value={formData.description} onChange={handleChange} - margin="normal" + variant="outlined" multiline rows={4} /> @@ -69,7 +80,7 @@ const EventForm: React.FC = () => { type="datetime-local" value={formData.date} onChange={handleChange} - margin="normal" + variant="outlined" required InputLabelProps={{ shrink: true, @@ -81,7 +92,7 @@ const EventForm: React.FC = () => { name="location" value={formData.location} onChange={handleChange} - margin="normal" + variant="outlined" required /> @@ -89,18 +100,20 @@ const EventForm: React.FC = () => { variant="contained" color="primary" type="submit" + size="large" > Create Event - -
+
+
); }; diff --git a/frontend/src/components/EventList.tsx b/frontend/src/components/EventList.tsx index 62b17ef..3af7268 100644 --- a/frontend/src/components/EventList.tsx +++ b/frontend/src/components/EventList.tsx @@ -7,7 +7,10 @@ import { CardContent, Typography, Grid, + CardActions, + IconButton, } from '@mui/material'; +import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'; import axios from 'axios'; interface Event { @@ -40,6 +43,10 @@ const EventList: React.FC = () => { navigate(`/events/${event.slug}/rsvp`); }; + const handleAdminClick = (event: Event) => { + navigate(`/events/${event.slug}/admin`); + }; + return ( @@ -78,6 +85,18 @@ const EventList: React.FC = () => { {event.description} + + { + e.stopPropagation(); + handleAdminClick(event); + }} + color="primary" + aria-label="admin" + > + + + ))} diff --git a/frontend/src/components/RSVPForm.tsx b/frontend/src/components/RSVPForm.tsx index 0c653b8..3d7bbf0 100644 --- a/frontend/src/components/RSVPForm.tsx +++ b/frontend/src/components/RSVPForm.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { useParams } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router-dom'; import axios from 'axios'; import { Box, @@ -37,6 +37,7 @@ const RSVPForm: React.FC = () => { const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); + const navigate = useNavigate(); const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; @@ -76,9 +77,16 @@ const RSVPForm: React.FC = () => { Thank You! - + Your RSVP has been submitted successfully. + );