77 lines
3.1 KiB
JavaScript
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;
|
|
}
|
|
});
|
|
}
|