simpleadmin-web/internal/web/assets/common.js

152 lines
6.1 KiB
JavaScript

// 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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
const MIN = 60 * 1000;
const ICON = {
search: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="7" cy="7" r="4.5"/><path d="m10.5 10.5 3 3"/></svg>',
plus: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>',
};
// 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 '<span class="faint">Player</span>';
return `<span class="rank" style="--c:${esc(rank.color)}"><i></i>${esc(rank.label)}</span>`;
}
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 ? "" : `<span style="width:${pct.toFixed(1)}%"></span>`;
return `<div class="${cls.join(" ")}">
<div class="term-top"><b>${fmtDuration(p.duration)}</b><span class="muted">${right}</span></div>
<div class="term-bar" role="img" aria-label="${esc(right)}">${bar}</div>
</div>`;
}
function statusTag(p) {
if (p.status === "active") return '<span class="tag red">Active</span>';
if (p.status === "expired") return '<span class="tag">Expired</span>';
return '<span class="tag green">Lifted</span>';
}
const COMM_LABEL = { GAG: "Gag", MUTE: "Mute", SILENCE: "Silence" };
const COMM_HELP = { GAG: "Text chat blocked", MUTE: "Voice blocked", SILENCE: "Text and voice blocked" };
function segmented(name, value, options) {
return `<div class="segmented" role="group" data-seg="${name}">
${options.map(([v, l]) => `<button type="button" data-value="${v}" aria-pressed="${v === value}">${l}</button>`).join("")}
</div>`;
}
function searchBox(placeholder, value) {
return `<label class="search">${ICON.search}<input type="search" id="q" placeholder="${esc(placeholder)}" value="${esc(value)}" aria-label="${esc(placeholder)}" autocomplete="off"></label>`;
}
function pager(list) {
const pages = Math.max(1, Math.ceil(list.total / list.pageSize));
if (pages <= 1) return "";
return `<div class="pager">
<button class="btn sm ghost" type="button" data-page="${list.page - 1}" ${list.page <= 1 ? "disabled" : ""}>Newer</button>
<span class="muted">Page ${list.page} of ${pages}</span>
<button class="btn sm ghost" type="button" data-page="${list.page + 1}" ${list.page >= pages ? "disabled" : ""}>Older</button>
</div>`;
}
function loading() { return '<div class="empty">Loading…</div>'; }
function errorBox(err) { return `<div class="empty">${esc(err.message)}</div>`; }
function closeDrawer() { $("#drawer-root").innerHTML = ""; }
function toast(msg) {
const root = $("#toast-root");
root.innerHTML = `<div class="toast">${esc(msg)}</div>`;
clearTimeout(toast.t);
toast.t = setTimeout(() => (root.innerHTML = ""), 3600);
}
const steamProfile = (sid) => `https://steamcommunity.com/profiles/${encodeURIComponent(sid)}`;