Add unique edit ID and email opt-in to RSVP submission

This commit is contained in:
Ryderjj89
2025-05-16 20:14:00 -04:00
parent 1766da9c25
commit 5c34d464c1
3 changed files with 172 additions and 14 deletions

View File

@@ -77,4 +77,44 @@ export async function sendRSVPEmail(data: RSVPEmailData) {
subject, subject,
html, html,
}); });
} }
export interface RSVPEditLinkEmailData {
eventTitle: string;
eventSlug: string;
name: string;
to: string;
editLink: string;
}
export async function sendRSVPEditLinkEmail(data: RSVPEditLinkEmailData) {
const {
eventTitle,
eventSlug,
name,
to,
editLink,
} = data;
const subject = `Edit Your RSVP for ${eventTitle}`;
const html = `
<h2>Edit Your RSVP</h2>
<p>Hello ${name},</p>
<p>You have successfully RSVP'd for the event "${eventTitle}".</p>
<p>You can edit your RSVP at any time by clicking the link below:</p>
<p><a href="${editLink}">${editLink}</a></p>
<p>Please save this email if you think you might need to edit your submission later.</p>
<p>Thank you!</p>
`;
await transporter.sendMail({
from: {
name: process.env.EMAIL_FROM_NAME || '',
address: process.env.EMAIL_FROM_ADDRESS || process.env.EMAIL_USER || '',
},
to,
subject,
html,
});
}

View File

