From 7a94fe442d631c9a9afcbc060808d12e78c4f12d Mon Sep 17 00:00:00 2001 From: Dorian Date: Sun, 7 Jun 2026 06:45:29 +0100 Subject: [PATCH] Fix email report section links --- cloudflare-buildEmailTemplate-replacement.js | 128 +++++++++++++++++++ src/App.vue | 56 ++++++-- 2 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 cloudflare-buildEmailTemplate-replacement.js diff --git a/cloudflare-buildEmailTemplate-replacement.js b/cloudflare-buildEmailTemplate-replacement.js new file mode 100644 index 0000000..c1ae30f --- /dev/null +++ b/cloudflare-buildEmailTemplate-replacement.js @@ -0,0 +1,128 @@ +function buildEmailTemplate(bodyText, data, isDE) { + const esc = (v) => String(v ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + + const fullName = [data.first_name, data.last_name].filter(Boolean).join(" ") || ""; + const greeting = fullName ? isDE ? `Liebe/Lieber ${esc(fullName)}` : `Hello ${esc(fullName)}` : isDE ? "Hallo" : "Hello"; + const risk = data.risk_level || "ASSESSED"; + const scenarios = (data.scenarios || "").split(",").map((s) => s.trim()).filter(Boolean); + const scenarioLabels = { + total_collapse: isDE ? "Totaler Kollaps" : "Total Collapse", + partial_collapse: isDE ? "Partieller Kollaps" : "Partial Collapse", + hyperinflation: isDE ? "Hyperinflation" : "Hyperinflation", + supply_shock: isDE ? "Versorgungsengpass" : "Supply Side Shock", + food_crisis: isDE ? "Lebensmittelkrise" : "Food Crisis", + bank_crisis: isDE ? "Banken- / Finanzkrise" : "Bank / Financial Crisis", + power_outage: isDE ? "Laengerer Stromausfall" : "Extended Power Outage", + water_failure: isDE ? "Wasserversorgungsausfall" : "Water Supply Failure", + war: isDE ? "Krieg / Bewaffneter Konflikt" : "War / Armed Conflict" + }; + + const scenarioBadges = scenarios.map((s) => + `${esc(scenarioLabels[s] || s)}` + ).join(""); + + const formatText = (text) => { + return esc(text).split(/\n\n+/).filter((p) => p.trim()).map((p) => { + if (p.startsWith("* ") || p.startsWith("- ") || p.startsWith("•") || p.includes("\n•")) { + const items = p.split("\n").filter((l) => l.trim()).map((l) => { + const clean = l.trim().replace(/^(\*|-|•)\s*/, ""); + const linked = clean.replace(/(https?:\/\/[^\s<]+)/g, `${isDE ? "Ansehen" : "View"}`); + const bolded = linked.replace(/\*\*(.+?)\*\*/g, '$1'); + return `
  • ${bolded}
  • `; + }).join(""); + return ``; + } + + const linked = p.replace(/(https?:\/\/[^\s<]+)/g, `${isDE ? "Ansehen" : "View"}`); + const bolded = linked.replace(/\*\*(.+?)\*\*/g, '$1'); + return `

    ${bolded}

    `; + }).join(""); + }; + + const body = formatText(bodyText); + const reportUrl = "https://plan-b.now/#results-section"; + const analysisUrl = "https://plan-b.now/#narrative-section"; + const recommendationsUrl = "https://plan-b.now/#recs-anchor"; + const budgetUrl = "https://plan-b.now/#panel-budget"; + const timelineUrl = "https://plan-b.now/#panel-timeline"; + const proteinUrl = "https://plan-b.now/#protein-offer-section"; + + return ` + + + + + ${isDE ? "Dein Plan-B Notfallplan" : "Your Plan-B Preparedness Plan"} + + +
    +
    +
    +
    Plan-B
    +
    ${isDE ? "Persoenlicher Vorsorgeplan" : "Personal Preparedness Plan"}
    +

    ${esc(getRiskLabel(risk, isDE))} ${isDE ? "Bericht" : "Readiness Report"}

    +
    + +
    +

    ${greeting},

    + +
    +
    ${isDE ? "Risikoprofil" : "Risk Profile"}
    +
    ${esc(getRiskLabel(risk, isDE))}
    + ${scenarioBadges ? `
    ${scenarioBadges}
    ` : ""} +
    + +
    ${body}
    + +
    + ${isDE ? "Meinen Bericht oeffnen" : "Open My Report"} +
    +
    + +
    +
    ${isDE ? "Direkt zu deinem Bericht" : "Jump back to your report"}
    + + + + + + + + + +
    + ${isDE ? "Analyse" : "Analysis"} + + ${isDE ? "Empfehlungen" : "Recommendations"} +
    + ${isDE ? "Budget" : "Budget"} + + ${isDE ? "Zeitplan" : "Timeline"} +
    + ${data.protein_security === "uncertain" ? `
    ${isDE ? "Proteinquelle sichern" : "Secure protein source"}
    ` : ""} +
    + +
    +
    ${isDE ? "Dein Profil" : "Your Profile"}
    + + ${erow(isDE ? "Name" : "Name", esc(fullName))} + ${erow(isDE ? "Wohnort" : "Location", esc([data.city, data.country].filter(Boolean).join(", ")))} + ${erow(isDE ? "Haushalt" : "Household", esc(householdLabel(data.household_size, isDE)))} + ${erow(isDE ? "Budget" : "Budget", data.budget_eur ? `EUR ${esc(data.budget_eur)}` : "")} + ${erow(isDE ? "Szenarien" : "Scenarios", esc(scenarios.map((s) => scenarioLabels[s] || s).join(", ")))} +
    +
    + +
    +

    ${isDE ? 'Du erhaeltst diese E-Mail, weil du das Plan-B Assessment abgeschlossen hast.' : 'You are receiving this because you completed the Plan-B assessment.'} ${isDE ? "Abmelden" : "Unsubscribe"}. ${isDE ? "Deine Daten werden nie verkauft." : "Your data is never sold."}

    +
    +
    +
    + +`; +} diff --git a/src/App.vue b/src/App.vue index bf7cc85..a41a967 100644 --- a/src/App.vue +++ b/src/App.vue @@ -880,7 +880,7 @@ const T = { } } -const WORKER_URL = 'https://planb-email.janwellmann.workers.dev/submit' +const WORKER_URL = import.meta.env.VITE_WORKER_URL || 'https://planb-email.janwellmann.workers.dev/submit' let currentLang = 'en' function setLang(lang) { @@ -1083,11 +1083,9 @@ let currentScenario = 1 let riskScore = 0 let riskLevelStr = '' -// Session persistence — keep the user's progress across reloads inside -// the same browser tab. `sessionStorage` is the right scope: progress -// dies when the tab closes (privacy-friendly default for a lead form), -// but reload / nav-away-and-back restores. Bump the key suffix when the -// QUESTIONS shape changes incompatibly. +// Report persistence — email links usually open a new browser tab, so +// results need to survive beyond tab-scoped sessionStorage. Bump the key +// suffix when the QUESTIONS shape changes incompatibly. const STATE_KEY = 'plan-b.state.v1' function getStage() { const results = document.getElementById('results-section') @@ -1100,18 +1098,21 @@ function saveState() { const stage = getStage() if (stage === 'home') { clearState(); return } try { - sessionStorage.setItem(STATE_KEY, JSON.stringify({ + const payload = JSON.stringify({ stage, currentQ, answers, currentScenario, currentLang, ts: Date.now(), - })) + }) + localStorage.setItem(STATE_KEY, payload) + sessionStorage.setItem(STATE_KEY, payload) } catch {} } function clearState() { try { sessionStorage.removeItem(STATE_KEY) } catch {} + try { localStorage.removeItem(STATE_KEY) } catch {} } function loadState() { try { - const raw = sessionStorage.getItem(STATE_KEY) + const raw = localStorage.getItem(STATE_KEY) || sessionStorage.getItem(STATE_KEY) if (!raw) return null const s = JSON.parse(raw) // Discard sessions older than 4 hours so a stale tab doesn't drop @@ -1874,6 +1875,39 @@ function revealResultsSequence() { }) } +function focusReportHash() { + const hash = decodeURIComponent(window.location.hash || '').replace('#', '') + if (!hash) return + + const panelByHash = { + 'results-section': null, + 'narrative-section': null, + 'recs-anchor': 'panel-recs', + 'panel-recs': 'panel-recs', + 'panel-budget': 'panel-budget', + 'panel-timeline': 'panel-timeline', + 'protein-offer-section': null, + } + if (!Object.prototype.hasOwnProperty.call(panelByHash, hash)) return + + const saved = loadState() + if ((!saved || saved.stage !== 'results') && getStage() !== 'results') return + + const panelId = panelByHash[hash] + if (panelId) { + const panel = document.getElementById(panelId) + if (panel) { + panel.open = true + panel.classList.add('revealed') + } + } + + const target = document.getElementById(hash) || (panelId ? document.getElementById(panelId) : null) + if (target) { + setTimeout(() => target.scrollIntoView({ behavior: 'smooth', block: 'start' }), 120) + } +} + // Three-stage intro overlay played on the user's first home-page visit // in this tab. Skipped on subsequent loads (sessionStorage flag), and // also skipped when restoring a saved quiz/results stage so a refresh @@ -2280,9 +2314,13 @@ onMounted(() => { const strip = document.querySelector('.scenario-strip') if(strip) strip.classList.add('hidden') showResults() + focusReportHash() } } + window.addEventListener('hashchange', focusReportHash) + focusReportHash() + // Wire up scenario strip + paint picker after DOM is mounted initScenarioStrip() initPaintPicker()