// Digest signup + product-event beacons. Loaded on every page (base.html.j2). // // Both halves degrade: the form is a real
that works with // this file blocked, and the beacons are fire-and-forget with no UI depending // on them. const BASE = new URL("./", import.meta.url).pathname.replace(/\/$/, ""); /** Report one product event. Aggregate-only: the server takes the referrer from * the request itself, so nothing identifying is sent from here. */ export function track(event) { try { const url = `${BASE}/api/v2/event`; const body = JSON.stringify({ event }); // sendBeacon survives the page unloading, which a fetch() started on a link // click usually does not — that's the whole point for nav/records clicks. if (navigator.sendBeacon) { navigator.sendBeacon(url, new Blob([body], { type: "application/json" })); } else { fetch(url, { method: "POST", body, headers: { "Content-Type": "application/json" }, keepalive: true }).catch(() => {}); } } catch { /* instrumentation must never break the page */ } } // Anything carrying data-event reports itself when activated. document.addEventListener("click", (e) => { const el = e.target.closest?.("[data-event]"); if (el) track(el.dataset.event); }); // --- digest form ------------------------------------------------------------- for (const form of document.querySelectorAll("form[data-digest]")) { const status = form.querySelector(".digest-status"); const button = form.querySelector("button[type=submit]"); form.addEventListener("submit", async (e) => { e.preventDefault(); const email = form.querySelector("input[name=email]")?.value?.trim(); if (!email) return; button.disabled = true; try { const res = await fetch(form.action, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, place: form.dataset.place || null }), }); const data = await res.json().catch(() => ({})); if (status) { status.textContent = data.message || "Something went wrong — try again."; status.hidden = false; } if (res.ok) { // Collapse the inputs on success so the confirmation is the only thing // left to read; the form is done either way. form.querySelector(".digest-row")?.setAttribute("hidden", ""); form.querySelector(".digest-note")?.setAttribute("hidden", ""); track("home.digest_signup"); } } catch { if (status) { status.textContent = "Couldn't reach the server — try again."; status.hidden = false; } } finally { button.disabled = false; } }); }