Add email notification settings to event creation form

This commit is contained in:
Ryderjj89
2025-05-16 18:42:55 -04:00
parent efe143ca68
commit cedd7b325f
3 changed files with 141 additions and 38 deletions

View File

@@ -152,7 +152,17 @@ app.get('/api/events/:slug', async (req: Request, res: Response) => {
app.post('/api/events', upload.single('wallpaper'), async (req: MulterRequest, res: Response) => { app.post('/api/events', upload.single('wallpaper'), async (req: MulterRequest, res: Response) => {
try { try {
const { title, description, date, location, needed_items, rsvp_cutoff_date, max_guests_per_rsvp } = req.body; const {
title,
description,
date,
location,
needed_items,
rsvp_cutoff_date,
max_guests_per_rsvp,
email_notifications_enabled,
email_recipients
} = req.body;
const wallpaperPath = req.file ? `${req.file.filename}` : null; const wallpaperPath = req.file ? `${req.file.filename}` : null;
// Generate a slug from the title // Generate a slug from the title
@@ -173,9 +183,12 @@ app.post('/api/events', upload.single('wallpaper'), async (req: MulterRequest, r
// Parse max_guests_per_rsvp to ensure it's a number // Parse max_guests_per_rsvp to ensure it's a number
const maxGuests = parseInt(max_guests_per_rsvp as string) || 0; const maxGuests = parseInt(max_guests_per_rsvp as string) || 0;
// Parse email_notifications_enabled to ensure it's a boolean
const emailNotificationsEnabled = email_notifications_enabled === 'true' || email_notifications_enabled === true;
const result = await db.run( const result = await db.run(
'INSERT INTO events (title, description, date, location, slug, needed_items, wallpaper, rsvp_cutoff_date, max_guests_per_rsvp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', 'INSERT INTO events (title, description, date, location, slug, needed_items, wallpaper, rsvp_cutoff_date, max_guests_per_rsvp, email_notifications_enabled, email_recipients) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[title, description, date, location, slug, JSON.stringify(parsedNeededItems), wallpaperPath, rsvp_cutoff_date, maxGuests] [title, description, date, location, slug, JSON.stringify(parsedNeededItems), wallpaperPath, rsvp_cutoff_date, maxGuests, emailNotificationsEnabled ? 1 : 0, email_recipients || '']
); );
res.status(201).json({ res.status(201).json({
@@ -184,7 +197,9 @@ app.post('/api/events', upload.single('wallpaper'), async (req: MulterRequest, r
wallpaper: wallpaperPath ? `/uploads/wallpapers/${wallpaperPath}` : null, wallpaper: wallpaperPath ? `/uploads/wallpapers/${wallpaperPath}` : null,
needed_items: parsedNeededItems, needed_items: parsedNeededItems,
rsvp_cutoff_date, rsvp_cutoff_date,
max_guests_per_rsvp: maxGuests max_guests_per_rsvp: maxGuests,
email_notifications_enabled: emailNotificationsEnabled,
email_recipients: email_recipients || ''
}); });
} catch (error) { } catch (error) {
console.error('Error creating event:', error); console.error('Error creating event:', error);
@@ -262,13 +277,19 @@ app.post('/api/events/:slug/rsvp', async (req: Request, res: Response) => {
const { slug } = req.params; const { slug } = req.params;
const { name, attending, bringing_guests, guest_count, guest_names, items_bringing, other_items } = req.body; const { name, attending, bringing_guests, guest_count, guest_names, items_bringing, other_items } = req.body;
const eventRows = await db.all('SELECT id FROM events WHERE slug = ?', [slug]); // Get the event with email notification settings
const eventRows = await db.all('SELECT id, title, slug, email_notifications_enabled, email_recipients FROM events WHERE slug = ?', [slug]);
if (eventRows.length === 0) { if (eventRows.length === 0) {
return res.status(404).json({ error: 'Event not found' }); return res.status(404).json({ error: 'Event not found' });
} }
const eventId = eventRows[0].id; const event = eventRows[0];
const eventId = event.id;
const eventTitle = event.title;
const eventSlug = event.slug;
const emailNotificationsEnabled = event.email_notifications_enabled;
const eventEmailRecipients = event.email_recipients;
// Parse items_bringing if it's a string // Parse items_bringing if it's a string
let parsedItemsBringing: string[] = []; let parsedItemsBringing: string[] = [];
@@ -299,39 +320,49 @@ app.post('/api/events/:slug/rsvp', async (req: Request, res: Response) => {
[eventId, name, attending, bringing_guests, guest_count, JSON.stringify(parsedGuestNames), JSON.stringify(parsedItemsBringing), other_items || ''] [eventId, name, attending, bringing_guests, guest_count, JSON.stringify(parsedGuestNames), JSON.stringify(parsedItemsBringing), other_items || '']
); );
// Fetch event title and slug for the email // Send email notifications if enabled for this event
const eventInfo = await db.get('SELECT title, slug FROM events WHERE id = ?', [eventId]); if (emailNotificationsEnabled) {
const eventTitle = eventInfo ? eventInfo.title : slug; // Get recipients from event settings
const eventSlug = eventInfo ? eventInfo.slug : slug; let recipients: string[] = [];
// Optionally send RSVP confirmation email to recipients // First try to use the event's email recipients
let recipients: string[] = []; if (eventEmailRecipients) {
if (process.env.EMAIL_RECIPIENTS) { recipients = eventEmailRecipients.split(',').map(addr => addr.trim()).filter(Boolean);
recipients = process.env.EMAIL_RECIPIENTS.split(',').map(addr => addr.trim()).filter(Boolean); }
} else if (process.env.EMAIL_USER) {
recipients = [process.env.EMAIL_USER]; // If no event recipients, fall back to environment variables
} if (recipients.length === 0) {
if (recipients.length > 0) { if (process.env.EMAIL_RECIPIENTS) {
try { recipients = process.env.EMAIL_RECIPIENTS.split(',').map(addr => addr.trim()).filter(Boolean);
for (const to of recipients) { } else if (process.env.EMAIL_USER) {
await sendRSVPEmail({ recipients = [process.env.EMAIL_USER];
eventTitle,
eventSlug,
name,
attending,
bringingGuests: bringing_guests,
guestCount: guest_count,
guestNames: parsedGuestNames,
itemsBringing: parsedItemsBringing,
otherItems: other_items || '',
to,
});
} }
} catch (emailErr) { }
console.error('Error sending RSVP email:', emailErr);
if (recipients.length > 0) {
try {
for (const to of recipients) {
await sendRSVPEmail({
eventTitle,
eventSlug,
name,
attending,
bringingGuests: bringing_guests,
guestCount: guest_count,
guestNames: parsedGuestNames,
itemsBringing: parsedItemsBringing,
otherItems: other_items || '',
to,
});
}
} catch (emailErr) {
console.error('Error sending RSVP email:', emailErr);
}
} else {
console.warn('No email recipients set. Skipping RSVP email notification.');
} }
} else { } else {
console.warn('No email recipients set. Skipping RSVP email notification.'); console.log('Email notifications disabled for this event. Skipping RSVP email notification.');
} }
// Return the complete RSVP data including the parsed arrays // Return the complete RSVP data including the parsed arrays
@@ -452,7 +483,17 @@ 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) => { app.put('/api/events/:slug', upload.single('wallpaper'), async (req: MulterRequest, res: Response) => {
try { try {
const { slug } = req.params; const { slug } = req.params;
const { title, description, date, location, needed_items, rsvp_cutoff_date, max_guests_per_rsvp } = req.body; const {
title,
description,
date,
location,
needed_items,
rsvp_cutoff_date,
max_guests_per_rsvp,
email_notifications_enabled,
email_recipients
} = req.body;
// Verify the event exists // Verify the event exists
const eventRows = await db.all('SELECT * FROM events WHERE slug = ?', [slug]); const eventRows = await db.all('SELECT * FROM events WHERE slug = ?', [slug]);
@@ -477,6 +518,16 @@ app.put('/api/events/:slug', upload.single('wallpaper'), async (req: MulterReque
const maxGuests = max_guests_per_rsvp !== undefined ? const maxGuests = max_guests_per_rsvp !== undefined ?
(parseInt(max_guests_per_rsvp as string) || 0) : (parseInt(max_guests_per_rsvp as string) || 0) :
eventRows[0].max_guests_per_rsvp || 0; eventRows[0].max_guests_per_rsvp || 0;
// Parse email_notifications_enabled to ensure it's a boolean
const emailNotificationsEnabled = email_notifications_enabled !== undefined ?
(email_notifications_enabled === 'true' || email_notifications_enabled === true) :
eventRows[0].email_notifications_enabled;
// Get email recipients
const emailRecipients = email_recipients !== undefined ?
email_recipients :
eventRows[0].email_recipients || '';
// Handle wallpaper update // Handle wallpaper update
let wallpaperPath = eventRows[0].wallpaper; let wallpaperPath = eventRows[0].wallpaper;
@@ -495,7 +546,7 @@ app.put('/api/events/:slug', upload.single('wallpaper'), async (req: MulterReque
// Update the event // Update the event
await db.run( await db.run(
'UPDATE events SET title = ?, description = ?, date = ?, location = ?, needed_items = ?, rsvp_cutoff_date = ?, wallpaper = ?, max_guests_per_rsvp = ? WHERE slug = ?', 'UPDATE events SET title = ?, description = ?, date = ?, location = ?, needed_items = ?, rsvp_cutoff_date = ?, wallpaper = ?, max_guests_per_rsvp = ?, email_notifications_enabled = ?, email_recipients = ? WHERE slug = ?',
[ [
title ?? eventRows[0].title, title ?? eventRows[0].title,
description === undefined ? eventRows[0].description : description, description === undefined ? eventRows[0].description : description,
@@ -505,6 +556,8 @@ app.put('/api/events/:slug', upload.single('wallpaper'), async (req: MulterReque
rsvp_cutoff_date !== undefined ? rsvp_cutoff_date : eventRows[0].rsvp_cutoff_date, rsvp_cutoff_date !== undefined ? rsvp_cutoff_date : eventRows[0].rsvp_cutoff_date,
wallpaperPath, wallpaperPath,
maxGuests, maxGuests,
emailNotificationsEnabled ? 1 : 0,
emailRecipients,
slug slug
] ]
); );
@@ -554,6 +607,8 @@ async function initializeDatabase() {
wallpaper TEXT, wallpaper TEXT,
rsvp_cutoff_date TEXT, rsvp_cutoff_date TEXT,
max_guests_per_rsvp INTEGER DEFAULT 0, max_guests_per_rsvp INTEGER DEFAULT 0,
email_notifications_enabled BOOLEAN DEFAULT 0,
email_recipients TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) )
`); `);

View File

@@ -10,6 +10,8 @@ import {
Chip, Chip,
IconButton, IconButton,
styled, styled,
Checkbox,
FormControlLabel,
} from '@mui/material'; } from '@mui/material';
import WallpaperIcon from '@mui/icons-material/Wallpaper'; import WallpaperIcon from '@mui/icons-material/Wallpaper';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
@@ -44,6 +46,8 @@ interface FormData {
needed_items: string[]; needed_items: string[];
rsvp_cutoff_date: string; rsvp_cutoff_date: string;
max_guests_per_rsvp: number; max_guests_per_rsvp: number;
email_notifications_enabled: boolean;
email_recipients: string;
} }
const EventForm: React.FC = () => { const EventForm: React.FC = () => {
@@ -56,6 +60,8 @@ const EventForm: React.FC = () => {
needed_items: [], needed_items: [],
rsvp_cutoff_date: '', rsvp_cutoff_date: '',
max_guests_per_rsvp: 0, max_guests_per_rsvp: 0,
email_notifications_enabled: false,
email_recipients: '',
}); });
const [wallpaper, setWallpaper] = useState<File | null>(null); const [wallpaper, setWallpaper] = useState<File | null>(null);
const [currentItem, setCurrentItem] = useState(''); const [currentItem, setCurrentItem] = useState('');
@@ -218,6 +224,46 @@ const EventForm: React.FC = () => {
helperText="Set to 0 for no additional guests, -1 for unlimited" helperText="Set to 0 for no additional guests, -1 for unlimited"
inputProps={{ min: -1 }} inputProps={{ min: -1 }}
/> />
<Box sx={{ display: 'flex', alignItems: 'center', mt: 2 }}>
<FormControlLabel
control={
<Checkbox
checked={formData.email_notifications_enabled}
onChange={(e) => {
setFormData((prev) => ({
...prev,
email_notifications_enabled: e.target.checked,
}));
}}
sx={{
color: 'rgba(255, 255, 255, 0.7)',
'&.Mui-checked': {
color: '#90caf9',
},
}}
/>
}
label="Enable Email Notifications"
sx={{
color: 'rgba(255, 255, 255, 0.9)',
}}
/>
</Box>
{formData.email_notifications_enabled && (
<DarkTextField
fullWidth
label="Email Recipients (comma separated)"
name="email_recipients"
value={formData.email_recipients}
onChange={handleChange}
variant="outlined"
placeholder="email1@example.com, email2@example.com"
helperText="Enter email addresses separated by commas"
sx={{ mt: 2 }}
/>
)}
<DarkTextField <DarkTextField
fullWidth fullWidth
label="Location" label="Location"

View File

@@ -10,6 +10,8 @@ export interface Event {
wallpaper?: string; wallpaper?: string;
rsvp_cutoff_date?: string; rsvp_cutoff_date?: string;
max_guests_per_rsvp?: number; max_guests_per_rsvp?: number;
email_notifications_enabled?: boolean;
email_recipients?: string;
} }
export interface Rsvp { export interface Rsvp {