Initial commit 🚀

This commit is contained in:
Felix Klement committed 2026-10-09 14:00:40 +02:00
commit fd58a8d32f
235 files changed
+19775

No files matched your search

+107
View File
@@ -0,0 +1,107 @@
/**
* The alpha sign-up confirmation email, in the site's voice and look: warm paper, warm black,
* one neo-grotesque, hairlines, one square button. Email clients ignore most CSS, so the layout
* is tables with inline styles, the type falls back from Inter Tight to Helvetica and Arial, and
* the logo travels as an inline attachment (cid) rather than a remote image.
*
* Colours are the tokens from src/styles/tokens.css, written out: email has no custom properties.
* The hairline is --line (ink at 14 %) flattened onto paper.
*/
import { fileURLToPath } from 'node:url';
const C = {
paper: '#E9E5DF',
ink: '#1A1A18',
muted: '#57564F',
line: '#CCC9C3',
};
const FONT = "'Inter Tight', Inter, 'Helvetica Neue', Helvetica, Arial, sans-serif";
const LOGO_CID = 'logo@ciphercell.de';
const LOGO = { path: fileURLToPath(new URL('./logo.png', import.meta.url)), width: 140, height: 32 };
const COPY = {
en: {
subject: 'Confirm your CipherVerifier alpha registration',
preheader: 'One click confirms your registration for the CipherVerifier alpha.',
title: 'Confirm your registration.',
body: (email) => `You registered ${email} for the CipherVerifier alpha. Confirm the address, and we will email you once, when the alpha is released. Nothing else.`,
button: 'Confirm registration',
expiry: 'The link is valid for 7 days. If you did not register, ignore this email. Your address is not stored.',
fallback: 'If the button does not work, open this address in your browser:',
},
de: {
subject: 'Bitte bestätigen Sie Ihre Registrierung für die CipherVerifier-Alpha',
preheader: 'Ein Klick bestätigt Ihre Registrierung für die Alpha von CipherVerifier.',
title: 'Registrierung bestätigen.',
body: (email) => `Sie haben ${email} für die Alpha von CipherVerifier registriert. Bestätigen Sie die Adresse, und wir schreiben Ihnen einmal, sobald die Alpha erscheint. Sonst nichts.`,
button: 'Registrierung bestätigen',
expiry: 'Der Link ist 7 Tage gültig. Wenn Sie sich nicht registriert haben, ignorieren Sie diese E-Mail. Ihre Adresse wird nicht gespeichert.',
fallback: 'Falls der Button nicht funktioniert, öffnen Sie diese Adresse im Browser:',
},
};
const ADDRESS = ['CipherCell', 'Innstr. 43, 94032 Passau'];
const SITE_LABEL = 'www.ciphercell.de';
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
/** { subject, text, html, attachments } for nodemailer. */
export function confirmMail({ lang, email, link, site }) {
const t = COPY[lang] ?? COPY.en;
const text = [
t.title,
'',
t.body(email),
'',
`${t.button}:`,
link,
'',
t.expiry,
'',
'-- ',
...ADDRESS,
SITE_LABEL,
].join('\n');
const p = (content, { size = 16, color = C.ink, lineHeight = 1.55, extra = '' } = {}) =>
`<p style="margin:0;font-family:${FONT};font-size:${size}px;line-height:${lineHeight};color:${color};${extra}">${content}</p>`;
const row = (content, padding) => `<tr><td style="padding:${padding};">${content}</td></tr>`;
const rule = `<tr><td style="padding:0;border-top:1px solid ${C.line};font-size:0;line-height:0;">&nbsp;</td></tr>`;
const html = `<!doctype html>
<html lang="${lang}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<meta name="supported-color-schemes" content="light">
<title>${esc(t.subject)}</title>
</head>
<body style="margin:0;padding:0;background:${C.paper};-webkit-text-size-adjust:100%;">
<div style="display:none;max-height:0;overflow:hidden;opacity:0;color:${C.paper};">${esc(t.preheader)}</div>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:${C.paper};">
<tr><td align="left" style="padding:40px 24px 48px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:560px;">
${row(`<a href="${esc(site)}" style="text-decoration:none;"><img src="cid:${LOGO_CID}" width="${LOGO.width}" height="${LOGO.height}" alt="CipherCell" style="display:block;border:0;width:${LOGO.width}px;height:${LOGO.height}px;font-family:${FONT};font-size:18px;font-weight:600;color:${C.ink};"></a>`, '0 0 20px')}
${rule}
${row(p(esc(t.title), { size: 34, lineHeight: 1.08, extra: 'font-weight:500;letter-spacing:-0.02em;' }), '48px 0 20px')}
${row(p(esc(t.body(email))), '0 0 32px')}
${row(`<table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td bgcolor="${C.ink}" style="background:${C.ink};">
<a href="${esc(link)}" style="display:inline-block;padding:15px 26px;font-family:${FONT};font-size:16px;line-height:1;font-weight:500;color:${C.paper};text-decoration:none;">${esc(t.button)}</a>
</td></tr></table>`, '0 0 32px')}
${row(p(esc(t.expiry), { size: 14, color: C.muted, lineHeight: 1.4 }), '0 0 16px')}
${row(p(`${esc(t.fallback)}<br><a href="${esc(link)}" style="color:${C.muted};text-decoration:underline;word-break:break-all;">${esc(link)}</a>`, { size: 14, color: C.muted, lineHeight: 1.4 }), '0 0 48px')}
${rule}
${row(p(`${ADDRESS.map(esc).join('<br>')}<br><a href="${esc(site)}" style="color:${C.muted};text-decoration:underline;">${SITE_LABEL}</a>`, { size: 14, color: C.muted, lineHeight: 1.4 }), '20px 0 0')}
</table>
</td></tr>
</table>
</body>
</html>`;
return {
subject: t.subject,
text,
html,
attachments: [{ filename: 'ciphercell.png', path: LOGO.path, cid: LOGO_CID }],
};
}