| 1 |
1 |
// Auteur : Simon-Pierre Boucher — contact@spboucher.ai |
| 2 |
2 |
// KA ID — courriels transactionnels via Resend (domaine groupe-ka.com). |
| 3 |
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). |
| 4 |
7 |
|
| 5 |
8 |
const RESEND_ENDPOINT = "https://api.resend.com/emails"; |
| 6 |
9 |
|
| 35 |
38 |
} |
| 36 |
39 |
} |
| 37 |
40 |
|
| 38 |
|
−/* ---------- gabarits (encre sur papier, style Groupe Ka) ---------- */ |
|
41 |
+/* ---------- système de design courriel (identité Groupe Ka) ---------- */ |
| 39 |
42 |
|
| 40 |
|
−function layout(title: string, inner: string): string { |
|
43 |
+const INK = "#141814"; |
|
44 |
+const INK_2 = "#4d5551"; |
|
45 |
+const INK_3 = "#8b928c"; |
|
46 |
+const PAPER = "#f5f3ee"; |
|
47 |
+const LIME = "#d9f26b"; |
|
48 |
+const GREEN = "#1c5c41"; |
|
49 |
+ |
|
50 |
+const F_DISPLAY = "'Space Grotesk',Arial,'Helvetica Neue',sans-serif"; |
|
51 |
+const F_BODY = "'Inter',Helvetica,Arial,sans-serif"; |
|
52 |
+const F_MONO = "'JetBrains Mono','SFMono-Regular',Menlo,Consolas,'Courier New',monospace"; |
|
53 |
+ |
|
54 |
+function layout(opts: { |
|
55 |
+ kicker: string; |
|
56 |
+ title: string; |
|
57 |
+ preheader: string; |
|
58 |
+ inner: string; |
|
59 |
+}): string { |
| 41 |
60 |
return `<!doctype html> |
| 42 |
|
−<html lang="fr"><body style="margin:0;padding:0;background:#f2f0ea;"> |
| 43 |
|
− <div style="max-width:520px;margin:0 auto;padding:32px 20px;font-family:Georgia,'Times New Roman',serif;color:#141814;"> |
| 44 |
|
− <p style="margin:0 0 18px;font-family:Menlo,Consolas,monospace;font-size:11px;letter-spacing:0.14em;text-transform:uppercase;color:#4a4f4a;"> |
| 45 |
|
− Groupe Ka · KA ID |
| 46 |
|
− </p> |
| 47 |
|
− <div style="background:#fbfaf6;border:1.5px solid rgba(20,24,20,0.35);padding:28px 26px;"> |
| 48 |
|
− <h1 style="margin:0 0 14px;font-size:22px;line-height:1.15;letter-spacing:-0.02em;">${title}</h1> |
| 49 |
|
− ${inner} |
| 50 |
|
− </div> |
| 51 |
|
− <p style="margin:16px 0 0;font-size:11.5px;line-height:1.5;color:#6a6f6a;"> |
| 52 |
|
− Un seul compte — le KA ID — pour toutes les plateformes du Groupe Ka.<br> |
| 53 |
|
− Si vous n'êtes pas à l'origine de cette demande, ignorez ce courriel. |
| 54 |
|
− </p> |
| 55 |
|
− </div> |
| 56 |
|
−</body></html>`; |
|
61 |
+<html lang="fr"> |
|
62 |
+<head> |
|
63 |
+ <meta charset="utf-8"> |
|
64 |
+ <meta name="viewport" content="width=device-width, initial-scale=1"> |
|
65 |
+ <meta name="color-scheme" content="light"> |
|
66 |
+ <meta name="supported-color-schemes" content="light"> |
|
67 |
+ <title>${opts.title.replace(/<[^>]+>/g, " ")}</title> |
|
68 |
+</head> |
|
69 |
+<body style="margin:0;padding:0;background:${PAPER};"> |
|
70 |
+ <div style="display:none;max-height:0;overflow:hidden;mso-hide:all;">${opts.preheader} ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌</div> |
|
71 |
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:${PAPER};"> |
|
72 |
+ <tr> |
|
73 |
+ <td align="center" style="padding:40px 16px 36px;"> |
|
74 |
+ <table role="presentation" cellpadding="0" cellspacing="0" style="width:560px;max-width:100%;"> |
|
75 |
+ |
|
76 |
+ <!-- manchette : marque + étiquette mono --> |
|
77 |
+ <tr> |
|
78 |
+ <td style="padding:0 2px 16px;"> |
|
79 |
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0"> |
|
80 |
+ <tr> |
|
81 |
+ <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> |
|
82 |
+ <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> |
|
83 |
+ </tr> |
|
84 |
+ </table> |
|
85 |
+ </td> |
|
86 |
+ </tr> |
|
87 |
+ |
|
88 |
+ <!-- carte : bordure encre + ombre décalée dure (signature du site) --> |
|
89 |
+ <tr> |
|
90 |
+ <td style="background:#ffffff;border:2px solid ${INK};border-right-width:8px;border-bottom-width:8px;border-radius:12px;padding:30px 28px 24px;"> |
|
91 |
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0"> |
|
92 |
+ <tr> |
|
93 |
+ <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> |
|
94 |
+ </tr> |
|
95 |
+ <tr> |
|
96 |
+ <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> |
|
97 |
+ </tr> |
|
98 |
+ <tr><td style="padding:14px 0 0;">${opts.inner}</td></tr> |
|
99 |
+ <tr> |
|
100 |
+ <td style="padding:24px 0 0;"> |
|
101 |
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0"> |
|
102 |
+ <tr><td style="border-top:1.5px dashed rgba(20,24,20,0.25);font-size:0;line-height:0;"> </td></tr> |
|
103 |
+ </table> |
|
104 |
+ </td> |
|
105 |
+ </tr> |
|
106 |
+ <tr> |
|
107 |
+ <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> |
|
108 |
+ </tr> |
|
109 |
+ </table> |
|
110 |
+ </td> |
|
111 |
+ </tr> |
|
112 |
+ |
|
113 |
+ <!-- pied : écosystème --> |
|
114 |
+ <tr> |
|
115 |
+ <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};"> |
|
116 |
+ Un seul compte · tout l'écosystème<br> |
|
117 |
+ <a href="https://www.groupe-ka.com" style="color:${GREEN};text-decoration:none;">www.groupe-ka.com</a> |
|
118 |
+ — Lou·Ka · Immo·Ka · Vrai-Prix · Auto·Ka · Food·Ka · Resto·Ka · Sorti·Ka · Créa·Ka · Job·Ka … |
|
119 |
+ </td> |
|
120 |
+ </tr> |
|
121 |
+ |
|
122 |
+ </table> |
|
123 |
+ </td> |
|
124 |
+ </tr> |
|
125 |
+ </table> |
|
126 |
+</body> |
|
127 |
+</html>`; |
|
128 |
+} |
|
129 |
+ |
|
130 |
+function paragraph(html: string): string { |
|
131 |
+ return `<p style="margin:0;font-family:${F_BODY};font-size:14.5px;line-height:1.65;color:${INK_2};">${html}</p>`; |
| 57 |
132 |
} |
| 58 |
133 |
|
|
134 |
+/** Bouton pilule signature : encre pleine, texte lime (btn-primary du site). */ |
| 59 |
135 |
function button(url: string, label: string): string { |
| 60 |
|
− return `<p style="margin:22px 0;"> |
| 61 |
|
− <a href="${url}" style="display:inline-block;background:#141814;color:#d9f26b;text-decoration:none;font-family:Menlo,Consolas,monospace;font-size:13px;letter-spacing:0.06em;padding:13px 22px;border:1.5px solid #141814;">${label}</a> |
| 62 |
|
− </p> |
| 63 |
|
− <p style="margin:0;font-size:12px;color:#6a6f6a;word-break:break-all;">Ou copiez ce lien : ${url}</p>`; |
|
136 |
+ return `<table role="presentation" cellpadding="0" cellspacing="0" style="margin:22px 0 10px;"> |
|
137 |
+ <tr> |
|
138 |
+ <td style="background:${INK};border-radius:999px;mso-padding-alt:14px 30px;"> |
|
139 |
+ <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> |
|
140 |
+ </td> |
|
141 |
+ </tr> |
|
142 |
+ </table> |
|
143 |
+ <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>`; |
|
144 |
+} |
|
145 |
+ |
|
146 |
+/** Pastille d'info mono (durée de validité, usage unique…). */ |
|
147 |
+function metaChips(items: string[]): string { |
|
148 |
+ 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>`; |
| 64 |
149 |
} |
| 65 |
150 |
|
|
151 |
+/* ---------- gabarits ---------- */ |
|
152 |
+ |
| 66 |
153 |
export function verificationEmail(name: string, url: string) { |
|
154 |
+ const inner = ` |
|
155 |
+ ${paragraph( |
|
156 |
+ `Bonjour <strong style="color:${INK};">${name}</strong>,<br> |
|
157 |
+ votre KA ID vient d'être créé — un seul compte pour toutes les |
|
158 |
+ plateformes du Groupe Ka. Dernière étape : confirmer que cette |
|
159 |
+ adresse est bien la vôtre. Vous serez connecté automatiquement.`, |
|
160 |
+ )} |
|
161 |
+ ${button(url, "Confirmer mon adresse")} |
|
162 |
+ ${metaChips(["Valable 24 heures", "Usage unique"])}`; |
| 67 |
163 |
return { |
| 68 |
164 |
subject: "Confirmez votre adresse — KA ID", |
| 69 |
|
− html: layout( |
| 70 |
|
− "Confirmez votre adresse courriel", |
| 71 |
|
− `<p style="margin:0;font-size:14.5px;line-height:1.6;">Bonjour ${name},<br> |
| 72 |
|
− votre KA ID vient d'être créé. Il ne reste qu'à confirmer que cette |
| 73 |
|
− adresse est bien la vôtre — le lien est valable 24 heures.</p> |
| 74 |
|
− ${button(url, "Confirmer mon adresse →")}`, |
| 75 |
|
− ), |
| 76 |
|
− text: `Bonjour ${name},\n\nConfirmez votre adresse pour activer votre KA ID (lien valable 24 h) :\n${url}\n\nSi vous n'êtes pas à l'origine de cette demande, ignorez ce courriel.`, |
|
165 |
+ html: layout({ |
|
166 |
+ kicker: "KA ID · Confirmation", |
|
167 |
+ title: "Une adresse à confirmer,<br>un écosystème à ouvrir", |
|
168 |
+ preheader: "Confirmez votre adresse pour activer votre KA ID — lien valable 24 heures.", |
|
169 |
+ inner, |
|
170 |
+ }), |
|
171 |
+ 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`, |
| 77 |
172 |
}; |
| 78 |
173 |
} |
| 79 |
174 |
|
| 80 |
175 |
export function magicLinkEmail(name: string, url: string) { |
|
176 |
+ const inner = ` |
|
177 |
+ ${paragraph( |
|
178 |
+ `Bonjour <strong style="color:${INK};">${name}</strong>,<br> |
|
179 |
+ vous avez demandé à vous connecter sans mot de passe. Un clic sur le |
|
180 |
+ bouton et vous êtes chez vous — sur toutes les plateformes du groupe.`, |
|
181 |
+ )} |
|
182 |
+ ${button(url, "Me connecter")} |
|
183 |
+ ${metaChips(["Valable 15 minutes", "Usage unique"])}`; |
| 81 |
184 |
return { |
| 82 |
185 |
subject: "Votre lien de connexion — KA ID", |
| 83 |
|
− html: layout( |
| 84 |
|
− "Connexion sans mot de passe", |
| 85 |
|
− `<p style="margin:0;font-size:14.5px;line-height:1.6;">Bonjour ${name},<br> |
| 86 |
|
− cliquez pour vous connecter à votre KA ID — aucun mot de passe requis. |
| 87 |
|
− Le lien est valable 15 minutes et ne sert qu'une fois.</p> |
| 88 |
|
− ${button(url, "Me connecter →")}`, |
| 89 |
|
− ), |
| 90 |
|
− 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.`, |
|
186 |
+ html: layout({ |
|
187 |
+ kicker: "KA ID · Connexion", |
|
188 |
+ title: "Votre lien de connexion,<br>sans mot de passe", |
|
189 |
+ preheader: "Un clic et vous êtes connecté — lien valable 15 minutes, usage unique.", |
|
190 |
+ inner, |
|
191 |
+ }), |
|
192 |
+ 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`, |
| 91 |
193 |
}; |
| 92 |
194 |
} |
| 93 |
195 |
|
| 94 |
196 |
export function loginCodeEmail(name: string, code: string) { |
|
197 |
+ // Panneau encre/lime — même langage que la carte de membre KA ID. |
|
198 |
+ const codePanel = `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:22px 0 0;"> |
|
199 |
+ <tr> |
|
200 |
+ <td align="center" style="background:${INK};border-radius:12px;padding:26px 18px 20px;"> |
|
201 |
+ <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> |
|
202 |
+ <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> |
|
203 |
+ <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> |
|
204 |
+ </td> |
|
205 |
+ </tr> |
|
206 |
+ </table>`; |
|
207 |
+ const inner = ` |
|
208 |
+ ${paragraph( |
|
209 |
+ `Bonjour <strong style="color:${INK};">${name}</strong>,<br> |
|
210 |
+ votre mot de passe est bon — il ne manque que ce code pour ouvrir la |
|
211 |
+ session. Saisissez-le sur la page de connexion :`, |
|
212 |
+ )} |
|
213 |
+ ${codePanel} |
|
214 |
+ <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>`; |
| 95 |
215 |
return { |
| 96 |
216 |
subject: `${code} — votre code de connexion KA ID`, |
| 97 |
|
− html: layout( |
| 98 |
|
− "Votre code de connexion", |
| 99 |
|
− `<p style="margin:0;font-size:14.5px;line-height:1.6;">Bonjour ${name},<br> |
| 100 |
|
− voici le code pour confirmer votre connexion. Il expire dans 10 minutes.</p> |
| 101 |
|
− <p style="margin:22px 0 0;font-family:Menlo,Consolas,monospace;font-size:32px;letter-spacing:0.35em;font-weight:bold;">${code}</p>`, |
| 102 |
|
− ), |
| 103 |
|
− text: `Bonjour ${name},\n\nVotre code de connexion KA ID (expire dans 10 minutes) : ${code}\n\nSi vous n'êtes pas à l'origine de cette connexion, changez votre mot de passe.`, |
|
217 |
+ html: layout({ |
|
218 |
+ kicker: "KA ID · Vérification", |
|
219 |
+ title: "Votre code de connexion", |
|
220 |
+ preheader: `Votre code : ${code} — il expire dans 10 minutes.`, |
|
221 |
+ inner, |
|
222 |
+ }), |
|
223 |
+ 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`, |
| 104 |
224 |
}; |
| 105 |
225 |
} |
| 106 |
226 |
|