Compare commits
No commits in common. "2bc971548573305c547a97401bc09e7e1aee3e25" and "9b06e2df3018a7a01fc176d8dcb804fa555e76aa" have entirely different histories.
2bc9715485
...
9b06e2df30
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -43,7 +43,3 @@ scripts/migration/ref_invoice.pdf
|
||||||
|
|
||||||
# Playwright snapshots
|
# Playwright snapshots
|
||||||
.playwright-mcp/
|
.playwright-mcp/
|
||||||
|
|
||||||
# Auto-generated backups from scripts/convert-html-to-unlayer.js
|
|
||||||
services/targo-hub/templates/*.bak-*.json
|
|
||||||
services/targo-hub/templates/*.bak-*.html
|
|
||||||
|
|
|
||||||
146
apps/ops/package-lock.json
generated
146
apps/ops/package-lock.json
generated
|
|
@ -12,9 +12,6 @@
|
||||||
"@twilio/voice-sdk": "^2.18.1",
|
"@twilio/voice-sdk": "^2.18.1",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"cytoscape": "^3.33.2",
|
"cytoscape": "^3.33.2",
|
||||||
"grapesjs": "^0.22.16",
|
|
||||||
"grapesjs-mjml": "^1.0.8",
|
|
||||||
"grapesjs-preset-newsletter": "^1.0.2",
|
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"lucide-vue-next": "^1.0.0",
|
"lucide-vue-next": "^1.0.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
|
|
@ -22,7 +19,6 @@
|
||||||
"sip.js": "^0.21.2",
|
"sip.js": "^0.21.2",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-chartjs": "^5.3.3",
|
"vue-chartjs": "^5.3.3",
|
||||||
"vue-email-editor": "^2.2.0",
|
|
||||||
"vue-router": "^4.3.0",
|
"vue-router": "^4.3.0",
|
||||||
"vuedraggable": "^4.1.0"
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
|
|
@ -2665,16 +2661,6 @@
|
||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/backbone": {
|
|
||||||
"version": "1.4.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/backbone/-/backbone-1.4.15.tgz",
|
|
||||||
"integrity": "sha512-WWeKtYlsIMtDyLbbhkb96taJMEbfQBnuz7yw1u0pkphCOtksemoWhIXhK74VRCY9hbjnsH3rsJu2uUiFtnsEYg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/jquery": "*",
|
|
||||||
"@types/underscore": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/body-parser": {
|
"node_modules/@types/body-parser": {
|
||||||
"version": "1.19.6",
|
"version": "1.19.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||||
|
|
@ -2795,12 +2781,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/jquery": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-Z+to+A2VkaHq1DfI2oSwsoCdhCHMpTSgjWzNcbNlRGYzksDBpPUgEcAL+RQjOBJRaLoEAOHXxqDGBVP+BblBwg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/mime": {
|
"node_modules/@types/mime": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||||
|
|
@ -2808,21 +2788,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/node": {
|
||||||
"version": "25.5.0",
|
"version": "25.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
|
||||||
|
|
@ -2897,12 +2862,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/underscore": {
|
|
||||||
"version": "1.13.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.13.0.tgz",
|
|
||||||
"integrity": "sha512-L6LBgy1f0EFQZ+7uSA57+n2g/s4Qs5r06Vwrwn0/nuK1de+adz00NWaztRQ30aEqw5qOaWbPI8u2cGQ52lj6VA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@ungap/structured-clone": {
|
"node_modules/@ungap/structured-clone": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||||
|
|
@ -2910,12 +2869,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/@unlayer/types": {
|
|
||||||
"version": "1.413.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@unlayer/types/-/types-1.413.0.tgz",
|
|
||||||
"integrity": "sha512-pOE9lKvP7ofnmfWZN+PTizw2GrwZNtePiMH3Yl8OSt/nYQL52X7N4SHd7dDd2c7ecJwWVWo8MfPY8QTon+44lw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@vitejs/plugin-vue": {
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
"version": "2.3.4",
|
"version": "2.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.3.4.tgz",
|
||||||
|
|
@ -3420,26 +3373,6 @@
|
||||||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/backbone": {
|
|
||||||
"version": "1.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/backbone/-/backbone-1.4.1.tgz",
|
|
||||||
"integrity": "sha512-ADy1ztN074YkWbHi8ojJVFe3vAanO/lrzMGZWUClIP7oDD/Pjy2vrASraUP+2EVCfIiTtCW4FChVow01XneivA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"underscore": ">=1.8.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/backbone-undo": {
|
|
||||||
"version": "0.2.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/backbone-undo/-/backbone-undo-0.2.6.tgz",
|
|
||||||
"integrity": "sha512-AsfpNiljLXlk7TcffDUu3EAUq7CxWbyTNwARWrql5XTzN4vh6WzEEBZYaKK4kTTz+iW1tSzqUooaGRIwO83kWA==",
|
|
||||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"backbone": ">=1.0.0",
|
|
||||||
"underscore": ">=1.4.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
|
@ -3937,18 +3870,6 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/codemirror": {
|
|
||||||
"version": "5.63.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.63.0.tgz",
|
|
||||||
"integrity": "sha512-KlLWRPggDg2rBD1Mx7/EqEhaBdy+ybBCVh/efgjBDsPpMeEu6MbTAJzIT4TuCzvmbTEgvKOGzVT6wdBTNusqrg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/codemirror-formatting": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/codemirror-formatting/-/codemirror-formatting-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-br9yM6eJI3pJHekEnoyHaBEb1B7XxxDjju+vRyBe8QGLp5saTIXXkZ+eFCTqXSAtI8QEZDFVEX2/SOjH2sVWRQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
|
@ -5849,38 +5770,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/grapesjs": {
|
|
||||||
"version": "0.22.16",
|
|
||||||
"resolved": "https://registry.npmjs.org/grapesjs/-/grapesjs-0.22.16.tgz",
|
|
||||||
"integrity": "sha512-kCfphgpC7pqJPuMYmIhMR6ueyB3+V67isdpMZOvmuGeWDMomkgzqRWOMH3matfdqIJW7LUivHZo9GeyVQAGmLw==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/backbone": "1.4.15",
|
|
||||||
"backbone": "1.4.1",
|
|
||||||
"backbone-undo": "0.2.6",
|
|
||||||
"codemirror": "5.63.0",
|
|
||||||
"codemirror-formatting": "1.0.0",
|
|
||||||
"html-entities": "~1.4.0",
|
|
||||||
"promise-polyfill": "8.3.0",
|
|
||||||
"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",
|
|
||||||
"integrity": "sha512-z8KJ1ZrTXfASSJZ/tHOcnpcWu4AMr2F/ZfQit+QjimNi3UGowwl7+Yjefuh3R7lbDTrXMMaxhCannCaJo/kPJw==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/graphemer": {
|
"node_modules/graphemer": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||||
|
|
@ -5982,12 +5871,6 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/html-entities": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/html-minifier-terser": {
|
"node_modules/html-minifier-terser": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz",
|
||||||
|
|
@ -7291,12 +7174,6 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
|
@ -7810,12 +7687,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/promise-polyfill": {
|
|
||||||
"version": "8.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz",
|
|
||||||
"integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
|
@ -9349,12 +9220,6 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/underscore": {
|
|
||||||
"version": "1.13.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
|
|
||||||
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.18.2",
|
"version": "7.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||||
|
|
@ -9613,17 +9478,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-email-editor": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-email-editor/-/vue-email-editor-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-aEXm0OHjZgeQqGsssfukqJm7kubfGBOPo9ddwGHMXLbzegJDZ0ou2h7NmRvPR+XaoRGYHdZXf9p7zVae5ACgWA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@unlayer/types": "^1.394.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vue": "^3.2.13"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vue-eslint-parser": {
|
"node_modules/vue-eslint-parser": {
|
||||||
"version": "9.4.3",
|
"version": "9.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,6 @@
|
||||||
"@twilio/voice-sdk": "^2.18.1",
|
"@twilio/voice-sdk": "^2.18.1",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"cytoscape": "^3.33.2",
|
"cytoscape": "^3.33.2",
|
||||||
"grapesjs": "^0.22.16",
|
|
||||||
"grapesjs-mjml": "^1.0.8",
|
|
||||||
"grapesjs-preset-newsletter": "^1.0.2",
|
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"lucide-vue-next": "^1.0.0",
|
"lucide-vue-next": "^1.0.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
|
|
@ -24,7 +21,6 @@
|
||||||
"sip.js": "^0.21.2",
|
"sip.js": "^0.21.2",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-chartjs": "^5.3.3",
|
"vue-chartjs": "^5.3.3",
|
||||||
"vue-email-editor": "^2.2.0",
|
|
||||||
"vue-router": "^4.3.0",
|
"vue-router": "^4.3.0",
|
||||||
"vuedraggable": "^4.1.0"
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,165 +0,0 @@
|
||||||
/**
|
|
||||||
* api/campaigns.js — Client for Hub /campaigns endpoints.
|
|
||||||
*
|
|
||||||
* Mirrors services/targo-hub/lib/campaigns.js. All gift-campaign requests
|
|
||||||
* go through the Hub which handles ERPNext auth + Mailjet send + SSE
|
|
||||||
* progress broadcast.
|
|
||||||
*
|
|
||||||
* Functions:
|
|
||||||
* parseCsvs({ map_csv, giftbit_csv, multi }) → preview matched send list
|
|
||||||
* createCampaign({ name, params, recipients }) → save + return id
|
|
||||||
* listCampaigns() → summaries
|
|
||||||
* getCampaign(id) → full detail
|
|
||||||
* updateCampaign(id, patch) → edit recipients/params
|
|
||||||
* sendCampaign(id) → fire background worker
|
|
||||||
* campaignSseUrl(id) → SSE URL for live updates
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { HUB_URL } from 'src/config/hub'
|
|
||||||
|
|
||||||
async function hubFetch (path, { method = 'GET', body } = {}) {
|
|
||||||
const opts = { method, headers: { 'Content-Type': 'application/json' } }
|
|
||||||
if (body) opts.body = JSON.stringify(body)
|
|
||||||
const res = await fetch(`${HUB_URL}${path}`, opts)
|
|
||||||
const text = await res.text()
|
|
||||||
let data
|
|
||||||
try { data = text ? JSON.parse(text) : {} }
|
|
||||||
catch { throw new Error(`Invalid JSON from ${path}: ${text.slice(0, 200)}`) }
|
|
||||||
if (!res.ok) {
|
|
||||||
const msg = data.error || `HTTP ${res.status}`
|
|
||||||
const err = new Error(msg)
|
|
||||||
err.status = res.status
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseCsvs ({ map_csv, giftbit_csv, multi = 'first' }) {
|
|
||||||
return hubFetch('/campaigns/parse', {
|
|
||||||
method: 'POST',
|
|
||||||
body: { map_csv, giftbit_csv, multi },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createCampaign ({ name, params, recipients }) {
|
|
||||||
return hubFetch('/campaigns', {
|
|
||||||
method: 'POST',
|
|
||||||
body: { name, params, recipients },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function listCampaigns () {
|
|
||||||
return hubFetch('/campaigns').then(r => r.campaigns || [])
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCampaign (id) {
|
|
||||||
return hubFetch(`/campaigns/${encodeURIComponent(id)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateCampaign (id, patch) {
|
|
||||||
return hubFetch(`/campaigns/${encodeURIComponent(id)}`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
body: patch,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sendCampaign (id) {
|
|
||||||
return hubFetch(`/campaigns/${encodeURIComponent(id)}/send`, {
|
|
||||||
method: 'POST',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the URL the browser hits to download the per-recipient CSV report
|
|
||||||
// (giftbit shortlink ↔ email ↔ name ↔ status). The hub serves it with the
|
|
||||||
// proper Content-Disposition so an <a download> click triggers a save.
|
|
||||||
export function campaignReportCsvUrl (id) {
|
|
||||||
return `${HUB_URL}/campaigns/${encodeURIComponent(id)}/report.csv`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Image assets (self-hosted on the hub, for GrapesJS asset manager) ───────
|
|
||||||
|
|
||||||
export function listAssets () {
|
|
||||||
return hubFetch('/campaigns/assets').then(r => r.assets || [])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload a File / Blob from the browser via base64-encoded JSON. Bypasses
|
|
||||||
// multipart parsing on the hub side (zero new deps) at the cost of ~33%
|
|
||||||
// payload overhead. Acceptable for the ≤5 MB images we permit.
|
|
||||||
export async function uploadAsset (file) {
|
|
||||||
const dataUrl = await new Promise((resolve, reject) => {
|
|
||||||
const r = new FileReader()
|
|
||||||
r.onload = () => resolve(r.result)
|
|
||||||
r.onerror = () => reject(new Error('FileReader failed'))
|
|
||||||
r.readAsDataURL(file)
|
|
||||||
})
|
|
||||||
return hubFetch('/campaigns/assets/upload', {
|
|
||||||
method: 'POST',
|
|
||||||
body: { name: file.name, data: dataUrl },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteAsset (filename) {
|
|
||||||
return hubFetch(`/campaigns/assets/${encodeURIComponent(filename)}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Template editing (used by the GrapesJS editor page) ─────────────────────
|
|
||||||
|
|
||||||
export function listTemplates () {
|
|
||||||
return hubFetch('/campaigns/templates').then(r => r.templates || [])
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTemplate (name) {
|
|
||||||
return hubFetch(`/campaigns/templates/${encodeURIComponent(name)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveTemplate(name, content, opts) — content is HTML by default.
|
|
||||||
// Optional opts.design = Unlayer design JSON (persisted alongside HTML so the
|
|
||||||
// editor can re-load the visual state on next open).
|
|
||||||
// Legacy opts.format = 'mjml' still supported for older callers (sends mjml).
|
|
||||||
export function saveTemplate (name, content, { format = 'html', design = null } = {}) {
|
|
||||||
const body = format === 'mjml' ? { mjml: content } : { html: content }
|
|
||||||
if (design) body.design = design
|
|
||||||
return hubFetch(`/campaigns/templates/${encodeURIComponent(name)}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function previewTemplate (name, { html, vars } = {}) {
|
|
||||||
return hubFetch(`/campaigns/templates/${encodeURIComponent(name)}/preview`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: { html, vars },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Translate the source template to a target language via Gemini.
|
|
||||||
// targetName must match the source's prefix (e.g. gift-email-fr → gift-email-en).
|
|
||||||
// override=true required if the target already exists.
|
|
||||||
export function translateTemplate (srcName, targetName, { override = false } = {}) {
|
|
||||||
return hubFetch(
|
|
||||||
`/campaigns/templates/${encodeURIComponent(srcName)}/translate-to/${encodeURIComponent(targetName)}`,
|
|
||||||
{ method: 'POST', body: { override } },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send ONE rendered email to a specific address for visual QA.
|
|
||||||
// Pass { to, vars, from?, subject? } — defaults filled in server-side.
|
|
||||||
export function testSendTemplate (name, { to, vars, from, subject } = {}) {
|
|
||||||
return hubFetch(`/campaigns/templates/${encodeURIComponent(name)}/test-send`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: { to, vars, from, subject },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the URL for the SSE channel of one campaign. The Hub broadcasts on
|
|
||||||
* topic `campaign:<id>` so we subscribe to that single topic. Use with:
|
|
||||||
* const es = new EventSource(campaignSseUrl(id))
|
|
||||||
* es.addEventListener('recipient-update', ev => { ... })
|
|
||||||
* es.addEventListener('campaign-done', ev => { ... })
|
|
||||||
*/
|
|
||||||
export function campaignSseUrl (id) {
|
|
||||||
return `${HUB_URL}/sse?topics=campaign:${encodeURIComponent(id)}`
|
|
||||||
}
|
|
||||||
|
|
@ -7,7 +7,6 @@ export const navItems = [
|
||||||
{ path: '/tickets', icon: 'Ticket', label: 'Tickets', requires: 'view_all_tickets' },
|
{ path: '/tickets', icon: 'Ticket', label: 'Tickets', requires: 'view_all_tickets' },
|
||||||
{ path: '/equipe', icon: 'UsersRound', label: 'Équipe', requires: 'manage_users' },
|
{ path: '/equipe', icon: 'UsersRound', label: 'Équipe', requires: 'manage_users' },
|
||||||
{ path: '/rapports', icon: 'BarChart3', label: 'Rapports', requires: 'view_dashboard_kpi' },
|
{ path: '/rapports', icon: 'BarChart3', label: 'Rapports', requires: 'view_dashboard_kpi' },
|
||||||
{ path: '/campaigns', icon: 'Gift', label: 'Campagnes', requires: 'manage_users' },
|
|
||||||
{ path: '/settings', icon: 'Settings', label: 'Paramètres', requires: 'view_settings' },
|
{ path: '/settings', icon: 'Settings', label: 'Paramètres', requires: 'view_settings' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,13 +123,13 @@ import { listDocs } from 'src/api/erp'
|
||||||
import { navItems as allNavItems } from 'src/config/nav'
|
import { navItems as allNavItems } from 'src/config/nav'
|
||||||
import {
|
import {
|
||||||
LayoutDashboard, Users, Truck, Ticket, UsersRound, BarChart3,
|
LayoutDashboard, Users, Truck, Ticket, UsersRound, BarChart3,
|
||||||
Gift, Settings, LogOut, PanelLeftOpen, PanelLeftClose,
|
Settings, LogOut, PanelLeftOpen, PanelLeftClose,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import ConversationPanel from 'src/components/shared/ConversationPanel.vue'
|
import ConversationPanel from 'src/components/shared/ConversationPanel.vue'
|
||||||
import { useConversations } from 'src/composables/useConversations'
|
import { useConversations } from 'src/composables/useConversations'
|
||||||
import FlowEditorDialog from 'src/components/flow-editor/FlowEditorDialog.vue'
|
import FlowEditorDialog from 'src/components/flow-editor/FlowEditorDialog.vue'
|
||||||
|
|
||||||
const icons = { LayoutDashboard, Users, Truck, Ticket, UsersRound, BarChart3, Gift, Settings, LogOut, PanelLeftOpen, PanelLeftClose }
|
const icons = { LayoutDashboard, Users, Truck, Ticket, UsersRound, BarChart3, Settings, LogOut, PanelLeftOpen, PanelLeftClose }
|
||||||
|
|
||||||
const { panelOpen, activeCount: convCount } = useConversations()
|
const { panelOpen, activeCount: convCount } = useConversations()
|
||||||
function toggleConvPanel () { panelOpen.value = !panelOpen.value }
|
function toggleConvPanel () { panelOpen.value = !panelOpen.value }
|
||||||
|
|
|
||||||
|
|
@ -1,203 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-page padding>
|
|
||||||
<div class="row items-center q-mb-md">
|
|
||||||
<q-btn flat dense round icon="arrow_back" :to="'/campaigns'" class="q-mr-sm" />
|
|
||||||
<div class="text-h5">{{ campaign?.name || id }}</div>
|
|
||||||
<q-chip dense class="q-ml-md" :color="statusColor(campaign?.status)" text-color="white" :label="statusLabel(campaign?.status)" />
|
|
||||||
<q-space />
|
|
||||||
<q-btn
|
|
||||||
v-if="campaign?.recipients?.length"
|
|
||||||
flat dense icon="file_download" label="CSV" class="q-mr-sm"
|
|
||||||
:href="reportCsvUrl" download
|
|
||||||
>
|
|
||||||
<q-tooltip>Télécharger le rapport (shortlinks Giftbit, emails, statuts d'envoi)</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn v-if="campaign?.status === 'draft'" unelevated color="primary" icon="send" label="Lancer l'envoi"
|
|
||||||
:loading="resending" @click="relaunch" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Counters bar -->
|
|
||||||
<div class="row q-col-gutter-sm q-mb-md" v-if="campaign">
|
|
||||||
<q-card flat bordered class="col-6 col-md-2"><q-card-section class="text-center">
|
|
||||||
<div class="text-h5">{{ campaign.counters?.total || campaign.recipients?.length || 0 }}</div>
|
|
||||||
<div class="text-caption text-grey-7">Total</div>
|
|
||||||
</q-card-section></q-card>
|
|
||||||
<q-card flat bordered class="col-6 col-md-2"><q-card-section class="text-center">
|
|
||||||
<div class="text-h5 text-positive">{{ counterFor('sent') + counterFor('opened') + counterFor('clicked') }}</div>
|
|
||||||
<div class="text-caption text-grey-7">Envoyés</div>
|
|
||||||
</q-card-section></q-card>
|
|
||||||
<q-card flat bordered class="col-6 col-md-2"><q-card-section class="text-center">
|
|
||||||
<div class="text-h5 text-blue">{{ counterFor('clicked') }}</div>
|
|
||||||
<div class="text-caption text-grey-7">Cliqués</div>
|
|
||||||
</q-card-section></q-card>
|
|
||||||
<q-card flat bordered class="col-6 col-md-2"><q-card-section class="text-center">
|
|
||||||
<div class="text-h5 text-orange">{{ counterFor('queued') }}</div>
|
|
||||||
<div class="text-caption text-grey-7">En attente</div>
|
|
||||||
</q-card-section></q-card>
|
|
||||||
<q-card flat bordered class="col-6 col-md-2"><q-card-section class="text-center">
|
|
||||||
<div class="text-h5 text-negative">{{ counterFor('failed') + counterFor('bounced') }}</div>
|
|
||||||
<div class="text-caption text-grey-7">Échecs</div>
|
|
||||||
</q-card-section></q-card>
|
|
||||||
<q-card flat bordered class="col-6 col-md-2"><q-card-section class="text-center">
|
|
||||||
<div class="text-h5 text-grey-7">{{ counterFor('pending') }}</div>
|
|
||||||
<div class="text-caption text-grey-7">Non envoyés</div>
|
|
||||||
</q-card-section></q-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-linear-progress
|
|
||||||
v-if="campaign && (campaign.status === 'sending' || campaign.status === 'completed')"
|
|
||||||
:value="sentRatio" :color="campaign.counters?.failed ? 'orange' : 'positive'" size="8px" class="q-mb-md"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-table
|
|
||||||
v-if="campaign"
|
|
||||||
:rows="campaign.recipients || []"
|
|
||||||
:columns="columns" row-key="email"
|
|
||||||
flat bordered dense
|
|
||||||
:pagination="{ rowsPerPage: 50 }"
|
|
||||||
:rows-per-page-options="[25, 50, 100, 0]"
|
|
||||||
>
|
|
||||||
<template v-slot:body-cell-status="props">
|
|
||||||
<q-td :props="props">
|
|
||||||
<q-chip dense size="sm" :color="statusColor(props.row.status)" text-color="white" :label="statusLabel(props.row.status)" />
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body-cell-customer="props">
|
|
||||||
<q-td :props="props">
|
|
||||||
<span v-if="props.row.customer_id">
|
|
||||||
<q-icon name="person" size="14px" class="q-mr-xs" />
|
|
||||||
<a :href="`/#/clients/${props.row.customer_id}`" target="_blank" style="color:var(--q-primary)">
|
|
||||||
{{ props.row.customer_name || props.row.customer_id }}
|
|
||||||
</a>
|
|
||||||
<q-chip dense size="xs" outline class="q-ml-xs">{{ props.row.match_method }}</q-chip>
|
|
||||||
</span>
|
|
||||||
<span v-else class="text-grey-6">—</span>
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body-cell-gift_url="props">
|
|
||||||
<q-td :props="props">
|
|
||||||
<a :href="props.row.gift_url" target="_blank" class="text-grey-7" style="font-family:monospace; font-size:0.78rem">
|
|
||||||
{{ shortLink(props.row.gift_url) }}
|
|
||||||
</a>
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body-cell-error="props">
|
|
||||||
<q-td :props="props">
|
|
||||||
<span v-if="props.row.error" class="text-negative text-caption">{{ props.row.error }}</span>
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
|
|
||||||
<div v-if="!campaign && !loading" class="text-center q-pa-xl text-grey-7">
|
|
||||||
<q-icon name="error_outline" size="48px" />
|
|
||||||
<div class="text-h6 q-mt-md">Campagne introuvable</div>
|
|
||||||
</div>
|
|
||||||
</q-page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
import { useQuasar } from 'quasar'
|
|
||||||
import { getCampaign, sendCampaign, campaignSseUrl, campaignReportCsvUrl } from 'src/api/campaigns'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const $q = useQuasar()
|
|
||||||
const id = route.params.id
|
|
||||||
const campaign = ref(null)
|
|
||||||
const loading = ref(true)
|
|
||||||
const resending = ref(false)
|
|
||||||
|
|
||||||
let es = null
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ name: 'firstname', label: 'Prénom', field: 'firstname', align: 'left' },
|
|
||||||
{ name: 'lastname', label: 'Nom', field: 'lastname', align: 'left' },
|
|
||||||
{ name: 'email', label: 'Email', field: 'email', align: 'left' },
|
|
||||||
{ name: 'civic_address', label: 'Adresse', field: 'civic_address', align: 'left' },
|
|
||||||
{ name: 'customer', label: 'Client lié', field: 'customer_name', align: 'left' },
|
|
||||||
{ name: 'gift_url', label: 'Shortlink', field: 'gift_url', align: 'left' },
|
|
||||||
{ name: 'status', label: 'Statut', field: 'status', align: 'left' },
|
|
||||||
{ name: 'error', label: 'Erreur', field: 'error', align: 'left' },
|
|
||||||
]
|
|
||||||
|
|
||||||
function counterFor (s) { return campaign.value?.counters?.[s] || 0 }
|
|
||||||
function statusColor (s) {
|
|
||||||
return {
|
|
||||||
pending: 'grey-5', queued: 'orange', sent: 'positive', opened: 'positive',
|
|
||||||
clicked: 'blue', failed: 'negative', bounced: 'negative',
|
|
||||||
draft: 'grey', sending: 'orange', completed: 'positive',
|
|
||||||
}[s] || 'grey-5'
|
|
||||||
}
|
|
||||||
function statusLabel (s) {
|
|
||||||
return {
|
|
||||||
pending: 'En attente', queued: 'En file', sent: 'Envoyé', opened: 'Ouvert',
|
|
||||||
clicked: 'Cliqué', failed: 'Échec', bounced: 'Rejeté',
|
|
||||||
draft: 'Brouillon', sending: 'En cours', completed: 'Terminée',
|
|
||||||
}[s] || s
|
|
||||||
}
|
|
||||||
function shortLink (u) { return (u || '').replace(/^https?:\/\//, '').slice(0, 28) + ((u || '').length > 35 ? '…' : '') }
|
|
||||||
|
|
||||||
const reportCsvUrl = computed(() => campaignReportCsvUrl(id))
|
|
||||||
|
|
||||||
const sentRatio = computed(() => {
|
|
||||||
const total = campaign.value?.counters?.total || 1
|
|
||||||
const done = counterFor('sent') + counterFor('opened') + counterFor('clicked')
|
|
||||||
+ counterFor('failed') + counterFor('bounced')
|
|
||||||
return Math.min(1, done / total)
|
|
||||||
})
|
|
||||||
|
|
||||||
async function load () {
|
|
||||||
loading.value = true
|
|
||||||
try { campaign.value = await getCampaign(id) }
|
|
||||||
catch (e) { $q.notify({ type: 'negative', message: e.message }) }
|
|
||||||
finally { loading.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
function subscribeSse () {
|
|
||||||
if (es) es.close()
|
|
||||||
es = new EventSource(campaignSseUrl(id))
|
|
||||||
es.addEventListener('recipient-update', (ev) => {
|
|
||||||
const data = JSON.parse(ev.data)
|
|
||||||
if (!campaign.value?.recipients) return
|
|
||||||
// Apply patch by index; counters will be re-rendered from recipients next refresh
|
|
||||||
if (campaign.value.recipients[data.i]) {
|
|
||||||
Object.assign(campaign.value.recipients[data.i], data.recipient)
|
|
||||||
// Recompute counters in-place for live update
|
|
||||||
const counters = { total: campaign.value.recipients.length }
|
|
||||||
for (const r of campaign.value.recipients) counters[r.status] = (counters[r.status] || 0) + 1
|
|
||||||
campaign.value.counters = counters
|
|
||||||
}
|
|
||||||
})
|
|
||||||
es.addEventListener('campaign-done', () => {
|
|
||||||
$q.notify({ type: 'positive', message: 'Campagne terminée' })
|
|
||||||
load()
|
|
||||||
})
|
|
||||||
es.addEventListener('campaign-status', (ev) => {
|
|
||||||
const data = JSON.parse(ev.data)
|
|
||||||
if (campaign.value) campaign.value.status = data.status
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function relaunch () {
|
|
||||||
resending.value = true
|
|
||||||
try {
|
|
||||||
await sendCampaign(id)
|
|
||||||
await load()
|
|
||||||
subscribeSse()
|
|
||||||
} catch (e) {
|
|
||||||
$q.notify({ type: 'negative', message: e.message })
|
|
||||||
} finally {
|
|
||||||
resending.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await load()
|
|
||||||
// Auto-subscribe to SSE if still running (or about to run)
|
|
||||||
if (campaign.value && ['draft','sending'].includes(campaign.value.status)) {
|
|
||||||
subscribeSse()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
onBeforeUnmount(() => { if (es) es.close() })
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,772 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-page padding>
|
|
||||||
<div class="row items-center q-mb-md">
|
|
||||||
<q-btn flat dense round icon="arrow_back" :to="'/campaigns'" class="q-mr-sm" />
|
|
||||||
<div class="text-h5">Nouvelle campagne</div>
|
|
||||||
<q-space />
|
|
||||||
<!-- Always-accessible jump-to-editor link. Opens in new tab so the
|
|
||||||
wizard's uploaded files + parsed recipients stay intact. Useful
|
|
||||||
when the user wants to tweak the template mid-import. -->
|
|
||||||
<q-btn flat color="primary" icon="palette" label="Éditer le template (nouvel onglet)"
|
|
||||||
type="a" href="/ops/#/campaigns/templates/gift-email-fr" target="_blank">
|
|
||||||
<q-tooltip>S'ouvre dans un nouvel onglet — tes fichiers uploadés restent intacts ici</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-stepper v-model="step" header-nav ref="stepper" color="primary" animated flat bordered class="bg-white">
|
|
||||||
|
|
||||||
<!-- Step 1 — Upload + parameters ─────────────────────────────────── -->
|
|
||||||
<q-step :name="1" title="Fichiers + paramètres" icon="upload_file" :done="step > 1" :header-nav="step > 1">
|
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-card flat bordered>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-subtitle1 q-mb-sm">1. Export Map CSV (brut)</div>
|
|
||||||
<div class="text-caption text-grey-7 q-mb-md">
|
|
||||||
Le fichier <code>selectionAdressesMap*.csv</code> tel qu'exporté de la sélection
|
|
||||||
d'adresses (pipe-delimited, préambule de 1 ligne accepté).
|
|
||||||
</div>
|
|
||||||
<q-file v-model="mapFile" label="Sélectionner le fichier" accept=".csv" outlined dense @update:model-value="readMapFile">
|
|
||||||
<template v-slot:prepend><q-icon name="attach_file" /></template>
|
|
||||||
</q-file>
|
|
||||||
<div v-if="mapPreview" class="text-caption q-mt-sm text-grey-7">
|
|
||||||
✓ {{ countMapRows(mapPreview) }} lignes de contacts (préambule + header retirés)
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<q-card flat bordered>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-subtitle1 q-mb-sm">2. Shortlinks Giftbit CSV</div>
|
|
||||||
<div class="text-caption text-grey-7 q-mb-md">
|
|
||||||
Le fichier <code>giftbit-gifts-<id>.csv</code> retourné par
|
|
||||||
<code>create_giftbit_campaign.js</code> (colonnes: firstname, lastname, email,
|
|
||||||
gift_url, giftbit_uuid, gift_value_cents).
|
|
||||||
</div>
|
|
||||||
<q-file v-model="giftFile" label="Sélectionner le fichier" accept=".csv" outlined dense @update:model-value="readGiftFile">
|
|
||||||
<template v-slot:prepend><q-icon name="attach_file" /></template>
|
|
||||||
</q-file>
|
|
||||||
<div v-if="giftPreview" class="text-caption q-mt-sm text-grey-7">
|
|
||||||
✓ {{ countGiftRows(giftPreview) }} cartes-cadeaux
|
|
||||||
<span v-if="giftFormatHint" class="text-grey-6">(format: {{ giftFormatHint }})</span>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-card flat bordered class="q-mt-md">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-subtitle1 q-mb-sm">Paramètres de la campagne</div>
|
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<q-input v-model="params.name" label="Nom interne" outlined dense class="col-12 col-md-6" />
|
|
||||||
<q-input v-model="params.amount" label="Montant (affiché)" outlined dense class="col-6 col-md-3" placeholder="60 $" />
|
|
||||||
<q-input v-model.number="params.commitment_months" type="number" label="Engagement (mois)" outlined dense class="col-6 col-md-3" />
|
|
||||||
<q-input v-model="params.subject" label="Sujet du courriel" outlined dense class="col-12 col-md-6" />
|
|
||||||
<q-input v-model="params.from" label="Expéditeur (From)" outlined dense class="col-12 col-md-6" placeholder="TARGO <support@targointernet.com>" />
|
|
||||||
<q-input v-model="params.expiry" label="Expiration (texte affiché)" outlined dense class="col-12 col-md-6" placeholder="31 décembre 2026" />
|
|
||||||
<q-input v-model.number="params.throttle_ms" type="number" label="Throttle (ms entre envois)" outlined dense class="col-12 col-md-6" />
|
|
||||||
<q-select v-model="params.multi" :options="multiOptions" emit-value map-options label="Emails multiples (couples)" outlined dense class="col-12 col-md-6" />
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
|
|
||||||
<q-stepper-navigation>
|
|
||||||
<q-btn flat color="primary" icon="person_add" label="Saisie manuelle (sans CSV)" @click="goManual" class="q-mr-sm">
|
|
||||||
<q-tooltip>Sauter l'import CSV et ajouter les destinataires un par un à l'étape suivante</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn unelevated color="primary" label="Suivant — Aperçu" icon-right="arrow_forward"
|
|
||||||
:disable="!mapPreview || !giftPreview || parsing"
|
|
||||||
:loading="parsing" @click="goPreview" />
|
|
||||||
</q-stepper-navigation>
|
|
||||||
</q-step>
|
|
||||||
|
|
||||||
<!-- Step 2 — Preview matched send list ─────────────────────────── -->
|
|
||||||
<q-step :name="2" title="Aperçu et matching" icon="preview" :done="step > 2" :header-nav="step > 2">
|
|
||||||
<!-- Counter strip — at a glance: total / paired / sendable / unmatched -->
|
|
||||||
<div class="row q-col-gutter-sm q-mb-md">
|
|
||||||
<q-card flat bordered class="col-6 col-md">
|
|
||||||
<q-card-section class="text-center q-pa-sm">
|
|
||||||
<div class="text-h5">{{ recipients.length }}</div>
|
|
||||||
<div class="text-caption text-grey-7">Paires contact ↔ lien</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
<q-card flat bordered class="col-6 col-md" :class="{ 'bg-positive-1': sendableCount > 0 }">
|
|
||||||
<q-card-section class="text-center q-pa-sm">
|
|
||||||
<div class="text-h5 text-positive">{{ sendableCount }}</div>
|
|
||||||
<div class="text-caption text-grey-7">À envoyer</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
<q-card flat bordered class="col-6 col-md">
|
|
||||||
<q-card-section class="text-center q-pa-sm">
|
|
||||||
<div class="text-h5 text-positive">{{ matchedCount }}</div>
|
|
||||||
<div class="text-caption text-grey-7">Client lié</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
<q-card flat bordered class="col-6 col-md">
|
|
||||||
<q-card-section class="text-center q-pa-sm">
|
|
||||||
<div class="text-h5 text-warning">{{ unmatchedCount }}</div>
|
|
||||||
<div class="text-caption text-grey-7">Sans client</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
<q-card flat bordered class="col-6 col-md" v-if="unpairedContacts.length">
|
|
||||||
<q-card-section class="text-center q-pa-sm">
|
|
||||||
<div class="text-h5 text-negative">{{ unpairedContacts.length }}</div>
|
|
||||||
<div class="text-caption text-grey-7">Sans lien</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
<q-card flat bordered class="col-6 col-md" v-if="unusedGifts.length">
|
|
||||||
<q-card-section class="text-center q-pa-sm">
|
|
||||||
<div class="text-h5 text-orange">{{ unusedGifts.length }}</div>
|
|
||||||
<div class="text-caption text-grey-7">Liens en surplus</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
<q-card flat bordered class="col-6 col-md" v-if="namesNeedingReview">
|
|
||||||
<q-card-section class="text-center q-pa-sm">
|
|
||||||
<div class="text-h5 text-amber-9">{{ namesNeedingReview }}</div>
|
|
||||||
<div class="text-caption text-grey-7">Noms à vérifier</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Auto-clean summary — informational, not blocking -->
|
|
||||||
<q-banner v-if="namesAutoCorrected || namesNeedingReview" class="bg-blue-1 text-blue-9 q-mb-sm" rounded dense>
|
|
||||||
<template v-slot:avatar><q-icon name="auto_fix_high" /></template>
|
|
||||||
<span v-if="namesAutoCorrected">
|
|
||||||
<strong>{{ namesAutoCorrected }} nom(s) auto-corrigés</strong> (Title Case, accents québécois,
|
|
||||||
prénoms composés séparés). L'icône ✨ verte dans le tableau indique les changements.
|
|
||||||
</span>
|
|
||||||
<span v-if="namesNeedingReview" :class="namesAutoCorrected ? 'q-ml-md' : ''">
|
|
||||||
<strong>{{ namesNeedingReview }} nom(s) suspects</strong> — icône ⚠ amber : cliquer la cellule
|
|
||||||
pour éditer en place.
|
|
||||||
</span>
|
|
||||||
</q-banner>
|
|
||||||
|
|
||||||
<!-- Imbalance banner: explicit explanation of what the imbalance means -->
|
|
||||||
<q-banner v-if="unpairedContacts.length || unusedGifts.length" class="bg-orange-1 text-orange-9 q-mb-md" rounded>
|
|
||||||
<template v-slot:avatar><q-icon name="warning" /></template>
|
|
||||||
<div v-if="unpairedContacts.length">
|
|
||||||
<strong>{{ unpairedContacts.length }} contact(s) sans lien-cadeau</strong> —
|
|
||||||
ils n'apparaissent PAS dans la liste d'envoi ci-dessous et ne recevront rien.
|
|
||||||
Pour les inclure, acquérir {{ unpairedContacts.length }} liens supplémentaires
|
|
||||||
chez Giftbit et re-uploader le fichier.
|
|
||||||
</div>
|
|
||||||
<div v-if="unusedGifts.length" :class="unpairedContacts.length ? 'q-mt-xs' : ''">
|
|
||||||
<strong>{{ unusedGifts.length }} lien(s) Giftbit non utilisés</strong> —
|
|
||||||
il y a plus de cartes-cadeaux que de contacts. Le surplus sera perdu si
|
|
||||||
la campagne est envoyée tel quel.
|
|
||||||
</div>
|
|
||||||
</q-banner>
|
|
||||||
|
|
||||||
<!-- ✓ Paired recipients (will be sent) ────────────────────────── -->
|
|
||||||
<div class="row items-center q-mb-xs">
|
|
||||||
<div class="text-subtitle1">
|
|
||||||
<q-icon name="link" /> Association contact ↔ lien-cadeau
|
|
||||||
<span class="text-caption text-grey-7">— vérifier avant d'approuver</span>
|
|
||||||
</div>
|
|
||||||
<q-space />
|
|
||||||
<q-btn unelevated dense color="primary" icon="person_add" label="Ajouter manuellement" @click="openManualDialog">
|
|
||||||
<q-tooltip>Ajouter un destinataire en saisissant les champs (sans passer par CSV)</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
<q-table
|
|
||||||
:rows="recipients" :columns="recipientColumns" row-key="row_index"
|
|
||||||
flat bordered dense :pagination="{ rowsPerPage: 25 }"
|
|
||||||
:rows-per-page-options="[10, 25, 50, 100, 0]"
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
<template v-slot:body-cell-row_index="props">
|
|
||||||
<q-td :props="props" class="text-grey-6">#{{ props.row.row_index }}</q-td>
|
|
||||||
</template>
|
|
||||||
<!-- Editable firstname cell with auto-clean indicators.
|
|
||||||
Click the cell → q-popup-edit opens, type new value, Enter saves.
|
|
||||||
Icons (left of name):
|
|
||||||
⚠ amber = nameWarning() heuristic flagged this (e.g. "deux prénoms collés")
|
|
||||||
✨ green = auto-cleaner changed something at parse-time
|
|
||||||
(Title Case, accent restoration, compound split) -->
|
|
||||||
<template v-slot:body-cell-firstname="props">
|
|
||||||
<q-td :props="props" style="cursor:pointer">
|
|
||||||
<q-icon v-if="props.row.name_warnings?.firstname" name="warning" color="amber-9" size="14px" class="q-mr-xs">
|
|
||||||
<q-tooltip>{{ props.row.name_warnings.firstname }}</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
<q-icon v-else-if="props.row.cleaned_changed && props.row.firstname !== props.row.firstname_raw"
|
|
||||||
name="auto_fix_high" color="positive" size="14px" class="q-mr-xs">
|
|
||||||
<q-tooltip>Auto-corrigé depuis "<strong>{{ props.row.firstname_raw }}</strong>"</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
{{ props.row.firstname }}
|
|
||||||
<q-popup-edit v-model="props.row.firstname" auto-save v-slot="scope" buttons label-set="OK" label-cancel="Annuler">
|
|
||||||
<q-input v-model="scope.value" dense autofocus :model-value="scope.value"
|
|
||||||
@keyup.enter="scope.set" @keyup.esc="scope.cancel" />
|
|
||||||
</q-popup-edit>
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body-cell-lastname="props">
|
|
||||||
<q-td :props="props" style="cursor:pointer">
|
|
||||||
<q-icon v-if="props.row.name_warnings?.lastname" name="warning" color="amber-9" size="14px" class="q-mr-xs">
|
|
||||||
<q-tooltip>{{ props.row.name_warnings.lastname }}</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
<q-icon v-else-if="props.row.cleaned_changed && props.row.lastname !== props.row.lastname_raw"
|
|
||||||
name="auto_fix_high" color="positive" size="14px" class="q-mr-xs">
|
|
||||||
<q-tooltip>Auto-corrigé depuis "<strong>{{ props.row.lastname_raw }}</strong>"</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
{{ props.row.lastname }}
|
|
||||||
<q-popup-edit v-model="props.row.lastname" auto-save v-slot="scope" buttons label-set="OK" label-cancel="Annuler">
|
|
||||||
<q-input v-model="scope.value" dense autofocus
|
|
||||||
@keyup.enter="scope.set" @keyup.esc="scope.cancel" />
|
|
||||||
</q-popup-edit>
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body-cell-gift_url="props">
|
|
||||||
<q-td :props="props">
|
|
||||||
<a :href="props.row.gift_url" target="_blank" style="color:var(--q-primary); font-family:monospace; font-size:0.78rem">
|
|
||||||
{{ shortenUrl(props.row.gift_url) }}
|
|
||||||
</a>
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body-cell-language="props">
|
|
||||||
<q-td :props="props">
|
|
||||||
<!-- Clickable chip — toggles FR ↔ EN inline. Default value comes
|
|
||||||
from the matched Customer.language (or 'fr' for unmatched).
|
|
||||||
The send-worker picks the template by this value at send
|
|
||||||
time, so flipping here changes which template gets used. -->
|
|
||||||
<q-chip dense clickable size="sm"
|
|
||||||
:color="props.row.language === 'en' ? 'blue-grey-6' : 'primary'"
|
|
||||||
text-color="white"
|
|
||||||
:label="(props.row.language || 'fr').toUpperCase()"
|
|
||||||
@click="props.row.language = props.row.language === 'en' ? 'fr' : 'en'">
|
|
||||||
<q-tooltip>Cliquer pour basculer FR ↔ EN</q-tooltip>
|
|
||||||
</q-chip>
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body-cell-match="props">
|
|
||||||
<q-td :props="props">
|
|
||||||
<q-chip v-if="props.row.manual" dense color="indigo-5" text-color="white" size="sm" icon="edit" label="manuel" />
|
|
||||||
<q-chip v-else-if="props.row.customer_id" dense color="positive" text-color="white" size="sm" :label="props.row.match_method" />
|
|
||||||
<q-chip v-else dense color="warning" text-color="white" size="sm" label="non lié" />
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body-cell-actions="props">
|
|
||||||
<q-td :props="props" class="text-right">
|
|
||||||
<q-btn flat dense size="sm" :icon="props.row.excluded ? 'add_circle' : 'block'"
|
|
||||||
:color="props.row.excluded ? 'positive' : 'negative'"
|
|
||||||
@click="props.row.excluded = !props.row.excluded">
|
|
||||||
<q-tooltip>{{ props.row.excluded ? 'Inclure' : 'Exclure' }}</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
|
|
||||||
<!-- ⚠ Contacts that have NO gift-url (won't be sent) ─────────── -->
|
|
||||||
<q-expansion-item v-if="unpairedContacts.length" expand-separator
|
|
||||||
icon="person_off" :label="`${unpairedContacts.length} contact(s) sans lien-cadeau (ne recevront pas)`"
|
|
||||||
header-class="bg-red-1 text-red-9">
|
|
||||||
<q-table
|
|
||||||
:rows="unpairedContacts" :columns="unpairedColumns" row-key="row_index"
|
|
||||||
flat dense hide-bottom :pagination="{ rowsPerPage: 0 }"
|
|
||||||
>
|
|
||||||
<template v-slot:body-cell-row_index="props">
|
|
||||||
<q-td :props="props" class="text-grey-6">#{{ props.row.row_index }}</q-td>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<!-- ⓘ Unused gift URLs (extra capacity) ────────────────────────── -->
|
|
||||||
<q-expansion-item v-if="unusedGifts.length" expand-separator
|
|
||||||
icon="card_giftcard"
|
|
||||||
:label="`${unusedGifts.length} lien(s) Giftbit non utilisés`"
|
|
||||||
header-class="bg-orange-1 text-orange-9"
|
|
||||||
class="q-mt-sm">
|
|
||||||
<q-list dense>
|
|
||||||
<q-item v-for="g in unusedGifts" :key="g.gift_url">
|
|
||||||
<q-item-section side class="text-grey-6">#{{ g.row_index }}</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<a :href="g.gift_url" target="_blank" style="color:var(--q-primary); font-family:monospace; font-size:0.85rem">{{ g.gift_url }}</a>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<!-- Action row: preview + edit template are quick-access utilities,
|
|
||||||
both non-destructive. The primary action is "Continuer" which
|
|
||||||
moves to Step 3 (still NOT the send — Step 3 has its own
|
|
||||||
explicit launch button). Icon changed from 'send' (confusing,
|
|
||||||
looked like it fired) to 'arrow_forward'. -->
|
|
||||||
<q-stepper-navigation>
|
|
||||||
<q-btn flat label="Retour" @click="step = 1" />
|
|
||||||
<q-space />
|
|
||||||
<q-btn flat color="primary" icon="visibility" label="Aperçu du courriel"
|
|
||||||
:disable="!firstPreviewable" @click="openPreview" class="q-mr-sm">
|
|
||||||
<q-tooltip>Voir le rendu du courriel avec les vraies données du destinataire #1 (n'envoie rien)</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn flat color="primary" icon="palette" label="Éditer le template"
|
|
||||||
type="a" :href="editorHref" target="_blank" class="q-mr-sm">
|
|
||||||
<q-tooltip>Ouvre l'éditeur dans un nouvel onglet — ton import reste ici intact</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn unelevated color="primary" :label="`Continuer — ${sendableCount} prêts`"
|
|
||||||
icon-right="arrow_forward"
|
|
||||||
@click="step = 3" :disable="sendableCount === 0">
|
|
||||||
<q-tooltip>Va à l'étape de confirmation finale. L'envoi ne démarre qu'au clic sur "Lancer l'envoi" de l'étape 3.</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</q-stepper-navigation>
|
|
||||||
</q-step>
|
|
||||||
|
|
||||||
<!-- Step 3 — Confirm + send ──────────────────────────────────────── -->
|
|
||||||
<q-step :name="3" title="Confirmation" icon="send">
|
|
||||||
<q-card flat bordered>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-subtitle1 q-mb-sm">Récapitulatif</div>
|
|
||||||
<q-list dense>
|
|
||||||
<q-item><q-item-section side>Nom</q-item-section><q-item-section>{{ params.name }}</q-item-section></q-item>
|
|
||||||
<q-item><q-item-section side>Destinataires actifs</q-item-section><q-item-section>{{ sendableCount }} (sur {{ recipients.length }} paires ; {{ excludedCount }} exclus, {{ unpairedContacts.length }} sans lien)</q-item-section></q-item>
|
|
||||||
<q-item><q-item-section side>Répartition par langue</q-item-section><q-item-section>{{ langBreakdown }}</q-item-section></q-item>
|
|
||||||
<q-item><q-item-section side>Montant affiché</q-item-section><q-item-section>{{ params.amount }}</q-item-section></q-item>
|
|
||||||
<q-item><q-item-section side>Engagement</q-item-section><q-item-section>{{ params.commitment_months }} mois</q-item-section></q-item>
|
|
||||||
<q-item><q-item-section side>Sujet</q-item-section><q-item-section>{{ params.subject }}</q-item-section></q-item>
|
|
||||||
<q-item><q-item-section side>Expéditeur</q-item-section><q-item-section>{{ params.from }}</q-item-section></q-item>
|
|
||||||
<q-item><q-item-section side>Throttle</q-item-section><q-item-section>{{ params.throttle_ms }} ms entre envois (≈ {{ Math.round((60 / (params.throttle_ms / 1000)) || 0) }} emails/min)</q-item-section></q-item>
|
|
||||||
<q-item><q-item-section side>Durée estimée</q-item-section><q-item-section>≈ {{ estimatedMinutes }} min</q-item-section></q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section class="bg-red-1 text-red-9">
|
|
||||||
<q-icon name="warning" /> <strong>Confirmation finale.</strong>
|
|
||||||
L'envoi démarre dès le clic sur <em>"Lancer l'envoi maintenant"</em>.
|
|
||||||
Tu seras redirigé vers la page de progression temps réel.
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
<q-stepper-navigation>
|
|
||||||
<q-btn flat label="Retour modifier" icon="arrow_back" @click="step = 2" />
|
|
||||||
<q-space />
|
|
||||||
<q-btn flat color="primary" icon="visibility" label="Aperçu courriel" :disable="!firstPreviewable" @click="openPreview" class="q-mr-sm" />
|
|
||||||
<q-btn unelevated color="negative" label="Lancer l'envoi maintenant" icon-right="send"
|
|
||||||
:loading="sending" @click="confirmAndLaunch" />
|
|
||||||
</q-stepper-navigation>
|
|
||||||
</q-step>
|
|
||||||
|
|
||||||
</q-stepper>
|
|
||||||
|
|
||||||
<!-- Manual-add dialog — push a recipient into recipients[] without going
|
|
||||||
through CSV parsing. Useful for ad-hoc gifts, retroactive top-ups,
|
|
||||||
or test sends to internal stakeholders. The matchCustomer() lookup
|
|
||||||
that the CSV path does is skipped here — customer_id stays empty
|
|
||||||
unless the user manually pastes it. -->
|
|
||||||
<q-dialog v-model="manualOpen" persistent>
|
|
||||||
<q-card style="min-width: 480px; max-width: 640px;">
|
|
||||||
<q-card-section class="row items-center q-pb-none">
|
|
||||||
<div class="text-h6"><q-icon name="person_add" class="q-mr-sm" />Ajouter un destinataire</div>
|
|
||||||
<q-space />
|
|
||||||
<q-btn flat dense round icon="close" v-close-popup />
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<q-form @submit="submitManualRow" class="q-gutter-sm">
|
|
||||||
<div class="row q-col-gutter-sm">
|
|
||||||
<q-input v-model="manualRow.firstname" label="Prénom *" outlined dense
|
|
||||||
class="col-12 col-sm-6" :rules="[v => !!v || 'requis']" autofocus />
|
|
||||||
<q-input v-model="manualRow.lastname" label="Nom" outlined dense
|
|
||||||
class="col-12 col-sm-6" />
|
|
||||||
</div>
|
|
||||||
<q-input v-model="manualRow.email" label="Email *" outlined dense type="email"
|
|
||||||
:rules="[v => !!v || 'requis', v => /.+@.+\..+/.test(v) || 'email invalide']" />
|
|
||||||
<q-input v-model="manualRow.gift_url" label="Lien-cadeau Giftbit *" outlined dense
|
|
||||||
placeholder="https://gft.link/..."
|
|
||||||
:rules="[v => !!v || 'requis', v => /^https?:\/\//.test(v) || 'doit commencer par http:// ou https://']" />
|
|
||||||
<q-input v-model="manualRow.civic_address" label="Adresse civique (pour {{description}})" outlined dense
|
|
||||||
placeholder="25 Rue des Hirondelles" />
|
|
||||||
<div class="row q-col-gutter-sm">
|
|
||||||
<q-input v-model="manualRow.city" label="Ville" outlined dense
|
|
||||||
placeholder="Ste-Clotilde" class="col-12 col-sm-7" />
|
|
||||||
<q-input v-model="manualRow.postal_code" label="Code postal" outlined dense
|
|
||||||
placeholder="J0L 1W0" class="col-12 col-sm-5" />
|
|
||||||
</div>
|
|
||||||
<div class="row q-col-gutter-sm">
|
|
||||||
<q-select v-model="manualRow.language" :options="languageOptions" emit-value map-options
|
|
||||||
label="Langue du template" outlined dense class="col-12 col-sm-6" />
|
|
||||||
<q-input v-model="manualRow.phone" label="Téléphone (optionnel)" outlined dense class="col-12 col-sm-6" />
|
|
||||||
</div>
|
|
||||||
<div class="row q-col-gutter-sm">
|
|
||||||
<q-input v-model="manualRow.amount" label="Montant affiché dans le courriel" outlined dense
|
|
||||||
:placeholder="`défaut: ${params.amount}`" class="col-12 col-sm-6">
|
|
||||||
<q-tooltip><span v-pre>Texte qui apparaîtra à la place de la variable {{amount}}. Laisse vide pour utiliser le montant de la campagne.</span></q-tooltip>
|
|
||||||
</q-input>
|
|
||||||
<q-input v-model.number="manualRow.gift_value_cents" type="number"
|
|
||||||
label="Valeur en cents (rapport)" outlined dense placeholder="5000"
|
|
||||||
class="col-12 col-sm-6" /></div>
|
|
||||||
<div class="text-caption text-grey-7 q-mt-xs">
|
|
||||||
<q-icon name="info" size="14px" /> Ville et code postal ne s'affichent pas dans le courriel —
|
|
||||||
ils servent à éviter une confusion entre deux clients du même nom dans le tableau ci-dessous
|
|
||||||
et dans le rapport CSV.
|
|
||||||
</div>
|
|
||||||
<div class="text-caption text-grey-7 q-mt-xs">
|
|
||||||
<q-icon name="info" size="14px" /> Aucun match ERPNext n'est tenté.
|
|
||||||
<span v-pre>Seule l'adresse civique apparaît dans le courriel (variable <code>{{description}}</code>).</span>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-md">
|
|
||||||
<q-space />
|
|
||||||
<q-btn flat label="Annuler" v-close-popup class="q-mr-sm" />
|
|
||||||
<q-btn unelevated color="primary" type="submit" icon="add" label="Ajouter" />
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<!-- Preview dialog — renders the actual template with the first sendable
|
|
||||||
recipient's data + the campaign params. Lets the user verify the
|
|
||||||
visual + content WITHOUT firing any emails. Toggleable FR/EN since
|
|
||||||
a mixed-language campaign would send both templates. -->
|
|
||||||
<q-dialog v-model="previewOpen" maximized persistent>
|
|
||||||
<q-card class="bg-grey-2">
|
|
||||||
<q-toolbar class="bg-white" style="border-bottom:1px solid #e5e7eb">
|
|
||||||
<q-icon name="visibility" class="q-mr-sm" />
|
|
||||||
<q-toolbar-title>
|
|
||||||
Aperçu du courriel
|
|
||||||
<span v-if="previewRecipient" class="text-caption text-grey-7">
|
|
||||||
· destinataire #{{ previewRecipient.row_index }} {{ previewRecipient.firstname }} {{ previewRecipient.lastname }}
|
|
||||||
</span>
|
|
||||||
</q-toolbar-title>
|
|
||||||
<q-btn-toggle v-model="previewLang" :options="[{label:'🇫🇷 FR', value:'fr'},{label:'🇺🇸 EN', value:'en'}]"
|
|
||||||
dense unelevated toggle-color="primary" @update:model-value="renderPreview" />
|
|
||||||
<q-btn flat icon="open_in_new" :href="editorHref" target="_blank" class="q-mx-sm">
|
|
||||||
<q-tooltip>Éditer dans un nouvel onglet</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn flat dense round icon="close" @click="previewOpen = false" />
|
|
||||||
</q-toolbar>
|
|
||||||
<q-banner v-if="previewLoading" class="bg-blue-1 text-blue-9">
|
|
||||||
<q-spinner size="sm" /> Rendu en cours…
|
|
||||||
</q-banner>
|
|
||||||
<q-card-section class="q-pa-md" style="height: calc(100vh - 60px); overflow:hidden">
|
|
||||||
<iframe :srcdoc="previewHtmlContent" style="width:100%; height:100%; border:1px solid #e5e7eb; background:#fff;"></iframe>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</q-page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { useQuasar } from 'quasar'
|
|
||||||
import { parseCsvs, createCampaign, sendCampaign, previewTemplate } from 'src/api/campaigns'
|
|
||||||
|
|
||||||
const $q = useQuasar()
|
|
||||||
const router = useRouter()
|
|
||||||
const step = ref(1)
|
|
||||||
|
|
||||||
const mapFile = ref(null)
|
|
||||||
const giftFile = ref(null)
|
|
||||||
const mapPreview = ref('')
|
|
||||||
const giftPreview = ref('')
|
|
||||||
|
|
||||||
const params = ref({
|
|
||||||
name: `Campagne ${new Date().toISOString().slice(0,10)}`,
|
|
||||||
amount: '60 $',
|
|
||||||
commitment_months: 3,
|
|
||||||
subject: '🎁 Un cadeau pour toi, de la part de TARGO',
|
|
||||||
from: 'TARGO <support@targointernet.com>',
|
|
||||||
expiry: '',
|
|
||||||
throttle_ms: 600,
|
|
||||||
multi: 'first',
|
|
||||||
})
|
|
||||||
const multiOptions = [
|
|
||||||
{ label: '1er email seulement (1 cadeau/foyer)', value: 'first' },
|
|
||||||
{ label: 'Séparer en 2 rangées (1 cadeau/personne)', value: 'split' },
|
|
||||||
{ label: 'Ignorer les couples', value: 'skip' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const parsing = ref(false)
|
|
||||||
const sending = ref(false)
|
|
||||||
const recipients = ref([])
|
|
||||||
const unpairedContacts = ref([])
|
|
||||||
const unusedGifts = ref([])
|
|
||||||
|
|
||||||
// row_index (#1, #2, ...) is the source-CSV position — invaluable for the
|
|
||||||
// user to cross-reference what they see here against the file they uploaded.
|
|
||||||
// gift_url is rendered as a clickable short label so contact↔link pairing
|
|
||||||
// can be eyeballed at a glance and the user can click through to verify
|
|
||||||
// the shortlink works.
|
|
||||||
const recipientColumns = [
|
|
||||||
{ name: 'row_index', label: '#', field: 'row_index', align: 'left' },
|
|
||||||
{ name: 'firstname', label: 'Prénom', field: 'firstname', align: 'left' },
|
|
||||||
{ name: 'lastname', label: 'Nom', field: 'lastname', align: 'left' },
|
|
||||||
{ name: 'email', label: 'Email', field: 'email', align: 'left' },
|
|
||||||
{ name: 'civic_address', label: 'Adresse', field: 'civic_address', align: 'left' },
|
|
||||||
{ name: 'city', label: 'Ville', field: r => r.city || r.postal_code || '', align: 'left' },
|
|
||||||
{ name: 'gift_url', label: 'Lien-cadeau', field: 'gift_url', align: 'left' },
|
|
||||||
{ name: 'language', label: 'Langue', field: 'language', align: 'left' },
|
|
||||||
{ name: 'match', label: 'Match', field: 'match_method', align: 'left' },
|
|
||||||
{ name: 'customer_name', label: 'Client ERPNext', field: 'customer_name', align: 'left' },
|
|
||||||
{ name: 'actions', label: '', field: '', align: 'right' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const unpairedColumns = [
|
|
||||||
{ name: 'row_index', label: '#', field: 'row_index', align: 'left' },
|
|
||||||
{ name: 'firstname', label: 'Prénom', field: 'firstname', align: 'left' },
|
|
||||||
{ name: 'lastname', label: 'Nom', field: 'lastname', align: 'left' },
|
|
||||||
{ name: 'email', label: 'Email', field: 'email', align: 'left' },
|
|
||||||
{ name: 'civic_address', label: 'Adresse', field: 'civic_address', align: 'left' },
|
|
||||||
{ name: 'postal_code', label: 'Code postal', field: 'postal_code', align: 'left' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const matchedCount = computed(() => recipients.value.filter(r => r.customer_id).length)
|
|
||||||
const unmatchedCount = computed(() => recipients.value.filter(r => !r.customer_id).length)
|
|
||||||
const excludedCount = computed(() => recipients.value.filter(r => r.excluded).length)
|
|
||||||
// Net number of emails that will actually be fired off (paired AND not excluded)
|
|
||||||
const sendableCount = computed(() => recipients.value.filter(r => !r.excluded && r.gift_url).length)
|
|
||||||
|
|
||||||
// Names that the auto-cleaner couldn't confidently fix. Heuristic warnings
|
|
||||||
// from the backend (digit in name, two names possibly stuck together, etc.).
|
|
||||||
// User should glance at these before sending.
|
|
||||||
const namesNeedingReview = computed(() =>
|
|
||||||
recipients.value.filter(r => r.name_warnings?.firstname || r.name_warnings?.lastname).length
|
|
||||||
)
|
|
||||||
const namesAutoCorrected = computed(() =>
|
|
||||||
recipients.value.filter(r => r.cleaned_changed).length
|
|
||||||
)
|
|
||||||
|
|
||||||
// FR / EN breakdown of the sendable recipients — useful preview before launch
|
|
||||||
// so the user knows which template will actually be used and how many.
|
|
||||||
const langBreakdown = computed(() => {
|
|
||||||
const counts = {}
|
|
||||||
for (const r of recipients.value) {
|
|
||||||
if (r.excluded || !r.gift_url) continue
|
|
||||||
const lang = (r.language || 'fr').toLowerCase()
|
|
||||||
counts[lang] = (counts[lang] || 0) + 1
|
|
||||||
}
|
|
||||||
const parts = Object.entries(counts).map(([l, n]) => `${n} × ${l.toUpperCase()}`)
|
|
||||||
return parts.length ? parts.join(', ') : '—'
|
|
||||||
})
|
|
||||||
const estimatedMinutes = computed(() => {
|
|
||||||
const per = (params.value.throttle_ms || 600) / 1000
|
|
||||||
return Math.max(1, Math.round((sendableCount.value * per) / 60))
|
|
||||||
})
|
|
||||||
|
|
||||||
function shortenUrl (u) {
|
|
||||||
if (!u) return ''
|
|
||||||
return u.replace(/^https?:\/\//, '').slice(0, 28) + (u.length > 35 ? '…' : '')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Preview dialog ──────────────────────────────────────────────────────────
|
|
||||||
// Renders the email through the hub's /campaigns/templates/:name/preview
|
|
||||||
// endpoint, using the first sendable recipient's data + the current campaign
|
|
||||||
// params. Non-destructive — no emails are fired by this action.
|
|
||||||
const previewOpen = ref(false)
|
|
||||||
const previewLoading = ref(false)
|
|
||||||
const previewHtmlContent = ref('')
|
|
||||||
const previewLang = ref('fr')
|
|
||||||
const previewRecipient = ref(null)
|
|
||||||
|
|
||||||
// First recipient that's actually going to be sent — used as the preview
|
|
||||||
// sample so the user sees real data, not synthetic placeholders.
|
|
||||||
const firstPreviewable = computed(() =>
|
|
||||||
recipients.value.find(r => !r.excluded && r.gift_url) || null
|
|
||||||
)
|
|
||||||
|
|
||||||
// Link to the template editor for the relevant language. Always opens
|
|
||||||
// in a new tab so the user's in-progress wizard state is preserved.
|
|
||||||
const editorHref = computed(() =>
|
|
||||||
`/ops/#/campaigns/templates/gift-email-${previewLang.value}`
|
|
||||||
)
|
|
||||||
|
|
||||||
async function openPreview () {
|
|
||||||
const r = firstPreviewable.value
|
|
||||||
if (!r) return
|
|
||||||
previewRecipient.value = r
|
|
||||||
// Default preview language to the recipient's actual language so the user
|
|
||||||
// first sees what THIS recipient will receive
|
|
||||||
previewLang.value = (r.language || 'fr').toLowerCase().split('-')[0]
|
|
||||||
previewOpen.value = true
|
|
||||||
await renderPreview()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function renderPreview () {
|
|
||||||
if (!previewRecipient.value) return
|
|
||||||
previewLoading.value = true
|
|
||||||
try {
|
|
||||||
const r = previewRecipient.value
|
|
||||||
const vars = {
|
|
||||||
firstname: r.firstname || (previewLang.value === 'en' ? 'dear customer' : 'cher client'),
|
|
||||||
lastname: r.lastname || '',
|
|
||||||
email: r.email,
|
|
||||||
description: r.civic_address || '',
|
|
||||||
gift_url: r.gift_url,
|
|
||||||
amount: params.value.amount,
|
|
||||||
expiry: params.value.expiry,
|
|
||||||
commitment_months: params.value.commitment_months,
|
|
||||||
year: new Date().getFullYear(),
|
|
||||||
}
|
|
||||||
const res = await previewTemplate(`gift-email-${previewLang.value}`, { vars })
|
|
||||||
previewHtmlContent.value = res.rendered || ''
|
|
||||||
} catch (e) {
|
|
||||||
$q.notify({ type: 'negative', message: 'Aperçu impossible: ' + e.message })
|
|
||||||
} finally {
|
|
||||||
previewLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrapper around launchSend that confirms one last time before firing. The
|
|
||||||
// Step 3 page is already a "confirmation step", but this dialog adds one
|
|
||||||
// final friction so accidental clicks don't fire 200 emails.
|
|
||||||
function confirmAndLaunch () {
|
|
||||||
$q.dialog({
|
|
||||||
title: 'Envoyer maintenant ?',
|
|
||||||
message: `Cette action enverra <strong>${sendableCount.value} courriel(s)</strong>
|
|
||||||
via Mailjet immédiatement. Pas annulable une fois démarré.`,
|
|
||||||
html: true,
|
|
||||||
persistent: true,
|
|
||||||
ok: { label: 'Oui, envoyer', color: 'negative', icon: 'send', unelevated: true },
|
|
||||||
cancel: { label: 'Annuler', flat: true },
|
|
||||||
}).onOk(() => {
|
|
||||||
launchSend()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function readFile (file) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const r = new FileReader()
|
|
||||||
r.onload = () => resolve(r.result)
|
|
||||||
r.onerror = reject
|
|
||||||
r.readAsText(file, 'utf-8')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
async function readMapFile (file) { if (file) mapPreview.value = await readFile(file) }
|
|
||||||
async function readGiftFile (file) { if (file) giftPreview.value = await readFile(file) }
|
|
||||||
|
|
||||||
// Counts must mirror the backend parser exactly so the user sees the same
|
|
||||||
// numbers in the preview as what Step 2 will receive.
|
|
||||||
|
|
||||||
// Map CSV format: 1-line title preamble + header row + N data rows.
|
|
||||||
// Returns N (the # of contact lines, excluding the preamble and header).
|
|
||||||
function countMapRows (text) {
|
|
||||||
if (!text) return 0
|
|
||||||
const lines = text.split(/\r?\n/).filter(l => l.trim())
|
|
||||||
// -1 for preamble, -1 for header
|
|
||||||
return Math.max(0, lines.length - 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Giftbit CSV: TWO formats
|
|
||||||
// 1. "Link Order" — headerless, one URL per line (each URL = 1 gift)
|
|
||||||
// 2. "Campaign export" — header row + N data rows (-1 for header)
|
|
||||||
// Detect like the backend: first non-empty line is a bare URL with no
|
|
||||||
// separator → no header.
|
|
||||||
function countGiftRows (text) {
|
|
||||||
if (!text) return 0
|
|
||||||
const cleaned = text.replace(/^/, '').trim()
|
|
||||||
if (!cleaned) return 0
|
|
||||||
const firstLine = cleaned.split(/\r?\n/, 1)[0].trim()
|
|
||||||
const isLinkOrder = /^https?:\/\/\S+$/.test(firstLine) &&
|
|
||||||
!firstLine.includes(',') && !firstLine.includes('\t') && !firstLine.includes('|')
|
|
||||||
const lines = cleaned.split(/\r?\n/).filter(l => l.trim())
|
|
||||||
return isLinkOrder ? lines.length : Math.max(0, lines.length - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show "Link Order" or "Campaign export" hint next to the gift count
|
|
||||||
const giftFormatHint = computed(() => {
|
|
||||||
if (!giftPreview.value) return ''
|
|
||||||
const firstLine = giftPreview.value.replace(/^/, '').trim().split(/\r?\n/, 1)[0].trim()
|
|
||||||
if (/^https?:\/\/\S+$/.test(firstLine) && !firstLine.includes(',') && !firstLine.includes('\t') && !firstLine.includes('|')) {
|
|
||||||
return 'Link Order'
|
|
||||||
}
|
|
||||||
return 'Campaign export'
|
|
||||||
})
|
|
||||||
|
|
||||||
async function goPreview () {
|
|
||||||
if (!mapPreview.value || !giftPreview.value) return
|
|
||||||
parsing.value = true
|
|
||||||
try {
|
|
||||||
const r = await parseCsvs({ map_csv: mapPreview.value, giftbit_csv: giftPreview.value, multi: params.value.multi })
|
|
||||||
recipients.value = r.recipients || []
|
|
||||||
unpairedContacts.value = r.unpaired_contacts || []
|
|
||||||
unusedGifts.value = r.unused_gifts || []
|
|
||||||
step.value = 2
|
|
||||||
} catch (e) {
|
|
||||||
$q.notify({ type: 'negative', message: 'Erreur de parsing: ' + e.message })
|
|
||||||
} finally {
|
|
||||||
parsing.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip CSV parsing entirely — empty recipients list, user adds rows manually
|
|
||||||
// via the "Ajouter manuellement" button on Step 2. The Suivant button is
|
|
||||||
// disabled if no rows, so the user can't accidentally proceed with nothing.
|
|
||||||
function goManual () {
|
|
||||||
recipients.value = []
|
|
||||||
unpairedContacts.value = []
|
|
||||||
unusedGifts.value = []
|
|
||||||
step.value = 2
|
|
||||||
// Open the dialog immediately — the most likely next action is adding a row
|
|
||||||
openManualDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Manual recipient entry ────────────────────────────────────────────────
|
|
||||||
// Lets the user add a recipient by typing the fields directly instead of
|
|
||||||
// importing from CSV. Useful for: small ad-hoc gifts, replacement sends after
|
|
||||||
// a bounce, internal QA test sends, or topping up an existing CSV import
|
|
||||||
// with a missed contact. No ERPNext matching is attempted on these rows.
|
|
||||||
const manualOpen = ref(false)
|
|
||||||
const languageOptions = [
|
|
||||||
{ label: '🇫🇷 Français', value: 'fr' },
|
|
||||||
{ label: '🇺🇸 English', value: 'en' },
|
|
||||||
]
|
|
||||||
function emptyManualRow () {
|
|
||||||
return {
|
|
||||||
firstname: '', lastname: '', email: '', phone: '',
|
|
||||||
civic_address: '', city: '', postal_code: '', language: 'fr',
|
|
||||||
gift_url: '', gift_value_cents: null, amount: '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const manualRow = ref(emptyManualRow())
|
|
||||||
|
|
||||||
function openManualDialog () {
|
|
||||||
manualRow.value = emptyManualRow()
|
|
||||||
manualOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitManualRow () {
|
|
||||||
// Determine next row_index. CSV-imported rows start at 1; manuals continue
|
|
||||||
// the sequence so #N reads naturally regardless of source.
|
|
||||||
const maxIdx = recipients.value.reduce((m, r) => Math.max(m, r.row_index || 0), 0)
|
|
||||||
recipients.value.push({
|
|
||||||
row_index: maxIdx + 1,
|
|
||||||
firstname: (manualRow.value.firstname || '').trim(),
|
|
||||||
lastname: (manualRow.value.lastname || '').trim(),
|
|
||||||
email: (manualRow.value.email || '').trim().toLowerCase(),
|
|
||||||
phone: (manualRow.value.phone || '').trim(),
|
|
||||||
civic_address: (manualRow.value.civic_address || '').trim(),
|
|
||||||
city: (manualRow.value.city || '').trim(),
|
|
||||||
postal_code: (manualRow.value.postal_code || '').trim().toUpperCase(),
|
|
||||||
language: manualRow.value.language || 'fr',
|
|
||||||
gift_url: (manualRow.value.gift_url || '').trim(),
|
|
||||||
gift_value_cents: manualRow.value.gift_value_cents || null,
|
|
||||||
// Per-recipient amount override. Empty string → falls back to campaign
|
|
||||||
// params.amount in the worker. Useful for manuals on a mixed-amount campaign.
|
|
||||||
amount: (manualRow.value.amount || '').trim() || null,
|
|
||||||
giftbit_uuid: null,
|
|
||||||
// Flag for downstream code + table display so a "manuel" chip can be shown
|
|
||||||
// and the match-method column doesn't read "non lié" misleadingly.
|
|
||||||
manual: true,
|
|
||||||
match_method: 'manuel',
|
|
||||||
customer_id: null,
|
|
||||||
status: 'pending',
|
|
||||||
excluded: false,
|
|
||||||
})
|
|
||||||
manualOpen.value = false
|
|
||||||
$q.notify({ type: 'positive', message: 'Destinataire ajouté' })
|
|
||||||
}
|
|
||||||
|
|
||||||
async function launchSend () {
|
|
||||||
sending.value = true
|
|
||||||
try {
|
|
||||||
const saved = await createCampaign({
|
|
||||||
name: params.value.name,
|
|
||||||
params: { ...params.value },
|
|
||||||
recipients: recipients.value,
|
|
||||||
})
|
|
||||||
await sendCampaign(saved.id)
|
|
||||||
router.push(`/campaigns/${saved.id}`)
|
|
||||||
} catch (e) {
|
|
||||||
$q.notify({ type: 'negative', message: 'Erreur: ' + e.message })
|
|
||||||
} finally {
|
|
||||||
sending.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-page padding>
|
|
||||||
<div class="row items-center q-mb-md">
|
|
||||||
<div class="text-h5">Campagnes</div>
|
|
||||||
<q-space />
|
|
||||||
<q-btn flat color="primary" icon="palette" label="Éditer le template" :to="'/campaigns/templates/gift-email-fr'" class="q-mr-sm" />
|
|
||||||
<q-btn unelevated color="primary" icon="add" label="Nouvelle campagne" :to="'/campaigns/new'" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-card flat bordered v-if="!loading && campaigns.length === 0" class="q-pa-xl text-center text-grey-7">
|
|
||||||
<q-icon name="card_giftcard" size="48px" class="q-mb-md" />
|
|
||||||
<div class="text-h6">Aucune campagne pour le moment</div>
|
|
||||||
<div class="q-mt-sm">
|
|
||||||
Une campagne envoie des cartes-cadeaux Giftbit par courriel personnalisé.
|
|
||||||
Importer 2 CSV (export Map + shortlinks Giftbit) et lancer l'envoi via Mailjet.
|
|
||||||
</div>
|
|
||||||
<q-btn class="q-mt-lg" color="primary" icon="add" label="Créer la première" :to="'/campaigns/new'" />
|
|
||||||
</q-card>
|
|
||||||
|
|
||||||
<q-table
|
|
||||||
v-else
|
|
||||||
:rows="campaigns"
|
|
||||||
:columns="columns"
|
|
||||||
row-key="id"
|
|
||||||
:loading="loading"
|
|
||||||
flat bordered
|
|
||||||
:pagination="{ rowsPerPage: 25, sortBy: 'created_at', descending: true }"
|
|
||||||
>
|
|
||||||
<template v-slot:body-cell-status="props">
|
|
||||||
<q-td :props="props">
|
|
||||||
<q-chip dense :color="statusColor(props.row.status)" text-color="white" :label="statusLabel(props.row.status)" />
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body-cell-progress="props">
|
|
||||||
<q-td :props="props">
|
|
||||||
<div class="row items-center q-gutter-xs">
|
|
||||||
<span class="text-grey-7">{{ (props.row.counters?.sent || 0) }} / {{ props.row.total || 0 }}</span>
|
|
||||||
<q-linear-progress
|
|
||||||
:value="(props.row.counters?.sent || 0) / Math.max(1, props.row.total || 1)"
|
|
||||||
size="6px"
|
|
||||||
:color="props.row.counters?.failed ? 'negative' : 'positive'"
|
|
||||||
style="min-width:80px"
|
|
||||||
/>
|
|
||||||
<span v-if="props.row.counters?.failed" class="text-negative text-caption">
|
|
||||||
{{ props.row.counters.failed }} échec(s)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body-cell-actions="props">
|
|
||||||
<q-td :props="props" class="text-right">
|
|
||||||
<q-btn flat dense color="primary" icon="visibility" :to="`/campaigns/${props.row.id}`" />
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</q-page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import { listCampaigns } from 'src/api/campaigns'
|
|
||||||
|
|
||||||
const campaigns = ref([])
|
|
||||||
const loading = ref(true)
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ name: 'name', label: 'Nom', field: 'name', align: 'left', sortable: true },
|
|
||||||
{ name: 'created_at', label: 'Créée', field: r => new Date(r.created_at).toLocaleString('fr-CA', { dateStyle: 'medium', timeStyle: 'short' }), align: 'left', sortable: true },
|
|
||||||
{ name: 'status', label: 'Statut', field: 'status', align: 'left' },
|
|
||||||
{ name: 'progress', label: 'Envois', field: 'counters', align: 'left' },
|
|
||||||
{ name: 'actions', label: '', field: '', align: 'right' },
|
|
||||||
]
|
|
||||||
|
|
||||||
function statusColor (s) {
|
|
||||||
return { draft: 'grey', sending: 'orange', completed: 'positive', failed: 'negative' }[s] || 'grey'
|
|
||||||
}
|
|
||||||
function statusLabel (s) {
|
|
||||||
return { draft: 'Brouillon', sending: 'En cours', completed: 'Terminée', failed: 'Échec' }[s] || s
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load () {
|
|
||||||
loading.value = true
|
|
||||||
try { campaigns.value = await listCampaigns() }
|
|
||||||
finally { loading.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(load)
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,500 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-page>
|
|
||||||
<!-- Top bar: template selector + saved chip + quick actions. The Unlayer
|
|
||||||
editor below has its own toolbar for blocks/preview/etc. -->
|
|
||||||
<div class="row items-center q-px-md q-py-sm bg-white" style="border-bottom:1px solid #e5e7eb">
|
|
||||||
<q-btn flat dense round icon="arrow_back" :to="'/campaigns'" class="q-mr-sm" />
|
|
||||||
<div class="text-h6 q-mr-md">Éditeur de template</div>
|
|
||||||
<q-select v-model="currentName" :options="templateOptions" emit-value map-options dense outlined
|
|
||||||
style="min-width:240px" @update:model-value="onSelectTemplate" />
|
|
||||||
<q-btn flat dense icon="add" label="Nouveau" color="primary" class="q-ml-sm" @click="newTemplateOpen = true">
|
|
||||||
<q-tooltip>Créer un nouveau template (vide ou copié depuis un existant)</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-chip v-if="lastSavedTs" dense size="sm" color="grey-2" text-color="grey-9" class="q-ml-sm" icon="cloud_done">
|
|
||||||
Sauvegardé · {{ lastSavedLabel }}
|
|
||||||
</q-chip>
|
|
||||||
<q-btn flat dense icon="code" class="q-ml-sm" color="grey-7">
|
|
||||||
<q-tooltip max-width="320px">
|
|
||||||
<strong>9 variables disponibles</strong> (Client / Offre / Système).
|
|
||||||
Insertion : clic dans un texte → barre flottante → icône <code>{}</code> Merge Tags.
|
|
||||||
Marche aussi dans les champs URL (boutons, images, mailto).
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-space />
|
|
||||||
<q-btn flat color="primary" icon="visibility" label="Aperçu inbox" class="q-mr-sm" @click="openPreview">
|
|
||||||
<q-tooltip>Voir le HTML rendu (substitué) tel que reçu par le destinataire</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn flat color="purple-7" icon="translate" label="Traduire (AI)" class="q-mr-sm"
|
|
||||||
:disable="!aiTranslateTargetName" @click="aiTranslateOpen = true">
|
|
||||||
<q-tooltip>{{ aiTranslateTargetName ? `Traduire vers ${aiTranslateTargetName} via Gemini` : 'Disponible pour les templates avec suffixe -fr ou -en' }}</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn flat color="orange-9" icon="send" label="Envoyer un test" class="q-mr-sm" @click="testSendOpen = true">
|
|
||||||
<q-tooltip>Envoyer un courriel réel à une adresse de test</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn unelevated color="primary" icon="save" label="Enregistrer"
|
|
||||||
:loading="saving" @click="saveTemplate" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Unlayer editor — Vue 3 native, no iframe, full features:
|
|
||||||
responsive preview, AMP blocks, Unsplash, file manager, dark mode,
|
|
||||||
layers/structure panel, design tokens, etc.
|
|
||||||
Wrapped in an explicit-sized container so the inner iframe gets
|
|
||||||
enough height/width (Quasar's q-page doesn't propagate dimensions
|
|
||||||
that easyEditor's nested iframe can pick up automatically). -->
|
|
||||||
<div style="height: calc(100vh - 60px); width: 100%; overflow: hidden; position: relative;">
|
|
||||||
<EmailEditor
|
|
||||||
ref="editor"
|
|
||||||
:options="editorOptions"
|
|
||||||
:min-height="'100%'"
|
|
||||||
style="height: 100%; width: 100%;"
|
|
||||||
@load="onEditorLoad"
|
|
||||||
@ready="onEditorReady"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Aperçu dialog — renders the latest saved HTML with sample vars -->
|
|
||||||
<q-dialog v-model="previewOpen" maximized persistent>
|
|
||||||
<q-card class="bg-grey-2">
|
|
||||||
<q-toolbar class="bg-white" style="border-bottom:1px solid #e5e7eb">
|
|
||||||
<q-icon name="visibility" class="q-mr-sm" />
|
|
||||||
<q-toolbar-title>Aperçu inbox · {{ currentName }}</q-toolbar-title>
|
|
||||||
<q-btn flat dense round icon="close" @click="previewOpen = false" />
|
|
||||||
</q-toolbar>
|
|
||||||
<q-banner v-if="previewLoading" class="bg-blue-1 text-blue-9"><q-spinner size="sm" /> Rendu en cours…</q-banner>
|
|
||||||
<q-card-section style="height: calc(100vh - 60px); overflow:hidden">
|
|
||||||
<iframe :srcdoc="previewHtmlContent" style="width:100%; height:100%; border:1px solid #e5e7eb; background:#fff;" />
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<!-- New template dialog — name + starter -->
|
|
||||||
<q-dialog v-model="newTemplateOpen" persistent>
|
|
||||||
<q-card style="min-width: 500px; max-width: 90vw">
|
|
||||||
<q-card-section class="row items-center bg-blue-1 text-blue-9">
|
|
||||||
<q-icon name="add" class="q-mr-sm" />
|
|
||||||
<div class="text-h6">Nouveau template</div>
|
|
||||||
<q-space />
|
|
||||||
<q-btn flat dense round icon="close" v-close-popup />
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<q-input v-model="newTemplateForm.suffix" outlined dense autofocus
|
|
||||||
label="Nom (suffixe après gift-email-)" :prefix="newTemplateForm.prefix + '-'"
|
|
||||||
:rules="[v => /^[a-z0-9-]+$/.test(v) || 'Lettres minuscules, chiffres et tirets seulement']"
|
|
||||||
hint="Exemple: summer-2026, automne-promo, anniversaire" class="q-mb-md" />
|
|
||||||
<q-select v-model="newTemplateForm.prefix" :options="['gift-email','newsletter','transactional']"
|
|
||||||
label="Type" outlined dense class="q-mb-md" />
|
|
||||||
<q-select v-model="newTemplateForm.starter" :options="starterOptions" emit-value map-options
|
|
||||||
label="Démarrer depuis" outlined dense class="q-mb-md" />
|
|
||||||
<q-banner v-if="newTemplateFinal" class="bg-grey-2 text-grey-8 q-mt-sm" rounded dense>
|
|
||||||
<q-icon name="info" class="q-mr-xs" />
|
|
||||||
Le template sera créé sous le nom <strong>{{ newTemplateFinal }}</strong>
|
|
||||||
<span v-if="newTemplateForm.starter !== 'blank'">
|
|
||||||
· contenu copié depuis <em>{{ newTemplateForm.starter }}</em>
|
|
||||||
</span>
|
|
||||||
</q-banner>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-actions align="right">
|
|
||||||
<q-btn flat label="Annuler" v-close-popup />
|
|
||||||
<q-btn unelevated color="primary" icon="add" label="Créer le template"
|
|
||||||
:loading="creating" :disable="!newTemplateValid" @click="createTemplate" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<!-- AI translate dialog -->
|
|
||||||
<q-dialog v-model="aiTranslateOpen" persistent>
|
|
||||||
<q-card style="min-width: 500px; max-width: 90vw">
|
|
||||||
<q-card-section class="row items-center bg-purple-1 text-purple-9">
|
|
||||||
<q-icon name="translate" class="q-mr-sm" />
|
|
||||||
<div class="text-h6">Traduire avec Gemini AI</div>
|
|
||||||
<q-space />
|
|
||||||
<q-btn flat dense round icon="close" v-close-popup />
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="q-mb-md">
|
|
||||||
Le contenu de <strong>{{ currentName }}</strong> sera traduit vers
|
|
||||||
<strong>{{ aiTranslateTargetName }}</strong> via Gemini Flash.
|
|
||||||
</div>
|
|
||||||
<q-banner class="bg-grey-2 text-grey-8 q-mb-md" rounded dense>
|
|
||||||
<q-icon name="info" class="q-mr-xs" />
|
|
||||||
<!-- v-pre on this span so Vue doesn't try to compile the literal
|
|
||||||
{{...}} braces inside the explanatory <code> tag. -->
|
|
||||||
<span v-pre>
|
|
||||||
Le AI préserve la structure HTML, les variables <code>{{...}}</code>,
|
|
||||||
les URLs, les noms de marque (TARGO, Giftbit) et les emojis.
|
|
||||||
Il traduit seulement le texte visible.
|
|
||||||
</span>
|
|
||||||
</q-banner>
|
|
||||||
<q-banner v-if="targetTemplateExists" class="bg-amber-1 text-amber-9 q-mb-md" rounded dense>
|
|
||||||
<q-icon name="warning" class="q-mr-xs" />
|
|
||||||
Le template <strong>{{ aiTranslateTargetName }}</strong> existe déjà.
|
|
||||||
La traduction va l'écraser (backup automatique avant).
|
|
||||||
<q-toggle v-model="aiTranslateOverride" label="Confirmer l'écrasement" class="q-mt-sm" />
|
|
||||||
</q-banner>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-actions align="right">
|
|
||||||
<q-btn flat label="Annuler" v-close-popup />
|
|
||||||
<q-btn unelevated color="purple-7" icon="translate" label="Traduire maintenant"
|
|
||||||
:loading="aiTranslating"
|
|
||||||
:disable="targetTemplateExists && !aiTranslateOverride"
|
|
||||||
@click="doAiTranslate" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<!-- Test-send dialog -->
|
|
||||||
<q-dialog v-model="testSendOpen" persistent>
|
|
||||||
<q-card style="min-width: 500px; max-width: 90vw">
|
|
||||||
<q-card-section class="row items-center bg-orange-1 text-orange-9">
|
|
||||||
<q-icon name="send" class="q-mr-sm" />
|
|
||||||
<div class="text-h6">Envoyer un test du courriel</div>
|
|
||||||
<q-space />
|
|
||||||
<q-btn flat dense round icon="close" v-close-popup />
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<q-input v-model="testSendForm.to" label="Adresse de destination" outlined dense type="email" autofocus class="q-mb-md" />
|
|
||||||
<q-input v-model="testSendForm.subject" label="Sujet" outlined dense class="q-mb-md" />
|
|
||||||
<div class="text-subtitle2 q-mb-sm">Variables</div>
|
|
||||||
<div class="row q-col-gutter-sm">
|
|
||||||
<q-input v-model="testSendForm.vars.firstname" label="firstname" outlined dense class="col-6" />
|
|
||||||
<q-input v-model="testSendForm.vars.lastname" label="lastname" outlined dense class="col-6" />
|
|
||||||
<q-input v-model="testSendForm.vars.amount" label="amount" outlined dense class="col-6" />
|
|
||||||
<q-input v-model="testSendForm.vars.commitment_months" label="commitment_months" outlined dense class="col-6" />
|
|
||||||
<q-input v-model="testSendForm.vars.gift_url" label="gift_url" outlined dense class="col-12" />
|
|
||||||
<q-input v-model="testSendForm.vars.description" label="description" outlined dense class="col-12" />
|
|
||||||
<q-input v-model="testSendForm.vars.expiry" label="expiry" outlined dense class="col-12" />
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section class="bg-grey-2">
|
|
||||||
<div class="text-caption text-grey-8">
|
|
||||||
Sauvegarde l'éditeur d'abord pour tester la dernière version.
|
|
||||||
Envoyé via Mailjet depuis <code>TARGO <support@targointernet.com></code>.
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-actions align="right">
|
|
||||||
<q-btn flat label="Annuler" v-close-popup />
|
|
||||||
<q-btn unelevated color="primary" icon-right="send" label="Envoyer le test"
|
|
||||||
:loading="testSending" :disable="!testSendForm.to" @click="doTestSend" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</q-page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted } from 'vue'
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
|
||||||
import { useQuasar } from 'quasar'
|
|
||||||
import { EmailEditor } from 'vue-email-editor'
|
|
||||||
import { listTemplates, getTemplate, saveTemplate as saveTemplateApi,
|
|
||||||
previewTemplate, testSendTemplate, translateTemplate } from 'src/api/campaigns'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
const $q = useQuasar()
|
|
||||||
|
|
||||||
// ── Editor ref + Unlayer configuration ──────────────────────────────────────
|
|
||||||
const editor = ref(null)
|
|
||||||
const editorReady = ref(false)
|
|
||||||
const saving = ref(false)
|
|
||||||
const currentName = ref(route.params.name || 'gift-email-fr')
|
|
||||||
|
|
||||||
// Unlayer editor options:
|
|
||||||
// - mergeTags: shown in the "Merge tags" panel, drag-droppable into text
|
|
||||||
// - features: Unsplash, file manager, etc. are all on by default
|
|
||||||
// - tools: which block types to expose
|
|
||||||
// - appearance: light theme matching our ops UI
|
|
||||||
const editorOptions = {
|
|
||||||
appearance: {
|
|
||||||
theme: 'modern_light',
|
|
||||||
panels: {
|
|
||||||
tools: { dock: 'left' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Merge tags organized by category — Unlayer shows these in a dropdown
|
|
||||||
// when editing a text block (click into text → toolbar → {} icon) and
|
|
||||||
// ALSO in URL fields (Button "Action URL", Image "Source", mailto links).
|
|
||||||
// The `sample` field is what Unlayer shows as a preview (so the user sees
|
|
||||||
// realistic content while editing); on send, the hub's Mustache renderer
|
|
||||||
// substitutes the actual value.
|
|
||||||
mergeTags: [
|
|
||||||
{
|
|
||||||
name: 'Client',
|
|
||||||
mergeTags: [
|
|
||||||
{ name: 'Prénom', value: '{{firstname}}', sample: 'Louis' },
|
|
||||||
{ name: 'Nom de famille', value: '{{lastname}}', sample: 'Tremblay' },
|
|
||||||
{ name: 'Courriel', value: '{{email}}', sample: 'louis@targo.ca' },
|
|
||||||
{ name: 'Adresse service', value: '{{description}}', sample: '123 Rue de la Rivière, Ste-Clotilde' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Offre',
|
|
||||||
mergeTags: [
|
|
||||||
{ name: 'Montant', value: '{{amount}}', sample: '60 $' },
|
|
||||||
{ name: 'Lien cadeau (URL)', value: '{{gift_url}}', sample: 'https://gft.link/abc' },
|
|
||||||
{ name: "Date d'expiration", value: '{{expiry}}', sample: '31 décembre 2026' },
|
|
||||||
{ name: 'Engagement (mois)', value: '{{commitment_months}}', sample: '3' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Système',
|
|
||||||
mergeTags: [
|
|
||||||
{ name: 'Année courante', value: '{{year}}', sample: '2026' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
// Display mode: 'email' (default, with mobile preview), 'web' for landing pages
|
|
||||||
displayMode: 'email',
|
|
||||||
// Locale for built-in strings
|
|
||||||
locale: 'fr-CA',
|
|
||||||
// Enable optional sidebar features (free tier limits some — see Unlayer docs)
|
|
||||||
features: {
|
|
||||||
// Built-in Unlayer template library — limited selection without projectId
|
|
||||||
// but still gives the user some pre-built starts to pick from.
|
|
||||||
templates: true,
|
|
||||||
// Unsplash image search panel
|
|
||||||
stockImages: true,
|
|
||||||
// Image upload (uses Unlayer's CDN by default; can be wired to our hub
|
|
||||||
// /campaigns/assets/upload endpoint via customJS later if we want to
|
|
||||||
// self-host uploads — for now their CDN is fine for ad-hoc images)
|
|
||||||
imageEditor: true,
|
|
||||||
// Undo/redo + history
|
|
||||||
undoRedo: true,
|
|
||||||
},
|
|
||||||
// Use Unlayer's free CDN. For paid users this would carry a projectId.
|
|
||||||
// Without a projectId the "Powered by Unlayer" badge shows in the sidebar.
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Template list (selector at the top) ─────────────────────────────────────
|
|
||||||
const templates = ref([])
|
|
||||||
const templateOptions = computed(() => templates.value.map(t => ({
|
|
||||||
label: `${t.name} (${Math.round(t.size / 1024)} KB)`,
|
|
||||||
value: t.name,
|
|
||||||
})))
|
|
||||||
|
|
||||||
async function loadAvailableTemplates () {
|
|
||||||
templates.value = await listTemplates()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── AI translation (Gemini via hub) ─────────────────────────────────────────
|
|
||||||
// Auto-detect source language from the template name suffix (-fr / -en) and
|
|
||||||
// compute the target name with the OPPOSITE suffix.
|
|
||||||
const aiTranslateOpen = ref(false)
|
|
||||||
const aiTranslating = ref(false)
|
|
||||||
const aiTranslateOverride = ref(false)
|
|
||||||
|
|
||||||
const aiTranslateTargetName = computed(() => {
|
|
||||||
const m = (currentName.value || '').match(/^(.+)-(fr|en)$/)
|
|
||||||
if (!m) return ''
|
|
||||||
const opposite = m[2] === 'fr' ? 'en' : 'fr'
|
|
||||||
return `${m[1]}-${opposite}`
|
|
||||||
})
|
|
||||||
|
|
||||||
const targetTemplateExists = computed(() =>
|
|
||||||
!!aiTranslateTargetName.value && !!templates.value.find(t => t.name === aiTranslateTargetName.value),
|
|
||||||
)
|
|
||||||
|
|
||||||
async function doAiTranslate () {
|
|
||||||
if (!aiTranslateTargetName.value) return
|
|
||||||
aiTranslating.value = true
|
|
||||||
try {
|
|
||||||
const r = await translateTemplate(currentName.value, aiTranslateTargetName.value, {
|
|
||||||
override: aiTranslateOverride.value,
|
|
||||||
})
|
|
||||||
aiTranslateOpen.value = false
|
|
||||||
aiTranslateOverride.value = false
|
|
||||||
await loadAvailableTemplates()
|
|
||||||
$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: `Traduction terminée : ${r.source} → ${r.target}`,
|
|
||||||
caption: `${r.src_bytes} → ${r.out_bytes} octets (${r.from_lang} → ${r.to_lang})`,
|
|
||||||
timeout: 6000,
|
|
||||||
actions: [
|
|
||||||
{ label: 'Ouvrir', color: 'white', handler: () => { onSelectTemplate(r.target) } },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
$q.notify({ type: 'negative', message: 'Échec traduction: ' + e.message, timeout: 6000 })
|
|
||||||
} finally {
|
|
||||||
aiTranslating.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── New template creation ───────────────────────────────────────────────────
|
|
||||||
const newTemplateOpen = ref(false)
|
|
||||||
const creating = ref(false)
|
|
||||||
const newTemplateForm = ref({
|
|
||||||
prefix: 'gift-email',
|
|
||||||
suffix: '',
|
|
||||||
starter: 'blank', // 'blank' | template name to copy from
|
|
||||||
})
|
|
||||||
|
|
||||||
const newTemplateFinal = computed(() => {
|
|
||||||
const { prefix, suffix } = newTemplateForm.value
|
|
||||||
if (!suffix || !/^[a-z0-9-]+$/.test(suffix)) return ''
|
|
||||||
return `${prefix}-${suffix}`
|
|
||||||
})
|
|
||||||
|
|
||||||
const newTemplateValid = computed(() =>
|
|
||||||
!!newTemplateFinal.value &&
|
|
||||||
!templates.value.find(t => t.name === newTemplateFinal.value),
|
|
||||||
)
|
|
||||||
|
|
||||||
const starterOptions = computed(() => [
|
|
||||||
{ label: 'Vide (canevas blanc)', value: 'blank' },
|
|
||||||
...templates.value.map(t => ({ label: `Copier depuis ${t.name}`, value: t.name })),
|
|
||||||
])
|
|
||||||
|
|
||||||
async function createTemplate () {
|
|
||||||
if (!newTemplateValid.value) return
|
|
||||||
creating.value = true
|
|
||||||
try {
|
|
||||||
const newName = newTemplateFinal.value
|
|
||||||
let html = '<div style="padding:32px;text-align:center;color:#64748B;font-family:sans-serif;">Nouveau template — drag des blocs depuis la sidebar pour commencer.</div>'
|
|
||||||
let design = null
|
|
||||||
if (newTemplateForm.value.starter !== 'blank') {
|
|
||||||
// Copy html + design from the chosen source template
|
|
||||||
const src = await getTemplate(newTemplateForm.value.starter)
|
|
||||||
html = src.html || html
|
|
||||||
design = src.design || null
|
|
||||||
}
|
|
||||||
await saveTemplateApi(newName, html, { design })
|
|
||||||
await loadAvailableTemplates()
|
|
||||||
newTemplateOpen.value = false
|
|
||||||
// Switch to the new template
|
|
||||||
currentName.value = newName
|
|
||||||
router.replace({ path: `/campaigns/templates/${newName}` })
|
|
||||||
loadTemplateIntoEditor(newName)
|
|
||||||
newTemplateForm.value.suffix = '' // reset for next time
|
|
||||||
$q.notify({ type: 'positive', message: `Template "${newName}" créé`, timeout: 3000 })
|
|
||||||
} catch (e) {
|
|
||||||
$q.notify({ type: 'negative', message: 'Erreur création: ' + e.message })
|
|
||||||
} finally {
|
|
||||||
creating.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Load template into the Unlayer canvas on editor ready + on switch ──────
|
|
||||||
async function loadTemplateIntoEditor (name) {
|
|
||||||
if (!editorReady.value || !editor.value) return
|
|
||||||
try {
|
|
||||||
const data = await getTemplate(name)
|
|
||||||
// Priority: Unlayer design JSON saved by a previous edit > nothing.
|
|
||||||
// Legacy MJML/HTML templates aren't auto-importable into Unlayer's
|
|
||||||
// component tree — the user reconstructs visually once, and the
|
|
||||||
// design saved by the next "Enregistrer" fixes future loads.
|
|
||||||
if (data.design) {
|
|
||||||
const design = typeof data.design === 'string' ? JSON.parse(data.design) : data.design
|
|
||||||
editor.value.loadDesign(design)
|
|
||||||
} else {
|
|
||||||
// No saved design — vue-email-editor 2.x doesn't have a loadBlank()
|
|
||||||
// method on the ref, so we just let the editor show its default
|
|
||||||
// empty state ("No content here. Drag content from left.") and
|
|
||||||
// notify the user to start building.
|
|
||||||
$q.notify({
|
|
||||||
type: 'info',
|
|
||||||
message: `Pas encore de design pour "${name}" — drag des blocs depuis la sidebar gauche pour construire la template, puis "Enregistrer".`,
|
|
||||||
timeout: 8000,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
$q.notify({ type: 'negative', message: 'Erreur de chargement: ' + e.message })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEditorLoad () {
|
|
||||||
// Fired once when the editor IFRAME loads (before the editor inside is ready)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onEditorReady () {
|
|
||||||
// Fired when the editor INSIDE the iframe is ready to accept loadDesign()
|
|
||||||
editorReady.value = true
|
|
||||||
await loadTemplateIntoEditor(currentName.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSelectTemplate (name) {
|
|
||||||
currentName.value = name
|
|
||||||
router.replace({ path: `/campaigns/templates/${name}` })
|
|
||||||
loadTemplateIntoEditor(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Save: export HTML + design JSON, POST to hub ────────────────────────────
|
|
||||||
const lastSavedTs = ref(null)
|
|
||||||
const lastSavedLabel = computed(() => {
|
|
||||||
if (!lastSavedTs.value) return ''
|
|
||||||
const diff = Math.floor((Date.now() - lastSavedTs.value) / 1000)
|
|
||||||
if (diff < 60) return `il y a ${diff}s`
|
|
||||||
if (diff < 3600) return `il y a ${Math.floor(diff / 60)}min`
|
|
||||||
return new Date(lastSavedTs.value).toLocaleTimeString('fr-CA')
|
|
||||||
})
|
|
||||||
|
|
||||||
function saveTemplate () {
|
|
||||||
if (!editor.value) return
|
|
||||||
saving.value = true
|
|
||||||
// exportHtml uses a callback (legacy Unlayer API) — wrap in a Promise
|
|
||||||
editor.value.exportHtml(async (data) => {
|
|
||||||
try {
|
|
||||||
const { html, design } = data
|
|
||||||
await saveTemplateApi(currentName.value, html, { design })
|
|
||||||
lastSavedTs.value = Date.now()
|
|
||||||
$q.notify({ type: 'positive', message: 'Template enregistré ✓', timeout: 2500 })
|
|
||||||
} catch (e) {
|
|
||||||
$q.notify({ type: 'negative', message: 'Erreur: ' + e.message })
|
|
||||||
} finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Preview dialog ──────────────────────────────────────────────────────────
|
|
||||||
const previewOpen = ref(false)
|
|
||||||
const previewLoading = ref(false)
|
|
||||||
const previewHtmlContent = ref('')
|
|
||||||
|
|
||||||
async function openPreview () {
|
|
||||||
previewOpen.value = true
|
|
||||||
previewLoading.value = true
|
|
||||||
try {
|
|
||||||
const r = await previewTemplate(currentName.value)
|
|
||||||
previewHtmlContent.value = r.rendered || ''
|
|
||||||
} catch (e) {
|
|
||||||
$q.notify({ type: 'negative', message: 'Aperçu impossible: ' + e.message })
|
|
||||||
} finally {
|
|
||||||
previewLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Test-send dialog ────────────────────────────────────────────────────────
|
|
||||||
const testSendOpen = ref(false)
|
|
||||||
const testSending = ref(false)
|
|
||||||
const testSendForm = ref({
|
|
||||||
to: 'louis@targo.ca',
|
|
||||||
subject: '[TEST] Aperçu du courriel TARGO',
|
|
||||||
vars: {
|
|
||||||
firstname: 'Louis', lastname: 'Test', amount: '60 $',
|
|
||||||
commitment_months: '3',
|
|
||||||
gift_url: 'https://gft.link/TEST123',
|
|
||||||
description: '123 Rue de Test, Ste-Clotilde',
|
|
||||||
expiry: '31 décembre 2026',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
async function doTestSend () {
|
|
||||||
testSending.value = true
|
|
||||||
try {
|
|
||||||
const r = await testSendTemplate(currentName.value, {
|
|
||||||
to: testSendForm.value.to.trim(),
|
|
||||||
subject: testSendForm.value.subject,
|
|
||||||
vars: testSendForm.value.vars,
|
|
||||||
})
|
|
||||||
$q.notify({ type: 'positive', message: `Test envoyé à ${r.to}`, caption: `${r.bytes} octets`, timeout: 5000 })
|
|
||||||
testSendOpen.value = false
|
|
||||||
} catch (e) {
|
|
||||||
$q.notify({ type: 'negative', message: 'Échec envoi: ' + e.message })
|
|
||||||
} finally {
|
|
||||||
testSending.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadAvailableTemplates)
|
|
||||||
</script>
|
|
||||||
|
|
@ -38,13 +38,6 @@ const routes = [
|
||||||
{ path: 'dispatch', component: () => import('src/pages/DispatchPage.vue') },
|
{ path: 'dispatch', component: () => import('src/pages/DispatchPage.vue') },
|
||||||
{ path: 'agent-flows', component: () => import('src/pages/AgentFlowsPage.vue') },
|
{ path: 'agent-flows', component: () => import('src/pages/AgentFlowsPage.vue') },
|
||||||
{ path: 'network', component: () => import('src/pages/NetworkPage.vue') },
|
{ path: 'network', component: () => import('src/pages/NetworkPage.vue') },
|
||||||
// Gift campaigns — list, new wizard (CSV upload + matching), per-campaign detail with live SSE updates
|
|
||||||
{ path: 'campaigns', component: () => import('src/modules/campaigns/pages/CampaignsListPage.vue') },
|
|
||||||
{ path: 'campaigns/new', component: () => import('src/modules/campaigns/pages/CampaignNewPage.vue') },
|
|
||||||
// Template editor route must be ABOVE /campaigns/:id otherwise the
|
|
||||||
// ':id' wildcard captures 'templates/...' and shows the detail page.
|
|
||||||
{ path: 'campaigns/templates/:name?', component: () => import('src/modules/campaigns/pages/TemplateEditorPage.vue'), props: true },
|
|
||||||
{ path: 'campaigns/:id', component: () => import('src/modules/campaigns/pages/CampaignDetailPage.vue'), props: true },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ domain level. The two known-validated senders on this account are:
|
||||||
The default for gift campaigns:
|
The default for gift campaigns:
|
||||||
|
|
||||||
```
|
```
|
||||||
--from "TARGO <support@targointernet.com>"
|
--from "Gigafibre Support <support@targointernet.com>"
|
||||||
```
|
```
|
||||||
|
|
||||||
Reasoning for `support@` over `noreply@`: campaigns INVITE a reply
|
Reasoning for `support@` over `noreply@`: campaigns INVITE a reply
|
||||||
|
|
@ -129,11 +129,10 @@ node send_gift_campaign.js \
|
||||||
--gifts /path/to/giftbit-gifts.csv \
|
--gifts /path/to/giftbit-gifts.csv \
|
||||||
--contacts /path/to/giftbit-contacts-A-first-email.csv \
|
--contacts /path/to/giftbit-contacts-A-first-email.csv \
|
||||||
--template ./templates/gift-email-fr.html \
|
--template ./templates/gift-email-fr.html \
|
||||||
--subject "🎁 Un cadeau pour vous, de la part de TARGO" \
|
--subject "🎁 Un cadeau pour vous, de la part de Gigafibre" \
|
||||||
--amount "60 $" \
|
--amount "50 $" \
|
||||||
--expiry "31 décembre 2026" \
|
--expiry "31 décembre 2026" \
|
||||||
--commitment-months 3 \
|
--from "Gigafibre Support <support@targointernet.com>" \
|
||||||
--from "TARGO <support@targointernet.com>" \
|
|
||||||
--dry-run
|
--dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -152,11 +151,10 @@ node send_gift_campaign.js \
|
||||||
--gifts /path/to/giftbit-gifts.csv \
|
--gifts /path/to/giftbit-gifts.csv \
|
||||||
--contacts /path/to/giftbit-contacts-A-first-email.csv \
|
--contacts /path/to/giftbit-contacts-A-first-email.csv \
|
||||||
--template ./templates/gift-email-fr.html \
|
--template ./templates/gift-email-fr.html \
|
||||||
--subject "🎁 Un cadeau pour vous, de la part de TARGO" \
|
--subject "🎁 Un cadeau pour vous, de la part de Gigafibre" \
|
||||||
--amount "60 $" \
|
--amount "50 $" \
|
||||||
--expiry "31 décembre 2026" \
|
--expiry "31 décembre 2026" \
|
||||||
--commitment-months 3 \
|
--from "Gigafibre Support <support@targointernet.com>" \
|
||||||
--from "TARGO <support@targointernet.com>" \
|
|
||||||
--smtp-host in-v3.mailjet.com --smtp-port 587 \
|
--smtp-host in-v3.mailjet.com --smtp-port 587 \
|
||||||
--smtp-user "$SMTP_USER" --smtp-pass "$SMTP_PASS" \
|
--smtp-user "$SMTP_USER" --smtp-pass "$SMTP_PASS" \
|
||||||
--throttle-ms 600
|
--throttle-ms 600
|
||||||
|
|
@ -187,13 +185,11 @@ Variables resolved at send time:
|
||||||
| `{{gift_url}}` | matched from the gifts CSV |
|
| `{{gift_url}}` | matched from the gifts CSV |
|
||||||
| `{{amount}}` | `--amount` CLI flag (e.g. `"50 $"`) |
|
| `{{amount}}` | `--amount` CLI flag (e.g. `"50 $"`) |
|
||||||
| `{{expiry}}` | `--expiry` CLI flag (e.g. `"31 décembre 2026"`) |
|
| `{{expiry}}` | `--expiry` CLI flag (e.g. `"31 décembre 2026"`) |
|
||||||
| `{{commitment_months}}` | `--commitment-months` CLI flag (default `3`) — used in the "Condition" pill and prorata-refund disclaimer of the retention offer |
|
|
||||||
|
|
||||||
The template uses a Mustache-style `{{#expiry}} ... {{/expiry}}` block for
|
The template uses a vintage `{{#expiry}} ... {{/expiry}}` block for the
|
||||||
the optional expiry line. The renderer keeps the contents when the
|
optional expiry line — currently rendered as plain text (the script's
|
||||||
matching variable is truthy and drops them entirely otherwise — so if you
|
simple `{{var}}` renderer doesn't strip the tags). If you don't want the
|
||||||
omit `--expiry` from the CLI, the "Le lien expire le …" sentence
|
expiry sentence, edit the template directly to remove that block.
|
||||||
disappears cleanly with no orphan tags showing.
|
|
||||||
|
|
||||||
## Source data — the two CSVs
|
## Source data — the two CSVs
|
||||||
|
|
||||||
|
|
|
||||||
25
scripts/campaigns/package-lock.json
generated
25
scripts/campaigns/package-lock.json
generated
|
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
"name": "campaigns",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "campaigns",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"nodemailer": "^8.0.7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/nodemailer": {
|
|
||||||
"version": "8.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.7.tgz",
|
|
||||||
"integrity": "sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow==",
|
|
||||||
"license": "MIT-0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"name": "campaigns",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "One-shot tool to send Giftbit gift cards to a list of contacts with a branded French email, bypassing Giftbit's English-only built-in delivery.",
|
|
||||||
"main": "create_giftbit_campaign.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"type": "commonjs",
|
|
||||||
"dependencies": {
|
|
||||||
"nodemailer": "^8.0.7"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
* --subject "Votre cadeau Gigafibre" \
|
* --subject "Votre cadeau Gigafibre" \
|
||||||
* --amount "50 $" \
|
* --amount "50 $" \
|
||||||
* --expiry "31 décembre 2026" \
|
* --expiry "31 décembre 2026" \
|
||||||
* --commitment-months 3 \
|
|
||||||
* --from "Gigafibre <noreply@gigafibre.ca>" \
|
* --from "Gigafibre <noreply@gigafibre.ca>" \
|
||||||
* --smtp-host in-v3.mailjet.com --smtp-port 587 \
|
* --smtp-host in-v3.mailjet.com --smtp-port 587 \
|
||||||
* --smtp-user $SMTP_USER --smtp-pass $SMTP_PASS \
|
* --smtp-user $SMTP_USER --smtp-pass $SMTP_PASS \
|
||||||
|
|
@ -76,9 +75,6 @@ const AMOUNT = args.amount || '50 $'
|
||||||
const EXPIRY = args.expiry || ''
|
const EXPIRY = args.expiry || ''
|
||||||
const SUBJECT = args.subject
|
const SUBJECT = args.subject
|
||||||
const FROM = args.from
|
const FROM = args.from
|
||||||
// Retention commitment used in the template's "Condition" pill and prorata
|
|
||||||
// disclaimer. Default 3 months. Override with --commitment-months 6 etc.
|
|
||||||
const COMMITMENT_MONTHS = args['commitment-months'] || '3'
|
|
||||||
|
|
||||||
// ── CSV parsing ────────────────────────────────────────────────────────────
|
// ── CSV parsing ────────────────────────────────────────────────────────────
|
||||||
// Minimal RFC-4180-ish parser. Handles quoted fields with embedded commas
|
// Minimal RFC-4180-ish parser. Handles quoted fields with embedded commas
|
||||||
|
|
@ -168,18 +164,7 @@ function matchByEmail (gifts, contacts, urlCol) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Template rendering ─────────────────────────────────────────────────────
|
// ── Template rendering ─────────────────────────────────────────────────────
|
||||||
// Supports two constructs:
|
|
||||||
// {{var}} simple substitution
|
|
||||||
// {{#var}}...{{/var}} section block: kept if var is truthy, dropped otherwise
|
|
||||||
//
|
|
||||||
// The section pass runs FIRST so that variable expansion can fill in the
|
|
||||||
// kept body. Non-greedy match with [\s\S] handles multi-line blocks (HTML
|
|
||||||
// templates span many lines between the open/close tags).
|
|
||||||
function render (tpl, vars) {
|
function render (tpl, vars) {
|
||||||
// Pass 1: section blocks (truthy → keep body, falsy → drop everything)
|
|
||||||
tpl = tpl.replace(/\{\{\s*#\s*(\w+)\s*\}\}([\s\S]*?)\{\{\s*\/\s*\1\s*\}\}/g,
|
|
||||||
(_, k, body) => (vars[k] ? body : ''))
|
|
||||||
// Pass 2: simple variable substitution
|
|
||||||
return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, k) => {
|
return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, k) => {
|
||||||
const v = vars[k]
|
const v = vars[k]
|
||||||
return v == null ? '' : String(v)
|
return v == null ? '' : String(v)
|
||||||
|
|
@ -269,7 +254,6 @@ async function main () {
|
||||||
gift_url,
|
gift_url,
|
||||||
amount: AMOUNT,
|
amount: AMOUNT,
|
||||||
expiry: EXPIRY,
|
expiry: EXPIRY,
|
||||||
commitment_months: COMMITMENT_MONTHS,
|
|
||||||
}
|
}
|
||||||
const html = render(tpl, vars)
|
const html = render(tpl, vars)
|
||||||
const ts = new Date().toISOString()
|
const ts = new Date().toISOString()
|
||||||
|
|
|
||||||
|
|
@ -1,200 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
'use strict'
|
|
||||||
/**
|
|
||||||
* setup_mailjet_webhook.js — Register the Hub's /campaigns/webhook URL with
|
|
||||||
* Mailjet's Event API for every event type we care about.
|
|
||||||
*
|
|
||||||
* Mailjet's API uses ONE eventcallbackurl record PER event type. We want to
|
|
||||||
* be notified about: sent, open, click, bounce, blocked, spam, unsub. So this
|
|
||||||
* script idempotently registers (POST) or updates (PUT) one record per type.
|
|
||||||
*
|
|
||||||
* Auth: SMTP_USER + SMTP_PASS env vars (same creds work for the REST API on
|
|
||||||
* Mailjet — they call them API_PUBLIC_KEY / API_PRIVATE_KEY but the values
|
|
||||||
* are identical to the SMTP credentials).
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* export SMTP_USER=<MJ_APIKEY_PUBLIC>
|
|
||||||
* export SMTP_PASS=<MJ_APIKEY_PRIVATE>
|
|
||||||
* node setup_mailjet_webhook.js --url https://msg.gigafibre.ca/campaigns/webhook
|
|
||||||
*
|
|
||||||
* # Production-safe defaults:
|
|
||||||
* # --is-backup false primary (not backup) callback URL
|
|
||||||
* # --group-events true send events as a JSON array (recommended,
|
|
||||||
* # minimizes hub load — one POST per ~50 events
|
|
||||||
* # instead of one POST per event)
|
|
||||||
*
|
|
||||||
* To inspect / delete what's registered:
|
|
||||||
* node setup_mailjet_webhook.js --list
|
|
||||||
* node setup_mailjet_webhook.js --delete <id>
|
|
||||||
*/
|
|
||||||
|
|
||||||
const https = require('https')
|
|
||||||
|
|
||||||
const ALL_EVENTS = ['sent', 'open', 'click', 'bounce', 'blocked', 'spam', 'unsub']
|
|
||||||
// Safe defaults: only events that aren't typically already claimed by other
|
|
||||||
// integrations (WP-Mail-SMTP on targo.ca currently owns sent/bounce/blocked
|
|
||||||
// — see `--list` output). open + click are the events the gift campaign
|
|
||||||
// actually needs for tracking; spam + unsub are nice-to-have signals.
|
|
||||||
const SAFE_EVENTS = ['open', 'click', 'spam', 'unsub']
|
|
||||||
|
|
||||||
function parseArgs (argv) {
|
|
||||||
const out = {}
|
|
||||||
for (let i = 2; i < argv.length; i++) {
|
|
||||||
const a = argv[i]
|
|
||||||
if (a.startsWith('--')) {
|
|
||||||
const k = a.slice(2); const next = argv[i + 1]
|
|
||||||
if (!next || next.startsWith('--')) out[k] = true
|
|
||||||
else { out[k] = next; i++ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
function mjApi (method, urlPath, body, { user, pass }) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const data = body ? JSON.stringify(body) : null
|
|
||||||
const auth = Buffer.from(`${user}:${pass}`).toString('base64')
|
|
||||||
const req = https.request({
|
|
||||||
host: 'api.mailjet.com',
|
|
||||||
path: '/v3/REST' + urlPath,
|
|
||||||
method,
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Basic ' + auth,
|
|
||||||
'Accept': 'application/json',
|
|
||||||
...(data ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } : {}),
|
|
||||||
},
|
|
||||||
}, res => {
|
|
||||||
let chunks = ''
|
|
||||||
res.on('data', c => { chunks += c })
|
|
||||||
res.on('end', () => {
|
|
||||||
try { resolve({ status: res.statusCode, body: chunks ? JSON.parse(chunks) : {} }) }
|
|
||||||
catch (e) { resolve({ status: res.statusCode, body: chunks }) }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
req.on('error', reject)
|
|
||||||
if (data) req.write(data)
|
|
||||||
req.end()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function listCallbacks (creds) {
|
|
||||||
const r = await mjApi('GET', '/eventcallbackurl?Limit=100', null, creds)
|
|
||||||
if (r.status !== 200) throw new Error(`GET eventcallbackurl ${r.status}: ${JSON.stringify(r.body)}`)
|
|
||||||
return r.body.Data || []
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteCallback (id, creds) {
|
|
||||||
const r = await mjApi('DELETE', `/eventcallbackurl/${id}`, null, creds)
|
|
||||||
return r.status === 204 || r.status === 200
|
|
||||||
}
|
|
||||||
|
|
||||||
async function upsert (eventType, url, isBackup, creds, existing) {
|
|
||||||
// existing array is the result of GET — find a matching record (same
|
|
||||||
// EventType + IsBackup combination). Mailjet only allows ONE primary +
|
|
||||||
// ONE backup URL per event, so this combination is the unique key.
|
|
||||||
const match = existing.find(r => r.EventType === eventType && Boolean(r.IsBackup) === isBackup)
|
|
||||||
const payload = { EventType: eventType, IsBackup: isBackup, Url: url, Status: 'alive', Version: 2 }
|
|
||||||
if (match) {
|
|
||||||
const r = await mjApi('PUT', `/eventcallbackurl/${match.ID}`, payload, creds)
|
|
||||||
return { action: 'updated', id: match.ID, status: r.status, ok: r.status === 200 }
|
|
||||||
} else {
|
|
||||||
const r = await mjApi('POST', '/eventcallbackurl', payload, creds)
|
|
||||||
const id = r.body.Data?.[0]?.ID
|
|
||||||
return { action: 'created', id, status: r.status, ok: r.status === 201 || r.status === 200 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main () {
|
|
||||||
const args = parseArgs(process.argv)
|
|
||||||
const user = process.env.SMTP_USER || process.env.MJ_APIKEY_PUBLIC
|
|
||||||
const pass = process.env.SMTP_PASS || process.env.MJ_APIKEY_PRIVATE
|
|
||||||
if (!user || !pass) {
|
|
||||||
console.error('Set SMTP_USER + SMTP_PASS (or MJ_APIKEY_PUBLIC + MJ_APIKEY_PRIVATE).')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
const creds = { user, pass }
|
|
||||||
|
|
||||||
// --list — dump current config and exit
|
|
||||||
if (args.list) {
|
|
||||||
const callbacks = await listCallbacks(creds)
|
|
||||||
console.log(`\n── Registered event callbacks: ${callbacks.length} ──`)
|
|
||||||
for (const c of callbacks) {
|
|
||||||
const flag = c.IsBackup ? '[BACKUP]' : '[PRIMARY]'
|
|
||||||
console.log(` ${flag} id=${c.ID} event=${c.EventType.padEnd(10)} status=${c.Status} v${c.Version} → ${c.Url}`)
|
|
||||||
}
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --delete <id>
|
|
||||||
if (args.delete && args.delete !== true) {
|
|
||||||
const ok = await deleteCallback(args.delete, creds)
|
|
||||||
console.log(ok ? ` ✓ deleted callback ${args.delete}` : ` ✗ delete failed`)
|
|
||||||
process.exit(ok ? 0 : 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = args.url
|
|
||||||
if (!url || url === true) {
|
|
||||||
console.error('Missing --url <callback-url>')
|
|
||||||
console.error('Example: --url https://msg.gigafibre.ca/campaigns/webhook')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
if (!url.startsWith('https://')) {
|
|
||||||
console.error('Mailjet requires HTTPS. Got:', url)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
const isBackup = args['is-backup'] === 'true' || args['is-backup'] === true
|
|
||||||
|
|
||||||
// Resolve which events to configure
|
|
||||||
let events
|
|
||||||
if (args.all) {
|
|
||||||
events = ALL_EVENTS
|
|
||||||
} else if (args.events && args.events !== true) {
|
|
||||||
events = args.events.split(',').map(e => e.trim()).filter(Boolean)
|
|
||||||
} else {
|
|
||||||
events = SAFE_EVENTS
|
|
||||||
}
|
|
||||||
|
|
||||||
const existing = await listCallbacks(creds)
|
|
||||||
|
|
||||||
// Pre-flight: detect conflicts with existing PRIMARY records pointing
|
|
||||||
// elsewhere. Refuse to overwrite unless --force-takeover is passed.
|
|
||||||
const conflicts = events
|
|
||||||
.map(ev => ({ ev, hit: existing.find(r => r.EventType === ev && Boolean(r.IsBackup) === isBackup) }))
|
|
||||||
.filter(c => c.hit && c.hit.Url !== url)
|
|
||||||
|
|
||||||
console.log(`\n── Mailjet Event API webhook setup ──`)
|
|
||||||
console.log(` callback URL: ${url}`)
|
|
||||||
console.log(` type: ${isBackup ? 'BACKUP' : 'PRIMARY'}`)
|
|
||||||
console.log(` events: ${events.join(', ')}`)
|
|
||||||
console.log(` existing: ${existing.length} records on the account`)
|
|
||||||
|
|
||||||
if (conflicts.length && !args['force-takeover']) {
|
|
||||||
console.log(`\n ⚠ Conflicts detected — these events already point elsewhere:`)
|
|
||||||
for (const c of conflicts) {
|
|
||||||
console.log(` • ${c.ev.padEnd(10)} → ${c.hit.Url} (id=${c.hit.ID})`)
|
|
||||||
}
|
|
||||||
console.log(`\n Refusing to overwrite without --force-takeover. Either:`)
|
|
||||||
console.log(` • Exclude the conflicting events: --events open,click`)
|
|
||||||
console.log(` • Or override the existing config: --force-takeover`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
console.log()
|
|
||||||
|
|
||||||
let okCount = 0
|
|
||||||
for (const ev of events) {
|
|
||||||
process.stdout.write(` ${ev.padEnd(10)} ... `)
|
|
||||||
const r = await upsert(ev, url, isBackup, creds, existing)
|
|
||||||
if (r.ok) {
|
|
||||||
okCount++
|
|
||||||
console.log(`✓ ${r.action} (id=${r.id})`)
|
|
||||||
} else {
|
|
||||||
console.log(`✗ status=${r.status}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n ${okCount}/${events.length} events configured.`)
|
|
||||||
console.log(`\n Verify with: node setup_mailjet_webhook.js --list`)
|
|
||||||
console.log(` Mailjet dashboard: Account Settings → REST API → Event tracking\n`)
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(e => { console.error('Fatal:', e); process.exit(1) })
|
|
||||||
|
|
@ -1,880 +0,0 @@
|
||||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
||||||
<head>
|
|
||||||
<!--[if gte mso 9]>
|
|
||||||
<xml>
|
|
||||||
<o:OfficeDocumentSettings>
|
|
||||||
<o:AllowPNG/>
|
|
||||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
||||||
</o:OfficeDocumentSettings>
|
|
||||||
</xml>
|
|
||||||
<![endif]-->
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="x-apple-disable-message-reformatting">
|
|
||||||
<!--[if !mso]><!--><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
<style type="text/css">
|
|
||||||
|
|
||||||
@media only screen and (min-width: 620px) {
|
|
||||||
.u-row {
|
|
||||||
width: 600px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-row .u-col {
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.u-row .u-col-100 {
|
|
||||||
width: 600px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 620px) {
|
|
||||||
.u-row-container {
|
|
||||||
max-width: 100% !important;
|
|
||||||
padding-left: 0px !important;
|
|
||||||
padding-right: 0px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-row {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-row .u-col {
|
|
||||||
display: block !important;
|
|
||||||
width: 100% !important;
|
|
||||||
min-width: 320px !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-row .u-col > div {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
body{margin:0;padding:0}table,td,tr{border-collapse:collapse;vertical-align:top}.ie-container table,.mso-container table{table-layout:fixed}*{line-height:inherit}a[x-apple-data-detectors=true]{color:inherit!important;text-decoration:none!important}
|
|
||||||
|
|
||||||
|
|
||||||
table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: underline; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Plus+Jakarta+Sans:400,500,600,700" rel="stylesheet" type="text/css"><!--<![endif]-->
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="clean-body u_body" style="margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #F5FAF7;color: #1B2E24">
|
|
||||||
<!--[if IE]><div class="ie-container"><![endif]-->
|
|
||||||
<!--[if mso]><div class="mso-container"><![endif]-->
|
|
||||||
<table role="presentation" id="u_body" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #F5FAF7;width:100%" cellpadding="0" cellspacing="0">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="display:none !important;visibility:hidden;mso-hide:all;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">
|
|
||||||
Because great connections aren't just about fiber — they're about people too.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr style="vertical-align: top">
|
|
||||||
<td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top">
|
|
||||||
<!--[if (mso)|(IE)]><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #F5FAF7;" bgcolor="#F5FAF7"><table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" align="center" style="border-collapse: collapse;"><tr><td style="padding:0;"><![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="u-row-container" style="padding: 0px;background-color: transparent;">
|
|
||||||
<div class="u-row" style="margin: 0 auto;min-width: 320px;max-width: 600px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;">
|
|
||||||
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
|
|
||||||
<!--[if (mso)|(IE)]><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;"><table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"><tr style="background-color: transparent;"><![endif]-->
|
|
||||||
|
|
||||||
<!--[if (mso)|(IE)]><td align="center" width="600" style="width: 600px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 20px 0px 0px;"><![endif]-->
|
|
||||||
<div class="u-col u-col-100" style="max-width: 320px;min-width: 600px;display: table-cell;vertical-align: top;">
|
|
||||||
<div style="height: 100%;width: 100% !important;">
|
|
||||||
<!--[if (!mso)&(!IE)]><!--><div style="box-sizing: border-box; height: 100%; padding: 20px 0px 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;"><!--<![endif]-->
|
|
||||||
|
|
||||||
<table style="font-family:'Plus Jakarta Sans', sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="overflow-wrap:break-word;word-break:break-word;padding:0px;font-family:'Plus Jakarta Sans', sans-serif;" align="left">
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">Because great connections aren't just about fiber — they're about people too.</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
aria-label="Your exclusive offer from 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" 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 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 + INTRO (4 blocs sémantiques) ════════
|
|
||||||
Bloc 1 — Greeting personnalisé
|
|
||||||
Bloc 2 — Manifeste brand TARGO (2 lignes qui voyagent ensemble)
|
|
||||||
Bloc 3 — Annonce du cadeau
|
|
||||||
Bloc 4 — Upsell forfaits + invitation contact
|
|
||||||
Chaque bloc est un mj-text indépendant = drag-droppable séparément. -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" 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 18px;text-align:center;"
|
|
||||||
>
|
|
||||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td 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;"
|
|
||||||
>Hey {{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:16px;line-height:1.5;text-align:justify;color:#374151;"
|
|
||||||
>Summer is here, and so is something special — for a limited time.</div>
|
|
||||||
<div
|
|
||||||
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;font-weight:500;line-height:1.5;text-align:justify;color:#1B2E24;"
|
|
||||||
>Thank you for choosing local. Your support helps keep our community connected.<br />
|
|
||||||
Because great connections aren't just about fiber — they're about people too.</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:justify;color:#374151;">Because our customers trust us, we're now able to offer the <strong>fastest plans around</strong>, with speeds up to <strong>3.5 Gbit/s</strong>.<br />
|
|
||||||
Whether you're looking for more speed, want to beat another offer, or just need to optimize your gear, don't be shy! We're right next door — and we genuinely love lending a hand.</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 1 CHIP ════════ -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" 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 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]-->
|
|
||||||
|
|
||||||
<!-- ════════ COMPACT INFO CARD (was 3 pills) ════════ -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" 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 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}} to spend at hundreds of your favorite stores<br><br><span style="display:inline-block;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/4b0b2a4a-5f99-416c-8873-8d3e4389b6f7/content" width="32" alt="Tim Hortons" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/14df433d-583c-4602-a403-d47ee84966a6/content" width="32" alt="Walmart" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/b8d3db5a-d39e-43ce-a84a-2f5dddbf0192/content" width="32" alt="Home Depot" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/9c9dfa18-2a3a-414a-b5ad-16d490c961b5/content" width="32" alt="IGA" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/a1e5f032-a192-4499-97ba-53b939712fa9/content" width="32" alt="Home Hardware" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><span style="font-size:13px;color:#64748B;vertical-align:middle;font-weight:400;">and more</span></span></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;"
|
|
||||||
>⚡ Instantly available on Giftbit — just click your {{amount}} to claim it!</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;"
|
|
||||||
>🤝 You just need to keep your subscription for {{commitment_months}} months or more.</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]-->
|
|
||||||
|
|
||||||
<!-- ════════ BIG GREEN CTA BUTTON ════════ -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" 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 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"
|
|
||||||
>
|
|
||||||
🎁 {{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" 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 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;"
|
|
||||||
>🪂 Cancellation before {{commitment_months}} months: only the prorated amount for the remaining months is refundable.</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" 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 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" 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 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;"
|
|
||||||
>Do nothing. No changes to your current subscription.</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" 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 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;"
|
|
||||||
>🤝 Thanks for helping keep our local economy buzzing!</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;"
|
|
||||||
>The TARGO Team</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" 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 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;"
|
|
||||||
>You're getting this email because you're a TARGO customer at <strong style="color:#1B2E24;">{{description}}</strong>.<br />
|
|
||||||
Got a question? Feel free to email us at
|
|
||||||
<a href="mailto:support@targo.ca" style="color:#00C853;text-decoration:none;">support@targo.ca</a>
|
|
||||||
or call us at
|
|
||||||
<a href="tel:5144480773" style="color:#00C853;text-decoration:none;">514-448-0773</a>.</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" 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 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>
|
|
||||||
· 1867 ch. de la rivière, Ste-Clotilde, QC<br />
|
|
||||||
© {{year}} TARGO Communications · All rights reserved.</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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!--[if (!mso)&(!IE)]><!--></div><!--<![endif]-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!--[if (mso)|(IE)]></td></tr></table></td><![endif]-->
|
|
||||||
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<!--[if mso]></div><![endif]-->
|
|
||||||
<!--[if IE]></div><![endif]-->
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,880 +1,99 @@
|
||||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
<!DOCTYPE html>
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
<html lang="fr">
|
||||||
<head>
|
<head>
|
||||||
<!--[if gte mso 9]>
|
<meta charset="UTF-8">
|
||||||
<xml>
|
|
||||||
<o:OfficeDocumentSettings>
|
|
||||||
<o:AllowPNG/>
|
|
||||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
||||||
</o:OfficeDocumentSettings>
|
|
||||||
</xml>
|
|
||||||
<![endif]-->
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="x-apple-disable-message-reformatting">
|
<title>Un cadeau de Gigafibre</title>
|
||||||
<!--[if !mso]><!--><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
<style type="text/css">
|
|
||||||
|
|
||||||
@media only screen and (min-width: 620px) {
|
|
||||||
.u-row {
|
|
||||||
width: 600px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-row .u-col {
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.u-row .u-col-100 {
|
|
||||||
width: 600px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 620px) {
|
|
||||||
.u-row-container {
|
|
||||||
max-width: 100% !important;
|
|
||||||
padding-left: 0px !important;
|
|
||||||
padding-right: 0px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-row {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-row .u-col {
|
|
||||||
display: block !important;
|
|
||||||
width: 100% !important;
|
|
||||||
min-width: 320px !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-row .u-col > div {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
body{margin:0;padding:0}table,td,tr{border-collapse:collapse;vertical-align:top}.ie-container table,.mso-container table{table-layout:fixed}*{line-height:inherit}a[x-apple-data-detectors=true]{color:inherit!important;text-decoration:none!important}
|
|
||||||
|
|
||||||
|
|
||||||
table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: underline; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Plus+Jakarta+Sans:400,500,600,700" rel="stylesheet" type="text/css"><!--<![endif]-->
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
<body style="margin:0; padding:0; background:#f5f6fa; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; color:#1f2937; line-height:1.5;">
|
||||||
|
|
||||||
<body class="clean-body u_body" style="margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #F5FAF7;color: #1B2E24">
|
<!-- Spacer above the card -->
|
||||||
<!--[if IE]><div class="ie-container"><![endif]-->
|
<div style="height:32px;"></div>
|
||||||
<!--[if mso]><div class="mso-container"><![endif]-->
|
|
||||||
<table role="presentation" id="u_body" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #F5FAF7;width:100%" cellpadding="0" cellspacing="0">
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||||
<tbody>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td style="display:none !important;visibility:hidden;mso-hide:all;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">
|
<td align="center">
|
||||||
Comme toi, on aime les connexions stables et les relations durables.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr style="vertical-align: top">
|
<table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0"
|
||||||
<td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top">
|
style="max-width:600px; background:#ffffff; border-radius:14px; overflow:hidden; box-shadow:0 6px 24px rgba(15,23,42,0.07);">
|
||||||
<!--[if (mso)|(IE)]><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #F5FAF7;" bgcolor="#F5FAF7"><table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" align="center" style="border-collapse: collapse;"><tr><td style="padding:0;"><![endif]-->
|
|
||||||
|
|
||||||
|
<!-- Header band -->
|
||||||
|
|
||||||
<div class="u-row-container" style="padding: 0px;background-color: transparent;">
|
|
||||||
<div class="u-row" style="margin: 0 auto;min-width: 320px;max-width: 600px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;">
|
|
||||||
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
|
|
||||||
<!--[if (mso)|(IE)]><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;"><table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"><tr style="background-color: transparent;"><![endif]-->
|
|
||||||
|
|
||||||
<!--[if (mso)|(IE)]><td align="center" width="600" style="width: 600px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 20px 0px 0px;"><![endif]-->
|
|
||||||
<div class="u-col u-col-100" style="max-width: 320px;min-width: 600px;display: table-cell;vertical-align: top;">
|
|
||||||
<div style="height: 100%;width: 100% !important;">
|
|
||||||
<!--[if (!mso)&(!IE)]><!--><div style="box-sizing: border-box; height: 100%; padding: 20px 0px 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;"><!--<![endif]-->
|
|
||||||
|
|
||||||
<table style="font-family:'Plus Jakarta Sans', sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td style="overflow-wrap:break-word;word-break:break-word;padding:0px;font-family:'Plus Jakarta Sans', sans-serif;" align="left">
|
<td style="background:linear-gradient(135deg,#4f46e5 0%, #7c3aed 100%); padding:36px 32px 28px; text-align:center; color:#ffffff;">
|
||||||
|
<div style="font-size:0.78rem; font-weight:700; letter-spacing:0.12em; text-transform:uppercase; opacity:0.85;">
|
||||||
<div>
|
Gigafibre · Récompense
|
||||||
<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" 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 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>
|
</div>
|
||||||
|
<div style="font-size:2.2rem; line-height:1.1; margin-top:10px; font-weight:800;">
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
🎁 Un cadeau pour vous
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
|
|
||||||
<!-- ════════ GREETING + INTRO (4 blocs sémantiques) ════════
|
|
||||||
Bloc 1 — Greeting personnalisé
|
|
||||||
Bloc 2 — Manifeste brand TARGO (2 lignes qui voyagent ensemble)
|
|
||||||
Bloc 3 — Annonce du cadeau
|
|
||||||
Bloc 4 — Upsell forfaits + invitation contact
|
|
||||||
Chaque bloc est un mj-text indépendant = drag-droppable séparément. -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" 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 18px;text-align:center;"
|
|
||||||
>
|
|
||||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td 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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td style="padding:36px 36px 12px;">
|
||||||
align="left" style="font-size:0px;padding:10px 25px;padding-bottom:14px;word-break:break-word;"
|
<p style="margin:0 0 16px; font-size:1.05rem;">Bonjour {{firstname}},</p>
|
||||||
>
|
|
||||||
<div
|
|
||||||
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:justify;color:#374151;"
|
|
||||||
>Avec l'arrivée de l'été, voici un <strong>cadeau pour toi, disponible pour un temps limité</strong>.</div>
|
|
||||||
<div
|
|
||||||
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;font-weight:500;line-height:1.5;text-align:justify;color:#1B2E24;"
|
|
||||||
>On veut te remercier pour ta loyauté envers l'achat local.<br />
|
|
||||||
Comme toi, on aime les connexions stables et les relations durables.</div>
|
|
||||||
|
|
||||||
</td>
|
<p style="margin:0 0 16px;">
|
||||||
</tr>
|
Merci de faire partie de la famille Gigafibre. Pour vous remercier
|
||||||
|
de votre fidélité, voici une carte-cadeau d'une valeur de
|
||||||
|
<strong>{{amount}}</strong>, utilisable sur les marchands de votre choix.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- CTA button -->
|
||||||
<tr>
|
<div style="text-align:center; margin:32px 0 28px;">
|
||||||
<td
|
<a href="{{gift_url}}"
|
||||||
align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0;word-break:break-word;"
|
style="display:inline-block; padding:16px 36px; background:#4f46e5; color:#ffffff;
|
||||||
>
|
text-decoration:none; font-weight:700; font-size:1.05rem;
|
||||||
<div
|
border-radius:10px; box-shadow:0 4px 12px rgba(79,70,229,0.35);">
|
||||||
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:justify;color:#374151;">Grâce à la confiance de nos clients, on offre maintenant les forfaits à <strong>la plus haute vitesse dans le secteur</strong>, jusqu'à <strong>3.5 Gbit/s</strong>.<br />
|
Récupérer mon cadeau →
|
||||||
Que tu souhaites plus de vitesse, battre une autre offre ou faire optimiser des équipements, n'hésite pas. On est juste à côté et on aime aider.</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 1 CHIP ════════ -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" 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 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]-->
|
|
||||||
|
|
||||||
<!-- ════════ COMPACT INFO CARD (was 3 pills) ════════ -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" 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 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<br><br><span style="display:inline-block;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/4b0b2a4a-5f99-416c-8873-8d3e4389b6f7/content" width="32" alt="Tim Hortons" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/14df433d-583c-4602-a403-d47ee84966a6/content" width="32" alt="Walmart" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/b8d3db5a-d39e-43ce-a84a-2f5dddbf0192/content" width="32" alt="Home Depot" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/9c9dfa18-2a3a-414a-b5ad-16d490c961b5/content" width="32" alt="IGA" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/a1e5f032-a192-4499-97ba-53b939712fa9/content" width="32" alt="Home Hardware" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><span style="font-size:13px;color:#64748B;vertical-align:middle;font-weight:400;">et plus</span></span></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;"
|
|
||||||
>⚡ Disponible instantanément sur Giftbit en cliquant sur ton montant</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;"
|
|
||||||
>🤝 Condition : Maintenir l'abonnement {{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]-->
|
|
||||||
|
|
||||||
<!-- ════════ BIG GREEN CTA BUTTON ════════ -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" 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 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"
|
|
||||||
>
|
|
||||||
🎁 {{amount}}
|
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
<p style="margin:0 0 6px; font-size:0.9rem; color:#6b7280;">
|
||||||
|
Le lien vous mène à une page sécurisée où vous pourrez choisir la
|
||||||
|
marque qui vous fait plaisir (Amazon, Tim Hortons, SAQ, App Store,
|
||||||
|
et plusieurs autres).
|
||||||
|
</p>
|
||||||
|
{{#expiry}}
|
||||||
|
<p style="margin:6px 0 0; font-size:0.85rem; color:#9ca3af;">
|
||||||
|
⏰ Le lien expire le <strong>{{expiry}}</strong>.
|
||||||
|
</p>
|
||||||
|
{{/expiry}}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<!-- Why this email -->
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td style="padding:0 36px 28px;">
|
||||||
align="center" class="cta-subtitle" style="font-size:0px;padding:0;word-break:break-word;"
|
<div style="border-top:1px solid #e5e7eb; padding-top:20px; font-size:0.82rem; color:#6b7280;">
|
||||||
>
|
Vous recevez ce cadeau parce que vous êtes client(e) Gigafibre à
|
||||||
|
l'adresse <strong style="color:#374151;">{{description}}</strong>.
|
||||||
<div
|
Si vous avez la moindre question, écrivez-nous à
|
||||||
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:center;color:#ffffff;"
|
<a href="mailto:facturation@targointernet.com" style="color:#4f46e5;">facturation@targointernet.com</a>
|
||||||
><!-- Sub-labels inside the button: not directly supported in mjml-button,
|
ou appelez-nous au <a href="tel:5142421500" style="color:#4f46e5;">514 242-1500</a>.
|
||||||
so we render them as a styled text block immediately below.
|
</div>
|
||||||
In the actual rendered output they appear visually under the
|
|
||||||
button text. --></div>
|
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</tbody>
|
<!-- Footer band -->
|
||||||
</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" 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>
|
<tr>
|
||||||
<td
|
<td style="background:#f9fafb; padding:18px 32px; text-align:center; border-top:1px solid #e5e7eb;">
|
||||||
style="border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;direction:ltr;font-size:0px;padding:10px 36px 22px;text-align:center;"
|
<div style="font-size:0.75rem; color:#9ca3af;">
|
||||||
>
|
Gigafibre — Internet fibre optique au Québec<br>
|
||||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:top;width:526px;" ><![endif]-->
|
<a href="https://www.gigafibre.ca" style="color:#9ca3af; text-decoration:underline;">www.gigafibre.ca</a>
|
||||||
|
|
||||||
<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;"
|
|
||||||
>🪂 Annulation avant {{commitment_months}} mois : seulement à rembourser au prorata des mois restants.</div>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
</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" 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 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>
|
</td>
|
||||||
</tr>
|
</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" 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 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. Aucun changement à ton abonnement actuel.</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" 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 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" 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 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 ? N'hésite pas à nous écrire à
|
|
||||||
<a href="mailto:support@targo.ca" style="color:#00C853;text-decoration:none;">support@targo.ca</a>
|
|
||||||
ou nous appeler au
|
|
||||||
<a href="tel:5144480773" style="color:#00C853;text-decoration:none;">514-448-0773</a>.</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" 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 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>
|
</table>
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
|
||||||
· 1867 ch. de la rivière, Ste-Clotilde, QC<br />
|
|
||||||
© {{year}} TARGO Communications · Tous droits réservés.</div>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
<!-- Spacer below the card -->
|
||||||
|
<div style="height:48px;"></div>
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!--[if (!mso)&(!IE)]><!--></div><!--<![endif]-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!--[if (mso)|(IE)]></td></tr></table></td><![endif]-->
|
|
||||||
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<!--[if mso]></div><![endif]-->
|
|
||||||
<!--[if IE]></div><![endif]-->
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
5
services/email-editor/.gitignore
vendored
5
services/email-editor/.gitignore
vendored
|
|
@ -1,5 +0,0 @@
|
||||||
node_modules/
|
|
||||||
dist/
|
|
||||||
.vite/
|
|
||||||
.DS_Store
|
|
||||||
*.log
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
# Multi-stage Dockerfile for the email-editor microservice.
|
|
||||||
# Stage 1: Vite build into dist/
|
|
||||||
# Stage 2: nginx serving the static files
|
|
||||||
|
|
||||||
# ── Stage 1: build ──
|
|
||||||
FROM node:20-alpine AS builder
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Install only what's needed for build (no native deps required)
|
|
||||||
COPY package.json package-lock.json* ./
|
|
||||||
RUN npm install --silent
|
|
||||||
|
|
||||||
COPY tsconfig.json vite.config.ts index.html ./
|
|
||||||
COPY src ./src
|
|
||||||
|
|
||||||
# Inline the prod hub URL at build time. Override via --build-arg on docker
|
|
||||||
# build if running against a different hub (e.g. staging).
|
|
||||||
ARG VITE_HUB_URL=https://msg.gigafibre.ca
|
|
||||||
ENV VITE_HUB_URL=$VITE_HUB_URL
|
|
||||||
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# ── Stage 2: nginx serve ──
|
|
||||||
FROM nginx:alpine
|
|
||||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
|
||||||
|
|
||||||
# Drop the default nginx config and inject a minimal SPA-friendly one
|
|
||||||
# (all routes serve index.html — easy-email is a SPA, query params drive
|
|
||||||
# which template to load).
|
|
||||||
RUN rm /etc/nginx/conf.d/default.conf
|
|
||||||
COPY <<EOF /etc/nginx/conf.d/default.conf
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name _;
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# Long cache for hashed assets (vite outputs them with content hash in name)
|
|
||||||
location /assets/ {
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
}
|
|
||||||
|
|
||||||
# SPA fallback — every request returns index.html so the React app handles
|
|
||||||
# routing client-side based on ?name=... query
|
|
||||||
location / {
|
|
||||||
try_files \$uri \$uri/ /index.html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
# TARGO Email Editor
|
|
||||||
|
|
||||||
Standalone email template editor microservice — React + Vite + [easy-email](https://github.com/zalify/easy-email)
|
|
||||||
(OSS WYSIWYG email builder, MJML-based).
|
|
||||||
|
|
||||||
Embedded as an iframe in the ops UI's `/campaigns/templates/:name` page.
|
|
||||||
Talks to the hub's `/campaigns/templates/*` REST endpoints for load/save.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
ops UI (Vue) → iframe → editor.gigafibre.ca → REST → msg.gigafibre.ca (hub)
|
|
||||||
└─ writes .mjml + .html
|
|
||||||
to /opt/targo-hub/templates/
|
|
||||||
```
|
|
||||||
|
|
||||||
## URL params
|
|
||||||
|
|
||||||
- `?name=gift-email-fr` — which template to load (defaults to gift-email-fr)
|
|
||||||
|
|
||||||
## postMessage protocol (to parent window)
|
|
||||||
|
|
||||||
Emitted on save success:
|
|
||||||
```js
|
|
||||||
{ type: 'email-editor:saved', template: 'gift-email-fr', ts: 1700000000000 }
|
|
||||||
```
|
|
||||||
|
|
||||||
The parent ops UI listens via:
|
|
||||||
```js
|
|
||||||
window.addEventListener('message', (e) => {
|
|
||||||
if (e.data.type === 'email-editor:saved') {
|
|
||||||
// refresh preview / show toast
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Local dev
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
npm run dev # http://localhost:5173?name=gift-email-fr
|
|
||||||
```
|
|
||||||
|
|
||||||
Set `VITE_HUB_URL` env to point at a non-prod hub if needed.
|
|
||||||
|
|
||||||
## Build + deploy
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose build
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Traefik auto-provisions HTTPS at `editor.gigafibre.ca` (Let's Encrypt via
|
|
||||||
the `letsencrypt` resolver shared with the rest of our stack).
|
|
||||||
|
|
||||||
## Known limitations (Phase 1)
|
|
||||||
|
|
||||||
- Existing MJML templates from the hub are NOT auto-imported into the
|
|
||||||
easy-email JSON tree (no MJML → JSON parser in easy-email out of the box).
|
|
||||||
The editor starts from an empty page. User rebuilds visually with the
|
|
||||||
hub's compiled HTML as visual reference.
|
|
||||||
- TODO Phase 3: integrate an MJML → easy-email-JSON parser (likely fork or
|
|
||||||
reverse-engineer JsonToMjml).
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
services:
|
|
||||||
email-editor:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
args:
|
|
||||||
# Override at build time if pointing at staging:
|
|
||||||
# docker compose build --build-arg VITE_HUB_URL=https://staging-msg.gigafibre.ca
|
|
||||||
VITE_HUB_URL: https://msg.gigafibre.ca
|
|
||||||
container_name: email-editor
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- proxy
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.docker.network=proxy"
|
|
||||||
# Public route — same Authentik forwardAuth pattern as ops UI could be
|
|
||||||
# added here too, but for now the editor is iframed from the (already
|
|
||||||
# authenticated) ops UI so external auth is layered through the parent.
|
|
||||||
# Leaving it open means anyone with the URL can edit templates — fine
|
|
||||||
# for the iframe-only use case; harden later if exposed standalone.
|
|
||||||
- "traefik.http.routers.email-editor.rule=Host(`editor.gigafibre.ca`)"
|
|
||||||
- "traefik.http.routers.email-editor.entrypoints=websecure"
|
|
||||||
- "traefik.http.routers.email-editor.tls.certresolver=letsencrypt"
|
|
||||||
- "traefik.http.services.email-editor.loadbalancer.server.port=80"
|
|
||||||
|
|
||||||
networks:
|
|
||||||
proxy:
|
|
||||||
external: true
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>TARGO email editor</title>
|
|
||||||
<!-- easy-email's required styles. Order matters: extensions first, then editor. -->
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/easy-email-editor@4.16.6/lib/style.css" />
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/easy-email-extensions@4.16.5/lib/style.css" />
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/@arco-themes/react-easy-email-theme/css/arco.css" />
|
|
||||||
</head>
|
|
||||||
<body style="margin:0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;">
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
3219
services/email-editor/package-lock.json
generated
3219
services/email-editor/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
"name": "targo-email-editor",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Standalone email editor microservice — easy-email (React) embedded via iframe in the ops UI. Talks to the targo-hub /campaigns/templates/* endpoints to load and save campaign templates.",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"easy-email-core": "^4.16.5",
|
|
||||||
"easy-email-editor": "^4.16.6",
|
|
||||||
"easy-email-extensions": "^4.16.5",
|
|
||||||
"react": "^18.3.1",
|
|
||||||
"react-dom": "^18.3.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/react": "^18.3.0",
|
|
||||||
"@types/react-dom": "^18.3.0",
|
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
|
||||||
"typescript": "^5.5.0",
|
|
||||||
"vite": "^5.4.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,234 +0,0 @@
|
||||||
import React, { useEffect, useState, useCallback } from 'react'
|
|
||||||
import { EmailEditorProvider, EmailEditor, IEmailTemplate } from 'easy-email-editor'
|
|
||||||
import { ExtensionProps, StandardLayout } from 'easy-email-extensions'
|
|
||||||
import { BasicType, AdvancedType, JsonToMjml } from 'easy-email-core'
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Targo email editor — wraps easy-email-editor with our hub integration:
|
|
||||||
//
|
|
||||||
// 1. On mount: read ?name=<template-name> from URL, GET its MJML from the hub
|
|
||||||
// 2. Render easy-email with the loaded MJML
|
|
||||||
// 3. On save (Cmd-S or button): convert easy-email JSON → MJML, PUT to hub
|
|
||||||
// 4. postMessage to parent window so the wrapping ops UI knows we saved
|
|
||||||
//
|
|
||||||
// Hub URL is read from VITE_HUB_URL env (defaults to msg.gigafibre.ca in prod).
|
|
||||||
// The hub does the MJML → HTML compilation server-side; we just send the MJML.
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const HUB_URL = (import.meta as any).env?.VITE_HUB_URL || 'https://msg.gigafibre.ca'
|
|
||||||
|
|
||||||
// Merge tags exposed to the editor's "Variables" panel. These map to the
|
|
||||||
// Mustache variables the hub renders at send time.
|
|
||||||
const MERGE_TAGS = {
|
|
||||||
firstname: '{{firstname}}',
|
|
||||||
lastname: '{{lastname}}',
|
|
||||||
email: '{{email}}',
|
|
||||||
amount: '{{amount}}',
|
|
||||||
gift_url: '{{gift_url}}',
|
|
||||||
description: '{{description}}',
|
|
||||||
expiry: '{{expiry}}',
|
|
||||||
commitment_months: '{{commitment_months}}',
|
|
||||||
year: '{{year}}',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Minimal initial template returned when the hub has no content yet (rare —
|
|
||||||
// since we always pre-create gift-email-fr.mjml). Kept defensive.
|
|
||||||
function emptyTemplate(): IEmailTemplate {
|
|
||||||
return {
|
|
||||||
subject: 'Une offre exclusive de TARGO',
|
|
||||||
subTitle: 'Comme toi, on aime les connexions stables et les relations durables.',
|
|
||||||
content: {
|
|
||||||
type: BasicType.PAGE,
|
|
||||||
data: {
|
|
||||||
value: {
|
|
||||||
breakpoint: '480px',
|
|
||||||
headAttributes: '',
|
|
||||||
'font-size': '16px',
|
|
||||||
'line-height': '1.5',
|
|
||||||
'font-family': "'Plus Jakarta Sans', Helvetica, Arial, sans-serif",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
attributes: {
|
|
||||||
'background-color': '#F5FAF7',
|
|
||||||
width: '600px',
|
|
||||||
},
|
|
||||||
children: [],
|
|
||||||
} as any,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EmailEditorApp() {
|
|
||||||
const [templateName, setTemplateName] = useState<string>('')
|
|
||||||
const [initialValues, setInitialValues] = useState<IEmailTemplate | null>(null)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [saving, setSaving] = useState(false)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
// Read template name from URL and fetch its MJML content from the hub on mount
|
|
||||||
useEffect(() => {
|
|
||||||
const params = new URLSearchParams(window.location.search)
|
|
||||||
const name = params.get('name') || 'gift-email-fr'
|
|
||||||
setTemplateName(name)
|
|
||||||
|
|
||||||
;(async () => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${HUB_URL}/campaigns/templates/${encodeURIComponent(name)}`)
|
|
||||||
if (!res.ok) throw new Error(`Hub returned ${res.status}`)
|
|
||||||
const data = await res.json()
|
|
||||||
// Three sources of truth, in priority order:
|
|
||||||
// 1. .json file → easy-email JSON tree (fast, full restore)
|
|
||||||
// 2. .mjml file → MJML source (no auto-importer, start blank)
|
|
||||||
// 3. nothing → empty page
|
|
||||||
if (data.json) {
|
|
||||||
try {
|
|
||||||
const parsed = typeof data.json === 'string' ? JSON.parse(data.json) : data.json
|
|
||||||
setInitialValues(parsed as IEmailTemplate)
|
|
||||||
} catch (e: any) {
|
|
||||||
setError(`Stored JSON is invalid (${e.message}) — starting blank`)
|
|
||||||
setInitialValues(emptyTemplate())
|
|
||||||
}
|
|
||||||
} else if (data.mjml) {
|
|
||||||
setError(`Existing MJML (${(data.mjml || '').length}b) cannot be auto-imported into easy-email. ` +
|
|
||||||
`Reconstructing this template once with the drag-drop blocks here will save an editable JSON snapshot for next time.`)
|
|
||||||
setInitialValues(emptyTemplate())
|
|
||||||
} else {
|
|
||||||
setInitialValues(emptyTemplate())
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
setError(`Could not load template "${name}": ${e.message}`)
|
|
||||||
setInitialValues(emptyTemplate())
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Save → convert easy-email's JSON tree to MJML, PUT to hub
|
|
||||||
const onSave = useCallback(async (values: IEmailTemplate) => {
|
|
||||||
if (!templateName) return
|
|
||||||
setSaving(true)
|
|
||||||
setError(null)
|
|
||||||
try {
|
|
||||||
const mjmlSource = JsonToMjml({
|
|
||||||
data: values.content as any,
|
|
||||||
mode: 'production',
|
|
||||||
context: values.content as any,
|
|
||||||
})
|
|
||||||
// Send BOTH the compiled MJML (for send-worker) AND the raw easy-email
|
|
||||||
// JSON tree (for next-load restore). Hub persists .mjml + .html + .json
|
|
||||||
// — the JSON file is the canonical editing source going forward.
|
|
||||||
const res = await fetch(`${HUB_URL}/campaigns/templates/${encodeURIComponent(templateName)}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ mjml: mjmlSource, json: values }),
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
const errBody = await res.json().catch(() => ({}))
|
|
||||||
throw new Error(errBody.error || `Hub returned ${res.status}`)
|
|
||||||
}
|
|
||||||
// Notify parent window (the ops UI iframing us) that we saved
|
|
||||||
if (window.parent !== window) {
|
|
||||||
window.parent.postMessage(
|
|
||||||
{ type: 'email-editor:saved', template: templateName, ts: Date.now() },
|
|
||||||
'*',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// Visual confirmation (toast handled by easy-email's own UI)
|
|
||||||
} catch (e: any) {
|
|
||||||
setError(`Save failed: ${e.message}`)
|
|
||||||
} finally {
|
|
||||||
setSaving(false)
|
|
||||||
}
|
|
||||||
}, [templateName])
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <div style={{ padding: 32, textAlign: 'center' }}>Loading template…</div>
|
|
||||||
}
|
|
||||||
if (!initialValues) {
|
|
||||||
return <div style={{ padding: 32, color: 'red' }}>{error}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
||||||
{/* Top bar — shows template name + save state + parent communication */}
|
|
||||||
<div style={{
|
|
||||||
padding: '8px 16px',
|
|
||||||
background: '#1B2E24',
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 14,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 12,
|
|
||||||
}}>
|
|
||||||
<strong>TARGO Email Editor</strong>
|
|
||||||
<span style={{ opacity: 0.7 }}>· {templateName}</span>
|
|
||||||
{saving && <span style={{ color: '#00C853' }}>· Saving…</span>}
|
|
||||||
{error && (
|
|
||||||
<span style={{ color: '#fbbf24', fontSize: 12, marginLeft: 'auto', maxWidth: '60%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
||||||
{error}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* Editor */}
|
|
||||||
<div style={{ flex: 1, overflow: 'hidden' }}>
|
|
||||||
<EmailEditorProvider
|
|
||||||
data={initialValues}
|
|
||||||
height="100%"
|
|
||||||
autoComplete
|
|
||||||
dashed={false}
|
|
||||||
mergeTags={MERGE_TAGS}
|
|
||||||
mergeTagGenerate={(tag: string) => `{{${tag}}}`}
|
|
||||||
onSubmit={onSave}
|
|
||||||
>
|
|
||||||
{() => (
|
|
||||||
<StandardLayout
|
|
||||||
showSourceCode
|
|
||||||
categories={DEFAULT_CATEGORIES}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</EmailEditorProvider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block categories shown in the left sidebar — same set easy-email uses by
|
|
||||||
// default, organized for email composition.
|
|
||||||
const DEFAULT_CATEGORIES: ExtensionProps['categories'] = [
|
|
||||||
{
|
|
||||||
label: 'Content',
|
|
||||||
active: true,
|
|
||||||
blocks: [
|
|
||||||
{ type: AdvancedType.TEXT },
|
|
||||||
{ type: AdvancedType.BUTTON },
|
|
||||||
{ type: AdvancedType.IMAGE },
|
|
||||||
{ type: AdvancedType.DIVIDER },
|
|
||||||
{ type: AdvancedType.SPACER },
|
|
||||||
{ type: AdvancedType.HERO },
|
|
||||||
{ type: AdvancedType.WRAPPER },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Layout',
|
|
||||||
active: true,
|
|
||||||
displayType: 'column',
|
|
||||||
blocks: [
|
|
||||||
{
|
|
||||||
title: '1 column',
|
|
||||||
payload: [['100%']],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '2 columns',
|
|
||||||
payload: [['50%', '50%']],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '3 columns',
|
|
||||||
payload: [['33%', '33%', '33%']],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '4 columns',
|
|
||||||
payload: [['25%', '25%', '25%', '25%']],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom/client'
|
|
||||||
import { EmailEditorApp } from './EmailEditorApp'
|
|
||||||
|
|
||||||
// Entry point — mounts the easy-email editor app on #root.
|
|
||||||
// Template name comes from URL query: ?name=gift-email-fr
|
|
||||||
// In production the page lives at editor.gigafibre.ca and is iframed from
|
|
||||||
// the ops UI's /campaigns/templates/:name route.
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<EmailEditorApp />
|
|
||||||
</React.StrictMode>,
|
|
||||||
)
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"strict": false,
|
|
||||||
"noUnusedLocals": false,
|
|
||||||
"noUnusedParameters": false,
|
|
||||||
"noFallthroughCasesInSwitch": true
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
import react from '@vitejs/plugin-react'
|
|
||||||
|
|
||||||
// Vite config — served at editor.gigafibre.ca behind Traefik in prod.
|
|
||||||
// In dev: `npm run dev` exposes http://localhost:5173.
|
|
||||||
// Base path is '/' since this is a standalone microservice (own domain), not
|
|
||||||
// a sub-path of another app.
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
server: {
|
|
||||||
port: 5173,
|
|
||||||
host: '0.0.0.0', // accessible from outside container in dev
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
outDir: 'dist',
|
|
||||||
assetsDir: 'assets',
|
|
||||||
sourcemap: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
@ -9,12 +9,6 @@ services:
|
||||||
- ./public:/app/public:ro
|
- ./public:/app/public:ro
|
||||||
- ./package.json:/app/package.json:ro
|
- ./package.json:/app/package.json:ro
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
# Templates RW so the campaign editor can save .html + .json + .mjml
|
|
||||||
# files via PUT /campaigns/templates/:name. Was :ro previously which
|
|
||||||
# broke save with EROFS — fixed when Unlayer started writing back.
|
|
||||||
- ./templates:/app/templates
|
|
||||||
# User-uploaded assets (images dragged into the editor)
|
|
||||||
- ./uploads:/app/uploads
|
|
||||||
- hub_modules:/app/node_modules
|
- hub_modules:/app/node_modules
|
||||||
command: sh -c "npm install --production 2>&1 | tail -1 && node server.js"
|
command: sh -c "npm install --production 2>&1 | tail -1 && node server.js"
|
||||||
env_file: .env
|
env_file: .env
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -47,15 +47,11 @@ async function sendEmail (opts) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mailOpts = {
|
const mailOpts = {
|
||||||
from: opts.from || cfg.MAIL_FROM,
|
from: cfg.MAIL_FROM,
|
||||||
to: opts.to,
|
to: opts.to,
|
||||||
subject: opts.subject,
|
subject: opts.subject,
|
||||||
html: opts.html,
|
html: opts.html,
|
||||||
attachments: [],
|
attachments: [],
|
||||||
// Custom headers (e.g. X-MJ-CustomID for Mailjet Event API webhook
|
|
||||||
// correlation — Mailjet echoes the CustomID back in every event so
|
|
||||||
// we can match webhook events to the originating recipient).
|
|
||||||
headers: opts.headers || {},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.pdfBuffer && opts.pdfFilename) {
|
if (opts.pdfBuffer && opts.pdfFilename) {
|
||||||
|
|
@ -69,16 +65,9 @@ async function sendEmail (opts) {
|
||||||
try {
|
try {
|
||||||
const info = await transport.sendMail(mailOpts)
|
const info = await transport.sendMail(mailOpts)
|
||||||
log(`Email sent to ${opts.to}: ${info.messageId || 'OK'}`)
|
log(`Email sent to ${opts.to}: ${info.messageId || 'OK'}`)
|
||||||
// Return the info object (always truthy) so callers can capture
|
return true
|
||||||
// info.messageId for tracking. Legacy `if (await sendEmail(...))`
|
|
||||||
// callers continue to work because the object is truthy.
|
|
||||||
return info || { messageId: null }
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(`Email send failed to ${opts.to}: ${e.message}`)
|
log(`Email send failed to ${opts.to}: ${e.message}`)
|
||||||
// Legacy contract: return false on failure. New callers that need the
|
|
||||||
// error string should check `Promise.allSettled` style or wrap in try
|
|
||||||
// (we don't throw here to preserve existing `if (await sendEmail(...))`
|
|
||||||
// call sites). The error is logged above.
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3015
services/targo-hub/package-lock.json
generated
3015
services/targo-hub/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -7,14 +7,13 @@
|
||||||
"start": "node server.js"
|
"start": "node server.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mjml": "^5.2.2",
|
|
||||||
"mongodb": "^6.12.0",
|
"mongodb": "^6.12.0",
|
||||||
"mqtt": "^5.15.1",
|
"mqtt": "^5.15.1",
|
||||||
"mysql2": "^3.11.0",
|
"mysql2": "^3.11.0",
|
||||||
"net-snmp": "^3.26.1",
|
|
||||||
"nodemailer": "^6.9.16",
|
"nodemailer": "^6.9.16",
|
||||||
"pg": "^8.13.0",
|
"pg": "^8.13.0",
|
||||||
"twilio": "^5.5.0",
|
"twilio": "^5.5.0",
|
||||||
"web-push": "^3.6.7"
|
"web-push": "^3.6.7",
|
||||||
|
"net-snmp": "^3.26.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
'use strict'
|
|
||||||
/**
|
|
||||||
* convert-html-to-unlayer.js — one-time converter from our existing compiled
|
|
||||||
* .html templates into Unlayer design JSON. Run after MJML→Unlayer migration
|
|
||||||
* so the visual editor loads existing templates instead of starting blank.
|
|
||||||
*
|
|
||||||
* Strategy: wrap the entire HTML body content in a single "Custom HTML" block
|
|
||||||
* inside a minimal Unlayer design. This is the MIN VIABLE conversion — the
|
|
||||||
* template renders correctly in the canvas, the user can edit the HTML
|
|
||||||
* directly, and they can incrementally replace the HTML block with native
|
|
||||||
* Unlayer blocks (Text, Image, Button) on their own schedule.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* node convert-html-to-unlayer.js gift-email-fr
|
|
||||||
* node convert-html-to-unlayer.js gift-email-en
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
function htmlToUnlayer (innerBodyHtml, opts = {}) {
|
|
||||||
const preheader = opts.preheader || ''
|
|
||||||
return {
|
|
||||||
counters: { u_row: 1, u_column: 1, u_content_html: 1 },
|
|
||||||
body: {
|
|
||||||
id: 'BODY-1',
|
|
||||||
rows: [
|
|
||||||
{
|
|
||||||
id: 'ROW-1',
|
|
||||||
cells: [1],
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
id: 'COL-1',
|
|
||||||
contents: [
|
|
||||||
{
|
|
||||||
id: 'HTML-1',
|
|
||||||
type: 'html',
|
|
||||||
values: {
|
|
||||||
html: innerBodyHtml,
|
|
||||||
hideDesktop: false,
|
|
||||||
displayCondition: null,
|
|
||||||
containerPadding: '0px',
|
|
||||||
_meta: { htmlID: 'u_content_html_1', htmlClassNames: 'u_content_html' },
|
|
||||||
selectable: true,
|
|
||||||
draggable: true,
|
|
||||||
duplicatable: true,
|
|
||||||
deletable: true,
|
|
||||||
hideable: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
values: {
|
|
||||||
_meta: { htmlID: 'u_column_1', htmlClassNames: 'u_column' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
values: {
|
|
||||||
displayCondition: null,
|
|
||||||
columns: false,
|
|
||||||
backgroundColor: '',
|
|
||||||
columnsBackgroundColor: '',
|
|
||||||
padding: '0px',
|
|
||||||
anchor: '',
|
|
||||||
hideDesktop: false,
|
|
||||||
_meta: { htmlID: 'u_row_1', htmlClassNames: 'u_row' },
|
|
||||||
selectable: true,
|
|
||||||
draggable: true,
|
|
||||||
duplicatable: true,
|
|
||||||
deletable: true,
|
|
||||||
hideable: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
values: {
|
|
||||||
popupPosition: 'center',
|
|
||||||
popupWidth: '600px',
|
|
||||||
popupHeight: 'auto',
|
|
||||||
borderRadius: '10px',
|
|
||||||
contentAlign: 'center',
|
|
||||||
contentVerticalAlign: 'center',
|
|
||||||
contentWidth: '600px',
|
|
||||||
fontFamily: {
|
|
||||||
label: 'Plus Jakarta Sans',
|
|
||||||
value: "'Plus Jakarta Sans', sans-serif",
|
|
||||||
url: 'https://fonts.googleapis.com/css?family=Plus+Jakarta+Sans:400,500,600,700',
|
|
||||||
},
|
|
||||||
textColor: '#1B2E24',
|
|
||||||
popupBackgroundColor: '#FFFFFF',
|
|
||||||
backgroundColor: '#F5FAF7',
|
|
||||||
preheaderText: preheader,
|
|
||||||
linkStyle: {
|
|
||||||
body: true,
|
|
||||||
linkColor: '#00C853',
|
|
||||||
linkHoverColor: '#005026',
|
|
||||||
linkUnderline: true,
|
|
||||||
linkHoverUnderline: true,
|
|
||||||
},
|
|
||||||
_meta: { htmlID: 'u_body', htmlClassNames: 'u_body' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
schemaVersion: 12,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── CLI ──────────────────────────────────────────────────────────────────
|
|
||||||
const name = process.argv[2]
|
|
||||||
if (!name) {
|
|
||||||
console.error('Usage: convert-html-to-unlayer.js <template-name>')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const TEMPLATES_DIR = path.resolve(__dirname, '..', 'templates')
|
|
||||||
const htmlPath = path.join(TEMPLATES_DIR, name + '.html')
|
|
||||||
const jsonPath = path.join(TEMPLATES_DIR, name + '.json')
|
|
||||||
|
|
||||||
if (!fs.existsSync(htmlPath)) {
|
|
||||||
console.error(`✗ No HTML at ${htmlPath}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullHtml = fs.readFileSync(htmlPath, 'utf8')
|
|
||||||
|
|
||||||
// Extract just the body inner content — Unlayer wraps everything in its own
|
|
||||||
// <html><head><body> at preview/export time, so we don't want duplicated
|
|
||||||
// doctype/head/body tags.
|
|
||||||
const bodyMatch = fullHtml.match(/<body[^>]*>([\s\S]*?)<\/body>/i)
|
|
||||||
const innerHtml = bodyMatch ? bodyMatch[1].trim() : fullHtml
|
|
||||||
|
|
||||||
// Pull preheader text if a hidden <div style="display:none"> is present
|
|
||||||
// (standard email preheader pattern, also what MJML's <mj-preview> compiles to)
|
|
||||||
const preheaderMatch = innerHtml.match(/<div[^>]*display:\s*none[^>]*>\s*([^<]+?)\s*<\/div>/i)
|
|
||||||
const preheader = preheaderMatch ? preheaderMatch[1].trim() : ''
|
|
||||||
|
|
||||||
const design = htmlToUnlayer(innerHtml, { preheader })
|
|
||||||
|
|
||||||
// Optional: backup existing .json
|
|
||||||
if (fs.existsSync(jsonPath)) {
|
|
||||||
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
||||||
fs.copyFileSync(jsonPath, jsonPath.replace(/\.json$/, `.bak-${ts}.json`))
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(jsonPath, JSON.stringify(design, null, 2), 'utf8')
|
|
||||||
|
|
||||||
console.log(`✓ Converted ${name}.html (${fullHtml.length}b) → ${name}.json (${JSON.stringify(design).length}b)`)
|
|
||||||
console.log(` preheader: "${preheader.slice(0, 80)}${preheader.length > 80 ? '…' : ''}"`)
|
|
||||||
console.log(` inner HTML: ${innerHtml.length}b in one Custom HTML block`)
|
|
||||||
|
|
@ -119,7 +119,6 @@ const server = http.createServer(async (req, res) => {
|
||||||
if (path.startsWith('/dispatch')) return dispatch.handle(req, res, method, path)
|
if (path.startsWith('/dispatch')) return dispatch.handle(req, res, method, path)
|
||||||
if (path.startsWith('/admin/pollers')) return require('./lib/poller-control').handle(req, res, method, path)
|
if (path.startsWith('/admin/pollers')) return require('./lib/poller-control').handle(req, res, method, path)
|
||||||
if (path.startsWith('/reports')) return require('./lib/reports').handle(req, res, method, path, url)
|
if (path.startsWith('/reports')) return require('./lib/reports').handle(req, res, method, path, url)
|
||||||
if (path.startsWith('/campaigns')) return require('./lib/campaigns').handle(req, res, method, path)
|
|
||||||
if (path.startsWith('/contract')) return require('./lib/contracts').handle(req, res, method, path)
|
if (path.startsWith('/contract')) return require('./lib/contracts').handle(req, res, method, path)
|
||||||
if (path.startsWith('/payments') || path === '/webhook/stripe') return require('./lib/payments').handle(req, res, method, path, url)
|
if (path.startsWith('/payments') || path === '/webhook/stripe') return require('./lib/payments').handle(req, res, method, path, url)
|
||||||
if (path === '/vision/barcodes' && method === 'POST') return vision.handleBarcodes(req, res)
|
if (path === '/vision/barcodes' && method === 'POST') return vision.handleBarcodes(req, res)
|
||||||
|
|
|
||||||
|
|
@ -1,880 +0,0 @@
|
||||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
||||||
<head>
|
|
||||||
<!--[if gte mso 9]>
|
|
||||||
<xml>
|
|
||||||
<o:OfficeDocumentSettings>
|
|
||||||
<o:AllowPNG/>
|
|
||||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
||||||
</o:OfficeDocumentSettings>
|
|
||||||
</xml>
|
|
||||||
<![endif]-->
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="x-apple-disable-message-reformatting">
|
|
||||||
<!--[if !mso]><!--><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
<style type="text/css">
|
|
||||||
|
|
||||||
@media only screen and (min-width: 620px) {
|
|
||||||
.u-row {
|
|
||||||
width: 600px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-row .u-col {
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.u-row .u-col-100 {
|
|
||||||
width: 600px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 620px) {
|
|
||||||
.u-row-container {
|
|
||||||
max-width: 100% !important;
|
|
||||||
padding-left: 0px !important;
|
|
||||||
padding-right: 0px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-row {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-row .u-col {
|
|
||||||
display: block !important;
|
|
||||||
width: 100% !important;
|
|
||||||
min-width: 320px !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-row .u-col > div {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
body{margin:0;padding:0}table,td,tr{border-collapse:collapse;vertical-align:top}.ie-container table,.mso-container table{table-layout:fixed}*{line-height:inherit}a[x-apple-data-detectors=true]{color:inherit!important;text-decoration:none!important}
|
|
||||||
|
|
||||||
|
|
||||||
table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: underline; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Plus+Jakarta+Sans:400,500,600,700" rel="stylesheet" type="text/css"><!--<![endif]-->
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="clean-body u_body" style="margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #F5FAF7;color: #1B2E24">
|
|
||||||
<!--[if IE]><div class="ie-container"><![endif]-->
|
|
||||||
<!--[if mso]><div class="mso-container"><![endif]-->
|
|
||||||
<table role="presentation" id="u_body" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #F5FAF7;width:100%" cellpadding="0" cellspacing="0">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="display:none !important;visibility:hidden;mso-hide:all;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">
|
|
||||||
Because great connections aren't just about fiber — they're about people too.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr style="vertical-align: top">
|
|
||||||
<td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top">
|
|
||||||
<!--[if (mso)|(IE)]><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #F5FAF7;" bgcolor="#F5FAF7"><table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" align="center" style="border-collapse: collapse;"><tr><td style="padding:0;"><![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="u-row-container" style="padding: 0px;background-color: transparent;">
|
|
||||||
<div class="u-row" style="margin: 0 auto;min-width: 320px;max-width: 600px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;">
|
|
||||||
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
|
|
||||||
<!--[if (mso)|(IE)]><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;"><table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"><tr style="background-color: transparent;"><![endif]-->
|
|
||||||
|
|
||||||
<!--[if (mso)|(IE)]><td align="center" width="600" style="width: 600px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 20px 0px 0px;"><![endif]-->
|
|
||||||
<div class="u-col u-col-100" style="max-width: 320px;min-width: 600px;display: table-cell;vertical-align: top;">
|
|
||||||
<div style="height: 100%;width: 100% !important;">
|
|
||||||
<!--[if (!mso)&(!IE)]><!--><div style="box-sizing: border-box; height: 100%; padding: 20px 0px 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;"><!--<![endif]-->
|
|
||||||
|
|
||||||
<table style="font-family:'Plus Jakarta Sans', sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="overflow-wrap:break-word;word-break:break-word;padding:0px;font-family:'Plus Jakarta Sans', sans-serif;" align="left">
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">Because great connections aren't just about fiber — they're about people too.</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
aria-label="Your exclusive offer from 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" 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 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 + INTRO (4 blocs sémantiques) ════════
|
|
||||||
Bloc 1 — Greeting personnalisé
|
|
||||||
Bloc 2 — Manifeste brand TARGO (2 lignes qui voyagent ensemble)
|
|
||||||
Bloc 3 — Annonce du cadeau
|
|
||||||
Bloc 4 — Upsell forfaits + invitation contact
|
|
||||||
Chaque bloc est un mj-text indépendant = drag-droppable séparément. -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" 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 18px;text-align:center;"
|
|
||||||
>
|
|
||||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td 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;"
|
|
||||||
>Hey {{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:16px;line-height:1.5;text-align:justify;color:#374151;"
|
|
||||||
>Summer is here, and so is something special — for a limited time.</div>
|
|
||||||
<div
|
|
||||||
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;font-weight:500;line-height:1.5;text-align:justify;color:#1B2E24;"
|
|
||||||
>Thank you for choosing local. Your support helps keep our community connected.<br />
|
|
||||||
Because great connections aren't just about fiber — they're about people too.</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:justify;color:#374151;">Because our customers trust us, we're now able to offer the <strong>fastest plans around</strong>, with speeds up to <strong>3.5 Gbit/s</strong>.<br />
|
|
||||||
Whether you're looking for more speed, want to beat another offer, or just need to optimize your gear, don't be shy! We're right next door — and we genuinely love lending a hand.</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 1 CHIP ════════ -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" 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 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]-->
|
|
||||||
|
|
||||||
<!-- ════════ COMPACT INFO CARD (was 3 pills) ════════ -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" 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 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}} to spend at hundreds of your favorite stores<br><br><span style="display:inline-block;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/4b0b2a4a-5f99-416c-8873-8d3e4389b6f7/content" width="32" alt="Tim Hortons" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/14df433d-583c-4602-a403-d47ee84966a6/content" width="32" alt="Walmart" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/b8d3db5a-d39e-43ce-a84a-2f5dddbf0192/content" width="32" alt="Home Depot" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/9c9dfa18-2a3a-414a-b5ad-16d490c961b5/content" width="32" alt="IGA" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/a1e5f032-a192-4499-97ba-53b939712fa9/content" width="32" alt="Home Hardware" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><span style="font-size:13px;color:#64748B;vertical-align:middle;font-weight:400;">and more</span></span></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;"
|
|
||||||
>⚡ Instantly available on Giftbit — just click your {{amount}} to claim it!</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;"
|
|
||||||
>🤝 You just need to keep your subscription for {{commitment_months}} months or more.</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]-->
|
|
||||||
|
|
||||||
<!-- ════════ BIG GREEN CTA BUTTON ════════ -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" 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 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"
|
|
||||||
>
|
|
||||||
🎁 {{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" 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 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;"
|
|
||||||
>🪂 Cancellation before {{commitment_months}} months: only the prorated amount for the remaining months is refundable.</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" 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 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" 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 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;"
|
|
||||||
>Do nothing. No changes to your current subscription.</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" 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 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;"
|
|
||||||
>🤝 Thanks for helping keep our local economy buzzing!</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;"
|
|
||||||
>The TARGO Team</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" 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 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;"
|
|
||||||
>You're getting this email because you're a TARGO customer at <strong style="color:#1B2E24;">{{description}}</strong>.<br />
|
|
||||||
Got a question? Feel free to email us at
|
|
||||||
<a href="mailto:support@targo.ca" style="color:#00C853;text-decoration:none;">support@targo.ca</a>
|
|
||||||
or call us at
|
|
||||||
<a href="tel:5144480773" style="color:#00C853;text-decoration:none;">514-448-0773</a>.</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" 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 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>
|
|
||||||
· 1867 ch. de la rivière, Ste-Clotilde, QC<br />
|
|
||||||
© {{year}} TARGO Communications · All rights reserved.</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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!--[if (!mso)&(!IE)]><!--></div><!--<![endif]-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!--[if (mso)|(IE)]></td></tr></table></td><![endif]-->
|
|
||||||
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<!--[if mso]></div><![endif]-->
|
|
||||||
<!--[if IE]></div><![endif]-->
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,880 +0,0 @@
|
||||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
||||||
<head>
|
|
||||||
<!--[if gte mso 9]>
|
|
||||||
<xml>
|
|
||||||
<o:OfficeDocumentSettings>
|
|
||||||
<o:AllowPNG/>
|
|
||||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
||||||
</o:OfficeDocumentSettings>
|
|
||||||
</xml>
|
|
||||||
<![endif]-->
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="x-apple-disable-message-reformatting">
|
|
||||||
<!--[if !mso]><!--><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
<style type="text/css">
|
|
||||||
|
|
||||||
@media only screen and (min-width: 620px) {
|
|
||||||
.u-row {
|
|
||||||
width: 600px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-row .u-col {
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.u-row .u-col-100 {
|
|
||||||
width: 600px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 620px) {
|
|
||||||
.u-row-container {
|
|
||||||
max-width: 100% !important;
|
|
||||||
padding-left: 0px !important;
|
|
||||||
padding-right: 0px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-row {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-row .u-col {
|
|
||||||
display: block !important;
|
|
||||||
width: 100% !important;
|
|
||||||
min-width: 320px !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-row .u-col > div {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
body{margin:0;padding:0}table,td,tr{border-collapse:collapse;vertical-align:top}.ie-container table,.mso-container table{table-layout:fixed}*{line-height:inherit}a[x-apple-data-detectors=true]{color:inherit!important;text-decoration:none!important}
|
|
||||||
|
|
||||||
|
|
||||||
table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: underline; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Plus+Jakarta+Sans:400,500,600,700" rel="stylesheet" type="text/css"><!--<![endif]-->
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="clean-body u_body" style="margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #F5FAF7;color: #1B2E24">
|
|
||||||
<!--[if IE]><div class="ie-container"><![endif]-->
|
|
||||||
<!--[if mso]><div class="mso-container"><![endif]-->
|
|
||||||
<table role="presentation" id="u_body" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #F5FAF7;width:100%" cellpadding="0" cellspacing="0">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="display:none !important;visibility:hidden;mso-hide:all;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.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr style="vertical-align: top">
|
|
||||||
<td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top">
|
|
||||||
<!--[if (mso)|(IE)]><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #F5FAF7;" bgcolor="#F5FAF7"><table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" align="center" style="border-collapse: collapse;"><tr><td style="padding:0;"><![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="u-row-container" style="padding: 0px;background-color: transparent;">
|
|
||||||
<div class="u-row" style="margin: 0 auto;min-width: 320px;max-width: 600px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;">
|
|
||||||
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
|
|
||||||
<!--[if (mso)|(IE)]><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;"><table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"><tr style="background-color: transparent;"><![endif]-->
|
|
||||||
|
|
||||||
<!--[if (mso)|(IE)]><td align="center" width="600" style="width: 600px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 20px 0px 0px;"><![endif]-->
|
|
||||||
<div class="u-col u-col-100" style="max-width: 320px;min-width: 600px;display: table-cell;vertical-align: top;">
|
|
||||||
<div style="height: 100%;width: 100% !important;">
|
|
||||||
<!--[if (!mso)&(!IE)]><!--><div style="box-sizing: border-box; height: 100%; padding: 20px 0px 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;"><!--<![endif]-->
|
|
||||||
|
|
||||||
<table style="font-family:'Plus Jakarta Sans', sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="overflow-wrap:break-word;word-break:break-word;padding:0px;font-family:'Plus Jakarta Sans', sans-serif;" align="left">
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<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" 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 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 + INTRO (4 blocs sémantiques) ════════
|
|
||||||
Bloc 1 — Greeting personnalisé
|
|
||||||
Bloc 2 — Manifeste brand TARGO (2 lignes qui voyagent ensemble)
|
|
||||||
Bloc 3 — Annonce du cadeau
|
|
||||||
Bloc 4 — Upsell forfaits + invitation contact
|
|
||||||
Chaque bloc est un mj-text indépendant = drag-droppable séparément. -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" 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 18px;text-align:center;"
|
|
||||||
>
|
|
||||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td 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:16px;line-height:1.5;text-align:justify;color:#374151;"
|
|
||||||
>Avec l'arrivée de l'été, voici un <strong>cadeau pour toi, disponible pour un temps limité</strong>.</div>
|
|
||||||
<div
|
|
||||||
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;font-weight:500;line-height:1.5;text-align:justify;color:#1B2E24;"
|
|
||||||
>On veut te remercier pour ta loyauté envers l'achat local.<br />
|
|
||||||
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:justify;color:#374151;">Grâce à la confiance de nos clients, on offre maintenant les forfaits à <strong>la plus haute vitesse dans le secteur</strong>, jusqu'à <strong>3.5 Gbit/s</strong>.<br />
|
|
||||||
Que tu souhaites plus de vitesse, battre une autre offre ou faire optimiser des équipements, n'hésite pas. On est juste à côté et on aime aider.</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 1 CHIP ════════ -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" 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 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]-->
|
|
||||||
|
|
||||||
<!-- ════════ COMPACT INFO CARD (was 3 pills) ════════ -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" 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 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<br><br><span style="display:inline-block;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/4b0b2a4a-5f99-416c-8873-8d3e4389b6f7/content" width="32" alt="Tim Hortons" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/14df433d-583c-4602-a403-d47ee84966a6/content" width="32" alt="Walmart" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/b8d3db5a-d39e-43ce-a84a-2f5dddbf0192/content" width="32" alt="Home Depot" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/9c9dfa18-2a3a-414a-b5ad-16d490c961b5/content" width="32" alt="IGA" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><img src="https://xqy3m.mjt.lu/img2/xqy3m/a1e5f032-a192-4499-97ba-53b939712fa9/content" width="32" alt="Home Hardware" style="width:32px;max-width:32px;height:auto;display:inline-block;border:0;margin-right:6px;vertical-align:middle;"><span style="font-size:13px;color:#64748B;vertical-align:middle;font-weight:400;">et plus</span></span></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;"
|
|
||||||
>⚡ Disponible instantanément sur Giftbit en cliquant sur ton montant</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;"
|
|
||||||
>🤝 Condition : Maintenir l'abonnement {{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]-->
|
|
||||||
|
|
||||||
<!-- ════════ BIG GREEN CTA BUTTON ════════ -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" 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 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"
|
|
||||||
>
|
|
||||||
🎁 {{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" 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 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;"
|
|
||||||
>🪂 Annulation avant {{commitment_months}} mois : seulement à rembourser au prorata des mois restants.</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" 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 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" 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 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. Aucun changement à ton abonnement actuel.</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" 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 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" 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 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 ? N'hésite pas à nous écrire à
|
|
||||||
<a href="mailto:support@targo.ca" style="color:#00C853;text-decoration:none;">support@targo.ca</a>
|
|
||||||
ou nous appeler au
|
|
||||||
<a href="tel:5144480773" style="color:#00C853;text-decoration:none;">514-448-0773</a>.</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" 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 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>
|
|
||||||
· 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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!--[if (!mso)&(!IE)]><!--></div><!--<![endif]-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!--[if (mso)|(IE)]></td></tr></table></td><![endif]-->
|
|
||||||
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<!--[if mso]></div><![endif]-->
|
|
||||||
<!--[if IE]></div><![endif]-->
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,362 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Une offre exclusive de TARGO</title>
|
|
||||||
<!-- Brand fonts: Space Grotesk for display, Plus Jakarta Sans for body.
|
|
||||||
Wrapped in MSO conditional comment so Outlook desktop skips the
|
|
||||||
Google Fonts request (it can't render them anyway) and falls back
|
|
||||||
to Helvetica via the font-family stack on each element. -->
|
|
||||||
<!--[if !mso]><!-->
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<!--<![endif]-->
|
|
||||||
</head>
|
|
||||||
<body style="margin:0; padding:0; background:#F5FAF7; font-family:'Plus Jakarta Sans','Helvetica Neue',Helvetica,Arial,sans-serif; color:#1B2E24; line-height:1.5;">
|
|
||||||
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="padding:32px 16px;">
|
|
||||||
|
|
||||||
<!-- Main card -->
|
|
||||||
<table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0"
|
|
||||||
style="max-width:600px; background:#ffffff; border:1px solid #e5e7eb; border-radius:12px; overflow:hidden;">
|
|
||||||
|
|
||||||
<!-- Logo header (clean, no colored band) -->
|
|
||||||
<tr>
|
|
||||||
<td style="padding:28px 36px 22px; border-bottom:1px solid #eef0ee;">
|
|
||||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/eed4d18c-8065-4c5f-b47c-58af63171cd0/content"
|
|
||||||
alt="TARGO" width="140"
|
|
||||||
style="display:block; border:0; outline:none; text-decoration:none; max-width:140px; height:auto;">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Greeting + hook -->
|
|
||||||
<tr>
|
|
||||||
<td style="padding:26px 36px 4px;">
|
|
||||||
<p style="margin:0 0 14px; font-size:1rem; color:#374151;">Bonjour {{firstname}},</p>
|
|
||||||
<p style="margin:0 0 10px; font-size:1.08rem; color:#1B2E24; font-weight:500;">
|
|
||||||
Comme toi, on aime les connexions stables et les relations durables.
|
|
||||||
</p>
|
|
||||||
<p style="margin:0; font-size:1rem; color:#374151;">
|
|
||||||
Avec l'arrivée de l'été, voici ton
|
|
||||||
<strong>offre exclusive pour un temps limité</strong> :
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Info pill: gift card amount -->
|
|
||||||
<tr>
|
|
||||||
<td style="padding:18px 36px 8px;">
|
|
||||||
<div style="background:#F5FAF7; border-radius:10px; padding:14px 18px;">
|
|
||||||
<div style="font-size:0.7rem; font-weight:700; letter-spacing:0.12em; color:#9ca3af; text-transform:uppercase; margin-bottom:4px;">
|
|
||||||
Carte-cadeau numérique
|
|
||||||
</div>
|
|
||||||
<div style="font-size:1.05rem; font-weight:700; color:#1B2E24;">
|
|
||||||
🎁 {{amount}} chez des centaines de marques
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Two-column: ENVOI + CONDITION -->
|
|
||||||
<tr>
|
|
||||||
<td style="padding:6px 36px 18px;">
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
|
||||||
<tr>
|
|
||||||
<td width="50%" style="padding-right:5px; vertical-align:top;">
|
|
||||||
<div style="background:#F5FAF7; border-radius:10px; padding:14px 16px;">
|
|
||||||
<div style="font-size:0.7rem; font-weight:700; letter-spacing:0.12em; color:#9ca3af; text-transform:uppercase; margin-bottom:4px;">
|
|
||||||
Envoi
|
|
||||||
</div>
|
|
||||||
<div style="font-size:0.95rem; font-weight:700; color:#1B2E24;">
|
|
||||||
⚡ Instantané à l'activation
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td width="50%" style="padding-left:5px; vertical-align:top;">
|
|
||||||
<div style="background:#F5FAF7; border-radius:10px; padding:14px 16px;">
|
|
||||||
<div style="font-size:0.7rem; font-weight:700; letter-spacing:0.12em; color:#9ca3af; text-transform:uppercase; margin-bottom:4px;">
|
|
||||||
Condition
|
|
||||||
</div>
|
|
||||||
<div style="font-size:0.95rem; font-weight:700; color:#1B2E24;">
|
|
||||||
🤝 Rester encore {{commitment_months}} mois ou +
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<tr><td style="padding:0 36px;"><div style="border-top:1px solid #eef0ee;"></div></td></tr>
|
|
||||||
|
|
||||||
<!-- Option 1 chip -->
|
|
||||||
<tr>
|
|
||||||
<td style="padding:22px 36px 10px;">
|
|
||||||
<span style="display:inline-block; background:#E6F9EE; color:#00C853; font-size:0.82rem; font-weight:700; padding:5px 12px; border-radius:6px;">
|
|
||||||
✅ Option 1
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Big green CTA card -->
|
|
||||||
<tr>
|
|
||||||
<td style="padding:0 36px 8px;">
|
|
||||||
<a href="{{gift_url}}" style="text-decoration:none; color:#ffffff; display:block;">
|
|
||||||
<!-- CTA card — gradient Targo officiel (135deg, #00C853 → #005026).
|
|
||||||
Outlook desktop will ignore the gradient and render the
|
|
||||||
solid #00C853 fallback (the gradient is the bgcolor's
|
|
||||||
fallback in nodemailer rendering). Acceptable degradation. -->
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"
|
|
||||||
bgcolor="#00C853"
|
|
||||||
style="background:#00C853; background-image:linear-gradient(135deg,#00C853 0%,#005026 100%); border-radius:12px;">
|
|
||||||
<tr>
|
|
||||||
<td style="padding:30px 24px; text-align:center;">
|
|
||||||
<div style="font-family:'Space Grotesk','Helvetica Neue',Helvetica,Arial,sans-serif; font-size:2.2rem; font-weight:700; line-height:1; margin-bottom:14px; color:#ffffff;">
|
|
||||||
🎁 {{amount}}
|
|
||||||
</div>
|
|
||||||
<div style="font-size:1.08rem; font-weight:700; color:#ffffff;">
|
|
||||||
Activer ma carte-cadeau
|
|
||||||
</div>
|
|
||||||
<div style="font-size:0.85rem; opacity:0.9; margin-top:8px; color:#ffffff;">
|
|
||||||
Choisir ma carte sur Giftbit →
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Prorata refund disclaimer -->
|
|
||||||
<tr>
|
|
||||||
<td style="padding:10px 36px 22px;">
|
|
||||||
<div style="font-size:0.85rem; color:#6b7280;">
|
|
||||||
🪂 En cas de départ avant {{commitment_months}} mois, le prorata du montant est remboursable.
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<tr><td style="padding:0 36px;"><div style="border-top:1px solid #eef0ee;"></div></td></tr>
|
|
||||||
|
|
||||||
<!-- Option 2 chip -->
|
|
||||||
<tr>
|
|
||||||
<td style="padding:22px 36px 8px;">
|
|
||||||
<span style="display:inline-block; background:#F5FAF7; color:#6b7280; font-size:0.82rem; font-weight:700; padding:5px 12px; border-radius:6px;">
|
|
||||||
⏭️ Option 2
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding:0 36px 22px;">
|
|
||||||
<div style="font-size:0.97rem; color:#4b5563; line-height:1.55;">
|
|
||||||
Ne rien faire. Ton abonnement mensuel se poursuit normalement,
|
|
||||||
sans engagement ni carte-cadeau.
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
{{#expiry}}
|
|
||||||
<!-- Optional expiry callout -->
|
|
||||||
<tr><td style="padding:0 36px;"><div style="border-top:1px solid #eef0ee;"></div></td></tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding:18px 36px 0;">
|
|
||||||
<div style="font-size:0.85rem; color:#9ca3af;">
|
|
||||||
⏰ Cette offre expire le <strong style="color:#374151;">{{expiry}}</strong>.
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{/expiry}}
|
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<tr><td style="padding:18px 36px 0;"><div style="border-top:1px solid #eef0ee;"></div></td></tr>
|
|
||||||
|
|
||||||
<!-- Signature -->
|
|
||||||
<tr>
|
|
||||||
<td style="padding:22px 36px 28px;">
|
|
||||||
<div style="font-size:0.97rem; color:#1B2E24;">
|
|
||||||
🤝 Merci de faire rouler l'économie de notre région avec nous !
|
|
||||||
</div>
|
|
||||||
<div style="font-size:0.9rem; color:#6b7280; margin-top:6px;">
|
|
||||||
L'équipe TARGO
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!-- Merchant brands grid — 4 cols × 3 rows = 12 logos
|
|
||||||
TO SWAP TO MAILJET-HOSTED LOGOS:
|
|
||||||
Replace each placeholder src URL below with the Mailjet CDN URL
|
|
||||||
you already have (same format as the TARGO logo:
|
|
||||||
https://xqy3m.mjt.lu/img2/xqy3m/<UUID>/content). The alt= attribute
|
|
||||||
stays as-is (used by screen readers + shown when images blocked).
|
|
||||||
Brand list in order: Amazon, IGA, Tim Hortons, $1 Plus (Dollarama),
|
|
||||||
Pizza Pizza, Home Depot, Best Buy, Walmart,
|
|
||||||
Petro-Canada, Esso, Home Hardware, Sobeys.
|
|
||||||
-->
|
|
||||||
<table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0"
|
|
||||||
style="max-width:600px; margin-top:8px;">
|
|
||||||
<tr>
|
|
||||||
<td style="padding:24px 36px 12px; text-align:center;">
|
|
||||||
<div style="font-size:1.02rem; font-weight:700; color:#00C853;">
|
|
||||||
Quelques exemples de choix pour votre carte cadeau :
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding:0 28px 8px;">
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
|
||||||
<!-- Row 1 — real Mailjet-hosted logos -->
|
|
||||||
<tr>
|
|
||||||
<td width="25%" style="padding:4px;">
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
|
||||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
|
||||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/31ffdf91-d2de-4ced-8b99-ad2221695abe/content" alt="Amazon" width="95" style="max-width:95px; height:auto; display:inline-block; border:0;">
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
<td width="25%" style="padding:4px;">
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
|
||||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
|
||||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/9c9dfa18-2a3a-414a-b5ad-16d490c961b5/content" alt="IGA" width="95" style="max-width:95px; height:auto; display:inline-block; border:0;">
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
<td width="25%" style="padding:4px;">
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
|
||||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
|
||||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/4b0b2a4a-5f99-416c-8873-8d3e4389b6f7/content" alt="Tim Hortons" width="95" style="max-width:95px; height:auto; display:inline-block; border:0;">
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
<td width="25%" style="padding:4px;">
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
|
||||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
|
||||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/162b988c-beb7-49b3-b85e-ccc12fa2c155/content" alt="$1 Plus" width="95" style="max-width:95px; height:auto; display:inline-block; border:0;">
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<!-- Row 2 — Mailjet-hosted brand logos (sourced from user's Passport template) -->
|
|
||||||
<tr>
|
|
||||||
<td width="25%" style="padding:4px;">
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
|
||||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
|
||||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/ef3b15eb-ec08-4551-ae27-ce249688185a/content" alt="Pizza Pizza" width="95" style="max-width:95px; height:auto; display:inline-block; border:0;">
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
<td width="25%" style="padding:4px;">
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
|
||||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
|
||||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/b8d3db5a-d39e-43ce-a84a-2f5dddbf0192/content" alt="Home Depot" width="95" style="max-width:95px; height:auto; display:inline-block; border:0;">
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
<td width="25%" style="padding:4px;">
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
|
||||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
|
||||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/cb965d5a-3e92-4f16-9e5c-b4939ce3cb91/content" alt="Best Buy" width="95" style="max-width:95px; height:auto; display:inline-block; border:0;">
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
<td width="25%" style="padding:4px;">
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
|
||||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
|
||||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/14df433d-583c-4602-a403-d47ee84966a6/content" alt="Walmart" width="95" style="max-width:95px; height:auto; display:inline-block; border:0;">
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<!-- Row 3 — Mailjet-hosted brand logos -->
|
|
||||||
<tr>
|
|
||||||
<td width="25%" style="padding:4px;">
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
|
||||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
|
||||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/36775a32-434a-41e1-bb7a-ec7b768e5ba0/content" alt="Petro-Canada" width="95" style="max-width:95px; height:auto; display:inline-block; border:0;">
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
<td width="25%" style="padding:4px;">
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
|
||||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
|
||||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/ff95b593-8ba3-4e57-9f01-f46cf0a2b33f/content" alt="Esso" width="95" style="max-width:95px; height:auto; display:inline-block; border:0;">
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
<td width="25%" style="padding:4px;">
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
|
||||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
|
||||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/a1e5f032-a192-4499-97ba-53b939712fa9/content" alt="Home Hardware" width="95" style="max-width:95px; height:auto; display:inline-block; border:0;">
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
<td width="25%" style="padding:4px;">
|
|
||||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
|
||||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
|
||||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/67bd791a-18c7-4d65-a77a-64c86cecc2b1/content" alt="Sobeys" width="95" style="max-width:95px; height:auto; display:inline-block; border:0;">
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!-- "Pourquoi cet email" + coordonnées officielles (per brand guide §11) -->
|
|
||||||
<table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0"
|
|
||||||
style="max-width:600px;">
|
|
||||||
<tr>
|
|
||||||
<td style="padding:18px 36px 8px; text-align:center;">
|
|
||||||
<div style="font-size:0.78rem; color:#64748B; line-height:1.55;">
|
|
||||||
Tu reçois ce courriel parce que tu es client(e) TARGO à
|
|
||||||
<strong style="color:#1B2E24;">{{description}}</strong>.<br>
|
|
||||||
Une question ? Écris-nous à
|
|
||||||
<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 448-0773</a>
|
|
||||||
/ <a href="tel:18558882746" style="color:#00C853; text-decoration:none;">1 855 888-2746</a>.
|
|
||||||
Support 7j/7.
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!-- Dark footer band — logo TARGO blanc (fond sombre, per brand guide §1)
|
|
||||||
+ adresse + copyright. Pas de slogan ni de wordmark stylisé en
|
|
||||||
CSS — on utilise le vrai logo image (variante blanche, "fonds
|
|
||||||
sombres" du brand guide).
|
|
||||||
TODO: la première fois, uploader targo-logo-white.svg/png via
|
|
||||||
l'éditeur de template → /campaigns/assets/upload, puis remplacer
|
|
||||||
la `src` ci-dessous par l'URL retournée. En attendant on utilise
|
|
||||||
le logo green qui se voit OK sur fond sombre (suffisant pour
|
|
||||||
test) mais pas pixel-perfect avec le guide. -->
|
|
||||||
<table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0"
|
|
||||||
style="max-width:600px; margin-top:12px; background:#1C1E26; border-radius:12px; overflow:hidden;">
|
|
||||||
<tr>
|
|
||||||
<td style="padding:26px 36px 22px; text-align:center;">
|
|
||||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/eed4d18c-8065-4c5f-b47c-58af63171cd0/content"
|
|
||||||
alt="TARGO" width="120"
|
|
||||||
style="display:inline-block; border:0; outline:none; text-decoration:none; max-width:120px; height:auto;">
|
|
||||||
<div style="font-size:0.7rem; color:rgba(255,255,255,0.45); margin-top:18px; line-height:1.55;">
|
|
||||||
<a href="https://www.targo.ca" style="color:rgba(255,255,255,0.7); text-decoration:none;">www.targo.ca</a>
|
|
||||||
· 1867 ch. de la rivière, Ste-Clotilde, QC<br>
|
|
||||||
© {{year}} TARGO Communications · Tous droits réservés.
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Loading…
Reference in New Issue
Block a user