From efe143ca68212a4b41bedc32afee175fc26872a4 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Fri, 16 May 2025 18:27:09 -0400 Subject: [PATCH] Add max guests per RSVP feature to event creation and RSVP forms --- backend/src/index.ts | 25 +++++-- frontend/src/components/EventForm.tsx | 22 +++++- frontend/src/components/RSVPForm.tsx | 98 ++++++++++++++++++++------- frontend/src/types.ts | 3 +- 4 files changed, 116 insertions(+), 32 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 739ceb1..05f8482 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -152,7 +152,7 @@ app.get('/api/events/:slug', async (req: Request, res: Response) => { app.post('/api/events', upload.single('wallpaper'), async (req: MulterRequest, res: Response) => { try { - const { title, description, date, location, needed_items, rsvp_cutoff_date } = req.body; + const { title, description, date, location, needed_items, rsvp_cutoff_date, max_guests_per_rsvp } = req.body; const wallpaperPath = req.file ? `${req.file.filename}` : null; // Generate a slug from the title @@ -170,9 +170,12 @@ app.post('/api/events', upload.single('wallpaper'), async (req: MulterRequest, r console.error('Error parsing needed_items:', e); } + // Parse max_guests_per_rsvp to ensure it's a number + const maxGuests = parseInt(max_guests_per_rsvp as string) || 0; + const result = await db.run( - 'INSERT INTO events (title, description, date, location, slug, needed_items, wallpaper, rsvp_cutoff_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', - [title, description, date, location, slug, JSON.stringify(parsedNeededItems), wallpaperPath, rsvp_cutoff_date] + 'INSERT INTO events (title, description, date, location, slug, needed_items, wallpaper, rsvp_cutoff_date, max_guests_per_rsvp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', + [title, description, date, location, slug, JSON.stringify(parsedNeededItems), wallpaperPath, rsvp_cutoff_date, maxGuests] ); res.status(201).json({ @@ -180,7 +183,8 @@ app.post('/api/events', upload.single('wallpaper'), async (req: MulterRequest, r slug, wallpaper: wallpaperPath ? `/uploads/wallpapers/${wallpaperPath}` : null, needed_items: parsedNeededItems, - rsvp_cutoff_date + rsvp_cutoff_date, + max_guests_per_rsvp: maxGuests }); } catch (error) { console.error('Error creating event:', error); @@ -448,7 +452,7 @@ app.put('/api/events/:slug/rsvps/:id', async (req: Request, res: Response) => { app.put('/api/events/:slug', upload.single('wallpaper'), async (req: MulterRequest, res: Response) => { try { const { slug } = req.params; - const { title, description, date, location, needed_items, rsvp_cutoff_date } = req.body; + const { title, description, date, location, needed_items, rsvp_cutoff_date, max_guests_per_rsvp } = req.body; // Verify the event exists const eventRows = await db.all('SELECT * FROM events WHERE slug = ?', [slug]); @@ -469,6 +473,11 @@ app.put('/api/events/:slug', upload.single('wallpaper'), async (req: MulterReque console.error('Error parsing needed_items:', e); } + // Parse max_guests_per_rsvp to ensure it's a number + const maxGuests = max_guests_per_rsvp !== undefined ? + (parseInt(max_guests_per_rsvp as string) || 0) : + eventRows[0].max_guests_per_rsvp || 0; + // Handle wallpaper update let wallpaperPath = eventRows[0].wallpaper; if (req.file) { @@ -486,7 +495,7 @@ app.put('/api/events/:slug', upload.single('wallpaper'), async (req: MulterReque // Update the event await db.run( - 'UPDATE events SET title = ?, description = ?, date = ?, location = ?, needed_items = ?, rsvp_cutoff_date = ?, wallpaper = ? WHERE slug = ?', + 'UPDATE events SET title = ?, description = ?, date = ?, location = ?, needed_items = ?, rsvp_cutoff_date = ?, wallpaper = ?, max_guests_per_rsvp = ? WHERE slug = ?', [ title ?? eventRows[0].title, description === undefined ? eventRows[0].description : description, @@ -495,6 +504,7 @@ app.put('/api/events/:slug', upload.single('wallpaper'), async (req: MulterReque JSON.stringify(parsedNeededItems), rsvp_cutoff_date !== undefined ? rsvp_cutoff_date : eventRows[0].rsvp_cutoff_date, wallpaperPath, + maxGuests, slug ] ); @@ -543,6 +553,7 @@ async function initializeDatabase() { needed_items TEXT, wallpaper TEXT, rsvp_cutoff_date TEXT, + max_guests_per_rsvp INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); @@ -579,4 +590,4 @@ app.get('*', (req: Request, res: Response) => { app.listen(port, async () => { console.log(`Server running on port ${port}`); await connectToDatabase(); -}); \ No newline at end of file +}); diff --git a/frontend/src/components/EventForm.tsx b/frontend/src/components/EventForm.tsx index 6c0a55f..e43e3ef 100644 --- a/frontend/src/components/EventForm.tsx +++ b/frontend/src/components/EventForm.tsx @@ -43,6 +43,7 @@ interface FormData { location: string; needed_items: string[]; rsvp_cutoff_date: string; + max_guests_per_rsvp: number; } const EventForm: React.FC = () => { @@ -54,6 +55,7 @@ const EventForm: React.FC = () => { location: '', needed_items: [], rsvp_cutoff_date: '', + max_guests_per_rsvp: 0, }); const [wallpaper, setWallpaper] = useState(null); const [currentItem, setCurrentItem] = useState(''); @@ -198,6 +200,24 @@ const EventForm: React.FC = () => { shrink: true, }} /> + + { + const value = parseInt(e.target.value); + setFormData((prev) => ({ + ...prev, + max_guests_per_rsvp: isNaN(value) ? 0 : value, + })); + }} + variant="outlined" + helperText="Set to 0 for no additional guests, -1 for unlimited" + inputProps={{ min: -1 }} + /> { ); }; -export default EventForm; \ No newline at end of file +export default EventForm; diff --git a/frontend/src/components/RSVPForm.tsx b/frontend/src/components/RSVPForm.tsx index 4182f1f..4b4fa4c 100644 --- a/frontend/src/components/RSVPForm.tsx +++ b/frontend/src/components/RSVPForm.tsx @@ -199,15 +199,35 @@ const RSVPForm: React.FC = () => { other_items: '' })); } else if (name === 'bringing_guests') { - // When bringing guests is changed - setFormData(prev => ({ - ...prev, - bringing_guests: value as 'yes' | 'no', - // If changing to 'yes', set guest count to 1 and initialize one empty name field - guest_count: value === 'yes' ? 1 : 0, - // Clear guest names if changing to 'no', otherwise initialize with empty string - guest_names: value === 'no' ? [] : [''] - })); + // When bringing guests is changed + setFormData(prev => { + const maxGuests = event?.max_guests_per_rsvp; + let initialGuestCount = 1; + + // If max_guests_per_rsvp is 0, don't allow guests + if (maxGuests === 0 && value === 'yes') { + return { + ...prev, + bringing_guests: 'no', + guest_count: 0, + guest_names: [] + }; + } + + // If max_guests_per_rsvp is set and not -1 (unlimited), limit initial count + if (maxGuests !== undefined && maxGuests !== -1 && maxGuests < initialGuestCount) { + initialGuestCount = maxGuests; + } + + return { + ...prev, + bringing_guests: value as 'yes' | 'no', + // If changing to 'yes', set guest count to appropriate value + guest_count: value === 'yes' ? initialGuestCount : 0, + // Clear guest names if changing to 'no', otherwise initialize with empty strings + guest_names: value === 'no' ? [] : Array(initialGuestCount).fill('') + }; + }); } else { setFormData(prev => ({ ...prev, @@ -465,19 +485,51 @@ const RSVPForm: React.FC = () => { {formData.bringing_guests === 'yes' && ( <> - + { + const value = parseInt(e.target.value); + if (isNaN(value)) return; + + // Check if there's a maximum guest limit + const maxGuests = event?.max_guests_per_rsvp; + let newCount = value; + + // If max_guests_per_rsvp is set and not -1 (unlimited), enforce the limit + if (maxGuests !== undefined && maxGuests !== -1 && value > maxGuests) { + newCount = maxGuests; + } + + // Ensure count is at least 1 + if (newCount < 1) newCount = 1; + + setFormData(prev => ({ + ...prev, + guest_count: newCount, + guest_names: Array(newCount).fill('').map((_, i) => prev.guest_names[i] || '') + })); + }} + fullWidth + variant="outlined" + required + inputProps={{ + min: 1, + max: event?.max_guests_per_rsvp === -1 ? undefined : event?.max_guests_per_rsvp + }} + error={formData.guest_count < 1} + helperText={ + formData.guest_count < 1 + ? "Number of guests must be at least 1" + : event?.max_guests_per_rsvp === 0 + ? "No additional guests allowed for this event" + : event?.max_guests_per_rsvp === -1 + ? "No limit on number of guests" + : `Maximum ${event?.max_guests_per_rsvp} additional guests allowed` + } + /> {Array.from({ length: formData.guest_count }).map((_, index) => ( { ); }; -export default RSVPForm; \ No newline at end of file +export default RSVPForm; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index bf444d5..02a52cd 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -9,6 +9,7 @@ export interface Event { needed_items: string[]; wallpaper?: string; rsvp_cutoff_date?: string; + max_guests_per_rsvp?: number; } export interface Rsvp { @@ -22,4 +23,4 @@ export interface Rsvp { items_bringing: string[] | string; other_items?: string; created_at: string; -} \ No newline at end of file +}