From f686d3ae2b900893ef9d2e94db082ba3248cb697 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 12:21:56 -0400 Subject: [PATCH 01/38] Refactor EventForm into sections and add thank you message field --- frontend/src/components/EventForm.tsx | 406 ++++++++++++++------------ 1 file changed, 221 insertions(+), 185 deletions(-) diff --git a/frontend/src/components/EventForm.tsx b/frontend/src/components/EventForm.tsx index 940bcde..cfbae36 100644 --- a/frontend/src/components/EventForm.tsx +++ b/frontend/src/components/EventForm.tsx @@ -12,6 +12,7 @@ import { styled, Checkbox, FormControlLabel, + Divider, // Added Divider for visual separation } from '@mui/material'; import WallpaperIcon from '@mui/icons-material/Wallpaper'; import DeleteIcon from '@mui/icons-material/Delete'; @@ -48,6 +49,7 @@ interface FormData { max_guests_per_rsvp: number; email_notifications_enabled: boolean; email_recipients: string; + thank_you_message: string; // Added thank you message field } const EventForm: React.FC = () => { @@ -62,6 +64,7 @@ const EventForm: React.FC = () => { max_guests_per_rsvp: 0, email_notifications_enabled: false, email_recipients: '', + thank_you_message: '', // Added thank you message state }); const [wallpaper, setWallpaper] = useState(null); const [currentItem, setCurrentItem] = useState(''); @@ -123,6 +126,9 @@ const EventForm: React.FC = () => { } }); + // Append thank you message + submitData.append('thank_you_message', formData.thank_you_message); + // Append wallpaper if selected if (wallpaper) { submitData.append('wallpaper', wallpaper); @@ -154,196 +160,215 @@ const EventForm: React.FC = () => { Create New Event - + {error && ( {error} )} - - - - - - - { - 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 }} - /> - - - - - Event Wallpaper + + {/* Part 1: Basic Event Details */} + + + Basic Event Details - - - {wallpaper && ( - setWallpaper(null)} - size="small" - sx={{ - color: '#f44336', - '&:hover': { - backgroundColor: 'rgba(244, 67, 54, 0.08)', - } - }} - > - - - )} - - {wallpaper && ( - - Selected: {wallpaper.name} - - )} - - - - - - Needed Items - - + - - - - {formData.needed_items.map((item, index) => ( - handleRemoveItem(index)} - sx={{ - bgcolor: 'rgba(144, 202, 249, 0.2)', - color: 'rgba(255, 255, 255, 0.9)', - '& .MuiChip-deleteIcon': { - color: 'rgba(255, 255, 255, 0.7)', - '&:hover': { - color: 'rgba(255, 255, 255, 0.9)', - } - } - }} - /> - ))} + /> + + { + 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 }} + /> - - - Email Notifications + + + {/* Part 2: Customization */} + + + Customization - - + + + + Event Wallpaper + + + + {wallpaper && ( + setWallpaper(null)} + size="small" + sx={{ + color: '#f44336', + '&:hover': { + backgroundColor: 'rgba(244, 67, 54, 0.08)', + } + }} + > + + + )} + + {wallpaper && ( + + Selected: {wallpaper.name} + + )} + + + + + + Needed Items + + + + + + + {formData.needed_items.map((item, index) => ( + handleRemoveItem(index)} + sx={{ + bgcolor: 'rgba(144, 202, 249, 0.2)', + color: 'rgba(255, 255, 255, 0.9)', + '& .MuiChip-deleteIcon': { + color: 'rgba(255, 255, 255, 0.7)', + '&:hover': { + color: 'rgba(255, 255, 255, 0.9)', + } + } + }} + /> + ))} + + + + + + + + {/* Part 3: Notifications and Messaging */} + + + Notifications and Messaging + + { color: 'rgba(255, 255, 255, 0.9)', }} /> - - - {formData.email_notifications_enabled && ( + + {formData.email_notifications_enabled && ( + + )} + - )} + + + + {formData.needed_items.map((item, index) => ( + handleRemoveItem(index)} + sx={{ + bgcolor: 'rgba(144, 202, 249, 0.2)', + color: 'rgba(255, 255, 255, 0.9)', + '& .MuiChip-deleteIcon': { + color: 'rgba(255, 255, 255, 0.7)', + '&:hover': { + color: 'rgba(255, 255, 255, 0.9)', + } + } + }} + /> + ))} + + @@ -311,58 +363,6 @@ const EventForm: React.FC = () => { style={{ display: 'none' }} /> - - - - Needed Items - - - - - - - {formData.needed_items.map((item, index) => ( - handleRemoveItem(index)} - sx={{ - bgcolor: 'rgba(144, 202, 249, 0.2)', - color: 'rgba(255, 255, 255, 0.9)', - '& .MuiChip-deleteIcon': { - color: 'rgba(255, 255, 255, 0.7)', - '&:hover': { - color: 'rgba(255, 255, 255, 0.9)', - } - } - }} - /> - ))} - - From eb7f77167ceaa7d4d194bc228031153d2ebb5c11 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 13:01:21 -0400 Subject: [PATCH 05/38] Update event conclusion email schedule and add logging --- backend/src/index.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 1d4e58d..bebf469 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -967,11 +967,16 @@ async function sendConclusionEmails() { } -// Schedule the task to run daily (e.g., at 1:00 AM) -cron.schedule('0 1 * * *', () => { +// Schedule the task to run daily (e.g., at 8:00 AM) +const scheduledTask = cron.schedule('0 8 * * *', () => { sendConclusionEmails(); +}, { + scheduled: true, + timezone: process.env.TZ || 'UTC' // Use TZ environment variable, default to UTC }); +console.log(`Event conclusion email scheduled task scheduled for ${scheduledTask.options.timezone} at ${scheduledTask.options.recoverMissedExecutions ? 'a time based on missed executions' : 'the specified cron pattern'}.`); + // Handle client-side routing app.get('*', (req: Request, res: Response) => { From 1a4f88419a55116c37d32d2d5f54c581ba1156c7 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 13:03:27 -0400 Subject: [PATCH 06/38] Add TZ environment variable to Docker Compose files --- docker-compose-build.yml | 1 + docker-compose.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/docker-compose-build.yml b/docker-compose-build.yml index 51626b2..30f09ea 100644 --- a/docker-compose-build.yml +++ b/docker-compose-build.yml @@ -19,6 +19,7 @@ services: - EMAIL_FROM_ADDRESS=your@email.com - EMAIL_SECURE=false - FRONTEND_BASE_URL=https://your-frontend-domain.com + - TZ=America/New_York # Set your desired timezone here restart: unless-stopped volumes: diff --git a/docker-compose.yml b/docker-compose.yml index 37619ea..ee9a8d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,7 @@ services: - EMAIL_FROM_ADDRESS=your@email.com - EMAIL_SECURE=false - FRONTEND_BASE_URL=https://your-frontend-domain.com + - TZ=America/New_York # Set your desired timezone here restart: unless-stopped volumes: From 76e6699b67c7021b17357f1a44f29867a0fde275 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 13:06:58 -0400 Subject: [PATCH 07/38] Fix TypeScript error accessing node-cron ScheduledTask options --- backend/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index bebf469..6f21365 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -975,7 +975,7 @@ const scheduledTask = cron.schedule('0 8 * * *', () => { timezone: process.env.TZ || 'UTC' // Use TZ environment variable, default to UTC }); -console.log(`Event conclusion email scheduled task scheduled for ${scheduledTask.options.timezone} at ${scheduledTask.options.recoverMissedExecutions ? 'a time based on missed executions' : 'the specified cron pattern'}.`); +console.log(`Event conclusion email scheduled task scheduled for timezone ${process.env.TZ || 'UTC'} at 8:00 AM.`); // Handle client-side routing From ec747a9761265582bf859c4273cb068c807d9ee7 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 13:20:02 -0400 Subject: [PATCH 08/38] Update EventAdmin form with title and email conclusion settings, adjust wallpaper preview --- frontend/src/components/EventAdmin.tsx | 49 ++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/EventAdmin.tsx b/frontend/src/components/EventAdmin.tsx index 4de3b12..86019bd 100644 --- a/frontend/src/components/EventAdmin.tsx +++ b/frontend/src/components/EventAdmin.tsx @@ -1104,6 +1104,13 @@ const EventAdmin: React.FC = () => { Update Event Information + {/* Event Title Field */} + setUpdateForm(prev => ({ ...prev, title: e.target.value }))} + fullWidth + /> { /> )} - + + {/* Event Conclusion Email Settings */} + + + Event Conclusion Email + + setUpdateForm(prev => ({ + ...prev, + event_conclusion_email_enabled: e.target.checked + }))} + /> + } + label="Enable Event Conclusion Email" + /> + + {updateForm.event_conclusion_email_enabled && ( + setUpdateForm(prev => ({ + ...prev, + event_conclusion_message: e.target.value + }))} + variant="outlined" + multiline + rows={4} + helperText="This message will be sent to attendees who opted for email notifications the day after the event." + sx={{ mt: 2 }} + /> + )} + + Wallpaper @@ -1187,8 +1230,8 @@ const EventAdmin: React.FC = () => { alt="Current wallpaper" sx={{ width: '100%', - height: 120, - objectFit: 'cover', + maxHeight: 200, // Increased max height for better viewing + objectFit: 'contain', // Changed to contain to show the whole image borderRadius: 1, }} /> From 5f08039fd9cc2c91e5b141687d49591fcde91292 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 13:23:12 -0400 Subject: [PATCH 09/38] Fix TypeScript error in EventAdmin by adding UpdateFormData interface --- frontend/src/components/EventAdmin.tsx | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/EventAdmin.tsx b/frontend/src/components/EventAdmin.tsx index 86019bd..e6108e9 100644 --- a/frontend/src/components/EventAdmin.tsx +++ b/frontend/src/components/EventAdmin.tsx @@ -75,6 +75,19 @@ interface EditFormData { other_items: string; } +interface UpdateFormData { + title: string; + description: string; + location: string; + date: string; + rsvp_cutoff_date: string; + wallpaper: File | null; + email_notifications_enabled: boolean; + email_recipients: string; + event_conclusion_email_enabled: boolean; + event_conclusion_message: string; +} + const EventAdmin: React.FC = () => { const { slug } = useParams<{ slug: string }>(); const navigate = useNavigate(); @@ -103,14 +116,17 @@ const EventAdmin: React.FC = () => { const [newItem, setNewItem] = useState(''); const [itemToDelete, setItemToDelete] = useState(null); const [updateInfoDialogOpen, setUpdateInfoDialogOpen] = useState(false); - const [updateForm, setUpdateForm] = useState({ + const [updateForm, setUpdateForm] = useState({ + title: '', description: '', location: '', date: '', rsvp_cutoff_date: '', - wallpaper: null as File | null, + wallpaper: null, email_notifications_enabled: false, - email_recipients: '' + email_recipients: '', + event_conclusion_email_enabled: false, + event_conclusion_message: '' }); useEffect(() => { From c731e41d6862728d6e846855bd198e3858c71d02 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 13:33:26 -0400 Subject: [PATCH 10/38] Fix frontend build error in EventAdmin --- frontend/src/components/EventAdmin.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/EventAdmin.tsx b/frontend/src/components/EventAdmin.tsx index e6108e9..575dce8 100644 --- a/frontend/src/components/EventAdmin.tsx +++ b/frontend/src/components/EventAdmin.tsx @@ -584,13 +584,16 @@ const EventAdmin: React.FC = () => { if (!event) return; setUpdateForm({ + title: event.title, // Include title 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) : '', wallpaper: null, email_notifications_enabled: event.email_notifications_enabled || false, - email_recipients: event.email_recipients || '' + email_recipients: event.email_recipients || '', + event_conclusion_email_enabled: event.event_conclusion_email_enabled || false, // Include new field + event_conclusion_message: event.event_conclusion_message || '' // Include new field }); setUpdateInfoDialogOpen(true); }; From 95ee023e4d0275ad8c0eb6f7be42d32056d4c4ba Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 13:36:08 -0400 Subject: [PATCH 11/38] Fix frontend build error in EventAdmin related to setUpdateForm --- frontend/src/components/EventAdmin.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/components/EventAdmin.tsx b/frontend/src/components/EventAdmin.tsx index 575dce8..236719a 100644 --- a/frontend/src/components/EventAdmin.tsx +++ b/frontend/src/components/EventAdmin.tsx @@ -63,6 +63,8 @@ interface Event { max_guests_per_rsvp?: number; email_notifications_enabled?: boolean; email_recipients?: string; + event_conclusion_email_enabled?: boolean; // Added event conclusion email toggle + event_conclusion_message?: string; // Added event conclusion message field } interface EditFormData { From aa106f5196ba17ab85f79f454f59e14807f3cf64 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 13:50:09 -0400 Subject: [PATCH 12/38] Update Event interface in EventAdmin with email conclusion fields --- frontend/src/components/EventAdmin.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/components/EventAdmin.tsx b/frontend/src/components/EventAdmin.tsx index 236719a..4fd26d8 100644 --- a/frontend/src/components/EventAdmin.tsx +++ b/frontend/src/components/EventAdmin.tsx @@ -1054,6 +1054,8 @@ const EventAdmin: React.FC = () => { setDeleteEventDialogOpen(false)} + maxWidth="sm" + fullWidth > Delete Event From 58c0eebbbac680955ef407755edf69d6f5ac7116 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 13:58:10 -0400 Subject: [PATCH 13/38] Update Event interface in EventAdmin with email conclusion fields --- frontend/src/components/EventAdmin.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/EventAdmin.tsx b/frontend/src/components/EventAdmin.tsx index 4fd26d8..6b8c04a 100644 --- a/frontend/src/components/EventAdmin.tsx +++ b/frontend/src/components/EventAdmin.tsx @@ -154,7 +154,8 @@ const EventAdmin: React.FC = () => { } setEvent(eventResponse.data); - + console.log('Fetched event data:', eventResponse.data); // Add logging + // Process needed items let items: string[] = []; if (eventResponse.data.needed_items) { From a6ef28beea3e7fbf6651aaf3c9eb1b7c7396b476 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 14:08:20 -0400 Subject: [PATCH 14/38] Add logging to EventAdmin update submit handler --- frontend/src/components/EventAdmin.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/EventAdmin.tsx b/frontend/src/components/EventAdmin.tsx index 6b8c04a..ef7362d 100644 --- a/frontend/src/components/EventAdmin.tsx +++ b/frontend/src/components/EventAdmin.tsx @@ -615,12 +615,16 @@ const EventAdmin: React.FC = () => { formData.append('needed_items', JSON.stringify(event.needed_items)); // Keep existing needed items formData.append('email_notifications_enabled', updateForm.email_notifications_enabled.toString()); formData.append('email_recipients', updateForm.email_recipients); - + formData.append('event_conclusion_email_enabled', updateForm.event_conclusion_email_enabled.toString()); // Append new field + formData.append('event_conclusion_message', updateForm.event_conclusion_message); // Append new field + // Append wallpaper if a new one was selected if (updateForm.wallpaper) { formData.append('wallpaper', updateForm.wallpaper); } - + + console.log('Submitting event update data:', Object.fromEntries(formData.entries())); // Add logging + const response = await axios.put(`/api/events/${slug}`, formData, { headers: { 'Content-Type': 'multipart/form-data', @@ -1295,4 +1299,4 @@ const EventAdmin: React.FC = () => { ); }; -export default EventAdmin; +export default EventAdmin; \ No newline at end of file From 60c414e4ee68ddaeacce1d021a0d3a994fd0efae Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 14:15:51 -0400 Subject: [PATCH 15/38] Fix event conclusion email data submission in EventForm --- frontend/src/components/EventForm.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/EventForm.tsx b/frontend/src/components/EventForm.tsx index fe0a6ab..4f15fca 100644 --- a/frontend/src/components/EventForm.tsx +++ b/frontend/src/components/EventForm.tsx @@ -49,7 +49,7 @@ interface FormData { max_guests_per_rsvp: number; email_notifications_enabled: boolean; email_recipients: string; - thank_you_message: string; // Added thank you message field + event_conclusion_message: string; // Renamed from thank_you_message event_conclusion_email_enabled: boolean; // Added state for event conclusion email toggle } @@ -65,7 +65,7 @@ const EventForm: React.FC = () => { max_guests_per_rsvp: 0, email_notifications_enabled: false, email_recipients: '', - thank_you_message: '', // Added thank you message state + event_conclusion_message: '', // Renamed state event_conclusion_email_enabled: false, // Initialize new state }); const [wallpaper, setWallpaper] = useState(null); @@ -128,10 +128,8 @@ const EventForm: React.FC = () => { } }); - // Append thank you message - submitData.append('thank_you_message', formData.thank_you_message); - // Append event conclusion email enabled state - submitData.append('event_conclusion_email_enabled', String(formData.event_conclusion_email_enabled)); + // Append event conclusion message + submitData.append('event_conclusion_message', formData.event_conclusion_message); // Use correct key and state variable // Append wallpaper if selected @@ -139,6 +137,8 @@ const EventForm: React.FC = () => { submitData.append('wallpaper', wallpaper); } + console.log('Submitting event creation data:', Object.fromEntries(submitData.entries())); // Add logging + await axios.post('/api/events', submitData, { headers: { 'Content-Type': 'multipart/form-data', From dd77fe978d1c440c3a37bfcf16b37e70cb68c310 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 14:19:42 -0400 Subject: [PATCH 16/38] Fix thank_you_message prop in EventForm TextField --- frontend/src/components/EventForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/EventForm.tsx b/frontend/src/components/EventForm.tsx index 4f15fca..a6c15a0 100644 --- a/frontend/src/components/EventForm.tsx +++ b/frontend/src/components/EventForm.tsx @@ -441,8 +441,8 @@ const EventForm: React.FC = () => { Date: Mon, 26 May 2025 14:26:17 -0400 Subject: [PATCH 17/38] Ensure event_conclusion_message is string in EventAdmin --- frontend/src/components/EventAdmin.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/EventAdmin.tsx b/frontend/src/components/EventAdmin.tsx index ef7362d..f542ac5 100644 --- a/frontend/src/components/EventAdmin.tsx +++ b/frontend/src/components/EventAdmin.tsx @@ -596,7 +596,7 @@ const EventAdmin: React.FC = () => { email_notifications_enabled: event.email_notifications_enabled || false, email_recipients: event.email_recipients || '', event_conclusion_email_enabled: event.event_conclusion_email_enabled || false, // Include new field - event_conclusion_message: event.event_conclusion_message || '' // Include new field + event_conclusion_message: String(event.event_conclusion_message || '') // Ensure it's a string }); setUpdateInfoDialogOpen(true); }; @@ -1299,4 +1299,4 @@ const EventAdmin: React.FC = () => { ); }; -export default EventAdmin; \ No newline at end of file +export default EventAdmin; From 7f709660b0962a71cae864f7122da7828c3d9d0c Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 14:44:14 -0400 Subject: [PATCH 18/38] Add logging to backend event GET route --- backend/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 6f21365..d962521 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -139,6 +139,7 @@ app.get('/api/events/:slug', async (req: Request, res: Response) => { // Parse needed_items from JSON string to array and boolean fields const event = rows[0]; + console.log('Raw event_conclusion_message from DB:', event.event_conclusion_message); // Add logging try { event.needed_items = event.needed_items ? JSON.parse(event.needed_items) : []; event.email_notifications_enabled = Boolean(event.email_notifications_enabled); @@ -989,4 +990,4 @@ app.listen(port, async () => { await connectToDatabase(); // Optionally run the task on startup for testing // sendConclusionEmails(); -}); +}); \ No newline at end of file From 66384d8495c1c1937a61b332824f8d9b39f73299 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 15:21:16 -0400 Subject: [PATCH 19/38] Fixing event conclusion message --- backend/src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index d962521..8531fb6 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -139,7 +139,7 @@ app.get('/api/events/:slug', async (req: Request, res: Response) => { // Parse needed_items from JSON string to array and boolean fields const event = rows[0]; - console.log('Raw event_conclusion_message from DB:', event.event_conclusion_message); // Add logging + console.log('Raw event_conclusion_message from DB:', event.event_conclusion_message); // Keep this line for now, it's helpful for debugging try { event.needed_items = event.needed_items ? JSON.parse(event.needed_items) : []; event.email_notifications_enabled = Boolean(event.email_notifications_enabled); @@ -156,7 +156,11 @@ app.get('/api/events/:slug', async (req: Request, res: Response) => { event.wallpaper = `/uploads/wallpapers/${event.wallpaper}`; } + // Explicitly ensure event_conclusion_message is a string or null before sending + event.event_conclusion_message = typeof event.event_conclusion_message === 'string' ? event.event_conclusion_message : null; + res.json(event); + } catch (error) { console.error('Error fetching event:', error); res.status(500).json({ error: 'Internal server error' }); From cd9e43ee5a647aa3676c09abd5b05cd57e6cdab7 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 15:43:32 -0400 Subject: [PATCH 20/38] Updated event conclusion message handling to ensure consistency with description box in EventForm and EventAdmin components. --- frontend/src/components/EventAdmin.tsx | 2 +- frontend/src/components/EventForm.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/EventAdmin.tsx b/frontend/src/components/EventAdmin.tsx index f542ac5..a6abb1c 100644 --- a/frontend/src/components/EventAdmin.tsx +++ b/frontend/src/components/EventAdmin.tsx @@ -616,7 +616,7 @@ const EventAdmin: React.FC = () => { formData.append('email_notifications_enabled', updateForm.email_notifications_enabled.toString()); formData.append('email_recipients', updateForm.email_recipients); formData.append('event_conclusion_email_enabled', updateForm.event_conclusion_email_enabled.toString()); // Append new field - formData.append('event_conclusion_message', updateForm.event_conclusion_message); // Append new field + formData.append('event_conclusion_message', String(updateForm.event_conclusion_message)); // Ensure it's a string // Append wallpaper if a new one was selected if (updateForm.wallpaper) { diff --git a/frontend/src/components/EventForm.tsx b/frontend/src/components/EventForm.tsx index a6c15a0..13a5dd8 100644 --- a/frontend/src/components/EventForm.tsx +++ b/frontend/src/components/EventForm.tsx @@ -129,7 +129,7 @@ const EventForm: React.FC = () => { }); // Append event conclusion message - submitData.append('event_conclusion_message', formData.event_conclusion_message); // Use correct key and state variable + submitData.append('event_conclusion_message', String(formData.event_conclusion_message)); // Ensure it's a string // Append wallpaper if selected From edd3903ffb2b960e63225dcf09b55ef7d40a27b1 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 15:53:51 -0400 Subject: [PATCH 21/38] Updated event conclusion message handling to ensure it is saved as null if not filled out, consistent with description field. --- backend/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 8531fb6..4264bfd 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -222,7 +222,7 @@ app.post('/api/events', upload.single('wallpaper'), async (req: MulterRequest, r emailNotificationsEnabled ? 1 : 0, email_recipients || '', eventConclusionEmailEnabled ? 1 : 0, // Save new field - event_conclusion_message || '' // Save new field + event_conclusion_message === undefined ? null : event_conclusion_message // Save new field ] ); @@ -994,4 +994,4 @@ app.listen(port, async () => { await connectToDatabase(); // Optionally run the task on startup for testing // sendConclusionEmails(); -}); \ No newline at end of file +}); From 7c09fd9d405f5a39adc39b953f333095a9fb612f Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 16:16:26 -0400 Subject: [PATCH 22/38] Fix event_conclusion_message displaying [object Object] in edit form - Changed String() conversion to direct assignment in EventAdmin.tsx - Now properly displays actual text content instead of [object Object] - Handles null/undefined values by defaulting to empty string --- frontend/src/components/EventAdmin.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/EventAdmin.tsx b/frontend/src/components/EventAdmin.tsx index a6abb1c..1ec8d9a 100644 --- a/frontend/src/components/EventAdmin.tsx +++ b/frontend/src/components/EventAdmin.tsx @@ -596,7 +596,7 @@ const EventAdmin: React.FC = () => { email_notifications_enabled: event.email_notifications_enabled || false, email_recipients: event.email_recipients || '', event_conclusion_email_enabled: event.event_conclusion_email_enabled || false, // Include new field - event_conclusion_message: String(event.event_conclusion_message || '') // Ensure it's a string + event_conclusion_message: event.event_conclusion_message || '' // Handle null/undefined properly }); setUpdateInfoDialogOpen(true); }; From d0395abc3afc47de47de3cc0fe4db72e29e16700 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 16:20:56 -0400 Subject: [PATCH 23/38] Fix event_conclusion_message being stored as [object Object] in database - Removed duplicate FormData.append() call for event_conclusion_message - The field was being appended twice: once in the forEach loop and once explicitly - This caused the backend to receive malformed data resulting in [object Object] - Now the field is only appended once through the normal form processing loop --- frontend/src/components/EventForm.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/components/EventForm.tsx b/frontend/src/components/EventForm.tsx index 13a5dd8..006480e 100644 --- a/frontend/src/components/EventForm.tsx +++ b/frontend/src/components/EventForm.tsx @@ -128,9 +128,6 @@ const EventForm: React.FC = () => { } }); - // Append event conclusion message - submitData.append('event_conclusion_message', String(formData.event_conclusion_message)); // Ensure it's a string - // Append wallpaper if selected if (wallpaper) { From f96a4d161ff75c4a49ce45fa56cfe8ee9bccce14 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 16:35:53 -0400 Subject: [PATCH 24/38] Make email address required on RSVP form - Removed email notification toggle - email is now always required - Moved email field to be required right after name field - Added helper text explaining users will receive confirmation with edit link - Simplified form validation and submission logic - Email confirmations are now sent automatically for all RSVPs --- frontend/src/components/RSVPForm.tsx | 71 +++++++++------------------- 1 file changed, 22 insertions(+), 49 deletions(-) diff --git a/frontend/src/components/RSVPForm.tsx b/frontend/src/components/RSVPForm.tsx index bff1ebd..8d134f1 100644 --- a/frontend/src/components/RSVPForm.tsx +++ b/frontend/src/components/RSVPForm.tsx @@ -24,28 +24,26 @@ import { Event } from '../types'; interface RSVPFormData { name: string; + email_address: string; // Required email field attending: string; bringing_guests: string; guest_count: number; guest_names: string[]; items_bringing: string[]; other_items: string; - send_email_confirmation: boolean; // New field for email opt-in - email_address: string; // New field for recipient email } const RSVPForm: React.FC = () => { const { slug } = useParams<{ slug: string }>(); const [formData, setFormData] = useState({ name: '', + email_address: '', // Required email field attending: '', bringing_guests: '', guest_count: 1, guest_names: [], items_bringing: [], - other_items: '', - send_email_confirmation: false, // Initialize to false - email_address: '' // Initialize to empty + other_items: '' }); const [neededItems, setNeededItems] = useState([]); const [claimedItems, setClaimedItems] = useState([]); @@ -209,9 +207,7 @@ const RSVPForm: React.FC = () => { guest_count: 0, guest_names: [], items_bringing: [], // Clear items when not attending - other_items: '', - send_email_confirmation: false, // Also reset email opt-in - email_address: '' // And email address + other_items: '' })); } else if (name === 'bringing_guests') { // When bringing guests is changed @@ -266,7 +262,7 @@ const RSVPForm: React.FC = () => { setError(null); // Validate required fields - if (!formData.name.trim() || !formData.attending) { + if (!formData.name.trim() || !formData.email_address.trim() || !formData.attending) { setError('Please fill in all required fields'); setIsSubmitting(false); return; @@ -296,7 +292,7 @@ const RSVPForm: React.FC = () => { ...formData, items_bringing: formData.items_bringing, other_items: splitOtherItems, - send_email_confirmation: formData.send_email_confirmation, + send_email_confirmation: true, // Always send email confirmation now email_address: formData.email_address.trim() }; const response = await axios.post(`/api/events/${slug}/rsvp`, submissionData); @@ -307,13 +303,8 @@ const RSVPForm: React.FC = () => { axios.get(`/api/events/${slug}/rsvps`) ]); - // Optionally display success message with edit link if email was sent - if (formData.send_email_confirmation && formData.email_address.trim()) { - // The backend sends the email, we just need to confirm success here - setSuccess(true); - } else { - setSuccess(true); // Still show success even if email wasn't sent - } + // Email confirmation is always sent now + setSuccess(true); // Process needed items let items: string[] = []; @@ -476,6 +467,18 @@ const RSVPForm: React.FC = () => { variant="outlined" /> + + Are you attending? { size="large" disabled={isSubmitting || !formData.name.trim() || + !formData.email_address.trim() || !formData.attending || (formData.attending === 'yes' && !formData.bringing_guests) || (formData.bringing_guests === 'yes' && (formData.guest_count < 1 || formData.guest_names.some(name => !name.trim())))} From ed7b7648a1e6473636044d5cd1a75a7ac3a9dc8c Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 16:51:27 -0400 Subject: [PATCH 26/38] Fix email field not populating in RSVP edit form - Changed email_address mapping from rsvpResponse.data.email_address to rsvpResponse.data.attendee_email - Database stores email as 'attendee_email' but frontend uses 'email_address' - This fixes the issue where email field would be blank after changing email address --- frontend/src/components/RSVPEditForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/RSVPEditForm.tsx b/frontend/src/components/RSVPEditForm.tsx index e3a0911..7a309b5 100644 --- a/frontend/src/components/RSVPEditForm.tsx +++ b/frontend/src/components/RSVPEditForm.tsx @@ -82,7 +82,7 @@ const RSVPEditForm: React.FC = () => { // Pre-fill the form with existing RSVP data setFormData({ name: rsvpResponse.data.name, - email_address: rsvpResponse.data.email_address || '', + email_address: rsvpResponse.data.attendee_email || '', attending: rsvpResponse.data.attending, bringing_guests: rsvpResponse.data.bringing_guests, guest_count: rsvpResponse.data.guest_count, From 7e4baa33884a2e00eab5641af881d5d21fb83228 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 17:15:52 -0400 Subject: [PATCH 27/38] Add email column and action buttons to EventAdmin RSVP table - Added email column to display attendee_email in the RSVP management table - Added email icon button to resend confirmation emails to attendees - Added clipboard icon button to copy RSVP edit links to clipboard - Updated RSVP interface to include attendee_email and edit_id fields - Added backend endpoint /api/rsvps/resend-email/:editId for resending emails - Email and copy buttons are disabled when email/edit_id are not available - Improved admin functionality for managing RSVPs and communicating with attendees --- backend/src/index.ts | 42 +++++++++++++++++ frontend/src/components/EventAdmin.tsx | 64 ++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/backend/src/index.ts b/backend/src/index.ts index d41d7dd..52e3aa5 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -522,6 +522,48 @@ app.get('/api/rsvps/edit/:editId', async (req: Request, res: Response) => { } }); +// Resend RSVP edit link email +app.post('/api/rsvps/resend-email/:editId', async (req: Request, res: Response) => { + try { + const { editId } = req.params; + + // Get RSVP and event details + const rsvp = await db.get(` + SELECT r.*, e.title, e.slug + FROM rsvps r + JOIN events e ON r.event_id = e.id + WHERE r.edit_id = ? + `, [editId]); + + if (!rsvp) { + return res.status(404).json({ error: 'RSVP not found' }); + } + + if (!rsvp.attendee_email) { + return res.status(400).json({ error: 'No email address associated with this RSVP' }); + } + + if (!process.env.EMAIL_USER) { + return res.status(500).json({ error: 'Email service not configured' }); + } + + // Send the edit link email + const editLink = `${process.env.FRONTEND_BASE_URL}/events/${rsvp.slug}/rsvp/edit/${editId}`; + await sendRSVPEditLinkEmail({ + eventTitle: rsvp.title, + eventSlug: rsvp.slug, + name: rsvp.name, + to: rsvp.attendee_email, + editLink, + }); + + res.json({ message: 'Email sent successfully' }); + } catch (error) { + console.error('Error resending RSVP edit link email:', error); + res.status(500).json({ error: 'Failed to send email' }); + } +}); + app.delete('/api/events/:slug/rsvps/:id', async (req: Request, res: Response) => { try { diff --git a/frontend/src/components/EventAdmin.tsx b/frontend/src/components/EventAdmin.tsx index 1ec8d9a..b5c152d 100644 --- a/frontend/src/components/EventAdmin.tsx +++ b/frontend/src/components/EventAdmin.tsx @@ -34,6 +34,8 @@ import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; import AddIcon from '@mui/icons-material/Add'; import WallpaperIcon from '@mui/icons-material/Wallpaper'; +import EmailIcon from '@mui/icons-material/Email'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import axios from 'axios'; interface RSVP { @@ -45,6 +47,8 @@ interface RSVP { guest_names: string[] | string; items_bringing: string[] | string; other_items?: string; + attendee_email?: string; + edit_id?: string; event_id?: number; created_at?: string; updated_at?: string; @@ -658,6 +662,48 @@ const EventAdmin: React.FC = () => { } }; + const handleSendEmail = async (rsvp: RSVP) => { + if (!rsvp.attendee_email || !rsvp.edit_id || !event) { + setError('Cannot send email: missing email address or edit ID'); + return; + } + + try { + // Create a backend endpoint to resend the edit link email + await axios.post(`/api/rsvps/resend-email/${rsvp.edit_id}`); + // Show success message (you could add a snackbar or toast here) + console.log(`Email sent to ${rsvp.attendee_email}`); + } catch (error) { + console.error('Error sending email:', error); + setError('Failed to send email'); + } + }; + + const handleCopyLink = async (rsvp: RSVP) => { + if (!rsvp.edit_id || !event) { + setError('Cannot copy link: missing edit ID'); + return; + } + + const editLink = `${window.location.origin}/events/${event.slug}/rsvp/edit/${rsvp.edit_id}`; + + try { + await navigator.clipboard.writeText(editLink); + // Show success message (you could add a snackbar or toast here) + console.log('Link copied to clipboard'); + } catch (error) { + console.error('Error copying to clipboard:', error); + // Fallback for older browsers + const textArea = document.createElement('textarea'); + textArea.value = editLink; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + console.log('Link copied to clipboard (fallback method)'); + } + }; + if (loading) { return ( @@ -854,6 +900,7 @@ const EventAdmin: React.FC = () => { Name + Email Attending Guests Needed Items @@ -865,6 +912,7 @@ const EventAdmin: React.FC = () => { {rsvps.map((rsvp: RSVP) => ( {rsvp.name || 'No name provided'} + {rsvp.attendee_email || 'No email provided'} {rsvp.attending ? rsvp.attending.charAt(0).toUpperCase() + rsvp.attending.slice(1) : @@ -917,9 +965,25 @@ const EventAdmin: React.FC = () => { handleDeleteRsvp(rsvp)} + sx={{ mr: 1 }} > + handleSendEmail(rsvp)} + sx={{ mr: 1 }} + disabled={!rsvp.attendee_email} + > + + + handleCopyLink(rsvp)} + disabled={!rsvp.edit_id} + > + + ))} From b6c548f25a3a62fe375c650960232dccdc2da6f5 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 17:29:44 -0400 Subject: [PATCH 28/38] Fix email field in EventAdmin RSVP edit form and add snackbar feedback - Added email_address field to EditFormData interface - Updated handleEditRsvp to populate email field from rsvp.attendee_email - Modified handleEditSubmit to include email_address in submission data - Added snackbar state variables for user feedback notifications - Enhanced handleSendEmail and handleCopyLink with success/error snackbar messages - Added handleSnackbarClose function for snackbar management - Improved user experience with immediate visual feedback for admin actions - Fixed issue where email field would show [object Object] instead of actual email text --- frontend/src/components/EventAdmin.tsx | 625 ++----------------------- 1 file changed, 32 insertions(+), 593 deletions(-) diff --git a/frontend/src/components/EventAdmin.tsx b/frontend/src/components/EventAdmin.tsx index b5c152d..d4fe464 100644 --- a/frontend/src/components/EventAdmin.tsx +++ b/frontend/src/components/EventAdmin.tsx @@ -29,6 +29,8 @@ import { ListItemText, Checkbox, Chip, + Snackbar, + Alert, } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; @@ -73,6 +75,7 @@ interface Event { interface EditFormData { name: string; + email_address: string; attending: 'yes' | 'no' | 'maybe'; bringing_guests: 'yes' | 'no'; guest_count: number; @@ -110,6 +113,7 @@ const EventAdmin: React.FC = () => { const [rsvpToEdit, setRsvpToEdit] = useState(null); const [editForm, setEditForm] = useState({ name: '', + email_address: '', attending: 'yes', bringing_guests: 'no', guest_count: 0, @@ -134,6 +138,9 @@ const EventAdmin: React.FC = () => { event_conclusion_email_enabled: false, event_conclusion_message: '' }); + const [snackbarOpen, setSnackbarOpen] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(''); + const [snackbarSeverity, setSnackbarSeverity] = useState<'success' | 'error'>('success'); useEffect(() => { fetchEventAndRsvps(); @@ -266,6 +273,7 @@ const EventAdmin: React.FC = () => { setRsvpToEdit(rsvp); setEditForm({ name: rsvp.name, + email_address: rsvp.attendee_email || '', attending: rsvp.attending || 'yes', bringing_guests: rsvp.bringing_guests || 'no', guest_count: typeof rsvp.guest_count === 'number' ? rsvp.guest_count : 0, @@ -379,6 +387,7 @@ const EventAdmin: React.FC = () => { // Prepare submission data in the exact format the backend expects const submissionData = { name: editForm.name, + email_address: editForm.email_address, attending: editForm.attending, bringing_guests: editForm.bringing_guests, guest_count: editForm.bringing_guests === 'yes' ? Math.max(1, parseInt(editForm.guest_count.toString(), 10)) : 0, @@ -417,6 +426,7 @@ const EventAdmin: React.FC = () => { const updatedRsvp: RSVP = { ...rsvpToEdit, ...submissionData, + attendee_email: editForm.email_address, guest_names: filteredGuestNames, items_bringing: editForm.items_bringing, other_items: splitOtherItems @@ -664,24 +674,30 @@ const EventAdmin: React.FC = () => { const handleSendEmail = async (rsvp: RSVP) => { if (!rsvp.attendee_email || !rsvp.edit_id || !event) { - setError('Cannot send email: missing email address or edit ID'); + setSnackbarMessage('Cannot send email: missing email address or edit ID'); + setSnackbarSeverity('error'); + setSnackbarOpen(true); return; } try { - // Create a backend endpoint to resend the edit link email await axios.post(`/api/rsvps/resend-email/${rsvp.edit_id}`); - // Show success message (you could add a snackbar or toast here) - console.log(`Email sent to ${rsvp.attendee_email}`); + setSnackbarMessage(`Email sent successfully to ${rsvp.attendee_email}`); + setSnackbarSeverity('success'); + setSnackbarOpen(true); } catch (error) { console.error('Error sending email:', error); - setError('Failed to send email'); + setSnackbarMessage('Failed to send email'); + setSnackbarSeverity('error'); + setSnackbarOpen(true); } }; const handleCopyLink = async (rsvp: RSVP) => { if (!rsvp.edit_id || !event) { - setError('Cannot copy link: missing edit ID'); + setSnackbarMessage('Cannot copy link: missing edit ID'); + setSnackbarSeverity('error'); + setSnackbarOpen(true); return; } @@ -689,8 +705,9 @@ const EventAdmin: React.FC = () => { try { await navigator.clipboard.writeText(editLink); - // Show success message (you could add a snackbar or toast here) - console.log('Link copied to clipboard'); + setSnackbarMessage('Link copied to clipboard successfully'); + setSnackbarSeverity('success'); + setSnackbarOpen(true); } catch (error) { console.error('Error copying to clipboard:', error); // Fallback for older browsers @@ -700,10 +717,16 @@ const EventAdmin: React.FC = () => { textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); - console.log('Link copied to clipboard (fallback method)'); + setSnackbarMessage('Link copied to clipboard successfully'); + setSnackbarSeverity('success'); + setSnackbarOpen(true); } }; + const handleSnackbarClose = () => { + setSnackbarOpen(false); + }; + if (loading) { return ( @@ -780,587 +803,3 @@ const EventAdmin: React.FC = () => { sx={{ minWidth: 'fit-content', whiteSpace: 'nowrap' - }} - > - Manage Items - - - - - - - - Info: {event.description || 'None'} - - - Location: {event.location} - - - Date: {new Date(event.date).toLocaleString()} - - {event.rsvp_cutoff_date && ( - - RSVP cut-off date: {new Date(event.rsvp_cutoff_date).toLocaleString()} - - )} - - - {/* Add items status section */} - - - Items Status - - - - - Still Needed: - - - {neededItems.map((item: string, index: number) => ( - - ))} - {neededItems.length === 0 && ( - - All items have been claimed - - )} - - - - - Claimed Items: - - - {claimedItems.map((item: string, index: number) => ( - - ))} - {claimedItems.length === 0 && ( - - No items have been claimed yet - - )} - - - {/* Other Items Section */} - - - Other Items: - - - {otherItems.length > 0 - ? otherItems.join(', ') - : 'No other items have been brought'} - - - - - - - RSVPs: {rsvps.length} | Total Guests: {rsvps.reduce((total, rsvp) => { - // Count the RSVP person as 1 if they're attending - const rsvpCount = rsvp.attending === 'yes' ? 1 : 0; - // Add their guests if they're bringing any - const guestCount = (rsvp.attending === 'yes' && rsvp.bringing_guests === 'yes') ? rsvp.guest_count : 0; - return total + rsvpCount + guestCount; - }, 0)} - - - - - - - Name - Email - Attending - Guests - Needed Items - Other Items - Actions - - - - {rsvps.map((rsvp: RSVP) => ( - - {rsvp.name || 'No name provided'} - {rsvp.attendee_email || 'No email provided'} - - {rsvp.attending ? - rsvp.attending.charAt(0).toUpperCase() + rsvp.attending.slice(1) : - 'Unknown' - } - - - {rsvp.bringing_guests === 'yes' ? - `${rsvp.guest_count || 0} (${Array.isArray(rsvp.guest_names) ? - rsvp.guest_names.join(', ') : - typeof rsvp.guest_names === 'string' ? - rsvp.guest_names.replace(/\s+/g, ', ') : - 'No names provided'})` : - 'No' - } - - - {Array.isArray(rsvp.items_bringing) ? - rsvp.items_bringing.map((item, index) => ( - - )) : - typeof rsvp.items_bringing === 'string' ? - JSON.parse(rsvp.items_bringing).map((item: string, index: number) => ( - - )) : - 'None' - } - - - {rsvp.other_items || 'None'} - - - handleEditRsvp(rsvp)} - sx={{ mr: 1 }} - > - - - handleDeleteRsvp(rsvp)} - sx={{ mr: 1 }} - > - - - handleSendEmail(rsvp)} - sx={{ mr: 1 }} - disabled={!rsvp.attendee_email} - > - - - handleCopyLink(rsvp)} - disabled={!rsvp.edit_id} - > - - - - - ))} - -
-
- - - setDeleteDialogOpen(false)} - > - Delete RSVP - - - Are you sure you want to delete {rsvpToDelete?.name}'s RSVP? - - - - - - - - - setEditDialogOpen(false)} - maxWidth="sm" - fullWidth - > - Edit RSVP - - - - - Attending - - - - Bringing Guests - - - {editForm.bringing_guests === 'yes' && ( - <> - - {/* Individual guest name fields */} - {Array.from({ length: editForm.guest_count }, (_, index) => ( - - ))} - - )} - - What items are you bringing? - - multiple - name="items_bringing" - value={editForm.items_bringing} - onChange={handleItemsChange} - input={} - renderValue={(selected) => ( - - {selected.map((value: string) => ( - - ))} - - )} - > - {Array.from(new Set([...neededItems, ...editForm.items_bringing])).map((item: string) => ( - - - - - ))} - - - setEditForm(prev => ({ ...prev, other_items: e.target.value }))} - fullWidth - multiline - rows={3} - /> - - - - - - - - - setDeleteEventDialogOpen(false)} - maxWidth="sm" - fullWidth - > - Delete Event - - - Are you sure you want to delete "{event.title}"? This action cannot be undone. - - - All RSVPs associated with this event will also be deleted. - - - - - - - - - setManageItemsDialogOpen(false)} - maxWidth="sm" - fullWidth - > - Manage Needed Items - - - - setNewItem(e.target.value)} - fullWidth - /> - - - - Current Items: - - - {event?.needed_items && Array.isArray(event.needed_items) && event.needed_items.map((item, index) => ( - handleRemoveItem(item)} - color={claimedItems.includes(item) ? "success" : "primary"} - /> - ))} - - - - - - - - - setUpdateInfoDialogOpen(false)} - maxWidth="sm" - fullWidth - > - Update Event Information - - - {/* Event Title Field */} - setUpdateForm(prev => ({ ...prev, title: e.target.value }))} - fullWidth - /> - setUpdateForm(prev => ({ ...prev, description: e.target.value }))} - fullWidth - multiline - rows={3} - /> - setUpdateForm(prev => ({ ...prev, location: e.target.value }))} - fullWidth - /> - setUpdateForm(prev => ({ ...prev, date: e.target.value }))} - fullWidth - InputLabelProps={{ - shrink: true, - }} - /> - setUpdateForm(prev => ({ ...prev, rsvp_cutoff_date: e.target.value }))} - fullWidth - InputLabelProps={{ - shrink: true, - }} - /> - - - Email Notifications - - setUpdateForm(prev => ({ - ...prev, - email_notifications_enabled: e.target.checked - }))} - /> - } - label="Enable Email Notifications" - /> - - {updateForm.email_notifications_enabled && ( - setUpdateForm(prev => ({ - ...prev, - email_recipients: e.target.value - }))} - variant="outlined" - placeholder="email1@example.com, email2@example.com" - helperText="Enter email addresses separated by commas" - sx={{ mt: 2 }} - /> - )} - - - {/* Event Conclusion Email Settings */} - - - Event Conclusion Email - - setUpdateForm(prev => ({ - ...prev, - event_conclusion_email_enabled: e.target.checked - }))} - /> - } - label="Enable Event Conclusion Email" - /> - - {updateForm.event_conclusion_email_enabled && ( - setUpdateForm(prev => ({ - ...prev, - event_conclusion_message: e.target.value - }))} - variant="outlined" - multiline - rows={4} - helperText="This message will be sent to attendees who opted for email notifications the day after the event." - sx={{ mt: 2 }} - /> - )} - - - - - Wallpaper - - {event.wallpaper && ( - - - Current wallpaper: - - - - )} - - {updateForm.wallpaper && ( - - Selected file: {updateForm.wallpaper.name} - - )} - - - - - - - - - - - - ); -}; - -export default EventAdmin; From a2f9c58f3f7925cb30ef232712ee9b2539b00324 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 17:37:48 -0400 Subject: [PATCH 29/38] Restore complete EventAdmin.tsx file with email field and snackbar functionality - Fixed missing hundreds of lines that were accidentally truncated - Restored complete component with all dialogs, tables, and functionality - Email field now properly populates in RSVP edit form from attendee_email - Added snackbar notifications for email sending and link copying actions - All admin functionality including item management, event updates, and RSVP editing is now complete --- frontend/src/components/EventAdmin.tsx | 601 +++++++++++++++++++++++++ 1 file changed, 601 insertions(+) diff --git a/frontend/src/components/EventAdmin.tsx b/frontend/src/components/EventAdmin.tsx index d4fe464..6d21820 100644 --- a/frontend/src/components/EventAdmin.tsx +++ b/frontend/src/components/EventAdmin.tsx @@ -803,3 +803,604 @@ const EventAdmin: React.FC = () => { sx={{ minWidth: 'fit-content', whiteSpace: 'nowrap' + }} + > + Manage Items + + + + + + + + Info: {event.description || 'None'} + + + Location: {event.location} + + + Date: {new Date(event.date).toLocaleString()} + + {event.rsvp_cutoff_date && ( + + RSVP cut-off date: {new Date(event.rsvp_cutoff_date).toLocaleString()} + + )} + + + {/* Add items status section */} + + + Items Status + + + + + Still Needed: + + + {neededItems.map((item: string, index: number) => ( + + ))} + {neededItems.length === 0 && ( + + All items have been claimed + + )} + + + + + Claimed Items: + + + {claimedItems.map((item: string, index: number) => ( + + ))} + {claimedItems.length === 0 && ( + + No items have been claimed yet + + )} + + + {/* Other Items Section */} + + + Other Items: + + + {otherItems.length > 0 + ? otherItems.join(', ') + : 'No other items have been brought'} + + + + + + + RSVPs: {rsvps.length} | Total Guests: {rsvps.reduce((total, rsvp) => { + // Count the RSVP person as 1 if they're attending + const rsvpCount = rsvp.attending === 'yes' ? 1 : 0; + // Add their guests if they're bringing any + const guestCount = (rsvp.attending === 'yes' && rsvp.bringing_guests === 'yes') ? rsvp.guest_count : 0; + return total + rsvpCount + guestCount; + }, 0)} + + + + + + + Name + Email + Attending + Guests + Needed Items + Other Items + Actions + + + + {rsvps.map((rsvp: RSVP) => ( + + {rsvp.name || 'No name provided'} + {rsvp.attendee_email || 'No email provided'} + + {rsvp.attending ? + rsvp.attending.charAt(0).toUpperCase() + rsvp.attending.slice(1) : + 'Unknown' + } + + + {rsvp.bringing_guests === 'yes' ? + `${rsvp.guest_count || 0} (${Array.isArray(rsvp.guest_names) ? + rsvp.guest_names.join(', ') : + typeof rsvp.guest_names === 'string' ? + rsvp.guest_names.replace(/\s+/g, ', ') : + 'No names provided'})` : + 'No' + } + + + {Array.isArray(rsvp.items_bringing) ? + rsvp.items_bringing.map((item, index) => ( + + )) : + typeof rsvp.items_bringing === 'string' ? + JSON.parse(rsvp.items_bringing).map((item: string, index: number) => ( + + )) : + 'None' + } + + + {rsvp.other_items || 'None'} + + + handleEditRsvp(rsvp)} + sx={{ mr: 1 }} + > + + + handleDeleteRsvp(rsvp)} + sx={{ mr: 1 }} + > + + + handleSendEmail(rsvp)} + sx={{ mr: 1 }} + disabled={!rsvp.attendee_email} + > + + + handleCopyLink(rsvp)} + disabled={!rsvp.edit_id} + > + + + + + ))} + +
+
+ + + setDeleteDialogOpen(false)} + > + Delete RSVP + + + Are you sure you want to delete {rsvpToDelete?.name}'s RSVP? + + + + + + + + + setEditDialogOpen(false)} + maxWidth="sm" + fullWidth + > + Edit RSVP + + + + + + Attending + + + + Bringing Guests + + + {editForm.bringing_guests === 'yes' && ( + <> + + {/* Individual guest name fields */} + {Array.from({ length: editForm.guest_count }, (_, index) => ( + + ))} + + )} + + What items are you bringing? + + multiple + name="items_bringing" + value={editForm.items_bringing} + onChange={handleItemsChange} + input={} + renderValue={(selected) => ( + + {selected.map((value: string) => ( + + ))} + + )} + > + {Array.from(new Set([...neededItems, ...editForm.items_bringing])).map((item: string) => ( + + + + + ))} + + + setEditForm(prev => ({ ...prev, other_items: e.target.value }))} + fullWidth + multiline + rows={3} + /> + + + + + + + + + setDeleteEventDialogOpen(false)} + maxWidth="sm" + fullWidth + > + Delete Event + + + Are you sure you want to delete "{event.title}"? This action cannot be undone. + + + All RSVPs associated with this event will also be deleted. + + + + + + + + + setManageItemsDialogOpen(false)} + maxWidth="sm" + fullWidth + > + Manage Needed Items + + + + setNewItem(e.target.value)} + fullWidth + /> + + + + Current Items: + + + {event?.needed_items && Array.isArray(event.needed_items) && event.needed_items.map((item, index) => ( + handleRemoveItem(item)} + color={claimedItems.includes(item) ? "success" : "primary"} + /> + ))} + + + + + + + + + setUpdateInfoDialogOpen(false)} + maxWidth="sm" + fullWidth + > + Update Event Information + + + {/* Event Title Field */} + setUpdateForm(prev => ({ ...prev, title: e.target.value }))} + fullWidth + /> + setUpdateForm(prev => ({ ...prev, description: e.target.value }))} + fullWidth + multiline + rows={3} + /> + setUpdateForm(prev => ({ ...prev, location: e.target.value }))} + fullWidth + /> + setUpdateForm(prev => ({ ...prev, date: e.target.value }))} + fullWidth + InputLabelProps={{ + shrink: true, + }} + /> + setUpdateForm(prev => ({ ...prev, rsvp_cutoff_date: e.target.value }))} + fullWidth + InputLabelProps={{ + shrink: true, + }} + /> + + + Email Notifications + + setUpdateForm(prev => ({ + ...prev, + email_notifications_enabled: e.target.checked + }))} + /> + } + label="Enable Email Notifications" + /> + + {updateForm.email_notifications_enabled && ( + setUpdateForm(prev => ({ + ...prev, + email_recipients: e.target.value + }))} + variant="outlined" + placeholder="email1@example.com, email2@example.com" + helperText="Enter email addresses separated by commas" + sx={{ mt: 2 }} + /> + )} + + + {/* Event Conclusion Email Settings */} + + + Event Conclusion Email + + setUpdateForm(prev => ({ + ...prev, + event_conclusion_email_enabled: e.target.checked + }))} + /> + } + label="Enable Event Conclusion Email" + /> + + {updateForm.event_conclusion_email_enabled && ( + setUpdateForm(prev => ({ + ...prev, + event_conclusion_message: e.target.value + }))} + variant="outlined" + multiline + rows={4} + helperText="This message will be sent to attendees who opted for email notifications the day after the event." + sx={{ mt: 2 }} + /> + )} + + + + + Wallpaper + + {event.wallpaper && ( + + + Current wallpaper: + + + + )} + + {updateForm.wallpaper && ( + + Selected file: {updateForm.wallpaper.name} + + )} + + + + + + + + + + + + {snackbarMessage} + + + + + + ); +}; + +export default EventAdmin; From 65d02964dc2fd495f357fdf70655dd066606069d Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 17:45:16 -0400 Subject: [PATCH 30/38] Fix email field not saving in EventAdmin RSVP edit form - Changed submission data field from 'email_address' to 'attendee_email' - Backend expects 'attendee_email' but frontend was sending 'email_address' - This fixes the issue where email updates wouldn't save to the database - Email field now properly updates when edited through the admin interface --- frontend/src/components/EventAdmin.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/EventAdmin.tsx b/frontend/src/components/EventAdmin.tsx index 6d21820..273a1fa 100644 --- a/frontend/src/components/EventAdmin.tsx +++ b/frontend/src/components/EventAdmin.tsx @@ -387,7 +387,7 @@ const EventAdmin: React.FC = () => { // Prepare submission data in the exact format the backend expects const submissionData = { name: editForm.name, - email_address: editForm.email_address, + attendee_email: editForm.email_address, attending: editForm.attending, bringing_guests: editForm.bringing_guests, guest_count: editForm.bringing_guests === 'yes' ? Math.max(1, parseInt(editForm.guest_count.toString(), 10)) : 0, From d7ed4d1e8564c2ce00604d92741b8e9ac4a6b400 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 18:10:53 -0400 Subject: [PATCH 31/38] Add ICS calendar file generation and 'Add to Calendar' button - Added generateICSContent() function to create RFC-compliant ICS calendar files - Added /api/events/:slug/calendar.ics endpoint to serve downloadable calendar files - Updated RSVP confirmation emails to include styled 'Add to Calendar!' button - ICS files include event title, description, location, date/time, and unique identifiers - Calendar files are automatically named with event slug for easy identification - Button links directly to ICS download endpoint for seamless calendar integration - Supports all major calendar applications (Outlook, Google Calendar, Apple Calendar, etc.) --- backend/src/email.ts | 77 ++++++++++++++++++++++++++++++++++++++++++++ backend/src/index.ts | 31 +++++++++++++++++- 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/backend/src/email.ts b/backend/src/email.ts index 914910e..3841087 100644 --- a/backend/src/email.ts +++ b/backend/src/email.ts @@ -1,5 +1,72 @@ import nodemailer from 'nodemailer'; +// Function to generate ICS calendar content +export function generateICSContent(eventData: { + title: string; + description: string; + location: string; + date: string; // ISO date string + slug: string; +}): string { + const { title, description, location, date, slug } = eventData; + + // Convert date to ICS format (YYYYMMDDTHHMMSSZ) + const eventDate = new Date(date); + const startDate = eventDate.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, ''); + + // Set end time to 2 hours after start time (default duration) + const endDate = new Date(eventDate.getTime() + 2 * 60 * 60 * 1000); + const endDateFormatted = endDate.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, ''); + + // Generate unique ID for the event + const uid = `${slug}-${Date.now()}@rsvp-manager`; + + // Current timestamp for DTSTAMP + const now = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, ''); + + // Clean description for ICS format (remove HTML, escape special chars) + const cleanDescription = description + .replace(/<[^>]*>/g, '') // Remove HTML tags + .replace(/\n/g, '\\n') // Escape newlines + .replace(/,/g, '\\,') // Escape commas + .replace(/;/g, '\\;') // Escape semicolons + .replace(/\\/g, '\\\\'); // Escape backslashes + + // Clean location for ICS format + const cleanLocation = location + .replace(/,/g, '\\,') + .replace(/;/g, '\\;') + .replace(/\\/g, '\\\\'); + + // Clean title for ICS format + const cleanTitle = title + .replace(/,/g, '\\,') + .replace(/;/g, '\\;') + .replace(/\\/g, '\\\\'); + + const icsContent = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//RSVP Manager//Event Calendar//EN', + 'CALSCALE:GREGORIAN', + 'METHOD:PUBLISH', + 'BEGIN:VEVENT', + `UID:${uid}`, + `DTSTAMP:${now}`, + `DTSTART:${startDate}`, + `DTEND:${endDateFormatted}`, + `SUMMARY:${cleanTitle}`, + `DESCRIPTION:${cleanDescription}`, + `LOCATION:${cleanLocation}`, + 'STATUS:CONFIRMED', + 'TRANSP:OPAQUE', + 'END:VEVENT', + 'END:VCALENDAR' + ].join('\r\n'); + + return icsContent; +} + const transporter = nodemailer.createTransport({ host: process.env.EMAIL_HOST, port: Number(process.env.EMAIL_PORT), @@ -98,11 +165,21 @@ export async function sendRSVPEditLinkEmail(data: RSVPEditLinkEmailData) { const subject = `Confirming your RSVP for ${eventTitle}`; // Update the subject line + // Generate calendar download link + const baseUrl = process.env.FRONTEND_BASE_URL || ''; + const calendarLink = `${baseUrl}/api/events/${eventSlug}/calendar.ics`; + const html = `

Hello ${name},

You have successfully RSVP'd for the event "${eventTitle}".

You can edit your RSVP at any time by clicking the link below:

${editLink}

+

+ + 📅 Add to Calendar! + +

Please save this email if you think you might need to edit your submission later.

Thank you!

`; diff --git a/backend/src/index.ts b/backend/src/index.ts index 52e3aa5..c0ba4d0 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -6,7 +6,7 @@ import dotenv from 'dotenv'; import path from 'path'; import multer from 'multer'; import fs from 'fs'; -import { sendRSVPEmail, sendRSVPEditLinkEmail, sendEventConclusionEmail } from './email'; // Import the new email function +import { sendRSVPEmail, sendRSVPEditLinkEmail, sendEventConclusionEmail, generateICSContent } from './email'; // Import the new email function and ICS generator import cron from 'node-cron'; // Import node-cron for scheduling dotenv.config(); @@ -1049,6 +1049,35 @@ const scheduledTask = cron.schedule('0 8 * * *', () => { console.log(`Event conclusion email scheduled task scheduled for timezone ${process.env.TZ || 'UTC'} at 8:00 AM.`); +// ICS Calendar file endpoint +app.get('/api/events/:slug/calendar.ics', async (req: Request, res: Response) => { + try { + const { slug } = req.params; + const event = await db.get('SELECT * FROM events WHERE slug = ?', [slug]); + + if (!event) { + return res.status(404).json({ error: 'Event not found' }); + } + + // Generate ICS content + const icsContent = generateICSContent({ + title: event.title, + description: event.description || '', + location: event.location || '', + date: event.date, + slug: event.slug + }); + + // Set appropriate headers for ICS file download + res.setHeader('Content-Type', 'text/calendar; charset=utf-8'); + res.setHeader('Content-Disposition', `attachment; filename="${event.slug}-calendar.ics"`); + res.send(icsContent); + } catch (error) { + console.error('Error generating ICS file:', error); + res.status(500).json({ error: 'Failed to generate calendar file' }); + } +}); + // Handle client-side routing app.get('*', (req: Request, res: Response) => { res.sendFile(path.join(__dirname, '../frontend/build/index.html')); From 2ee244a354a6f5b6103267832f86348e1a7ff60a Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 18:19:52 -0400 Subject: [PATCH 32/38] Improve ICS calendar generation and email button styling - Changed default event duration from 2 hours to 4 hours - Fixed location field escaping issue that was adding unwanted backslashes - Updated 'Add to Calendar' button styling to remove green background - Button now uses blue border and text instead of solid green background - Location field in ICS files now displays properly without escape characters --- backend/src/email.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/src/email.ts b/backend/src/email.ts index 3841087..a48d977 100644 --- a/backend/src/email.ts +++ b/backend/src/email.ts @@ -14,8 +14,8 @@ export function generateICSContent(eventData: { const eventDate = new Date(date); const startDate = eventDate.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, ''); - // Set end time to 2 hours after start time (default duration) - const endDate = new Date(eventDate.getTime() + 2 * 60 * 60 * 1000); + // Set end time to 4 hours after start time (default duration) + const endDate = new Date(eventDate.getTime() + 4 * 60 * 60 * 1000); const endDateFormatted = endDate.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, ''); // Generate unique ID for the event @@ -35,8 +35,7 @@ export function generateICSContent(eventData: { // Clean location for ICS format const cleanLocation = location .replace(/,/g, '\\,') - .replace(/;/g, '\\;') - .replace(/\\/g, '\\\\'); + .replace(/;/g, '\\;'); // Clean title for ICS format const cleanTitle = title @@ -176,7 +175,7 @@ export async function sendRSVPEditLinkEmail(data: RSVPEditLinkEmailData) {

${editLink}

+ style="color: #0066cc; padding: 12px 24px; text-decoration: none; border: 2px solid #0066cc; border-radius: 4px; display: inline-block; font-weight: bold;"> 📅 Add to Calendar!

From a9025f87789d51ceec59174d41779f9f0c3696a2 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 18:24:59 -0400 Subject: [PATCH 33/38] Add RSVP cutoff note to ICS calendar files - Updated generateICSContent function to accept optional rsvp_cutoff_date parameter - Added formatted RSVP cutoff note to calendar event descriptions with bold 'Note:' text - Backend now passes rsvp_cutoff_date to ICS generation when creating calendar files - Calendar events now include helpful reminder about RSVP deadlines for attendees - Note displays formatted cutoff date with full weekday, date, time and timezone info --- backend/src/email.ts | 24 ++++++++++++++++++++++-- backend/src/index.ts | 3 ++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/backend/src/email.ts b/backend/src/email.ts index a48d977..1e82c1f 100644 --- a/backend/src/email.ts +++ b/backend/src/email.ts @@ -7,8 +7,9 @@ export function generateICSContent(eventData: { location: string; date: string; // ISO date string slug: string; + rsvp_cutoff_date?: string; // Optional RSVP cutoff date }): string { - const { title, description, location, date, slug } = eventData; + const { title, description, location, date, slug, rsvp_cutoff_date } = eventData; // Convert date to ICS format (YYYYMMDDTHHMMSSZ) const eventDate = new Date(date); @@ -24,8 +25,27 @@ export function generateICSContent(eventData: { // Current timestamp for DTSTAMP const now = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, ''); + // Build description with RSVP cutoff note + let fullDescription = description || ''; + + if (rsvp_cutoff_date) { + const cutoffDate = new Date(rsvp_cutoff_date); + const formattedCutoff = cutoffDate.toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + timeZoneName: 'short' + }); + + const rsvpNote = `\\n\\n**Note:** The RSVP cut-off for this event is ${formattedCutoff}. Make sure you get your reservation in before then!`; + fullDescription += rsvpNote; + } + // Clean description for ICS format (remove HTML, escape special chars) - const cleanDescription = description + const cleanDescription = fullDescription .replace(/<[^>]*>/g, '') // Remove HTML tags .replace(/\n/g, '\\n') // Escape newlines .replace(/,/g, '\\,') // Escape commas diff --git a/backend/src/index.ts b/backend/src/index.ts index c0ba4d0..19ed3f7 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1065,7 +1065,8 @@ app.get('/api/events/:slug/calendar.ics', async (req: Request, res: Response) => description: event.description || '', location: event.location || '', date: event.date, - slug: event.slug + slug: event.slug, + rsvp_cutoff_date: event.rsvp_cutoff_date }); // Set appropriate headers for ICS file download From 5cf181cabd09cbd7c7f31de0a4a8f8101073e84c Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 26 May 2025 18:31:32 -0400 Subject: [PATCH 34/38] Fix ICS calendar description formatting issues - Removed markdown formatting (**Note:**) as it doesn't work in ICS files - Fixed double-escaping issue by removing backslash escaping from description - Reordered escape sequence to prevent double escaping of newlines - RSVP cutoff note now displays properly without unwanted backslashes - Calendar descriptions now show clean, readable text with proper line breaks --- backend/src/email.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/email.ts b/backend/src/email.ts index 1e82c1f..ec8a8c0 100644 --- a/backend/src/email.ts +++ b/backend/src/email.ts @@ -40,17 +40,16 @@ export function generateICSContent(eventData: { timeZoneName: 'short' }); - const rsvpNote = `\\n\\n**Note:** The RSVP cut-off for this event is ${formattedCutoff}. Make sure you get your reservation in before then!`; + const rsvpNote = `\n\nNote: The RSVP cut-off for this event is ${formattedCutoff}. Make sure you get your reservation in before then!`; fullDescription += rsvpNote; } // Clean description for ICS format (remove HTML, escape special chars) const cleanDescription = fullDescription .replace(/<[^>]*>/g, '') // Remove HTML tags - .replace(/\n/g, '\\n') // Escape newlines .replace(/,/g, '\\,') // Escape commas .replace(/;/g, '\\;') // Escape semicolons - .replace(/\\/g, '\\\\'); // Escape backslashes + .replace(/\n/g, '\\n'); // Escape newlines (do this last to avoid double escaping) // Clean location for ICS format const cleanLocation = location From eac93f120ca148410db067c45b34e9ef610b876b Mon Sep 17 00:00:00 2001 From: Joshua Ryder Date: Mon, 26 May 2025 18:53:09 -0400 Subject: [PATCH 35/38] Update README.md Added in new features and updates --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6d2101a..cd17f7b 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ This project was created completely by the [Cursor AI Code Editor](https://www.c - Comprehensive admin interface for event management - Email notifications for submitted RSVPs - Individual submission links so users can edit their submissions + - ICS calendar event so users can add your event to their calendar + - Customizable thank you/event conclusion message that can be automatically sent to your guests at 8 AM the following day + - This time is based on your local time zone that you specify with the TZ environment variable - Item Coordination - Create and manage lists of needed items for events @@ -25,8 +28,10 @@ This project was created completely by the [Cursor AI Code Editor](https://www.c - Guest Management - Track attendance status (yes/no) - Support for bringing additional guests - - Keep track of guest names + - Keep track of guest names and email addresses - View all RSVPs and items being brought + - Edit any part of a user's submission + - Re-send confirmation emails or just copy the unique submission link to send to the user in a message - Modern, Responsive UI - Clean, intuitive interface @@ -67,7 +72,7 @@ There are 2 branches, latest & dev. #### Environment Variables -These variables below are all for the email notifications. If you want to be able to send email notifications, each of these needs to be provided and filled out. +These variables below are all for the email notifications. If you want to be able to send email notifications correctly, each of these needs to be provided and filled out. | Variable | Description | | ------------- | ------------- | @@ -78,6 +83,7 @@ These variables below are all for the email notifications. If you want to be abl | EMAIL_FROM_NAME | Name displayed in the "from" on email notifications | | EMAIL_FROM_ADDRESS | Email displayed in the "from" on email notifications | | FRONTEND_BASE_URL | The main URL for your instance. This will be used in the links that are sent in the email notificiations, eg. https://rsvp.example.com | +| TZ | Your [time zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) to use for scheduling the event conclusion/thank you messages | #### Docker Compose @@ -105,6 +111,7 @@ docker run -d --name rsvp-manager \ -e EMAIL_FROM_ADDRESS=name@example.com \ -e EMAIL_SECURE=true or false \ -e FRONTEND_BASE_URL=https://rsvp.example.com \ + -e TZ= --restart unless-stopped \ ryderjj89/rsvp-manager: ``` From f1bc53c9075e058a55d6824129e214380305d7c4 Mon Sep 17 00:00:00 2001 From: Joshua Ryder Date: Mon, 26 May 2025 18:55:17 -0400 Subject: [PATCH 36/38] Update README.md Updated email notification info --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cd17f7b..b5bd03a 100644 --- a/README.md +++ b/README.md @@ -145,9 +145,9 @@ docker run -d --name rsvp-manager \ ## Email Notifications -By setting up the environment variables in the `docker-compose.yml`, you can have notifications sent to the recipients of your choice when someone submits an RSVP to an event. The notification will include the details of their submission and links to view or manage the RSVPs for that event. +By setting up the environment variables in the `docker-compose.yml`, you can have notifications sent to the recipients of your choice when someone submits an RSVP to an event. The notification will include the details of their submission and links to view or manage the RSVPs for that event. -Users will also have the option to get an email confirmation of their submission that will include a unique link to view/edit their submission! +Email notifications will also be sent to users when they complete their submission that will serve as a confirmation of their RSVP. It will include a link to view/edit their own submission. It will also include a link to a downloadable ICS file that users can use to add the event to their calendar! ## Authentication with Authentik From a6709f02c5ddb92dd153c755420870d4004162ef Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Mon, 2 Jun 2025 19:25:18 -0400 Subject: [PATCH 37/38] Updated send event conclusion to make sure it's set to true by default --- frontend/src/components/RSVPForm.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/RSVPForm.tsx b/frontend/src/components/RSVPForm.tsx index 8d134f1..b8a8414 100644 --- a/frontend/src/components/RSVPForm.tsx +++ b/frontend/src/components/RSVPForm.tsx @@ -293,7 +293,8 @@ const RSVPForm: React.FC = () => { items_bringing: formData.items_bringing, other_items: splitOtherItems, send_email_confirmation: true, // Always send email confirmation now - email_address: formData.email_address.trim() + email_address: formData.email_address.trim(), + send_event_conclusion_email: true, // Always send true for conclusion email }; const response = await axios.post(`/api/events/${slug}/rsvp`, submissionData); From 62ea29611d0b26eb71c9aaf7922259fa385e4440 Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Tue, 3 Jun 2025 17:12:44 -0400 Subject: [PATCH 38/38] Updated conclusion email message to remove anything extra --- backend/src/email.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/email.ts b/backend/src/email.ts index ec8a8c0..0e0d5d5 100644 --- a/backend/src/email.ts +++ b/backend/src/email.ts @@ -228,12 +228,11 @@ export async function sendEventConclusionEmail(data: EventConclusionEmailData) { to, } = data; - const subject = `Thank You for Attending ${eventTitle}!`; // Subject for the conclusion email + const subject = `Thank you for attending ${eventTitle}!`; // Subject for the conclusion email const html = `

Hello ${attendeeName},

${message}

-

Thank you for attending!

`; await transporter.sendMail({