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 ? "Direkt zu deinem Bericht" : "Jump back to your report"}
+
+ ${data.protein_security === "uncertain" ? `
` : ""}
+
+
+
+
${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()