@@ -6,10 +6,21 @@ import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
import multer from 'multer'; import multer from 'multer';
import fs from 'fs'; import fs from 'fs';
import { sendRSVPEmail } from './email'; import { sendRSVPEmail, sendRSVPEditLinkEmail } from './email'; // Import the new email function
dotenv.config(); dotenv.config();
// Function to generate a random alphanumeric string
function generateAlphanumericId(length: number): string {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
const app = express(); const app = express();
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
@@ -275,7 +286,17 @@ app.get('/api/events/:slug/rsvps', async (req: Request, res: Response) => {
app.post('/api/events/:slug/rsvp', async (req: Request, res: Response) => { app.post('/api/events/:slug/rsvp', async (req: Request, res: Response) => {
try { try {
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,
send_email_confirmation, // New field for email opt-in
email_address // New field for recipient email
} = req.body;
// Get the event with email notification settings // 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]); const eventRows = await db.all('SELECT id, title, slug, email_notifications_enabled, email_recipients FROM events WHERE slug = ?', [slug]);
@@ -315,12 +336,23 @@ app.post('/api/events/:slug/rsvp', async (req: Request, res: Response) => {
console.error('Error parsing guest_names:', e); console.error('Error parsing guest_names:', e);
} }
// Generate a unique edit ID
let editId = '';
let isUnique = false;
while (!isUnique) {
editId = generateAlphanumericId(16);
const existingRsvp = await db.get('SELECT id FROM rsvps WHERE edit_id = ?', [editId]);
if (!existingRsvp) {
isUnique = true;
}
}
const result = await db.run( const result = await db.run(
'INSERT INTO rsvps (event_id, name, attending, bringing_guests, guest_count, guest_names, items_bringing, other_items) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', 'INSERT INTO rsvps (event_id, name, attending, bringing_guests, guest_count, guest_names, items_bringing, other_items, edit_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
[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 || '', editId]
); );
// Send email notifications if enabled for this event // Send email notifications to event recipients if enabled for this event
if (emailNotificationsEnabled) { if (emailNotificationsEnabled) {
// Get recipients from event settings // Get recipients from event settings
let recipients: string[] = []; let recipients: string[] = [];
@@ -352,16 +384,43 @@ app.post('/api/events/:slug/rsvp', async (req: Request, res: Response) => {
}); });
} }
} catch (emailErr) { } catch (emailErr) {
console.error('Error sending RSVP email:', emailErr); console.error('Error sending RSVP email to event recipients:', emailErr);
} }
} else { } else {
console.warn('No email recipients set. Skipping RSVP email notification.'); console.warn('No event email recipients set. Skipping RSVP email notification to event recipients.');
} }
} else { } else {
console.log('Email notifications disabled for this event. Skipping RSVP email notification.'); console.log('Email notifications disabled for this event. Skipping RSVP email notification to event recipients.');
} }
// Return the complete RSVP data including the parsed arrays // Send email confirmation with edit link to the submitter if requested
const sendEmailConfirmationBool = send_email_confirmation === 'true' || send_email_confirmation === true;
const submitterEmail = email_address?.trim();
if (sendEmailConfirmationBool && submitterEmail && process.env.EMAIL_USER) {
try {
const editLink = `${process.env.FRONTEND_BASE_URL}/events/${eventSlug}/rsvp/edit/${editId}`;
await sendRSVPEditLinkEmail({
eventTitle,
eventSlug,
name,
to: submitterEmail,
editLink,
});
console.log(`Sent RSVP edit link email to ${submitterEmail}`);
} catch (emailErr) {
console.error('Error sending RSVP edit link email:', emailErr);
}
} else if (sendEmailConfirmationBool && !submitterEmail) {
console.warn('Email confirmation requested but no email address provided. Skipping edit link email.');
} else if (sendEmailConfirmationBool && !process.env.EMAIL_USER) {
console.warn('Email confirmation requested but EMAIL_USER environment variable is not set. Cannot send edit link email.');
} else {
console.log('Email confirmation not requested. Skipping edit link email.');
}
// Return the complete RSVP data including the parsed arrays and edit_id
res.status(201).json({ res.status(201).json({
id: result.lastID, id: result.lastID,
event_id: eventId, event_id: eventId,
@@ -372,6 +431,7 @@ app.post('/api/events/:slug/rsvp', async (req: Request, res: Response) => {
guest_names: parsedGuestNames, guest_names: parsedGuestNames,
items_bringing: parsedItemsBringing, items_bringing: parsedItemsBringing,
other_items: other_items || '', other_items: other_items || '',
edit_id: editId,
created_at: new Date().toISOString() created_at: new Date().toISOString()
}); });
} catch (error) { } catch (error) {
@@ -621,6 +681,7 @@ async function initializeDatabase() {
guest_names TEXT, guest_names TEXT,
items_bringing TEXT, items_bringing TEXT,
other_items TEXT, other_items TEXT,
edit_id TEXT UNIQUE, -- Add a column for the unique edit ID
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
) )

View File

@@ -29,6 +29,8 @@ interface RSVPFormData {
guest_names: string[]; guest_names: string[];
items_bringing: string[]; items_bringing: string[];
other_items: 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 RSVPForm: React.FC = () => {
@@ -40,7 +42,9 @@ const RSVPForm: React.FC = () => {
guest_count: 1, guest_count: 1,
guest_names: [], guest_names: [],
items_bringing: [], items_bringing: [],
other_items: '' other_items: '',
send_email_confirmation: false, // Initialize to false
email_address: '' // Initialize to empty
}); });
const [neededItems, setNeededItems] = useState<string[]>([]); const [neededItems, setNeededItems] = useState<string[]>([]);
const [claimedItems, setClaimedItems] = useState<string[]>([]); const [claimedItems, setClaimedItems] = useState<string[]>([]);
@@ -184,6 +188,14 @@ const RSVPForm: React.FC = () => {
})); }));
}; };
const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: checked
}));
};
const handleSelectChange = (e: SelectChangeEvent<string>) => { const handleSelectChange = (e: SelectChangeEvent<string>) => {
const { name, value } = e.target; const { name, value } = e.target;
@@ -196,7 +208,9 @@ const RSVPForm: React.FC = () => {
guest_count: 0, guest_count: 0,
guest_names: [], guest_names: [],
items_bringing: [], // Clear items when not attending items_bringing: [], // Clear items when not attending
other_items: '' other_items: '',
send_email_confirmation: false, // Also reset email opt-in
email_address: '' // And email address
})); }));
} else if (name === 'bringing_guests') { } else if (name === 'bringing_guests') {
// When bringing guests is changed // When bringing guests is changed
@@ -280,7 +294,9 @@ const RSVPForm: React.FC = () => {
const submissionData = { const submissionData = {
...formData, ...formData,
items_bringing: formData.items_bringing, items_bringing: formData.items_bringing,
other_items: splitOtherItems other_items: splitOtherItems,
send_email_confirmation: formData.send_email_confirmation,
email_address: formData.email_address.trim()
}; };
const response = await axios.post(`/api/events/${slug}/rsvp`, submissionData); const response = await axios.post(`/api/events/${slug}/rsvp`, submissionData);
@@ -290,6 +306,14 @@ const RSVPForm: React.FC = () => {
axios.get(`/api/events/${slug}/rsvps`) 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
}
// Process needed items // Process needed items
let items: string[] = []; let items: string[] = [];
if (eventResponse.data.needed_items) { if (eventResponse.data.needed_items) {
@@ -586,6 +610,37 @@ const RSVPForm: React.FC = () => {
</> </>
)} )}
{/* Email Notification Section */}
<Box sx={{ mt: 3, borderTop: '1px solid rgba(0, 0, 0, 0.12)', pt: 3 }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
To receive a link to edit your submission later, please enable email notifications below.
</Typography>
<FormControlLabel
control={
<Checkbox
checked={formData.send_email_confirmation}
onChange={handleCheckboxChange}
name="send_email_confirmation"
/>
}
label="Send me an email confirmation with an edit link"
/>
{formData.send_email_confirmation && (
<TextField
fullWidth
label="Your Email Address"
name="email_address"
type="email"
value={formData.email_address}
onChange={handleChange}
variant="outlined"
required // Make email required if checkbox is checked
sx={{ mt: 2 }}
/>
)}
</Box>
<Button <Button
type="submit" type="submit"
variant="contained" variant="contained"
@@ -595,7 +650,9 @@ const RSVPForm: React.FC = () => {
!formData.name.trim() || !formData.name.trim() ||
!formData.attending || !formData.attending ||
(formData.attending === 'yes' && !formData.bringing_guests) || (formData.attending === 'yes' && !formData.bringing_guests) ||
(formData.bringing_guests === 'yes' && (formData.guest_count < 1 || formData.guest_names.some(name => !name.trim())))} (formData.bringing_guests === 'yes' && (formData.guest_count < 1 || formData.guest_names.some(name => !name.trim()))) ||
(formData.send_email_confirmation && !formData.email_address.trim()) // Disable if email confirmation is checked but email is empty
}
sx={{ mt: 2 }} sx={{ mt: 2 }}
> >
{isSubmitting ? 'Submitting...' : 'Submit RSVP'} {isSubmitting ? 'Submitting...' : 'Submit RSVP'}