From 2a9f2018267d394e90b644dea0ee68bdf9fc558b Mon Sep 17 00:00:00 2001 From: louispaulb Date: Thu, 9 Jul 2026 13:26:45 -0400 Subject: [PATCH] Events: personal rsvp_url per-language, address field, logo spacing - rsvpLink() now appends &lang= when generating a recipient's personal link, so the English invitation's link opens the English RSVP form instead of defaulting to French. Threaded through sendTest and the mass-send worker. - New optional per-language "address" field (editor input, SEED default for fete-20-ans, Unlayer template var), rendered under the date with a pin icon in both the festive email and the RSVP page. - Logo: +20px bottom margin in both the email template and the RSVP page. Co-Authored-By: Claude Opus 4.8 --- apps/ops/src/pages/EventsPage.vue | 9 ++++++--- services/targo-hub/lib/events.js | 25 ++++++++++++++++++------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/apps/ops/src/pages/EventsPage.vue b/apps/ops/src/pages/EventsPage.vue index 73c8807..ec1c709 100644 --- a/apps/ops/src/pages/EventsPage.vue +++ b/apps/ops/src/pages/EventsPage.vue @@ -156,6 +156,9 @@ + + +
Programme
@@ -542,7 +545,7 @@ const uploadingAtt = ref(false) function attLangLabel (l) { return l === 'fr' ? 'FR' : l === 'en' ? 'EN' : 'Les deux' } function attLangColor (l) { return l === 'fr' ? 'blue-7' : l === 'en' ? 'deep-orange-7' : 'grey-6' } -const emptyLang = () => ({ name: '', tagline: '', when: '', body: '', program: [], program_line: '', limited: '', closing: '', notes: [] }) +const emptyLang = () => ({ name: '', tagline: '', when: '', address: '', body: '', program: [], program_line: '', limited: '', closing: '', notes: [] }) const emptyForm = () => ({ id: '', active: true, date_iso: '', badge_top: '', badge_bottom: '', capacity: 0, email_template: '', fr: emptyLang(), en: emptyLang() }) const form = ref(emptyForm()) @@ -581,7 +584,7 @@ async function openEdit () { for (const lg of ['fr', 'en']) { const src = c[lg] || {} f[lg] = { - name: src.name || '', tagline: src.tagline || '', when: src.when || '', body: src.body || '', + name: src.name || '', tagline: src.tagline || '', when: src.when || '', address: src.address || '', body: src.body || '', program: Array.isArray(src.program) ? src.program.map(p => ({ icon: p.icon || '', label: p.label || '', sub: p.sub || '' })) : [], program_line: src.program_line || '', limited: src.limited || '', closing: src.closing || '', notes: Array.isArray(src.notes) ? [...src.notes] : [], @@ -605,7 +608,7 @@ async function openInvitePreview () { invitePreviewOpen.value = true; await load function cleanLang (c) { return { - name: (c.name || '').trim(), tagline: (c.tagline || '').trim(), when: (c.when || '').trim(), body: (c.body || '').trim(), + name: (c.name || '').trim(), tagline: (c.tagline || '').trim(), when: (c.when || '').trim(), address: (c.address || '').trim(), body: (c.body || '').trim(), program: (c.program || []).filter(p => (p.label || '').trim() || (p.icon || '').trim()), program_line: (c.program_line || '').trim(), limited: (c.limited || '').trim(), closing: (c.closing || '').trim(), notes: (c.notes || []).map(n => (n || '').trim()).filter(Boolean), diff --git a/services/targo-hub/lib/events.js b/services/targo-hub/lib/events.js index 3cd7c70..834a91b 100644 --- a/services/targo-hub/lib/events.js +++ b/services/targo-hub/lib/events.js @@ -119,6 +119,7 @@ const SEED_CONTENT = { name: 'Targo fête ses 20 ans', tagline: "Toute l'équipe de Targo est heureuse de vous inviter à notre grande fête d'anniversaire !", when: '1ᵉʳ août, de 10 h à 15 h', + address: 'À nos bureaux : 1867 chemin de la rivière, Ste-Clotilde', body: "Joignez-vous à nous pour une journée de plaisir, de rencontres et de célébration en compagnie des employés de Targo et de leurs familles.", program: [ { icon: '🚚', label: 'Food truck', sub: 'repas inclus' }, @@ -139,6 +140,7 @@ const SEED_CONTENT = { name: 'Targo turns 20!', tagline: 'The whole Targo team is happy to invite you to our big anniversary celebration!', when: 'August 1st, 10 a.m. to 3 p.m.', + address: 'At our offices: 1867 chemin de la rivière, Ste-Clotilde', body: 'Join us for a day of fun, meeting people and celebrating alongside the Targo team and their families.', program: [ { icon: '🚚', label: 'Food truck', sub: 'meal included' }, @@ -224,6 +226,7 @@ function sanitizeLangContent (c) { name: String(c.name || '').slice(0, 160), tagline: String(c.tagline || '').slice(0, 300), when: String(c.when || '').slice(0, 120), + address: String(c.address || '').slice(0, 200), body: String(c.body || '').slice(0, 1200), program: sanitizeProgram(c.program), program_line: String(c.program_line || '').slice(0, 200), @@ -299,9 +302,14 @@ function keyFor (resolved, form) { } // ── Lien perso + courriel d'invitation ───────────────────────────────────── -function rsvpLink (eventId, customerId, name, email, ttlHours = 60 * 24) { +// `lang` (optionnel) → ajouté en ?lang= EXPLICITE sur l'URL : la page RSVP priorise TOUJOURS ce +// paramètre de requête sur la langue du jeton (cf handle() : `if (!url.searchParams.get('lang') && p.lang)`). +// Sans ça, la version EN du courriel pointait vers la même URL que la FR (défaut FR). Nécessaire +// même si le modèle éditable (Unlayer) ne permet pas de fixer un href différent par langue. +function rsvpLink (eventId, customerId, name, email, ttlHours = 60 * 24, lang = '') { const tok = generateCustomerToken(customerId, name, email, ttlHours) - return `${pub()}/rsvp/${encodeURIComponent(eventId)}?t=${encodeURIComponent(tok)}` + const l = lang ? `&lang=${encodeURIComponent(normLang(lang))}` : '' + return `${pub()}/rsvp/${encodeURIComponent(eventId)}?t=${encodeURIComponent(tok)}${l}` } function inviteSubject (eventId, lang) { const e = getEvent(eventId); if (!e) return '' @@ -312,7 +320,7 @@ function inviteTemplateVars (e, lang, name, rsvpUrl) { const t = e[lang] return { firstname: firstName(name), name: name || '', rsvp_url: rsvpUrl, gift_url: rsvpUrl, // gift_url = alias (templates cadeau réutilisables) - event_name: t.name, tagline: t.tagline, when: t.when, body: t.body, closing: t.closing, + event_name: t.name, tagline: t.tagline, when: t.when, address: t.address, body: t.body, closing: t.closing, year: String(new Date().getFullYear()), lang, } } @@ -345,12 +353,13 @@ function inviteEmailFestive (e, lang, name, rsvpUrl) { + `` + `
🎈🎉🎈
` + (badge ? `
🎉 ${esc(badge)} 🎉

` : '
') - + `TARGO` + + `TARGO` + `

${esc(t.name)} 🎉

` + `

${esc(t.tagline)}

` + `

${esc(t.greet(firstName(name)) || '')}

` + `

${esc(t.body)}

` + (t.when ? `
📅 ${esc(t.when)}
` : '') + + (t.address ? `

📍 ${esc(t.address)}

` : '') + ((t.program || []).length ? `${prog}
` : '') + (t.program_line ? `

${esc(t.program_line)}

` : '') + (t.limited ? `
⏳ ${esc(t.limited)}
` : '') @@ -440,7 +449,7 @@ async function sendTest ({ eventId, emails, channel = 'mailjet', from = '', lang const atts = attachmentPayload(channel, loadSendAttachments(event, lang)) // pièces jointes de la langue testée (+ 'both') const results = [] for (const em of emails) { - const link = rsvpLink(eventId, 'TEST-' + em, name || 'Test', em, 24) + const link = rsvpLink(eventId, 'TEST-' + em, name || 'Test', em, 24, lang) const html = inviteEmail(eventId, lang, name, link) const subject = '[TEST] ' + inviteSubject(eventId, lang) try { const r = await sendInviteMessage({ channel, to: em, subject, html, from, attachments: atts }); results.push({ email: em, ok: r.ok, error: r.error }) } @@ -490,7 +499,7 @@ async function sendEventCampaignAsync (id, eventId, channel, from) { if (r.status !== 'pending') continue const lang = normLang(r.language) const name = ((r.firstname || '') + ' ' + (r.lastname || '')).trim() - const rsvpUrl = rsvpLink(eventId, r.customer_id || ('x-' + r.email), name, r.email) + const rsvpUrl = rsvpLink(eventId, r.customer_id || ('x-' + r.email), name, r.email, 60 * 24, lang) r.rsvp_url = rsvpUrl const html = inviteEmail(eventId, lang, name, rsvpUrl) const subject = inviteSubject(eventId, lang) @@ -749,7 +758,7 @@ body{margin:0;font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-s background:var(--g);color:#fff;font-weight:800;line-height:1;box-shadow:0 8px 20px rgba(33,163,74,.3);margin-bottom:12px} .badge b{font-size:1.35rem}.badge span{font-size:.6rem;letter-spacing:1px} .badge.emoji{font-size:2rem} -.logo{height:34px;width:auto;display:block;margin:0 auto 14px} +.logo{height:34px;width:auto;display:block;margin:0 auto 34px} h1{font-size:1.7rem;font-weight:800;letter-spacing:-.5px;margin:0 0 6px;color:var(--ink)} .tag{color:var(--g);font-weight:700;font-size:1.02rem;margin:0 0 4px} .card{background:#fff;border:1px solid #eef2f0;border-radius:20px;box-shadow:0 16px 44px rgba(23,58,94,.09);padding:26px 22px;margin-bottom:16px} @@ -757,6 +766,7 @@ p{line-height:1.6;font-size:1rem;margin:0 0 12px;color:#334155} .when{display:flex;align-items:center;justify-content:center;gap:10px;background:var(--ink);color:#fff;font-weight:700; font-size:1.12rem;border-radius:14px;padding:13px 18px;margin:4px 0 16px} .when .cal{font-size:1.3rem} +.addr{display:flex;align-items:center;justify-content:center;gap:6px;color:#374151;font-weight:600;font-size:.92rem;margin:-8px 0 16px;text-align:center} .prog-wrap{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;margin:6px 0 4px} .prog{flex:1 1 90px;max-width:110px;text-align:center;background:#f8fafc;border:1px solid #eef2f6;border-radius:14px;padding:12px 6px} .prog-ic{font-size:1.7rem;line-height:1} @@ -799,6 +809,7 @@ button:hover{filter:brightness(1.05)}button:active{transform:translateY(1px)}but
${t.when ? `
📅${esc(t.when)}
` : ''} + ${t.address ? `
📍 ${esc(t.address)}
` : ''}

${esc(t.body)}

${program ? `
${program}
` : ''} ${t.program_line ? `

${esc(t.program_line)}

` : ''}