// Helpers shared by the staff panel (admin.js) and the public site (public.js). const $ = (sel, root = document) => root.querySelector(sel); const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); const MIN = 60 * 1000; const ICON = { search: '', plus: '', }; // api calls the panel's JSON API and throws an Error carrying the server's message on failure. async function api(path, { method = "GET", body } = {}) { const opts = { method, headers: { "X-Requested-With": "simpleadmin-web" }, credentials: "same-origin" }; if (body !== undefined) { opts.headers["Content-Type"] = "application/json"; opts.body = JSON.stringify(body); } let res; try { res = await fetch(path, opts); } catch { throw new Error("Couldn't reach the panel. Check your connection."); } let data = null; try { data = await res.json(); } catch { /* empty or non-JSON body */ } if (!res.ok) throw new Error(data?.error || `The panel answered ${res.status}.`); return data; } function qs(params) { const u = new URLSearchParams(); for (const [k, v] of Object.entries(params)) if (v !== undefined && v !== null && v !== "") u.set(k, v); const s = u.toString(); return s ? `?${s}` : ""; } function debounce(fn, ms) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; } function fmtDuration(min) { if (min === 0) return "Permanent"; if (min < 60) return `${min} min`; if (min < 1440) return `${Math.round(min / 60)} h`; const d = Math.round(min / 1440); return d === 1 ? "1 day" : `${d} days`; } function fmtAgo(iso) { const m = Math.round((Date.now() - new Date(iso).getTime()) / MIN); if (m < 1) return "now"; if (m < 60) return `${m} min ago`; if (m < 1440) return `${Math.round(m / 60)} h ago`; const d = Math.round(m / 1440); return d === 1 ? "yesterday" : `${d} days ago`; } function fmtLeft(min) { if (min < 60) return `${Math.max(1, Math.round(min))} min left`; if (min < 1440) return `${Math.round(min / 60)} h left`; return `${Math.round(min / 1440)} d left`; } function fmtDate(iso) { return new Date(iso).toLocaleString(undefined, { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }); } // Minutes served and left on a timed penalty. With SimpleAdmin's online-only time mode, "passed" // counts minutes served. function served(p) { if (p.passed != null && p.status === "active" && !p.ends) return { done: p.passed, left: p.duration - p.passed }; const start = new Date(p.created).getTime(); const end = p.ends ? new Date(p.ends).getTime() : start + p.duration * MIN; const done = (Math.min(Date.now(), end) - start) / MIN; return { done, left: (end - Date.now()) / MIN }; } function rankTag(rank) { if (!rank) return 'Player'; return `${esc(rank.label)}`; } function initial(name) { return esc(String(name || "").replace(/[^a-z0-9]/gi, "").charAt(0).toUpperCase() || "?"); } // Time served against the full term: the bar under each length. function termCell(p) { const cls = ["term", p.kind === "comm" ? "comm" : ""]; let right; let pct = 100; if (p.duration === 0) { cls.push("perm"); right = p.status === "lifted" ? "Lifted" : "Never expires"; if (p.status === "lifted") cls.push("lifted"); } else { const s = served(p); pct = Math.max(0, Math.min(100, (s.done / p.duration) * 100)); if (p.status === "active") right = fmtLeft(s.left); else if (p.status === "expired") { right = "Served"; cls.push("done"); } else { right = "Lifted"; cls.push("lifted"); } } const bar = p.duration === 0 ? "" : ``; return `