thermograph/frontend/static/digest.js
Emi Griffith d6df04eab2 Subtree-merge thermograph-frontend (origin/main) into frontend/
git-subtree-dir: frontend
git-subtree-mainline: a4be7066e5
git-subtree-split: 3a98146da4
2026-07-22 22:01:11 -07:00

77 lines
3.1 KiB
JavaScript

// Digest signup + product-event beacons. Loaded on every page (base.html.j2).
//
// Both halves degrade: the form is a real <form method="post"> that works with
// this file blocked, and the beacons are fire-and-forget with no UI depending
// on them.
// Backend's own origin+base (+ pinned API version), shared with account.js
// rather than a second independent import.meta.url-derived copy (repo-split
// Stage 5 — this file, like account.js, is always served by backend, possibly
// from a page on a different origin once frontend/backend genuinely split).
import { uv } from "./account.js";
/** 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 = uv("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.
// Spec limitation: sendBeacon has no credentials/header control at all, so
// it can never carry the auth cookie cross-origin — fine here, event
// tracking is anonymous and needs no auth either way.
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;
}
});
}