From c0225640895928df7d2f126b26961ee6f3efeb61 Mon Sep 17 00:00:00 2001 From: Clove Twilight Date: Wed, 17 Jun 2026 09:46:56 +0100 Subject: [PATCH] Add headmate fronting thing --- css/main.css | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 2 + js/fronting.js | 95 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+) create mode 100644 js/fronting.js diff --git a/css/main.css b/css/main.css index eec9b87..5014259 100644 --- a/css/main.css +++ b/css/main.css @@ -3630,3 +3630,110 @@ body:has(.presence-stage) { #lc-embed { left: 0.5rem !important; bottom: 0.5rem !important; } #lc-embed > section > div { min-width: 0 !important; } } + +/* ===================================================================== + * fronting.js — "currently fronting" box (sits below the terminal) + * ===================================================================== */ +.fronting-card { + position: relative; + z-index: 1; + width: 100%; + margin: 0.9rem auto 0; + padding: 0.85rem 1rem; + background: var(--mantle); + border: 1px solid var(--surface-1); + border-radius: 14px; + box-shadow: 0 18px 50px -22px rgba(0, 0, 0, 0.6); +} + +.fronting-card[hidden] { display: none; } + +.fr-head { + display: flex; + align-items: center; + gap: 0.45rem; + margin-bottom: 0.7rem; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--subtext-0); +} + +.fr-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--green); + box-shadow: 0 0 0 0 rgba(166, 227, 161, 0.55); + animation: fr-pulse 2.4s ease-out infinite; +} + +@keyframes fr-pulse { + 0% { box-shadow: 0 0 0 0 rgba(166, 227, 161, 0.5); } + 70% { box-shadow: 0 0 0 7px rgba(166, 227, 161, 0); } + 100% { box-shadow: 0 0 0 0 rgba(166, 227, 161, 0); } +} + +@media (prefers-reduced-motion: reduce) { + .fr-dot { animation: none; } +} + +.fr-members { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; +} + +.fr-member { + --fr-accent: var(--pink); + display: flex; + align-items: center; + gap: 0.6rem; + flex: 1 1 auto; + min-width: 0; + padding: 0.5rem 0.75rem 0.5rem 0.5rem; + border-radius: 12px; + background: var(--surface-0); + border: 1px solid var(--surface-1); + border-left: 3px solid var(--fr-accent); + color: var(--text); +} + +.fr-av { + width: 38px; + height: 38px; + border-radius: 50%; + object-fit: cover; + flex: 0 0 auto; + border: 2px solid var(--fr-accent); + background: var(--crust); +} + +.fr-av--empty { display: inline-block; } + +.fr-meta { + display: flex; + flex-direction: column; + min-width: 0; + line-height: 1.25; +} + +.fr-name { + font-weight: 600; + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.fr-pronouns { + font-size: 0.74rem; + color: var(--subtext-0); +} + +.fr-empty { + font-size: 0.82rem; + color: var(--subtext-0); + font-style: italic; +} diff --git a/index.html b/index.html index 9d92864..0a63463 100644 --- a/index.html +++ b/index.html @@ -60,6 +60,7 @@

(fae/faer)

+
@@ -69,6 +70,7 @@ + diff --git a/js/fronting.js b/js/fronting.js new file mode 100644 index 0000000..f574615 --- /dev/null +++ b/js/fronting.js @@ -0,0 +1,95 @@ +/* ===================================================================== + * 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, """); + } + // 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 = + '
' + + '' + + 'Currently fronting' + + '
' + + '
'; + 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 + ? '' + : ''; + const pronouns = m.pronouns + ? '' + esc(m.pronouns) + '' : ""; + return '
' + + av + + '' + + '' + esc(name) + '' + + pronouns + + '' + + '
'; + } + + function render(members) { + if (!Array.isArray(members) || !members.length) { + // No one registered as fronting — keep the box but say so. + membersEl.innerHTML = 'no one is currently fronting'; + 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); }); +})();