Add headmate fronting thing
This commit is contained in:
parent
39c692ece1
commit
c022564089
107
css/main.css
107
css/main.css
|
|
@ -3630,3 +3630,110 @@ body:has(.presence-stage) {
|
||||||
#lc-embed { left: 0.5rem !important; bottom: 0.5rem !important; }
|
#lc-embed { left: 0.5rem !important; bottom: 0.5rem !important; }
|
||||||
#lc-embed > section > div { min-width: 0 !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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@
|
||||||
<h2 class="pronouns">(fae/faer)</h2>
|
<h2 class="pronouns">(fae/faer)</h2>
|
||||||
</header>
|
</header>
|
||||||
<div class="terminal" id="terminal"></div>
|
<div class="terminal" id="terminal"></div>
|
||||||
|
<div id="fronting"></div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div id="visitor-counter" aria-label="Visitor count"></div>
|
<div id="visitor-counter" aria-label="Visitor count"></div>
|
||||||
|
|
@ -69,6 +70,7 @@
|
||||||
<script src="/js/flavors.js"></script>
|
<script src="/js/flavors.js"></script>
|
||||||
<script src="/js/dev-mode.js"></script>
|
<script src="/js/dev-mode.js"></script>
|
||||||
<script src="/js/terminal.js"></script>
|
<script src="/js/terminal.js"></script>
|
||||||
|
<script src="/js/fronting.js"></script>
|
||||||
<script src="/js/site-switcher.js"></script>
|
<script src="/js/site-switcher.js"></script>
|
||||||
<!-- lanyard.cafe keyring (webring) — styled to match the site in css/main.css (#lc-embed) -->
|
<!-- lanyard.cafe keyring (webring) — styled to match the site in css/main.css (#lc-embed) -->
|
||||||
<script src="https://lanyard.cafe/api/embed.js" data-theme="dark"></script>
|
<script src="https://lanyard.cafe/api/embed.js" data-theme="dark"></script>
|
||||||
|
|
|
||||||
|
|
@ -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, ">").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); });
|
||||||
|
})();
|
||||||
Loading…
Reference in New Issue