103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
/**
|
|
* 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");
|
|
}
|