feat(email-editor): Phase 1 — scaffold easy-email microservice for visual template editing
GrapesJS-mjml proved broken on our content (plugin v1.0.8 incompatible
with MJML v5 — canvas stays empty on load). Pivot to easy-email, a
mature OSS WYSIWYG email editor (MJML-based, MIT license, 4k stars).
Architecture: standalone React+Vite microservice deployed at
editor.gigafibre.ca, iframed from the ops UI's
/campaigns/templates/:name page. Talks to the hub's existing REST
endpoints (/campaigns/templates/*) for load + save. The hub stays the
source of truth — easy-email is purely the editing UI.
Scaffold delivered in this commit (Phase 1):
- services/email-editor/ — new top-level service directory
- package.json: React 18 + easy-email-{core,editor,extensions} 4.16.x
+ Vite 5 + TypeScript 5
- vite.config.ts: standard dev/build config, port 5173 in dev
- tsconfig.json: strict-false to keep iteration fast
- index.html: loads easy-email CSS bundles from unpkg (extensions, editor,
arco theme)
- src/main.tsx: React entry, mounts EmailEditorApp on #root
- src/EmailEditorApp.tsx:
• Reads template name from ?name=... URL param (defaults gift-email-fr)
• GET ${VITE_HUB_URL}/campaigns/templates/:name on mount
• Renders <EmailEditorProvider> + <StandardLayout> with our merge tags
map (firstname, amount, gift_url, description, expiry, etc.) so the
Variables panel shows our Mustache placeholders
• On save: JsonToMjml() converts easy-email's JSON → MJML, PUT to hub
→ hub compiles to HTML and persists both files
• postMessage({type: 'email-editor:saved', ...}) to parent window so
the iframing ops UI knows to refresh
- Dockerfile: multi-stage (Vite build → nginx alpine serve). SPA fallback
in nginx config so all routes return index.html.
- docker-compose.yml: container behind Traefik at editor.gigafibre.ca
with Let's Encrypt TLS via the shared proxy network.
- README.md documents the arch, URL params, postMessage protocol, dev
workflow, and the Phase 1 limitation (no MJML→JSON importer — editor
starts from empty page until Phase 3).
- .gitignore: standard node/vite/dist exclusions.
Build verified locally: 83 modules transformed, ~2.8 MB bundle (840 KB
gzipped) — large but acceptable since easy-email packages the full
email builder + drag-drop canvas.
Phase 2 (next): Docker deploy on prod + replace GrapesJS in the ops UI
TemplateEditorPage with an iframe pointing here.
Phase 3 (later): MJML → easy-email JSON parser so existing templates
auto-import into the canvas instead of starting blank.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
2fe8d3f50e
commit
0b6377fa58
5
services/email-editor/.gitignore
vendored
Normal file
5
services/email-editor/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.vite/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
52
services/email-editor/Dockerfile
Normal file
52
services/email-editor/Dockerfile
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# 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
|
||||||
63
services/email-editor/README.md
Normal file
63
services/email-editor/README.md
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# 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).
|
||||||
28
services/email-editor/docker-compose.yml
Normal file
28
services/email-editor/docker-compose.yml
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
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
|
||||||
16
services/email-editor/index.html
Normal file
16
services/email-editor/index.html
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!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
Normal file
3219
services/email-editor/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
services/email-editor/package.json
Normal file
25
services/email-editor/package.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
225
services/email-editor/src/EmailEditorApp.tsx
Normal file
225
services/email-editor/src/EmailEditorApp.tsx
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
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()
|
||||||
|
// The hub's getTemplate returns { name, format, mjml?, html }
|
||||||
|
// For MJML templates we'd ideally parse the MJML into easy-email's JSON
|
||||||
|
// tree — but easy-email doesn't ship an MJML importer. Workaround: we
|
||||||
|
// start the editor with an EMPTY page and let the user rebuild visually
|
||||||
|
// OR stash the existing MJML as the editor's content. For first iteration,
|
||||||
|
// empty + the user reconstructs is honest about the limitation.
|
||||||
|
// The compiled HTML preview remains available so they can see what
|
||||||
|
// they're rebuilding TOWARD.
|
||||||
|
// TODO Phase 3: integrate mjml-react-email or similar MJML→JSON parser
|
||||||
|
setInitialValues(emptyTemplate())
|
||||||
|
setError(`Note: existing template MJML (${data.format}, ${(data.mjml || '').length}b) ` +
|
||||||
|
`not auto-imported — easy-email starts from an empty page. ` +
|
||||||
|
`Use the hub's compiled HTML as a visual reference and rebuild here.`)
|
||||||
|
} 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,
|
||||||
|
})
|
||||||
|
const res = await fetch(`${HUB_URL}/campaigns/templates/${encodeURIComponent(templateName)}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ mjml: mjmlSource }),
|
||||||
|
})
|
||||||
|
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%']],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
14
services/email-editor/src/main.tsx
Normal file
14
services/email-editor/src/main.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
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>,
|
||||||
|
)
|
||||||
20
services/email-editor/tsconfig.json
Normal file
20
services/email-editor/tsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
19
services/email-editor/vite.config.ts
Normal file
19
services/email-editor/vite.config.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user