Fix email report section links
This commit is contained in:
128
cloudflare-buildEmailTemplate-replacement.js
Normal file
128
cloudflare-buildEmailTemplate-replacement.js
Normal file
@@ -0,0 +1,128 @@
|
||||
function buildEmailTemplate(bodyText, data, isDE) {
|
||||
const esc = (v) => String(v ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.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) =>
|
||||
`<span style="display:inline-block;background:#F2F2F0;border:1px solid #E4E4E0;padding:5px 9px;font-size:11px;line-height:1.3;color:#5A5A54;margin:3px 4px 3px 0">${esc(scenarioLabels[s] || s)}</span>`
|
||||
).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, `<a href="$1" style="color:#4A8A68;font-weight:700;text-decoration:underline">${isDE ? "Ansehen" : "View"}</a>`);
|
||||
const bolded = linked.replace(/\*\*(.+?)\*\*/g, '<strong style="color:#1A1A18">$1</strong>');
|
||||
return `<li style="margin:0 0 10px;line-height:1.7">${bolded}</li>`;
|
||||
}).join("");
|
||||
return `<ul style="margin:12px 0 18px;padding-left:20px;color:#5A5A54">${items}</ul>`;
|
||||
}
|
||||
|
||||
const linked = p.replace(/(https?:\/\/[^\s<]+)/g, `<a href="$1" style="color:#4A8A68;font-weight:700;text-decoration:underline">${isDE ? "Ansehen" : "View"}</a>`);
|
||||
const bolded = linked.replace(/\*\*(.+?)\*\*/g, '<strong style="color:#1A1A18">$1</strong>');
|
||||
return `<p style="margin:0 0 17px;line-height:1.72">${bolded}</p>`;
|
||||
}).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 `<!doctype html>
|
||||
<html lang="${isDE ? "de" : "en"}">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<title>${isDE ? "Dein Plan-B Notfallplan" : "Your Plan-B Preparedness Plan"}</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#FAFAFA;font-family:Arial,Helvetica,sans-serif;color:#5A5A54;">
|
||||
<div style="max-width:680px;margin:0 auto;padding:30px 12px;">
|
||||
<div style="background:#FAFAFA;border:1px solid #E4E4E0;overflow:hidden;">
|
||||
<div style="padding:34px 32px 28px;border-bottom:1px solid #E4E4E0;">
|
||||
<div style="font-family:Georgia,'Times New Roman',serif;font-size:42px;line-height:1;letter-spacing:.04em;color:#1A1A18;">Plan<span style="color:#5A9A78">-B</span></div>
|
||||
<div style="margin-top:22px;font-size:11px;line-height:1.4;letter-spacing:3px;text-transform:uppercase;color:#5A9A78;font-weight:700;">${isDE ? "Persoenlicher Vorsorgeplan" : "Personal Preparedness Plan"}</div>
|
||||
<h1 style="margin:14px 0 0;font-size:28px;line-height:1.14;letter-spacing:2px;text-transform:uppercase;color:#1A1A18;font-weight:800;">${esc(getRiskLabel(risk, isDE))} ${isDE ? "Bericht" : "Readiness Report"}</h1>
|
||||
</div>
|
||||
|
||||
<div style="padding:30px 32px;">
|
||||
<p style="margin:0 0 22px;font-size:16px;line-height:1.7;color:#3A3A34;">${greeting},</p>
|
||||
|
||||
<div style="background:#E8F2EC;border-left:4px solid #5A9A78;padding:18px 18px 18px 20px;margin-bottom:18px;">
|
||||
<div style="font-size:10px;line-height:1.4;letter-spacing:2px;text-transform:uppercase;color:#8A8A84;font-weight:700;">${isDE ? "Risikoprofil" : "Risk Profile"}</div>
|
||||
<div style="margin-top:7px;font-size:20px;line-height:1.2;color:#1A1A18;font-weight:800;">${esc(getRiskLabel(risk, isDE))}</div>
|
||||
${scenarioBadges ? `<div style="margin-top:12px">${scenarioBadges}</div>` : ""}
|
||||
</div>
|
||||
|
||||
<div style="font-size:15px;line-height:1.72;color:#5A5A54;">${body}</div>
|
||||
|
||||
<div style="margin-top:32px;">
|
||||
<a href="${reportUrl}" style="display:inline-block;background:#1A1A18;color:#FAFAFA;text-decoration:none;padding:16px 24px;border-radius:0;font-size:12px;font-weight:700;letter-spacing:2.4px;text-transform:uppercase">${isDE ? "Meinen Bericht oeffnen" : "Open My Report"}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin:0 32px 28px;background:#FAFAFA;border:1px solid #E4E4E0;padding:18px;">
|
||||
<div style="font-size:10px;color:#8A8A84;letter-spacing:2px;text-transform:uppercase;margin-bottom:12px;font-weight:700;">${isDE ? "Direkt zu deinem Bericht" : "Jump back to your report"}</div>
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<tr>
|
||||
<td style="width:50%;padding:0 6px 8px 0;">
|
||||
<a href="${analysisUrl}" style="display:block;background:#F2F2F0;border:1px solid #E4E4E0;color:#1A1A18;text-decoration:none;padding:13px 14px;font-size:12px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;">${isDE ? "Analyse" : "Analysis"}</a>
|
||||
</td>
|
||||
<td style="width:50%;padding:0 0 8px 6px;">
|
||||
<a href="${recommendationsUrl}" style="display:block;background:#F2F2F0;border:1px solid #E4E4E0;color:#1A1A18;text-decoration:none;padding:13px 14px;font-size:12px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;">${isDE ? "Empfehlungen" : "Recommendations"}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:50%;padding:0 6px 0 0;">
|
||||
<a href="${budgetUrl}" style="display:block;background:#F2F2F0;border:1px solid #E4E4E0;color:#1A1A18;text-decoration:none;padding:13px 14px;font-size:12px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;">${isDE ? "Budget" : "Budget"}</a>
|
||||
</td>
|
||||
<td style="width:50%;padding:0 0 0 6px;">
|
||||
<a href="${timelineUrl}" style="display:block;background:#F2F2F0;border:1px solid #E4E4E0;color:#1A1A18;text-decoration:none;padding:13px 14px;font-size:12px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;">${isDE ? "Zeitplan" : "Timeline"}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
${data.protein_security === "uncertain" ? `<div style="margin-top:10px;"><a href="${proteinUrl}" style="display:block;background:#E8F2EC;border:1px solid #C7DDCE;color:#1A1A18;text-decoration:none;padding:13px 14px;font-size:12px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;">${isDE ? "Proteinquelle sichern" : "Secure protein source"}</a></div>` : ""}
|
||||
</div>
|
||||
|
||||
<div style="margin:0 32px 28px;background:#F2F2F0;border:1px solid #E4E4E0;padding:18px;">
|
||||
<div style="font-size:10px;color:#8A8A84;letter-spacing:2px;text-transform:uppercase;margin-bottom:10px;font-weight:700;">${isDE ? "Dein Profil" : "Your Profile"}</div>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:13px;line-height:1.5">
|
||||
${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(", ")))}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="padding:20px 32px 26px;background:#F2F2F0;border-top:1px solid #E4E4E0;">
|
||||
<p style="margin:0;color:#8A8A84;font-size:12px;line-height:1.7;">${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.'} <a href="mailto:hello@plan-b.now?subject=unsubscribe" style="color:#4A8A68;">${isDE ? "Abmelden" : "Unsubscribe"}</a>. ${isDE ? "Deine Daten werden nie verkauft." : "Your data is never sold."}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
56
src/App.vue
56
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()
|
||||
|
||||
Reference in New Issue
Block a user