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'));