// server/api/events/calendar-feed.get.ts import { createNocoDBEventsClient } from '~/server/utils/nocodb-events'; export default defineEventHandler(async (event) => { try { const query = getQuery(event); const { user_id, user_role, format } = query; if (!user_id) { throw createError({ statusCode: 400, statusMessage: 'User ID is required' }); } const eventsClient = createNocoDBEventsClient(); // Get events for the user (next 6 months) const now = new Date(); const sixMonthsLater = new Date(); sixMonthsLater.setMonth(now.getMonth() + 6); const filters = { start_date: now.toISOString(), end_date: sixMonthsLater.toISOString(), user_role: (user_role as string) || 'user', status: 'active' }; const response = await eventsClient.findUserEvents(user_id as string, filters); const events = response.list || []; // Generate iCal content const icalContent = generateICalContent(events, { calendarName: 'MonacoUSA Events', timezone: 'Europe/Paris', includeRSVPStatus: true }); // Set appropriate headers for calendar subscription setHeader(event, 'Content-Type', 'text/calendar; charset=utf-8'); setHeader(event, 'Content-Disposition', 'attachment; filename="monacousa-events.ics"'); setHeader(event, 'Cache-Control', 'no-cache, must-revalidate'); return icalContent; } catch (error) { console.error('Error generating calendar feed:', error); throw createError({ statusCode: 500, statusMessage: 'Failed to generate calendar feed' }); } }); /** * Generate iCal content from events array */ function generateICalContent(events: any[], options: { calendarName: string; timezone: string; includeRSVPStatus: boolean; }): string { const now = new Date(); const lines: string[] = []; // iCal header lines.push('BEGIN:VCALENDAR'); lines.push('VERSION:2.0'); lines.push('PRODID:-//MonacoUSA//Portal//EN'); lines.push(`X-WR-CALNAME:${options.calendarName}`); lines.push(`X-WR-TIMEZONE:${options.timezone}`); lines.push('X-WR-CALDESC:Events from MonacoUSA Portal'); lines.push('CALSCALE:GREGORIAN'); lines.push('METHOD:PUBLISH'); // Add timezone definition lines.push('BEGIN:VTIMEZONE'); lines.push(`TZID:${options.timezone}`); lines.push('BEGIN:STANDARD'); lines.push('DTSTART:19701025T030000'); lines.push('RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU'); lines.push('TZNAME:CET'); lines.push('TZOFFSETFROM:+0200'); lines.push('TZOFFSETTO:+0100'); lines.push('END:STANDARD'); lines.push('BEGIN:DAYLIGHT'); lines.push('DTSTART:19700329T020000'); lines.push('RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU'); lines.push('TZNAME:CEST'); lines.push('TZOFFSETFROM:+0100'); lines.push('TZOFFSETTO:+0200'); lines.push('END:DAYLIGHT'); lines.push('END:VTIMEZONE'); // Process each event events.forEach(eventItem => { if (!eventItem.start_datetime || !eventItem.end_datetime) return; const startDate = new Date(eventItem.start_datetime); const endDate = new Date(eventItem.end_datetime); const createdDate = eventItem.created_at ? new Date(eventItem.created_at) : now; const modifiedDate = eventItem.updated_at ? new Date(eventItem.updated_at) : now; lines.push('BEGIN:VEVENT'); lines.push(`UID:${eventItem.id}@monacousa.org`); lines.push(`DTSTART;TZID=${options.timezone}:${formatICalDateTime(startDate)}`); lines.push(`DTEND;TZID=${options.timezone}:${formatICalDateTime(endDate)}`); lines.push(`DTSTAMP:${formatICalDateTime(now)}`); lines.push(`CREATED:${formatICalDateTime(createdDate)}`); lines.push(`LAST-MODIFIED:${formatICalDateTime(modifiedDate)}`); lines.push(`SUMMARY:${escapeICalText(eventItem.title)}`); // Add description with event details let description = eventItem.description || ''; if (eventItem.is_paid === 'true') { description += `\\n\\nEvent Fee: €${eventItem.cost_members || eventItem.cost_non_members || 'TBD'}`; if (eventItem.cost_members && eventItem.cost_non_members) { description += ` (Members: €${eventItem.cost_members}, Non-members: €${eventItem.cost_non_members})`; } } // Add RSVP status if available if (options.includeRSVPStatus && eventItem.user_rsvp) { const rsvpStatus = eventItem.user_rsvp.rsvp_status; description += `\\n\\nRSVP Status: ${rsvpStatus.toUpperCase()}`; if (rsvpStatus === 'confirmed' && eventItem.user_rsvp.payment_reference) { description += `\\nPayment Reference: ${eventItem.user_rsvp.payment_reference}`; } } if (description) { lines.push(`DESCRIPTION:${escapeICalText(description)}`); } // Add location if (eventItem.location) { lines.push(`LOCATION:${escapeICalText(eventItem.location)}`); } // Add categories based on event type const eventTypeLabels = { 'meeting': 'Meeting', 'social': 'Social Event', 'fundraiser': 'Fundraiser', 'workshop': 'Workshop', 'board-only': 'Board Meeting' }; const category = eventTypeLabels[eventItem.event_type as keyof typeof eventTypeLabels] || 'Event'; lines.push(`CATEGORIES:${category}`); // Add status let eventStatus = 'CONFIRMED'; if (eventItem.status === 'cancelled') { eventStatus = 'CANCELLED'; } else if (eventItem.status === 'draft') { eventStatus = 'TENTATIVE'; } lines.push(`STATUS:${eventStatus}`); // Add transparency for paid events if (eventItem.is_paid === 'true') { lines.push('TRANSP:OPAQUE'); } else { lines.push('TRANSP:TRANSPARENT'); } // Add URL (link back to portal) lines.push(`URL:https://portal.monacousa.org/dashboard/events`); lines.push('END:VEVENT'); }); // iCal footer lines.push('END:VCALENDAR'); // Join with CRLF as per RFC 5545 return lines.join('\r\n'); } /** * Format date for iCal (YYYYMMDDTHHMMSS format) */ function formatICalDateTime(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}${month}${day}T${hours}${minutes}${seconds}`; } /** * Escape special characters for iCal text fields */ function escapeICalText(text: string): string { if (!text) return ''; return text .replace(/\\/g, '\\\\') // Escape backslashes .replace(/;/g, '\\;') // Escape semicolons .replace(/,/g, '\\,') // Escape commas .replace(/\n/g, '\\n') // Escape newlines .replace(/\r/g, '') // Remove carriage returns .replace(/"/g, '\\"'); // Escape quotes }