/* The one HTML layout every ThreadCount email is built from. * * Lifted out of lib/billing-mail.cjs so the coordinator and staff emails read the same way as the * billing ones: the same colours, the same text wordmark, one button, a footer. CommonJS with no * dependencies so the app, the timers and the previews all render the same file. * * Colour is hard-coded from app/globals.css (`--color-*`): email clients do not read CSS * variables. Tables, inline styles, a single 600px column, no images — the wordmark is text, so the * mail looks the same with images blocked. * * Slots: `eyebrow` (the small label above the headline), `title`, `preheader`, `intro` (paragraphs), * `rows` (a details table of [label, value]), `list` (garment lines or bullets), `code` (a big * collection code), `cta` ({label, href}), `closing` (paragraphs in the quieter colour), `footer` * ({facility, contact, links: [[label, href]], entity, textLinks}). `text` is built from the same * parts for callers that have no hand-written plain-text body. */ const C = { bg: "#f3f2f2", surface: "#eae9e9", text: "#201e1d", accent: "#ec3013", accent700: "#b8240e", accent300: "#ffc4b8", divider: "#cfcccb", n600: "#6c6764", n700: "#57534f", n800: "#3a3735", white: "#ffffff", }; const FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"; const MONO = "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } function money(cents, currency) { return new Intl.NumberFormat("en-AU", { style: "currency", currency: (currency || "AUD").toUpperCase() }).format((cents || 0) / 100); } function longDate(d) { const x = d instanceof Date ? d : new Date(d); return x.toLocaleDateString("en-AU", { day: "numeric", month: "long", year: "numeric", timeZone: "Australia/Brisbane" }); } function siteUrl() { return (process.env.NEXT_PUBLIC_SITE_URL || "https://threadcount.tech").replace(/\/+$/, ""); } /** * @param {{ eyebrow?: string, title: string, preheader?: string, intro?: string | string[], * rows?: [string, string][], list?: string[], code?: { label?: string, value: string }, * cta?: { label: string, href: string }, closing?: string | string[], * footer?: { facility?: string, contact?: string, links?: [string, string][], entity?: string, textLinks?: [string, string][] } }} o * @returns {{ html: string, text: string }} */ function layout(o) { const { eyebrow, title, preheader, intro, rows, list, code, cta, closing, footer } = o; const base = siteUrl(); const f = footer || {}; const entity = f.entity === undefined ? (process.env.INVOICE_ENTITY || "ThreadCount") : f.entity; const abn = f.entity === undefined && process.env.INVOICE_ABN ? ` · ABN ${process.env.INVOICE_ABN}` : ""; const links = f.links || [["Support", `${base}/support`]]; const paras = (Array.isArray(intro) ? intro : [intro]).filter(Boolean); const closingParas = (Array.isArray(closing) ? closing : [closing]).filter(Boolean); const rowsHtml = rows && rows.length ? ` ${rows.map(([k, v]) => ``).join("")}
${esc(k)} ${esc(v)}
` : ""; const listHtml = list && list.length ? ` ${list.map((l) => ``).join("")}
${esc(l)}
` : ""; const codeHtml = code ? `
${esc(code.label || "Collection code")}
${esc(code.value)}
` : ""; const ctaHtml = cta ? `
${esc(cta.label)}
` : ""; const html = ` ${esc(title)}
${esc(preheader || title)}
  ThreadCount
${esc(eyebrow || "ThreadCount")}
${esc(title)}
${paras.map((p) => `

${esc(p)}

`).join("")} ${listHtml} ${rowsHtml} ${codeHtml} ${ctaHtml} ${closingParas.map((p) => `

${esc(p)}

`).join("")}
${f.facility ? `${esc(f.facility)}${f.contact ? ` · ${esc(f.contact)}` : ""}
` : ""} ${links.map(([label, href]) => `${esc(label)}`).join(" · ")}${links.length ? "
" : ""} ${esc(entity)}${esc(abn)}
`; const textLinks = f.textLinks || links; const text = [ title.toUpperCase(), "", ...paras, "", ...(list && list.length ? [...list, ""] : []), ...(rows && rows.length ? rows.map(([k, v]) => `${k}: ${v}`) : []), ...(rows && rows.length ? [""] : []), ...(code ? [`${code.label || "Collection code"}: ${code.value}`, ""] : []), ...(cta ? [`${cta.label}: ${cta.href}`, ""] : []), ...closingParas, ...(closingParas.length ? [""] : []), f.facility ? `${f.facility}${f.contact ? ` · ${f.contact}` : ""}` : "", ...textLinks.map(([label, href]) => `${label}: ${href}`), `${entity}${abn}`, ].filter((l, i, a) => !(l === "" && a[i - 1] === "")).join("\n"); return { html, text }; } module.exports = { layout, esc, money, longDate, siteUrl, C, FONT };