Add email notification settings to event creation form
This commit is contained in:
@@ -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
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user