Add unique edit ID and email opt-in to RSVP submission
This commit is contained in:
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
Reference in New Issue
Block a user