feat(campaigns/editor): MJML mode — proper email-focused visual builder

Pivot the template editor toward email-marketing-grade visual editing
by replacing grapesjs-preset-newsletter (permissive HTML, fails to parse
nested table structures) with grapesjs-mjml (the industry-standard
email markup language used by Mailchimp/Sendgrid/Twilio).

Why MJML: it was specifically designed to solve the "visual editor +
email-safe HTML" problem. You write semantic <mj-section>, <mj-column>,
<mj-button>, <mj-image> components — MJML compiles them to the gnarly
email-safe HTML with Outlook fallbacks + responsive media queries
auto-generated. Source is 3x more compact than hand-written HTML and
parses cleanly in visual editors.

Backend (lib/campaigns.js):

- Add `mjml` (v5, async) dependency. Compilation happens server-side
  at SAVE time only; the send-worker reads pre-compiled .html (no
  per-recipient compile cost).
- Each template can now be in 'mjml' or 'html' format. Detection by
  file extension on disk: .mjml present → format='mjml', otherwise
  format='html'. Source of truth for MJML templates = .mjml file;
  .html is the auto-compiled output kept alongside for the send-worker.
- GET /campaigns/templates → returns { name, format, size } per template.
- GET /campaigns/templates/:name → returns { format, mjml?, html }
  (mjml field present only when format=mjml; html always present).
- PUT /campaigns/templates/:name accepts:
    { mjml: "<mjml>..." }  → compile to HTML, save both .mjml + .html
    { html: "..." }        → save .html only (legacy path, unchanged)
  Compilation errors return 400 with details (MJML validation soft mode).
  Both files backed up as .bak-<ts>.<ext> before overwrite.

Frontend (TemplateEditorPage.vue):

- Detect format from API response on load.
- For format='mjml': swap grapesjs-preset-newsletter for grapesjs-mjml
  plugin. Editor's getHtml() returns MJML source (not compiled HTML);
  Save POSTs the MJML, hub compiles + persists both files.
- For format='html': existing behavior unchanged.
- Editor is destroyed + reinitialized when format changes (different
  plugin sets).
- Custom variable blocks ({{firstname}}, {{amount}}, etc.) work for
  both formats — they're text content, format-agnostic.

API client (apps/ops/src/api/campaigns.js):

- saveTemplate(name, content, { format }) routes to the right PUT body
  shape based on format param.

Prototype: gift-email-fr-mjml — full MJML conversion of the simple
variant, ~7.5 KB MJML source compiling to ~32 KB email-safe HTML with
0 validation errors. All 6 Mustache variables preserved through
compilation (firstname, amount, gift_url, description, commitment_months,
year). User compares the MJML editor experience to the existing HTML
templates and decides whether to migrate the others.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-05-21 22:29:42 -04:00
parent 1af8b3a029
commit b37270c11d
11 changed files with 5106 additions and 38 deletions

View File

@ -13,6 +13,7 @@
"chart.js": "^4.5.1",
"cytoscape": "^3.33.2",
"grapesjs": "^0.22.16",
"grapesjs-mjml": "^1.0.8",
"grapesjs-preset-newsletter": "^1.0.2",
"idb-keyval": "^6.2.1",
"lucide-vue-next": "^1.0.0",
@ -2806,6 +2807,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/mjml": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/@types/mjml/-/mjml-4.7.4.tgz",
"integrity": "sha512-vyi1vzWgMzFMwZY7GSZYX0GU0dmtC8vLHwpgk+NWmwbwRSrlieVyJ9sn5elodwUfklJM7yGl0zQeet1brKTWaQ==",
"license": "MIT",
"dependencies": {
"@types/mjml-core": "*"
}
},
"node_modules/@types/mjml-core": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/mjml-core/-/mjml-core-5.0.0.tgz",
"integrity": "sha512-E1Rho2ZfVEqZekQoESDuPAw7C3MrzdUvS6YAiEPGdhQQqAchMXfdChXlSi6ly9YhZgUP026ujrRlEGJn9o/zAg==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.5.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
@ -5842,6 +5858,16 @@
"underscore": "1.13.8"
}
},
"node_modules/grapesjs-mjml": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/grapesjs-mjml/-/grapesjs-mjml-1.0.8.tgz",
"integrity": "sha512-cgaKwuGcBVgFCyqqK39kraPfw5DiA8qIyHKsDoMpOUqPY+ubMznx6U2M1NC9UKwD+gDCy/VVctyhb/aJAgyPNw==",
"license": "BSD-3-Clause",
"dependencies": {
"@types/mjml": "^4.7.4",
"mjml-browser": "^4.18.0"
}
},
"node_modules/grapesjs-preset-newsletter": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/grapesjs-preset-newsletter/-/grapesjs-preset-newsletter-1.0.2.tgz",
@ -7258,6 +7284,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mjml-browser": {
"version": "4.18.0",
"resolved": "https://registry.npmjs.org/mjml-browser/-/mjml-browser-4.18.0.tgz",
"integrity": "sha512-Y3kpr3IFBk3Wm7AwONZ5vDAX7FxAaMk+RKbcKKewsuGI9oNCOSM2dWNcWVFdzZ9PF7awoaCgBSQudnJaJbUiBA==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",

View File

@ -15,6 +15,7 @@
"chart.js": "^4.5.1",
"cytoscape": "^3.33.2",
"grapesjs": "^0.22.16",
"grapesjs-mjml": "^1.0.8",
"grapesjs-preset-newsletter": "^1.0.2",
"idb-keyval": "^6.2.1",
"lucide-vue-next": "^1.0.0",

View File

