/** * Calendar-link helpers — port of schedule-jeffemmett/src/lib/calendar-links.ts. * * Used in confirmation emails so guests can add the booking to their calendar * with one click, and in `.ics` attachments for Apple Calendar / Outlook. */ function formatUtcDate(iso: string): string { return new Date(iso).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, ""); } export interface CalLinkParams { startUtc: string; endUtc: string; title: string; meetingLink?: string | null; guestNote?: string | null; locationOverride?: string | null; } export function getGoogleCalendarUrl(p: CalLinkParams): string { const start = formatUtcDate(p.startUtc); const end = formatUtcDate(p.endUtc); const details = [p.guestNote ? `Note: ${p.guestNote}` : "", p.meetingLink ? `Join: ${p.meetingLink}` : ""].filter(Boolean).join("\n"); const q = new URLSearchParams({ action: "TEMPLATE", text: p.title, dates: `${start}/${end}`, ...(details && { details }), ...(p.meetingLink ? { location: p.meetingLink } : p.locationOverride ? { location: p.locationOverride } : {}), }); return `https://calendar.google.com/calendar/render?${q.toString()}`; } export function getOutlookCalendarUrl(p: CalLinkParams): string { const body = [p.guestNote ? `Note: ${p.guestNote}` : "", p.meetingLink ? `Join: ${p.meetingLink}` : ""].filter(Boolean).join("\n"); const q = new URLSearchParams({ path: "/calendar/action/compose", rru: "addevent", subject: p.title, startdt: new Date(p.startUtc).toISOString(), enddt: new Date(p.endUtc).toISOString(), ...(body && { body }), ...(p.meetingLink ? { location: p.meetingLink } : {}), }); return `https://outlook.live.com/calendar/0/action/compose?${q.toString()}`; } export function getYahooCalendarUrl(p: CalLinkParams): string { const start = formatUtcDate(p.startUtc); const end = formatUtcDate(p.endUtc); const desc = [p.guestNote ? `Note: ${p.guestNote}` : "", p.meetingLink ? `Join: ${p.meetingLink}` : ""].filter(Boolean).join("\n"); const q = new URLSearchParams({ v: "60", title: p.title, st: start, et: end, ...(desc && { desc }), ...(p.meetingLink ? { in_loc: p.meetingLink } : {}), }); return `https://calendar.yahoo.com/?${q.toString()}`; } export interface IcsParams extends CalLinkParams { attendees?: string[]; organizerEmail: string; organizerName: string; uidDomain?: string; method?: "REQUEST" | "PUBLISH" | "CANCEL"; } export function generateIcsContent(p: IcsParams): string { const start = formatUtcDate(p.startUtc); const end = formatUtcDate(p.endUtc); const uid = `${Date.now()}-${Math.random().toString(36).slice(2)}@${p.uidDomain || "rschedule.rspace.online"}`; const stamp = formatUtcDate(new Date().toISOString()); const description = [p.guestNote ? `Note: ${p.guestNote}` : "", p.meetingLink ? `Join: ${p.meetingLink}` : ""].filter(Boolean).join("\\n"); const method = p.method ?? (p.attendees?.length ? "REQUEST" : "PUBLISH"); const lines = [ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//rSchedule//Booking//EN", "CALSCALE:GREGORIAN", `METHOD:${method}`, "BEGIN:VEVENT", `UID:${uid}`, `DTSTAMP:${stamp}`, `DTSTART:${start}`, `DTEND:${end}`, `SUMMARY:${p.title.replace(/([;,\\\n])/g, "\\$1")}`, ...(description ? [`DESCRIPTION:${description}`] : []), ...(p.meetingLink ? [`LOCATION:${p.meetingLink}`, `URL:${p.meetingLink}`] : []), `ORGANIZER;CN=${p.organizerName}:mailto:${p.organizerEmail}`, `ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=${p.organizerName}:mailto:${p.organizerEmail}`, ...(p.attendees || []).map((e) => `ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:${e}`), "END:VEVENT", "END:VCALENDAR", ]; return lines.join("\r\n"); }