96 lines
3.4 KiB
JavaScript
96 lines
3.4 KiB
JavaScript
/* =====================================================================
|
|
* fronting.js — homepage "who's fronting" box.
|
|
*
|
|
* Polls the system's PluralKit-style API for the current fronter(s) and
|
|
* renders a small card. Each member links to their page on the system
|
|
* site. Refreshes every 30s so switches show without a reload.
|
|
* ===================================================================== */
|
|
(function fronting() {
|
|
"use strict";
|
|
|
|
const mount = document.getElementById("fronting");
|
|
if (!mount) return;
|
|
|
|
const API = "https://doughmination.co.uk/api/fronters";
|
|
const POLL_MS = 30000;
|
|
|
|
function esc(s) {
|
|
return String(s == null ? "" : s)
|
|
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
}
|
|
// member.color is a 6-char hex string (no leading #), may be null.
|
|
function colorHex(c) {
|
|
return /^[0-9a-fA-F]{6}$/.test(c || "") ? "#" + c : null;
|
|
}
|
|
|
|
// ---- build the shell ----------------------------------------------------
|
|
const card = document.createElement("section");
|
|
card.id = "fronting";
|
|
card.className = "fronting-card";
|
|
card.hidden = true;
|
|
card.setAttribute("aria-label", "Currently fronting");
|
|
card.innerHTML =
|
|
'<div class="fr-head">' +
|
|
'<span class="fr-dot" aria-hidden="true"></span>' +
|
|
'<span class="fr-label">Currently fronting</span>' +
|
|
'</div>' +
|
|
'<div class="fr-members"></div>';
|
|
mount.replaceWith(card);
|
|
|
|
const membersEl = card.querySelector(".fr-members");
|
|
|
|
function memberHtml(m) {
|
|
const name = m.display_name || m.name || "Unknown";
|
|
const accent = colorHex(m.color);
|
|
const av = m.avatar_url
|
|
? '<img class="fr-av" src="' + esc(m.avatar_url) + '" alt="" referrerpolicy="no-referrer" loading="lazy">'
|
|
: '<span class="fr-av fr-av--empty" aria-hidden="true"></span>';
|
|
const pronouns = m.pronouns
|
|
? '<span class="fr-pronouns">' + esc(m.pronouns) + '</span>' : "";
|
|
return '<div class="fr-member"' +
|
|
(accent ? ' style="--fr-accent: ' + accent + '"' : "") + '>' +
|
|
av +
|
|
'<span class="fr-meta">' +
|
|
'<span class="fr-name">' + esc(name) + '</span>' +
|
|
pronouns +
|
|
'</span>' +
|
|
'</div>';
|
|
}
|
|
|
|
function render(members) {
|
|
if (!Array.isArray(members) || !members.length) {
|
|
// No one registered as fronting — keep the box but say so.
|
|
membersEl.innerHTML = '<span class="fr-empty">no one is currently fronting</span>';
|
|
card.hidden = false;
|
|
return;
|
|
}
|
|
membersEl.innerHTML = members.map(memberHtml).join("");
|
|
card.hidden = false;
|
|
}
|
|
|
|
let failed = false;
|
|
function load() {
|
|
fetch(API, { headers: { Accept: "application/json" } })
|
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
.then(function (j) {
|
|
if (!j) throw new Error("bad response");
|
|
render(j.members || []);
|
|
failed = false;
|
|
})
|
|
.catch(function () {
|
|
// On first failure hide the box quietly; if it was already showing,
|
|
// leave the last-known fronters up rather than flashing an error.
|
|
if (!failed && card.hidden) card.hidden = true;
|
|
failed = true;
|
|
});
|
|
}
|
|
|
|
load();
|
|
const timer = setInterval(load, POLL_MS);
|
|
// Refresh immediately when the tab becomes visible again.
|
|
document.addEventListener("visibilitychange", function () {
|
|
if (!document.hidden) load();
|
|
});
|
|
window.addEventListener("beforeunload", function () { clearInterval(timer); });
|
|
})();
|