Groupe KA — site du holding + KA ID (compte unique & SSO des 7 plateformes). Next.js 16, SQLite, Google & Apple login.
TypeScript 70.4%
HTML 18.4%
JavaScript 4%
Python 3.8%
CSS 3.4%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// KA ID — courriels transactionnels via Resend (domaine groupe-ka.com).3// API REST directe (fetch), zéro dépendance — même philosophie que scrypt.4// Gabarits « éditorial sharp » : encre #141814, lime #d9f26b, mono uppercase,5// carte à bordure encre + ombre décalée dure — l'identité exacte du site,6// en tables HTML compatibles clients courriel (Gmail, Apple Mail, Outlook).78const RESEND_ENDPOINT = "https://api.resend.com/emails";910export const MAIL_FROM =11 process.env.RESEND_FROM ?? "KA ID — Groupe Ka <ka-id@groupe-ka.com>";1213export async function sendEmail(opts: {14 to: string | string[];15 subject: string;16 html: string;17 text: string;18 /** expéditeur personnalisé (boîtes admin @groupe-ka.com) — défaut : MAIL_FROM */19 from?: string;20 replyTo?: string;21 cc?: string[];22}): Promise<{ id: string | null }> {23 const key = process.env.RESEND_API_KEY;24 if (!key) throw new Error("RESEND_API_KEY manquant");25 const res = await fetch(RESEND_ENDPOINT, {26 method: "POST",27 headers: {28 Authorization: `Bearer ${key}`,29 "Content-Type": "application/json",30 },31 body: JSON.stringify({32 from: opts.from ?? MAIL_FROM,33 to: Array.isArray(opts.to) ? opts.to : [opts.to],34 ...(opts.cc?.length ? { cc: opts.cc } : {}),35 ...(opts.replyTo ? { reply_to: opts.replyTo } : {}),36 subject: opts.subject,37 html: opts.html,38 text: opts.text,39 }),40 });41 if (!res.ok) {42 const detail = await res.text().catch(() => "");43 throw new Error(`Resend ${res.status} : ${detail.slice(0, 300)}`);44 }45 const data = (await res.json().catch(() => ({}))) as { id?: string };46 return { id: data.id ?? null };47}4849/* ---------- système de design courriel (identité Groupe Ka) ---------- */5051const INK = "#141814";52const INK_2 = "#4d5551";53const INK_3 = "#8b928c";54const PAPER = "#f5f3ee";55const LIME = "#d9f26b";56const GREEN = "#1c5c41";5758const F_DISPLAY = "'Space Grotesk',Arial,'Helvetica Neue',sans-serif";59const F_BODY = "'Inter',Helvetica,Arial,sans-serif";60const F_MONO = "'JetBrains Mono','SFMono-Regular',Menlo,Consolas,'Courier New',monospace";6162function layout(opts: {63 kicker: string;64 title: string;65 preheader: string;66 inner: string;67}): string {68 return `<!doctype html>69<html lang="fr">70<head>71 <meta charset="utf-8">72 <meta name="viewport" content="width=device-width, initial-scale=1">73 <meta name="color-scheme" content="light">74 <meta name="supported-color-schemes" content="light">75 <title>${opts.title.replace(/<[^>]+>/g, " ")}</title>76</head>77<body style="margin:0;padding:0;background:${PAPER};">78 <div style="display:none;max-height:0;overflow:hidden;mso-hide:all;">${opts.preheader} ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌</div>79 <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:${PAPER};">80 <tr>81 <td align="center" style="padding:40px 16px 36px;">82 <table role="presentation" cellpadding="0" cellspacing="0" style="width:560px;max-width:100%;">8384 <!-- manchette : marque + étiquette mono -->85 <tr>86 <td style="padding:0 2px 16px;">87 <table role="presentation" width="100%" cellpadding="0" cellspacing="0">88 <tr>89 <td style="font-family:${F_DISPLAY};font-weight:700;font-size:22px;letter-spacing:-0.03em;color:${INK};white-space:nowrap;">Groupe <span style="display:inline-block;background:${LIME};color:${INK};padding:0 7px 3px;border-radius:6px;">Ka</span></td>90 <td align="right" style="font-family:${F_MONO};font-size:10px;font-weight:700;letter-spacing:0.16em;text-transform:uppercase;color:${INK_3};">${opts.kicker}</td>91 </tr>92 </table>93 </td>94 </tr>9596 <!-- carte : bordure encre + ombre décalée dure (signature du site) -->97 <tr>98 <td style="background:#ffffff;border:2px solid ${INK};border-right-width:8px;border-bottom-width:8px;border-radius:12px;padding:30px 28px 24px;">99 <table role="presentation" width="100%" cellpadding="0" cellspacing="0">100 <tr>101 <td style="font-family:${F_MONO};font-size:11px;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:${GREEN};"><span style="display:inline-block;width:22px;height:2px;background:${GREEN};vertical-align:middle;"> </span> KA ID</td>102 </tr>103 <tr>104 <td style="padding:12px 0 0;font-family:${F_DISPLAY};font-weight:700;font-size:27px;line-height:1.08;letter-spacing:-0.03em;color:${INK};">${opts.title}</td>105 </tr>106 <tr><td style="padding:14px 0 0;">${opts.inner}</td></tr>107 <tr>108 <td style="padding:24px 0 0;">109 <table role="presentation" width="100%" cellpadding="0" cellspacing="0">110 <tr><td style="border-top:1.5px dashed rgba(20,24,20,0.25);font-size:0;line-height:0;"> </td></tr>111 </table>112 </td>113 </tr>114 <tr>115 <td style="padding:8px 0 0;font-family:${F_MONO};font-size:10.5px;line-height:1.7;color:${INK_3};">Vous n'êtes pas à l'origine de cette demande ? Ignorez ce courriel — rien ne se passera sans lui.</td>116 </tr>117 </table>118 </td>119 </tr>120121 <!-- pied : écosystème -->122 <tr>123 <td style="padding:20px 2px 0;font-family:${F_MONO};font-size:9.5px;font-weight:700;line-height:1.9;letter-spacing:0.1em;text-transform:uppercase;color:${INK_3};">124 Un seul compte · tout l'écosystème<br>125 <a href="https://www.groupe-ka.com" style="color:${GREEN};text-decoration:none;">www.groupe-ka.com</a>126 — Lou·Ka · Immo·Ka · Vrai-Prix · Auto·Ka · Food·Ka · Sorti·Ka · Job·Ka …127 </td>128 </tr>129130 </table>131 </td>132 </tr>133 </table>134</body>135</html>`;136}137138function paragraph(html: string): string {139 return `<p style="margin:0;font-family:${F_BODY};font-size:14.5px;line-height:1.65;color:${INK_2};">${html}</p>`;140}141142/** Bouton pilule signature : encre pleine, texte lime (btn-primary du site). */143function button(url: string, label: string): string {144 return `<table role="presentation" cellpadding="0" cellspacing="0" style="margin:22px 0 10px;">145 <tr>146 <td style="background:${INK};border-radius:999px;mso-padding-alt:14px 30px;">147 <a href="${url}" style="display:inline-block;padding:14px 30px;font-family:${F_DISPLAY};font-weight:700;font-size:15px;letter-spacing:-0.01em;color:${LIME};text-decoration:none;border-radius:999px;">${label} →</a>148 </td>149 </tr>150 </table>151 <p style="margin:0;font-family:${F_MONO};font-size:10px;line-height:1.7;color:${INK_3};word-break:break-all;">Le bouton ne répond pas ? Copiez ce lien :<br><a href="${url}" style="color:${GREEN};text-decoration:underline;">${url}</a></p>`;152}153154/** Pastille d'info mono (durée de validité, usage unique…). */155function metaChips(items: string[]): string {156 return `<p style="margin:16px 0 0;font-family:${F_MONO};font-size:9.5px;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:${INK_3};">${items.join(" · ")}</p>`;157}158159/* ---------- gabarits ---------- */160161export function verificationEmail(name: string, url: string) {162 const inner = `163 ${paragraph(164 `Bonjour <strong style="color:${INK};">${name}</strong>,<br>165 votre KA ID vient d'être créé — un seul compte pour toutes les166 plateformes du Groupe Ka. Dernière étape : confirmer que cette167 adresse est bien la vôtre. Vous serez connecté automatiquement.`,168 )}169 ${button(url, "Confirmer mon adresse")}170 ${metaChips(["Valable 24 heures", "Usage unique"])}`;171 return {172 subject: "Confirmez votre adresse — KA ID",173 html: layout({174 kicker: "KA ID · Confirmation",175 title: "Une adresse à confirmer,<br>un écosystème à ouvrir",176 preheader: "Confirmez votre adresse pour activer votre KA ID — lien valable 24 heures.",177 inner,178 }),179 text: `Bonjour ${name},\n\nVotre KA ID vient d'être créé. Confirmez votre adresse pour l'activer (lien valable 24 h, usage unique) :\n${url}\n\nSi vous n'êtes pas à l'origine de cette demande, ignorez ce courriel.\n\nGroupe Ka — www.groupe-ka.com`,180 };181}182183export function magicLinkEmail(name: string, url: string) {184 const inner = `185 ${paragraph(186 `Bonjour <strong style="color:${INK};">${name}</strong>,<br>187 vous avez demandé à vous connecter sans mot de passe. Un clic sur le188 bouton et vous êtes chez vous — sur toutes les plateformes du groupe.`,189 )}190 ${button(url, "Me connecter")}191 ${metaChips(["Valable 15 minutes", "Usage unique"])}`;192 return {193 subject: "Votre lien de connexion — KA ID",194 html: layout({195 kicker: "KA ID · Connexion",196 title: "Votre lien de connexion,<br>sans mot de passe",197 preheader: "Un clic et vous êtes connecté — lien valable 15 minutes, usage unique.",198 inner,199 }),200 text: `Bonjour ${name},\n\nVotre lien de connexion KA ID (valable 15 minutes, usage unique) :\n${url}\n\nSi vous n'êtes pas à l'origine de cette demande, ignorez ce courriel.\n\nGroupe Ka — www.groupe-ka.com`,201 };202}203204export function loginCodeEmail(name: string, code: string) {205 // Panneau encre/lime — même langage que la carte de membre KA ID.206 const codePanel = `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:22px 0 0;">207 <tr>208 <td align="center" style="background:${INK};border-radius:12px;padding:26px 18px 20px;">209 <p style="margin:0 0 12px;font-family:${F_MONO};font-size:10px;font-weight:700;letter-spacing:0.26em;text-transform:uppercase;color:rgba(217,242,107,0.65);">Code de connexion</p>210 <p style="margin:0;font-family:${F_MONO};font-weight:700;font-size:42px;line-height:1;letter-spacing:0.28em;color:${LIME};">${code}</p>211 <p style="margin:16px 0 0;font-family:${F_MONO};font-size:9px;font-weight:700;letter-spacing:0.2em;text-transform:uppercase;color:rgba(245,243,238,0.45);">Valable 10 minutes · Usage unique</p>212 </td>213 </tr>214 </table>`;215 const inner = `216 ${paragraph(217 `Bonjour <strong style="color:${INK};">${name}</strong>,<br>218 votre mot de passe est bon — il ne manque que ce code pour ouvrir la219 session. Saisissez-le sur la page de connexion :`,220 )}221 ${codePanel}222 <p style="margin:14px 0 0;font-family:${F_BODY};font-size:12.5px;line-height:1.6;color:${INK_2};">Ce n'était pas vous ? Quelqu'un connaît alors votre mot de passe — <a href="https://www.groupe-ka.com/compte" style="color:${GREEN};">changez-le dès maintenant</a>.</p>`;223 return {224 subject: `${code} — votre code de connexion KA ID`,225 html: layout({226 kicker: "KA ID · Vérification",227 title: "Votre code de connexion",228 preheader: `Votre code : ${code} — il expire dans 10 minutes.`,229 inner,230 }),231 text: `Bonjour ${name},\n\nVotre code de connexion KA ID (expire dans 10 minutes, usage unique) : ${code}\n\nSi vous n'êtes pas à l'origine de cette connexion, changez votre mot de passe sur www.groupe-ka.com/compte.\n\nGroupe Ka — www.groupe-ka.com`,232 };233}234