// shared helpers + global chrome (stats bar) used by every admin page. // each page-specific script depends on this loading first. const PAGE = 50; const api = (path, opts) => fetch(path, opts).then(r => { if (!r.ok) throw new Error(r.status); return r.json(); }); function toast(msg, err) { const el = document.getElementById("toast"); if (!el) return; document.getElementById("toast-msg").textContent = msg; el.className = "show" + (err ? " error" : ""); clearTimeout(toast._t); toast._t = setTimeout(() => el.className = "", 2500); } function badgeHtml(status) { const s = status || "null"; const cls = s === "ok" ? "ok" : s === "error" ? "err" : s === "pending" ? "pending" : "null"; return `${s}`; } function escapeHtml(s) { return String(s ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } // ── url query-param helpers ──────────────────────────────────────────────── // // filters and sort state live in the url so reloads and shared links keep // their shape. queryGet reads a single param, queryAll returns them all, // queryWrite replaces the query string with the cleaned-up params (empty // values removed). we use replaceState so each filter change doesnt spam // history. function queryGet(key, fallback = "") { const v = new URLSearchParams(location.search).get(key); return v == null ? fallback : v; } function queryAll() { return Object.fromEntries(new URLSearchParams(location.search)); } function queryWrite(params) { const clean = {}; for (const [k, v] of Object.entries(params)) { if (v === "" || v == null) continue; clean[k] = v; } const qs = new URLSearchParams(clean).toString(); const next = location.pathname + (qs ? "?" + qs : ""); history.replaceState(null, "", next); } // apply current query params onto form inputs — call on page init function queryApplyToInputs(bindings) { for (const [id, key] of Object.entries(bindings)) { const el = document.getElementById(id); if (!el) continue; const v = queryGet(key, ""); if (v !== "") el.value = v; } } // global stats bar — small counters shown on articles/events/stats pages async function loadGlobalStats() { const bar = document.getElementById("statsBar"); if (!bar) return; try { const data = await api("/admin/api/stats"); const t = document.getElementById("s-total"); if (t) t.textContent = data.total.toLocaleString(); const c = document.getElementById("s-content"); if (c) c.textContent = data.withContent.toLocaleString(); const em = document.getElementById("s-embed"); if (em) em.textContent = data.withEmbedding.toLocaleString(); const ev = document.getElementById("s-events"); if (ev) ev.textContent = data.eventCount.toLocaleString(); } catch (_) { /* ignore — stats bar is best-effort */ } } // common overlay close-on-backdrop wiring function wireOverlays() { document.querySelectorAll(".overlay").forEach(ov => { ov.addEventListener("click", e => { if (e.target === ov) ov.classList.remove("open"); }); }); } document.addEventListener("DOMContentLoaded", () => { wireOverlays(); loadGlobalStats(); });