71 lines
2.7 KiB
JavaScript
71 lines
2.7 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.
|
||
|
|
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|