111 lines
3.3 KiB
JavaScript
111 lines
3.3 KiB
JavaScript
// 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 `<span class="badge ${cls}">${s}</span>`;
|
|
}
|
|
|
|
|
|
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();
|
|
});
|