Files
dezky/services/platform-api/src/scheduling/email/booking-templates.ts
T
2026-06-07 00:31:33 +02:00

138 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 `<!doctype html><html><body style="margin:0;background:#f6f6f7;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#1a1a1a">
<div style="max-width:520px;margin:0 auto;padding:32px 16px">
<div style="background:#fff;border:1px solid #ececec;border-radius:14px;overflow:hidden">
<div style="height:6px;background:${accent}"></div>
<div style="padding:28px">
<div style="font-size:13px;letter-spacing:.08em;text-transform:uppercase;color:#888">${escapeHtml(brandName)}</div>
<h1 style="font-size:20px;margin:8px 0 16px">${escapeHtml(heading)}</h1>
${bodyHtml}
</div>
</div>
<p style="text-align:center;color:#aaa;font-size:12px;margin-top:16px">Powered by ${escapeHtml(brandName)}</p>
</div></body></html>`
}
function escapeHtml(s: string): string {
return s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]!))
}
export function confirmationEmail(ctx: BookingEmailContext): RenderedEmail {
const accent = ctx.brandColor || '#1a1a1a'
const when = fmtRange(ctx.startUtc, ctx.endUtc, ctx.attendeeTimezone)
const subject = `Confirmed: ${ctx.eventTitle} with ${ctx.hostName}`
const text = [
`Hi ${ctx.attendeeName},`,
``,
`Your booking is confirmed.`,
``,
`${ctx.eventTitle} with ${ctx.hostName}`,
when,
ctx.location ? `Location: ${ctx.location}` : '',
``,
`A calendar invite is attached.`,
`Need to change it? ${ctx.manageUrl}`,
].filter(Boolean).join('\n')
const html = shell(accent, ctx.brandName, 'Your booking is confirmed', `
<p style="margin:0 0 6px"><strong>${escapeHtml(ctx.eventTitle)}</strong> with ${escapeHtml(ctx.hostName)}</p>
<p style="margin:0 0 4px;color:#444">${escapeHtml(when)}</p>
${ctx.location ? `<p style="margin:0 0 4px;color:#444">${escapeHtml(ctx.location)}</p>` : ''}
<p style="margin:18px 0 0;font-size:14px;color:#666">A calendar invite (.ics) is attached.</p>
<p style="margin:18px 0 0"><a href="${ctx.manageUrl}" style="display:inline-block;background:${accent};color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-size:14px">Reschedule or cancel</a></p>
`)
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}`, `
<p style="margin:0 0 12px;color:#444">This is a reminder of your upcoming booking ${escapeHtml(lead)}.</p>
<p style="margin:0 0 6px"><strong>${escapeHtml(ctx.eventTitle)}</strong> with ${escapeHtml(ctx.hostName)}</p>
<p style="margin:0 0 4px;color:#444">${escapeHtml(when)}</p>
${ctx.location ? `<p style="margin:0 0 4px;color:#444">${escapeHtml(ctx.location)}</p>` : ''}
<p style="margin:18px 0 0"><a href="${ctx.manageUrl}" style="display:inline-block;background:${accent};color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-size:14px">Reschedule or cancel</a></p>
`)
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', `
<p style="margin:0 0 6px"><strong>${escapeHtml(ctx.eventTitle)}</strong> with ${escapeHtml(ctx.hostName)}</p>
<p style="margin:0 0 4px;color:#444;text-decoration:line-through">${escapeHtml(when)}</p>
<p style="margin:18px 0 0"><a href="${ctx.manageUrl}" style="display:inline-block;background:${accent};color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-size:14px">Book a new time</a></p>
`)
return { subject, text, html }
}