// Branded, dependency-free booking email templates (text + HTML). Per CLAUDE.md // the brand surface is whitelabel: `brandName`/`brandColor` come from the tenant, // not fixed dezky styling. End-user copy may be localized later; English for now. export interface BookingEmailContext { brandName: string brandColor?: string eventTitle: string hostName: string attendeeName: string startUtc: Date endUtc: Date attendeeTimezone: string location?: string manageUrl: string } export interface RenderedEmail { subject: string text: string html: string } function fmtRange(start: Date, end: Date, tz: string): string { const date = new Intl.DateTimeFormat('en-GB', { timeZone: tz, weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', }).format(start) const t = (d: Date) => new Intl.DateTimeFormat('en-GB', { timeZone: tz, hour: '2-digit', minute: '2-digit', hour12: false }).format(d) return `${date}, ${t(start)}–${t(end)} (${tz})` } function shell(accent: string, brandName: string, heading: string, bodyHtml: string): string { return `
Powered by ${escapeHtml(brandName)}
${escapeHtml(ctx.eventTitle)} with ${escapeHtml(ctx.hostName)}
${escapeHtml(when)}
${ctx.location ? `${escapeHtml(ctx.location)}
` : ''}A calendar invite (.ics) is attached.
`) return { subject, text, html } } // Human-friendly lead time, e.g. 1440 -> "tomorrow", 60 -> "in 1 hour". function fmtLeadTime(minutes: number): string { if (minutes % 1440 === 0) { const days = minutes / 1440 return days === 1 ? 'tomorrow' : `in ${days} days` } if (minutes % 60 === 0) { const hours = minutes / 60 return `in ${hours} hour${hours === 1 ? '' : 's'}` } return `in ${minutes} minutes` } export function reminderEmail(ctx: BookingEmailContext & { offsetMinutes: number }): RenderedEmail { const accent = ctx.brandColor || '#1a1a1a' const when = fmtRange(ctx.startUtc, ctx.endUtc, ctx.attendeeTimezone) const lead = fmtLeadTime(ctx.offsetMinutes) const subject = `Reminder: ${ctx.eventTitle} with ${ctx.hostName} ${lead}` const text = [ `Hi ${ctx.attendeeName},`, ``, `This is a reminder of your upcoming booking ${lead}.`, ``, `${ctx.eventTitle} with ${ctx.hostName}`, when, ctx.location ? `Location: ${ctx.location}` : '', ``, `Need to change it? ${ctx.manageUrl}`, ].filter(Boolean).join('\n') const html = shell(accent, ctx.brandName, `Your booking is ${lead}`, `This is a reminder of your upcoming booking ${escapeHtml(lead)}.
${escapeHtml(ctx.eventTitle)} with ${escapeHtml(ctx.hostName)}
${escapeHtml(when)}
${ctx.location ? `${escapeHtml(ctx.location)}
` : ''} `) return { subject, text, html } } export function cancellationEmail(ctx: BookingEmailContext): RenderedEmail { const accent = ctx.brandColor || '#1a1a1a' const when = fmtRange(ctx.startUtc, ctx.endUtc, ctx.attendeeTimezone) const subject = `Cancelled: ${ctx.eventTitle} with ${ctx.hostName}` const text = [ `Hi ${ctx.attendeeName},`, ``, `Your booking has been cancelled.`, ``, `${ctx.eventTitle} with ${ctx.hostName}`, when, ``, `You can book a new time here: ${ctx.manageUrl}`, ].join('\n') const html = shell(accent, ctx.brandName, 'Your booking was cancelled', `${escapeHtml(ctx.eventTitle)} with ${escapeHtml(ctx.hostName)}
${escapeHtml(when)}
`) return { subject, text, html } }