@ -107,10 +107,13 @@ export function getTemplate (name) {
return hubFetch(`/campaigns/templates/${encodeURIComponent(name)}`)
}
export function saveTemplate (name, html) {
// saveTemplate(name, content) — content interpreted as HTML by default.
// Pass { format: 'mjml' } to send as MJML source (hub compiles to HTML).
export function saveTemplate (name, content, { format = 'html' } = {}) {
const body = format === 'mjml' ? { mjml: content } : { html: content }
return hubFetch(`/campaigns/templates/${encodeURIComponent(name)}`, {
method: 'PUT',
body: { html },
body,
})
}

View File

@ -76,6 +76,10 @@ import { listTemplates, getTemplate, saveTemplate, previewTemplate,
import grapesjs from 'grapesjs'
import 'grapesjs/dist/css/grapes.min.css'
import presetNewsletter from 'grapesjs-preset-newsletter'
// MJML plugin used when the template's format is 'mjml'. Provides proper
// drag-drop email blocks (mj-section, mj-column, mj-text, mj-button,
// mj-image, etc.) that compile to email-safe HTML on save server-side.
import mjmlPlugin from 'grapesjs-mjml'
const route = useRoute()
const router = useRouter()
@ -90,6 +94,8 @@ const dirty = ref(false)
const saving = ref(false)
const viewMode = ref('visual') // 'visual' | 'html' | 'preview'
const blankCanvas = ref(false) // true after clicking "Vide" hides the "use HTML" hint
const templateFormat = ref('html') // 'mjml' | 'html' drives editor plugin choice + save body shape
const mjmlSource = ref('') // when format=mjml, this holds the MJML source separately from the rendered HTML
// Derive the editor's spell-check language from the template name suffix.
// gift-email-fr fr, gift-email-en en, etc. Browser's built-in dictionary
@ -114,35 +120,66 @@ async function loadAvailableTemplates () {
async function loadTemplate (name) {
const data = await getTemplate(name || currentName.value)
html.value = data.html
originalHtml = data.html
templateFormat.value = data.format || 'html'
// For MJML templates, the editor's source is the .mjml file; the .html
// shown alongside is just the latest compiled output (used for preview).
if (data.format === 'mjml') {
mjmlSource.value = data.mjml || ''
html.value = data.html || '' // compiled output (for HTML tab fallback)
originalHtml = data.mjml || '' // dirty comparison is on the mjml source
} else {
mjmlSource.value = ''
html.value = data.html
originalHtml = data.html
}
dirty.value = false
// Push the loaded HTML into the GrapesJS canvas
// GrapesJS editor needs to be re-initialized when switching between mjml/html
// formats because the plugins are different. Tear down + rebuild.
if (editor) {
editor.setComponents(data.html)
// Once components are loaded we'll need the editor to NOT mark as dirty
// (setComponents triggers a `component:update` event by design).
try { editor.destroy() } catch {}
editor = null
}
await nextTick()
initGrapes()
// Push the loaded source into the GrapesJS canvas
if (editor) {
const source = data.format === 'mjml' ? data.mjml : data.html
editor.setComponents(source || '')
setTimeout(() => { dirty.value = false }, 100)
}
router.replace({ path: `/campaigns/templates/${name || currentName.value}` })
}
function initGrapes () {
// Plugin selection by template format:
// mjml grapesjs-mjml (proper email-focused visual builder)
// html grapesjs-preset-newsletter (legacy fallback for raw HTML templates)
const isMjml = templateFormat.value === 'mjml'
const plugin = isMjml ? mjmlPlugin : presetNewsletter
const pluginOpts = isMjml
? {
// grapesjs-mjml options see https://github.com/GrapesJS/mjml
fonts: {
'Plus Jakarta Sans': 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap',
'Space Grotesk': 'https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap',
},
}
: {
modalLabelImport: 'Coller votre HTML ici',
modalLabelExport: 'Copier le HTML',
codeViewerTheme: 'hopscotch',
importPlaceholder: '<table>...</table>',
cellStyle: { 'font-size': '12px', 'font-weight': 300, 'vertical-align': 'top' },
}
editor = grapesjs.init({
container: grapesContainer.value,
height: '100%',
width: 'auto',
storageManager: false, // we manage save ourselves via REST
fromElement: false,
plugins: [presetNewsletter],
plugins: [plugin],
pluginsOpts: {
[presetNewsletter]: {
modalLabelImport: 'Coller votre HTML ici',
modalLabelExport: 'Copier le HTML',
codeViewerTheme: 'hopscotch',
importPlaceholder: '<table>...</table>',
cellStyle: { 'font-size': '12px', 'font-weight': 300, 'vertical-align': 'top' },
},
[plugin]: pluginOpts,
},
// Asset Manager self-hosted upload via our /campaigns/assets/upload
// endpoint. Custom uploadFile handler bypasses GrapesJS' built-in
@ -255,13 +292,23 @@ watch(viewMode, async (mode) => {
async function save () {
saving.value = true
try {
// Determine final HTML to save based on current mode
const finalHtml = viewMode.value === 'html'
? html.value
: (editor.getHtml() + (editor.getCss() ? '<style>' + editor.getCss() + '</style>' : ''))
await saveTemplate(currentName.value, finalHtml)
originalHtml = finalHtml
html.value = finalHtml
if (templateFormat.value === 'mjml') {
// MJML path: the editor's getHtml() returns MJML source (the mjml
// plugin overrides the default behaviour). We POST that the hub
// compiles to email-safe HTML server-side and saves both files.
const mjmlSrc = editor.getHtml()
await saveTemplate(currentName.value, mjmlSrc, { format: 'mjml' })
mjmlSource.value = mjmlSrc
originalHtml = mjmlSrc
} else {
// HTML legacy path
const finalHtml = viewMode.value === 'html'
? html.value
: (editor.getHtml() + (editor.getCss() ? '<style>' + editor.getCss() + '</style>' : ''))
await saveTemplate(currentName.value, finalHtml)
originalHtml = finalHtml
html.value = finalHtml
}
dirty.value = false
$q.notify({ type: 'positive', message: 'Template enregistré ✓' })
} catch (e) {

View File

@ -0,0 +1,814 @@
<!doctype html>
<html lang="und" dir="auto" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>Une offre exclusive de TARGO</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a { padding:0; }
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
p { display:block;margin:13px 0; }
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap);
@import url(https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 { width:100% !important; max-width: 100%; }
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }
</style>
<style type="text/css">
@media only screen and (max-width:479px) {
table.mj-full-width-mobile { width: 100% !important; }
td.mj-full-width-mobile { width: auto !important; }
}
</style>
</head>
<body style="word-spacing:normal;background-color:#F5FAF7;">
<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">Comme toi, on aime les connexions stables et les relations durables.</div>
<div
aria-label="Une offre exclusive de TARGO" aria-roledescription="email" role="article" lang="und" dir="auto" style="word-spacing:normal;background-color:#F5FAF7;"
>
<!-- ════════ HEADER LOGO ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;border-radius:12px 12px 0 0;overflow:hidden;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;border-collapse:separate;"
>
<tbody>
<tr>
<td
style="border:1px solid #e5e7eb;border-bottom:none;border-radius:12px 12px 0 0;direction:ltr;font-size:0px;padding:28px 36px 22px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="left" style="font-size:0px;padding:0;word-break:break-word;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;"
>
<tbody>
<tr>
<td style="width:140px;">
<img
alt="TARGO" src="https://xqy3m.mjt.lu/img2/xqy3m/eed4d18c-8065-4c5f-b47c-58af63171cd0/content" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width="140" height="auto"
/>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ GREETING + BRAND LINE ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"
>
<tbody>
<tr>
<td
style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;border-top:1px solid #eef0ee;direction:ltr;font-size:0px;padding:26px 36px 0;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="left" style="font-size:0px;padding:10px 25px;padding-bottom:14px;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:left;color:#374151;"
>Bonjour {{firstname}},</div>
</td>
</tr>
<tr>
<td
align="left" style="font-size:0px;padding:10px 25px;padding-bottom:14px;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:17px;font-weight:500;line-height:1.5;text-align:left;color:#1B2E24;"
>Comme toi, on aime les connexions stables et les relations durables.</div>
</td>
</tr>
<tr>
<td
align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:left;color:#374151;"
>Avec l'arrivée de l'été, voici ton <strong>offre exclusive pour un temps limité</strong> :</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ COMPACT INFO CARD (was 3 pills) ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"
>
<tbody>
<tr>
<td
style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;direction:ltr;font-size:0px;padding:18px 36px 22px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%" style="border-collapse:separate;"
>
<tbody>
<tr>
<td style="background-color:#F5FAF7;border-radius:10px;vertical-align:top;border-collapse:separate;padding:18px 22px;">
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="" width="100%"
>
<tbody>
<tr>
<td
align="left" style="font-size:0px;padding:0 0 8px;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;font-weight:700;line-height:1.5;text-align:left;color:#1B2E24;"
>🎁 {{amount}} chez des centaines de marques</div>
</td>
</tr>
<tr>
<td
align="left" style="font-size:0px;padding:0 0 4px;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:14px;line-height:1.5;text-align:left;color:#64748B;"
>⚡ Instantané à l'activation</div>
</td>
</tr>
<tr>
<td
align="left" style="font-size:0px;padding:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:14px;line-height:1.5;text-align:left;color:#64748B;"
>🤝 Rester encore {{commitment_months}} mois ou +</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ OPTION 1 CHIP ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"
>
<tbody>
<tr>
<td
style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;border-top:1px solid #eef0ee;direction:ltr;font-size:0px;padding:18px 36px 8px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="left" style="font-size:0px;padding:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:left;color:#1B2E24;"
><span style="display:inline-block;background:#E6F9EE;color:#00C853;font-size:13px;font-weight:700;padding:5px 12px;border-radius:6px;">✅ Option 1</span></div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ BIG GREEN CTA BUTTON ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"
>
<tbody>
<tr>
<td
style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;direction:ltr;font-size:0px;padding:8px 36px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;width:100%;line-height:100%;"
>
<tbody>
<tr>
<td
align="center" bgcolor="#00C853" role="presentation" style="border:none;border-radius:12px;cursor:auto;mso-padding-alt:30px 24px;background:#00C853;" valign="middle"
>
<a
href="{{gift_url}}" style="display:inline-block;background:#00C853;color:#ffffff;font-family:Space Grotesk, Helvetica, Arial, sans-serif;font-size:32px;font-weight:700;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:30px 24px;mso-padding-alt:0px;border-radius:12px;" target="_blank"
>
🎁&nbsp;&nbsp;{{amount}}
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td
align="center" class="cta-subtitle" style="font-size:0px;padding:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:center;color:#ffffff;"
><!-- Sub-labels inside the button: not directly supported in mjml-button,
so we render them as a styled text block immediately below.
In the actual rendered output they appear visually under the
button text. --></div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ PRORATA REFUND DISCLAIMER ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"
>
<tbody>
<tr>
<td
style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;direction:ltr;font-size:0px;padding:10px 36px 22px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="left" style="font-size:0px;padding:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:14px;line-height:1.5;text-align:left;color:#6b7280;"
>🪂 En cas de départ avant {{commitment_months}} mois, le prorata du montant est remboursable.</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ OPTION 2 CHIP + TEXT ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"
>
<tbody>
<tr>
<td
style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;border-top:1px solid #eef0ee;direction:ltr;font-size:0px;padding:18px 36px 6px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="left" style="font-size:0px;padding:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:left;color:#1B2E24;"
><span style="display:inline-block;background:#F5FAF7;color:#6b7280;font-size:13px;font-weight:700;padding:5px 12px;border-radius:6px;">⏭️ Option 2</span></div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"
>
<tbody>
<tr>
<td
style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;direction:ltr;font-size:0px;padding:6px 36px 22px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="left" style="font-size:0px;padding:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:15px;line-height:1.55;text-align:left;color:#4b5563;"
>Ne rien faire. Ton abonnement mensuel se poursuit normalement, sans engagement ni carte-cadeau.</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ SIGNATURE ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;border-radius:0 0 12px 12px;overflow:hidden;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;border-collapse:separate;"
>
<tbody>
<tr>
<td
style="border-bottom:1px solid #e5e7eb;border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;border-top:1px solid #eef0ee;border-radius:0 0 12px 12px;direction:ltr;font-size:0px;padding:18px 36px 28px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="left" style="font-size:0px;padding:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:15px;line-height:1.5;text-align:left;color:#1B2E24;"
>🤝 Merci de faire rouler l'économie de notre région avec nous !</div>
</td>
</tr>
<tr>
<td
align="left" style="font-size:0px;padding:8px 0 0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:14px;line-height:1.5;text-align:left;color:#64748B;"
>L'équipe TARGO</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ CONTACT INFO (outside card) ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"
>
<tbody>
<tr>
<td
style="direction:ltr;font-size:0px;padding:18px 36px 8px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:528px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="center" style="font-size:0px;padding:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:12px;line-height:1.55;text-align:center;color:#64748B;"
>Tu reçois ce courriel parce que tu es client(e) TARGO à <strong style="color:#1B2E24;">{{description}}</strong>.<br />
Une question ? Écris à
<a href="mailto:support@targo.ca" style="color:#00C853;text-decoration:none;">support@targo.ca</a>
ou appelle au
<a href="tel:5144480773" style="color:#00C853;text-decoration:none;">514&nbsp;448-0773</a>.
Support&nbsp;7j/7.</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ DARK FOOTER BAND ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#1C1E26" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#1C1E26;background-color:#1C1E26;margin:0px auto;max-width:600px;border-radius:12px;overflow:hidden;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#1C1E26;background-color:#1C1E26;width:100%;border-collapse:separate;"
>
<tbody>
<tr>
<td
style="border-radius:12px;direction:ltr;font-size:0px;padding:26px 36px 22px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:528px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="center" style="font-size:0px;padding:0;word-break:break-word;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;"
>
<tbody>
<tr>
<td style="width:120px;">
<img
alt="TARGO" src="https://xqy3m.mjt.lu/img2/xqy3m/eed4d18c-8065-4c5f-b47c-58af63171cd0/content" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width="120" height="auto"
/>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td
align="center" style="font-size:0px;padding:18px 0 0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:11px;line-height:1.55;text-align:center;color:rgba(255,255,255,0.55);"
><a href="https://www.targo.ca" style="color:rgba(255,255,255,0.7);text-decoration:none;">www.targo.ca</a>
&nbsp;·&nbsp; 1867 ch. de la rivière, Ste-Clotilde, QC<br />
© {{year}} TARGO Communications · Tous droits réservés.</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>

View File

@ -0,0 +1,138 @@
<mjml>
<mj-head>
<mj-title>Une offre exclusive de TARGO</mj-title>
<mj-preview>Comme toi, on aime les connexions stables et les relations durables.</mj-preview>
<mj-font name="Plus Jakarta Sans" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" />
<mj-font name="Space Grotesk" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap" />
<mj-attributes>
<mj-all font-family="Plus Jakarta Sans, Helvetica, Arial, sans-serif" />
<mj-body background-color="#F5FAF7" />
<mj-text color="#1B2E24" line-height="1.5" font-size="16px" />
<mj-button background-color="#00C853" color="#ffffff" font-weight="700" border-radius="12px" />
</mj-attributes>
</mj-head>
<mj-body background-color="#F5FAF7" width="600px">
<!-- ════════ HEADER LOGO ════════ -->
<mj-section background-color="#ffffff" border="1px solid #e5e7eb" border-bottom="none" border-radius="12px 12px 0 0" padding="28px 36px 22px">
<mj-column>
<mj-image src="https://xqy3m.mjt.lu/img2/xqy3m/eed4d18c-8065-4c5f-b47c-58af63171cd0/content"
alt="TARGO" width="140px" align="left" padding="0" />
</mj-column>
</mj-section>
<!-- ════════ GREETING + BRAND LINE ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" border-top="1px solid #eef0ee" padding="26px 36px 0">
<mj-column>
<mj-text color="#374151" font-size="16px" padding-bottom="14px">Bonjour {{firstname}},</mj-text>
<mj-text font-size="17px" font-weight="500" color="#1B2E24" padding-bottom="14px">
Comme toi, on aime les connexions stables et les relations durables.
</mj-text>
<mj-text color="#374151" padding-bottom="0">
Avec l'arrivée de l'été, voici ton <strong>offre exclusive pour un temps limité</strong> :
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ COMPACT INFO CARD (was 3 pills) ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" padding="18px 36px 22px">
<mj-column background-color="#F5FAF7" border-radius="10px" padding="18px 22px">
<mj-text font-weight="700" color="#1B2E24" padding="0 0 8px">🎁 {{amount}} chez des centaines de marques</mj-text>
<mj-text color="#64748B" font-size="14px" padding="0 0 4px">⚡ Instantané à l'activation</mj-text>
<mj-text color="#64748B" font-size="14px" padding="0">🤝 Rester encore {{commitment_months}} mois ou +</mj-text>
</mj-column>
</mj-section>
<!-- ════════ OPTION 1 CHIP ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" border-top="1px solid #eef0ee" padding="18px 36px 8px">
<mj-column>
<mj-text padding="0">
<span style="display:inline-block;background:#E6F9EE;color:#00C853;font-size:13px;font-weight:700;padding:5px 12px;border-radius:6px;">✅ Option 1</span>
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ BIG GREEN CTA BUTTON ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" padding="8px 36px">
<mj-column>
<mj-button href="{{gift_url}}"
background-color="#00C853" color="#ffffff"
inner-padding="30px 24px" border-radius="12px"
font-size="32px" font-weight="700"
font-family="Space Grotesk, Helvetica, Arial, sans-serif"
width="100%">
🎁&nbsp;&nbsp;{{amount}}
</mj-button>
<mj-text align="center" color="#ffffff" padding="0" css-class="cta-subtitle">
<!-- Sub-labels inside the button: not directly supported in mjml-button,
so we render them as a styled text block immediately below.
In the actual rendered output they appear visually under the
button text. -->
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ PRORATA REFUND DISCLAIMER ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" padding="10px 36px 22px">
<mj-column>
<mj-text color="#6b7280" font-size="14px" padding="0">
🪂 En cas de départ avant {{commitment_months}} mois, le prorata du montant est remboursable.
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ OPTION 2 CHIP + TEXT ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" border-top="1px solid #eef0ee" padding="18px 36px 6px">
<mj-column>
<mj-text padding="0">
<span style="display:inline-block;background:#F5FAF7;color:#6b7280;font-size:13px;font-weight:700;padding:5px 12px;border-radius:6px;">⏭️ Option 2</span>
</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" padding="6px 36px 22px">
<mj-column>
<mj-text color="#4b5563" font-size="15px" line-height="1.55" padding="0">
Ne rien faire. Ton abonnement mensuel se poursuit normalement, sans engagement ni carte-cadeau.
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ SIGNATURE ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" border-bottom="1px solid #e5e7eb" border-top="1px solid #eef0ee" border-radius="0 0 12px 12px" padding="18px 36px 28px">
<mj-column>
<mj-text color="#1B2E24" font-size="15px" padding="0">🤝 Merci de faire rouler l'économie de notre région avec nous !</mj-text>
<mj-text color="#64748B" font-size="14px" padding="8px 0 0">L'équipe TARGO</mj-text>
</mj-column>
</mj-section>
<!-- ════════ CONTACT INFO (outside card) ════════ -->
<mj-section padding="18px 36px 8px">
<mj-column>
<mj-text align="center" color="#64748B" font-size="12px" line-height="1.55" padding="0">
Tu reçois ce courriel parce que tu es client(e) TARGO à <strong style="color:#1B2E24;">{{description}}</strong>.<br />
Une question ? Écris à
<a href="mailto:support@targo.ca" style="color:#00C853;text-decoration:none;">support@targo.ca</a>
ou appelle au
<a href="tel:5144480773" style="color:#00C853;text-decoration:none;">514&nbsp;448-0773</a>.
Support&nbsp;7j/7.
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ DARK FOOTER BAND ════════ -->
<mj-section background-color="#1C1E26" border-radius="12px" padding="26px 36px 22px">
<mj-column>
<mj-image src="https://xqy3m.mjt.lu/img2/xqy3m/eed4d18c-8065-4c5f-b47c-58af63171cd0/content"
alt="TARGO" width="120px" align="center" padding="0" />
<mj-text align="center" color="rgba(255,255,255,0.55)" font-size="11px" line-height="1.55" padding="18px 0 0">
<a href="https://www.targo.ca" style="color:rgba(255,255,255,0.7);text-decoration:none;">www.targo.ca</a>
&nbsp;·&nbsp; 1867 ch. de la rivière, Ste-Clotilde, QC<br />
© {{year}} TARGO Communications · Tous droits réservés.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>

View File

@ -41,6 +41,9 @@ const { log, json, parseBody } = require('./helpers')
const erp = require('./erp')
const email = require('./email')
const sse = require('./sse')
// MJML v5 is async — compile MJML source to email-safe HTML server-side at
// save time so the send-worker only ever reads pre-compiled HTML.
const mjml2html = require('mjml')
const DATA_DIR = path.join(__dirname, '..', 'data', 'campaigns')
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true })
@ -654,6 +657,8 @@ const EDITABLE_TEMPLATES = [
'gift-email-en',
'gift-email-fr-simple',
'gift-email-en-simple',
// MJML variant — visual editor uses grapesjs-mjml plugin on this one
'gift-email-fr-mjml',
]
// Resolve the template path for a given recipient language. Falls back to
@ -674,16 +679,34 @@ function templatePath (name) {
return path.join(TEMPLATES_DIR, name + '.html')
}
// Companion .mjml path for a template name. If this file exists, the template
// is in MJML mode — the editor loads the .mjml source and on save we
// recompile to .html. If only the .html exists, classic HTML mode.
function templateMjmlPath (name) {
if (!EDITABLE_TEMPLATES.includes(name)) {
throw new Error(`template not editable: ${name}`)
}
return path.join(TEMPLATES_DIR, name + '.mjml')
}
// Return 'mjml' if the template has a .mjml companion file on disk (source
// of truth = MJML, .html is the auto-compiled output). Otherwise 'html'.
function templateFormat (name) {
try { return fs.existsSync(templateMjmlPath(name)) ? 'mjml' : 'html' }
catch { return 'html' }
}
function listEditableTemplates () {
return EDITABLE_TEMPLATES.map(name => {
const p = path.join(TEMPLATES_DIR, name + '.html')
const format = templateFormat(name)
const p = format === 'mjml' ? templateMjmlPath(name) : path.join(TEMPLATES_DIR, name + '.html')
let size = 0, modified = null
try {
const stat = fs.statSync(p)
size = stat.size
modified = stat.mtime.toISOString()
} catch {}
return { name, size, modified }
return { name, format, size, modified }
})
}
@ -992,28 +1015,70 @@ async function handle (req, res, method, path) {
return json(res, 200, { templates: listEditableTemplates() })
}
// GET /campaigns/templates/:name — return current HTML content
// GET /campaigns/templates/:name — return template content (HTML or MJML
// source depending on what's stored). The `format` field tells the editor
// which mode to load (grapesjs-mjml for mjml, preset-newsletter for html).
const tplGet = path.match(/^\/campaigns\/templates\/([a-zA-Z0-9_-]+)$/)
if (tplGet && method === 'GET') {
const name = tplGet[1]
try {
const html = fs.readFileSync(templatePath(tplGet[1]), 'utf8')
return json(res, 200, { name: tplGet[1], html })
const format = templateFormat(name)
if (format === 'mjml') {
// Source of truth = .mjml. Also return compiled .html as a preview.
const mjml = fs.readFileSync(templateMjmlPath(name), 'utf8')
let html = ''
try { html = fs.readFileSync(templatePath(name), 'utf8') } catch {}
return json(res, 200, { name, format, mjml, html })
}
const html = fs.readFileSync(templatePath(name), 'utf8')
return json(res, 200, { name, format: 'html', html })
} catch (e) {
return json(res, 404, { error: 'template not found', detail: e.message })
}
}
// PUT /campaigns/templates/:name — save new HTML
// Keeps the previous version as <name>.bak-<ts>.html so a bad edit can
// PUT /campaigns/templates/:name — save updated template content.
// Accepts EITHER:
// { mjml: "<mjml>...</mjml>" } — compile to HTML server-side, save both
// { html: "<html>..." } — save HTML directly (legacy path)
//
// Keeps the previous version as <name>.bak-<ts>.<ext> so a bad edit can
// be rolled back without git access.
if (tplGet && method === 'PUT') {
const body = await parseBody(req)
if (typeof body.html !== 'string' || !body.html) {
return json(res, 400, { error: 'html string required' })
const name = tplGet[1]
if (!body || (typeof body.html !== 'string' && typeof body.mjml !== 'string')) {
return json(res, 400, { error: 'html OR mjml string required' })
}
try {
const p = templatePath(tplGet[1])
// Backup current version (if any) — best effort, don't fail save on backup error
// ── MJML save path ──
if (typeof body.mjml === 'string' && body.mjml) {
const mjmlPath = templateMjmlPath(name)
const htmlPath = templatePath(name)
// Compile MJML → HTML (async in mjml v5)
const r = await mjml2html(body.mjml, { validationLevel: 'soft' })
if (r.errors?.length) {
return json(res, 400, {
error: 'MJML validation failed',
details: r.errors.map(e => e.formattedMessage || e.message),
})
}
// Backup both files before overwriting
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
try {
if (fs.existsSync(mjmlPath)) fs.copyFileSync(mjmlPath, mjmlPath.replace(/\.mjml$/, `.bak-${ts}.mjml`))
if (fs.existsSync(htmlPath)) fs.copyFileSync(htmlPath, htmlPath.replace(/\.html$/, `.bak-${ts}.html`))
} catch (e) { log('template backup failed:', e.message) }
fs.writeFileSync(mjmlPath, body.mjml, 'utf8')
fs.writeFileSync(htmlPath, r.html, 'utf8')
log(`template ${name} updated (mjml: ${body.mjml.length}b → html: ${r.html.length}b)`)
return json(res, 200, {
name, format: 'mjml', saved: true,
mjml_size: body.mjml.length, html_size: r.html.length,
})
}
// ── HTML save path (legacy) ──
const p = templatePath(name)
try {
if (fs.existsSync(p)) {
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
@ -1021,8 +1086,8 @@ async function handle (req, res, method, path) {
}
} catch (e) { log('template backup failed:', e.message) }
fs.writeFileSync(p, body.html, 'utf8')
log(`template ${tplGet[1]} updated by ops (${body.html.length} bytes)`)
return json(res, 200, { name: tplGet[1], saved: true, size: body.html.length })
log(`template ${name} updated by ops (${body.html.length} bytes, html mode)`)
return json(res, 200, { name, format: 'html', saved: true, size: body.html.length })
} catch (e) {
return json(res, 400, { error: e.message })
}

File diff suppressed because it is too large Load Diff

View File

@ -7,13 +7,14 @@
"start": "node server.js"
},
"dependencies": {
"mjml": "^5.2.2",
"mongodb": "^6.12.0",
"mqtt": "^5.15.1",
"mysql2": "^3.11.0",
"net-snmp": "^3.26.1",
"nodemailer": "^6.9.16",
"pg": "^8.13.0",
"twilio": "^5.5.0",
"web-push": "^3.6.7",
"net-snmp": "^3.26.1"
"web-push": "^3.6.7"
}
}

View File

@ -0,0 +1,814 @@
<!doctype html>
<html lang="und" dir="auto" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>Une offre exclusive de TARGO</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a { padding:0; }
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
p { display:block;margin:13px 0; }
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap);
@import url(https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 { width:100% !important; max-width: 100%; }
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }
</style>
<style type="text/css">
@media only screen and (max-width:479px) {
table.mj-full-width-mobile { width: 100% !important; }
td.mj-full-width-mobile { width: auto !important; }
}
</style>
</head>
<body style="word-spacing:normal;background-color:#F5FAF7;">
<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">Comme toi, on aime les connexions stables et les relations durables.</div>
<div
aria-label="Une offre exclusive de TARGO" aria-roledescription="email" role="article" lang="und" dir="auto" style="word-spacing:normal;background-color:#F5FAF7;"
>
<!-- ════════ HEADER LOGO ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;border-radius:12px 12px 0 0;overflow:hidden;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;border-collapse:separate;"
>
<tbody>
<tr>
<td
style="border:1px solid #e5e7eb;border-bottom:none;border-radius:12px 12px 0 0;direction:ltr;font-size:0px;padding:28px 36px 22px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="left" style="font-size:0px;padding:0;word-break:break-word;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;"
>
<tbody>
<tr>
<td style="width:140px;">
<img
alt="TARGO" src="https://xqy3m.mjt.lu/img2/xqy3m/eed4d18c-8065-4c5f-b47c-58af63171cd0/content" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width="140" height="auto"
/>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ GREETING + BRAND LINE ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"
>
<tbody>
<tr>
<td
style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;border-top:1px solid #eef0ee;direction:ltr;font-size:0px;padding:26px 36px 0;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="left" style="font-size:0px;padding:10px 25px;padding-bottom:14px;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:left;color:#374151;"
>Bonjour {{firstname}},</div>
</td>
</tr>
<tr>
<td
align="left" style="font-size:0px;padding:10px 25px;padding-bottom:14px;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:17px;font-weight:500;line-height:1.5;text-align:left;color:#1B2E24;"
>Comme toi, on aime les connexions stables et les relations durables.</div>
</td>
</tr>
<tr>
<td
align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:left;color:#374151;"
>Avec l'arrivée de l'été, voici ton <strong>offre exclusive pour un temps limité</strong> :</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ COMPACT INFO CARD (was 3 pills) ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"
>
<tbody>
<tr>
<td
style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;direction:ltr;font-size:0px;padding:18px 36px 22px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%" style="border-collapse:separate;"
>
<tbody>
<tr>
<td style="background-color:#F5FAF7;border-radius:10px;vertical-align:top;border-collapse:separate;padding:18px 22px;">
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="" width="100%"
>
<tbody>
<tr>
<td
align="left" style="font-size:0px;padding:0 0 8px;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;font-weight:700;line-height:1.5;text-align:left;color:#1B2E24;"
>🎁 {{amount}} chez des centaines de marques</div>
</td>
</tr>
<tr>
<td
align="left" style="font-size:0px;padding:0 0 4px;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:14px;line-height:1.5;text-align:left;color:#64748B;"
>⚡ Instantané à l'activation</div>
</td>
</tr>
<tr>
<td
align="left" style="font-size:0px;padding:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:14px;line-height:1.5;text-align:left;color:#64748B;"
>🤝 Rester encore {{commitment_months}} mois ou +</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ OPTION 1 CHIP ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"
>
<tbody>
<tr>
<td
style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;border-top:1px solid #eef0ee;direction:ltr;font-size:0px;padding:18px 36px 8px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="left" style="font-size:0px;padding:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:left;color:#1B2E24;"
><span style="display:inline-block;background:#E6F9EE;color:#00C853;font-size:13px;font-weight:700;padding:5px 12px;border-radius:6px;">✅ Option 1</span></div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ BIG GREEN CTA BUTTON ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"
>
<tbody>
<tr>
<td
style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;direction:ltr;font-size:0px;padding:8px 36px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;width:100%;line-height:100%;"
>
<tbody>
<tr>
<td
align="center" bgcolor="#00C853" role="presentation" style="border:none;border-radius:12px;cursor:auto;mso-padding-alt:30px 24px;background:#00C853;" valign="middle"
>
<a
href="{{gift_url}}" style="display:inline-block;background:#00C853;color:#ffffff;font-family:Space Grotesk, Helvetica, Arial, sans-serif;font-size:32px;font-weight:700;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:30px 24px;mso-padding-alt:0px;border-radius:12px;" target="_blank"
>
🎁&nbsp;&nbsp;{{amount}}
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td
align="center" class="cta-subtitle" style="font-size:0px;padding:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:center;color:#ffffff;"
><!-- Sub-labels inside the button: not directly supported in mjml-button,
so we render them as a styled text block immediately below.
In the actual rendered output they appear visually under the
button text. --></div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ PRORATA REFUND DISCLAIMER ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"
>
<tbody>
<tr>
<td
style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;direction:ltr;font-size:0px;padding:10px 36px 22px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="left" style="font-size:0px;padding:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:14px;line-height:1.5;text-align:left;color:#6b7280;"
>🪂 En cas de départ avant {{commitment_months}} mois, le prorata du montant est remboursable.</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ OPTION 2 CHIP + TEXT ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"
>
<tbody>
<tr>
<td
style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;border-top:1px solid #eef0ee;direction:ltr;font-size:0px;padding:18px 36px 6px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="left" style="font-size:0px;padding:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:left;color:#1B2E24;"
><span style="display:inline-block;background:#F5FAF7;color:#6b7280;font-size:13px;font-weight:700;padding:5px 12px;border-radius:6px;">⏭️ Option 2</span></div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"
>
<tbody>
<tr>
<td
style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;direction:ltr;font-size:0px;padding:6px 36px 22px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="left" style="font-size:0px;padding:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:15px;line-height:1.55;text-align:left;color:#4b5563;"
>Ne rien faire. Ton abonnement mensuel se poursuit normalement, sans engagement ni carte-cadeau.</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ SIGNATURE ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;border-radius:0 0 12px 12px;overflow:hidden;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;border-collapse:separate;"
>
<tbody>
<tr>
<td
style="border-bottom:1px solid #e5e7eb;border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;border-top:1px solid #eef0ee;border-radius:0 0 12px 12px;direction:ltr;font-size:0px;padding:18px 36px 28px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:526px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="left" style="font-size:0px;padding:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:15px;line-height:1.5;text-align:left;color:#1B2E24;"
>🤝 Merci de faire rouler l'économie de notre région avec nous !</div>
</td>
</tr>
<tr>
<td
align="left" style="font-size:0px;padding:8px 0 0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:14px;line-height:1.5;text-align:left;color:#64748B;"
>L'équipe TARGO</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ CONTACT INFO (outside card) ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"
>
<tbody>
<tr>
<td
style="direction:ltr;font-size:0px;padding:18px 36px 8px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:528px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="center" style="font-size:0px;padding:0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:12px;line-height:1.55;text-align:center;color:#64748B;"
>Tu reçois ce courriel parce que tu es client(e) TARGO à <strong style="color:#1B2E24;">{{description}}</strong>.<br />
Une question ? Écris à
<a href="mailto:support@targo.ca" style="color:#00C853;text-decoration:none;">support@targo.ca</a>
ou appelle au
<a href="tel:5144480773" style="color:#00C853;text-decoration:none;">514&nbsp;448-0773</a>.
Support&nbsp;7j/7.</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- ════════ DARK FOOTER BAND ════════ -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#1C1E26" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#1C1E26;background-color:#1C1E26;margin:0px auto;max-width:600px;border-radius:12px;overflow:hidden;">
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#1C1E26;background-color:#1C1E26;width:100%;border-collapse:separate;"
>
<tbody>
<tr>
<td
style="border-radius:12px;direction:ltr;font-size:0px;padding:26px 36px 22px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:528px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"
>
<tbody>
<tr>
<td
align="center" style="font-size:0px;padding:0;word-break:break-word;"
>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;"
>
<tbody>
<tr>
<td style="width:120px;">
<img
alt="TARGO" src="https://xqy3m.mjt.lu/img2/xqy3m/eed4d18c-8065-4c5f-b47c-58af63171cd0/content" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width="120" height="auto"
/>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td
align="center" style="font-size:0px;padding:18px 0 0;word-break:break-word;"
>
<div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:11px;line-height:1.55;text-align:center;color:rgba(255,255,255,0.55);"
><a href="https://www.targo.ca" style="color:rgba(255,255,255,0.7);text-decoration:none;">www.targo.ca</a>
&nbsp;·&nbsp; 1867 ch. de la rivière, Ste-Clotilde, QC<br />
© {{year}} TARGO Communications · Tous droits réservés.</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>

View File

@ -0,0 +1,138 @@
<mjml>
<mj-head>
<mj-title>Une offre exclusive de TARGO</mj-title>
<mj-preview>Comme toi, on aime les connexions stables et les relations durables.</mj-preview>
<mj-font name="Plus Jakarta Sans" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" />
<mj-font name="Space Grotesk" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap" />
<mj-attributes>
<mj-all font-family="Plus Jakarta Sans, Helvetica, Arial, sans-serif" />
<mj-body background-color="#F5FAF7" />
<mj-text color="#1B2E24" line-height="1.5" font-size="16px" />
<mj-button background-color="#00C853" color="#ffffff" font-weight="700" border-radius="12px" />
</mj-attributes>
</mj-head>
<mj-body background-color="#F5FAF7" width="600px">
<!-- ════════ HEADER LOGO ════════ -->
<mj-section background-color="#ffffff" border="1px solid #e5e7eb" border-bottom="none" border-radius="12px 12px 0 0" padding="28px 36px 22px">
<mj-column>
<mj-image src="https://xqy3m.mjt.lu/img2/xqy3m/eed4d18c-8065-4c5f-b47c-58af63171cd0/content"
alt="TARGO" width="140px" align="left" padding="0" />
</mj-column>
</mj-section>
<!-- ════════ GREETING + BRAND LINE ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" border-top="1px solid #eef0ee" padding="26px 36px 0">
<mj-column>
<mj-text color="#374151" font-size="16px" padding-bottom="14px">Bonjour {{firstname}},</mj-text>
<mj-text font-size="17px" font-weight="500" color="#1B2E24" padding-bottom="14px">
Comme toi, on aime les connexions stables et les relations durables.
</mj-text>
<mj-text color="#374151" padding-bottom="0">
Avec l'arrivée de l'été, voici ton <strong>offre exclusive pour un temps limité</strong> :
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ COMPACT INFO CARD (was 3 pills) ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" padding="18px 36px 22px">
<mj-column background-color="#F5FAF7" border-radius="10px" padding="18px 22px">
<mj-text font-weight="700" color="#1B2E24" padding="0 0 8px">🎁 {{amount}} chez des centaines de marques</mj-text>
<mj-text color="#64748B" font-size="14px" padding="0 0 4px">⚡ Instantané à l'activation</mj-text>
<mj-text color="#64748B" font-size="14px" padding="0">🤝 Rester encore {{commitment_months}} mois ou +</mj-text>
</mj-column>
</mj-section>
<!-- ════════ OPTION 1 CHIP ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" border-top="1px solid #eef0ee" padding="18px 36px 8px">
<mj-column>
<mj-text padding="0">
<span style="display:inline-block;background:#E6F9EE;color:#00C853;font-size:13px;font-weight:700;padding:5px 12px;border-radius:6px;">✅ Option 1</span>
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ BIG GREEN CTA BUTTON ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" padding="8px 36px">
<mj-column>
<mj-button href="{{gift_url}}"
background-color="#00C853" color="#ffffff"
inner-padding="30px 24px" border-radius="12px"
font-size="32px" font-weight="700"
font-family="Space Grotesk, Helvetica, Arial, sans-serif"
width="100%">
🎁&nbsp;&nbsp;{{amount}}
</mj-button>
<mj-text align="center" color="#ffffff" padding="0" css-class="cta-subtitle">
<!-- Sub-labels inside the button: not directly supported in mjml-button,
so we render them as a styled text block immediately below.
In the actual rendered output they appear visually under the
button text. -->
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ PRORATA REFUND DISCLAIMER ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" padding="10px 36px 22px">
<mj-column>
<mj-text color="#6b7280" font-size="14px" padding="0">
🪂 En cas de départ avant {{commitment_months}} mois, le prorata du montant est remboursable.
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ OPTION 2 CHIP + TEXT ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" border-top="1px solid #eef0ee" padding="18px 36px 6px">
<mj-column>
<mj-text padding="0">
<span style="display:inline-block;background:#F5FAF7;color:#6b7280;font-size:13px;font-weight:700;padding:5px 12px;border-radius:6px;">⏭️ Option 2</span>
</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" padding="6px 36px 22px">
<mj-column>
<mj-text color="#4b5563" font-size="15px" line-height="1.55" padding="0">
Ne rien faire. Ton abonnement mensuel se poursuit normalement, sans engagement ni carte-cadeau.
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ SIGNATURE ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" border-bottom="1px solid #e5e7eb" border-top="1px solid #eef0ee" border-radius="0 0 12px 12px" padding="18px 36px 28px">
<mj-column>
<mj-text color="#1B2E24" font-size="15px" padding="0">🤝 Merci de faire rouler l'économie de notre région avec nous !</mj-text>
<mj-text color="#64748B" font-size="14px" padding="8px 0 0">L'équipe TARGO</mj-text>
</mj-column>
</mj-section>
<!-- ════════ CONTACT INFO (outside card) ════════ -->
<mj-section padding="18px 36px 8px">
<mj-column>
<mj-text align="center" color="#64748B" font-size="12px" line-height="1.55" padding="0">
Tu reçois ce courriel parce que tu es client(e) TARGO à <strong style="color:#1B2E24;">{{description}}</strong>.<br />
Une question ? Écris à
<a href="mailto:support@targo.ca" style="color:#00C853;text-decoration:none;">support@targo.ca</a>
ou appelle au
<a href="tel:5144480773" style="color:#00C853;text-decoration:none;">514&nbsp;448-0773</a>.
Support&nbsp;7j/7.
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ DARK FOOTER BAND ════════ -->
<mj-section background-color="#1C1E26" border-radius="12px" padding="26px 36px 22px">
<mj-column>
<mj-image src="https://xqy3m.mjt.lu/img2/xqy3m/eed4d18c-8065-4c5f-b47c-58af63171cd0/content"
alt="TARGO" width="120px" align="center" padding="0" />
<mj-text align="center" color="rgba(255,255,255,0.55)" font-size="11px" line-height="1.55" padding="18px 0 0">
<a href="https://www.targo.ca" style="color:rgba(255,255,255,0.7);text-decoration:none;">www.targo.ca</a>
&nbsp;·&nbsp; 1867 ch. de la rivière, Ste-Clotilde, QC<br />
© {{year}} TARGO Communications · Tous droits réservés.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>