AAAAAAAAAAA

This commit is contained in:
Clove 2026-06-24 08:30:33 +01:00
parent fc619033b3
commit 4592f76d14
11 changed files with 179 additions and 176 deletions

View File

@ -20,7 +20,7 @@
<!-- SEO Meta Tags --> <!-- SEO Meta Tags -->
<meta name="description" content="Clove Twilight's live Discord presence — current status, activity, and what fae is up to right now."> <meta name="description" content="Clove Twilight's live Discord presence — current status, activity, and what fae is up to right now.">
<meta name="keywords" content="Clove Twilight, c.stupid.cat, Discord, presence, status, Lanyard"> <meta name="keywords" content="Clove Twilight, c.stupid.cat, Discord, presence, status, Doughmination, Restful">
<meta name="author" content="doughmination"> <meta name="author" content="doughmination">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"> <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">

View File

@ -1,6 +1,6 @@
// Ari was here uwu /* Ari was here uwu
// Professional boob lover * Professional boob lover
// girls kissing,,, * girls kissing,,, */
console.log(` console.log(`
@ -35,10 +35,10 @@ console.log(`
`); `);
// mmmmmmmmmmmmmmmmm girls kissing,,,,, /* mmmmmmmmmmmmmmmmm girls kissing,,,,, */
document.querySelectorAll("[data-href]").forEach((el) => { document.querySelectorAll("[data-href]").forEach((el) => {
// cursor handled in css ([data-href] + [role="link"]) so the custom PNG isn't overridden /* Cursor is handled in CSS ([data-href] + [role="link"]) so the custom PNG isn't overridden. */
if (!el.hasAttribute("role")) el.setAttribute("role", "link"); if (!el.hasAttribute("role")) el.setAttribute("role", "link");
if (!el.hasAttribute("tabindex")) el.setAttribute("tabindex", "0"); if (!el.hasAttribute("tabindex")) el.setAttribute("tabindex", "0");
@ -59,7 +59,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
/* ========================== flavors.js ========================= */ /* ========================== flavors.js ========================= */
(function flavors() { (function flavors() {
// Metadata only — colors are defined in the per-flavor CSS files. /* Metadata only, colors are defined in the per-flavor CSS files. */
const FLAVORS = { const FLAVORS = {
mocha: { label: "Mocha", dot: "#f5c2e7" }, mocha: { label: "Mocha", dot: "#f5c2e7" },
macchiato: { label: "Macchiato", dot: "#f5bde6" }, macchiato: { label: "Macchiato", dot: "#f5bde6" },
@ -73,33 +73,33 @@ document.querySelectorAll("[data-href]").forEach((el) => {
function apply(name) { function apply(name) {
const f = FLAVORS[name] || FLAVORS.mocha; const f = FLAVORS[name] || FLAVORS.mocha;
root.setAttribute("data-flavor", name); // CSS does the rest root.setAttribute("data-flavor", name); /* CSS does the rest */
const meta = document.querySelector('meta[name="theme-color"]'); const meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute("content", f.dot); if (meta) meta.setAttribute("content", f.dot);
} }
let current = ls.getItem("ctpFlavor"); let current = ls.getItem("ctpFlavor");
if (!ORDER.includes(current)) current = "mocha"; if (!ORDER.includes(current)) current = "mocha";
apply(current); // the <head> snippet already set this to avoid a flash apply(current); /* the <head> snippet already set this to avoid a flash */
// ---- top-right corner icon button ---- /* ---- top-right corner icon button ---- */
const bar = document.createElement("div"); const bar = document.createElement("div");
bar.className = "beta-bar"; bar.className = "beta-bar";
bar.innerHTML = ` bar.innerHTML = `
<button class="beta-btn" id="flavor-btn" type="button"> <button class="beta-btn" id="flavor-btn" type="button">
<img class="beta-icon" alt=""> <img class="beta-icon" alt="">
</button>`; </button>`;
// Group the single-item widgets (discord + theme toggle) into one /* Group the single-item widgets (discord + theme toggle) into one top
// top bar. On mobile they sit side by side; on desktop both stay * bar. On mobile they sit side by side, on desktop both stay
// position:fixed, so this wrapper is zero-size and invisible. * position:fixed, so this wrapper ends up zero-size and invisible. */
let topbar = document.querySelector(".topbar"); let topbar = document.querySelector(".topbar");
if (!topbar) { if (!topbar) {
topbar = document.createElement("div"); topbar = document.createElement("div");
topbar.className = "topbar"; topbar.className = "topbar";
document.body.insertBefore(topbar, document.body.firstChild); document.body.insertBefore(topbar, document.body.firstChild);
const dc = document.getElementById("discord"); const dc = document.getElementById("discord");
// Don't hijack the presence card when it's the centerpiece of the /* Don't hijack the presence card when it's the centerpiece of the
// dedicated /discord page (it lives inside .presence-stage there). * dedicated /discord page (it lives inside .presence-stage there). */
if (dc && !dc.closest(".presence-stage")) topbar.appendChild(dc); if (dc && !dc.closest(".presence-stage")) topbar.appendChild(dc);
} }
topbar.appendChild(bar); topbar.appendChild(bar);
@ -109,7 +109,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
function paintBtn() { function paintBtn() {
const f = FLAVORS[current]; const f = FLAVORS[current];
icon.src = `/assets/theme/${current}.png`; // e.g. /assets/theme/mocha.png icon.src = `/assets/theme/${current}.png`; /* e.g. /assets/theme/mocha.png */
icon.alt = f.label; icon.alt = f.label;
btn.title = `Theme: ${f.label} (click to cycle)`; btn.title = `Theme: ${f.label} (click to cycle)`;
} }
@ -122,7 +122,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
paintBtn(); paintBtn();
}); });
// ---- cat collection button (sits next to the theme button) ---- /* ---- cat collection button (sits next to the theme button) ---- */
const catBtn = document.createElement("button"); const catBtn = document.createElement("button");
catBtn.className = "beta-btn"; catBtn.className = "beta-btn";
catBtn.id = "cat-btn"; catBtn.id = "cat-btn";
@ -138,7 +138,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
})(); })();
/* ===================== cat.js (oneko.js) ======================= */ /* ===================== cat.js (oneko.js) ======================= */
// oneko.js: https://github.com/adryd325/oneko.js /* oneko.js: https://github.com/adryd325/oneko.js */
(function oneko() { (function oneko() {
const isReducedMotion = const isReducedMotion =
@ -296,7 +296,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
let lastFrameTimestamp; let lastFrameTimestamp;
function onAnimationFrame(timestamp) { function onAnimationFrame(timestamp) {
// Stops execution if the neko element is removed from DOM /* Stop running if the neko element is removed from the DOM. */
if (!nekoEl.isConnected) { if (!nekoEl.isConnected) {
return; return;
} }
@ -323,7 +323,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
function idle() { function idle() {
idleTime += 1; idleTime += 1;
// every ~ 20 seconds /* Roughly every 20 seconds. */
if ( if (
idleTime > 10 && idleTime > 10 &&
Math.floor(Math.random() * 200) == 0 && Math.floor(Math.random() * 200) == 0 &&
@ -392,7 +392,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
if (idleTime > 1) { if (idleTime > 1) {
setSprite("alert", 0); setSprite("alert", 0);
// count down after being alerted before moving /* Count down after being alerted, before moving. */
idleTime = Math.min(idleTime, 7); idleTime = Math.min(idleTime, 7);
idleTime -= 1; idleTime -= 1;
return; return;
@ -422,23 +422,23 @@ const BASE_SPRITE = "/assets/oneko/classics/classic.png";
let CAT_MODES = []; let CAT_MODES = [];
// Order the category sections appear in the menu /* Order the category sections appear in within the menu. */
const CATEGORY_ORDER = ["Classics", "Pride", "Cats", "Romance", "Gaming", "Pokémon", "Other Animals", "Things", "Rare"]; const CATEGORY_ORDER = ["Classics", "Pride", "Cats", "Romance", "Gaming", "Pokémon", "Other Animals", "Things", "Rare"];
// click-count goals (total clicks on the cat) /* Click-count goals (total clicks on the cat). */
const CLICK_GOALS = { filter: 13, romance: 69, weed: 420 }; const CLICK_GOALS = { filter: 13, romance: 69, weed: 420 };
const SPRITE = BASE_SPRITE; // base sprite used for filter modes + previews const SPRITE = BASE_SPRITE; /* base sprite used for filter modes + previews */
const IDLE_POS = "-97px -97px"; // idle frame, inset 1px to avoid neighbour-frame bleed const IDLE_POS = "-97px -97px"; /* idle frame, inset 1px to avoid neighbour-frame bleed */
const spriteFor = (c) => c.sprite || BASE_SPRITE; const spriteFor = (c) => c.sprite || BASE_SPRITE;
(async function catModes() { (async function catModes() {
try { try {
// index.json lists which per-folder configs to load (one per oneko folder) /* index.json lists which per-folder configs to load, one per oneko folder. */
const index = await fetch("/js/on/index.json").then((r) => { const index = await fetch("/js/on/index.json").then((r) => {
if (!r.ok) throw new Error(`index.json (${r.status})`); if (!r.ok) throw new Error(`index.json (${r.status})`);
return r.json(); return r.json();
}); });
// load every /js/on/<folder>.json and merge them into one list /* Load every /js/on/<folder>.json and merge them into one list. */
const lists = await Promise.all( const lists = await Promise.all(
index.map((name) => index.map((name) =>
fetch(`/js/on/${name}.json`) fetch(`/js/on/${name}.json`)
@ -455,19 +455,19 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
if (!oneko) return; if (!oneko) return;
oneko.style.pointerEvents = "auto"; oneko.style.pointerEvents = "auto";
// cursor handled in css (#oneko) so the custom pointer PNG isn't overridden /* Cursor is handled in CSS (#oneko) so the custom pointer PNG isn't overridden. */
const ls = window.localStorage; const ls = window.localStorage;
let clicks = parseInt(ls.getItem("onekoClicks") || "0", 10); let clicks = parseInt(ls.getItem("onekoClicks") || "0", 10);
let mode = parseInt(ls.getItem("onekoMode") || "0", 10); let mode = parseInt(ls.getItem("onekoMode") || "0", 10);
// permanently-earned methods (konami, gold, pokemon, + any click goal hit) /* Permanently-earned methods (konami, gold, pokemon, plus any click goal hit). */
let unlocks; let unlocks;
try { unlocks = new Set(JSON.parse(ls.getItem("onekoUnlocks") || "[]")); } try { unlocks = new Set(JSON.parse(ls.getItem("onekoUnlocks") || "[]")); }
catch (e) { unlocks = new Set(); } catch (e) { unlocks = new Set(); }
const saveUnlocks = () => ls.setItem("onekoUnlocks", JSON.stringify([...unlocks])); const saveUnlocks = () => ls.setItem("onekoUnlocks", JSON.stringify([...unlocks]));
// Returns true if a method was newly unlocked (false if already had it) /* Returns true if a method was newly unlocked, false if already had it. */
function unlockMethod(key) { function unlockMethod(key) {
if (unlocks.has(key)) return false; if (unlocks.has(key)) return false;
unlocks.add(key); unlocks.add(key);
@ -481,7 +481,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
const key = methodOf(CAT_MODES[i]); const key = methodOf(CAT_MODES[i]);
if (key === "gay") return true; if (key === "gay") return true;
if (key in CLICK_GOALS) return clicks >= CLICK_GOALS[key] || unlocks.has(key); if (key in CLICK_GOALS) return clicks >= CLICK_GOALS[key] || unlocks.has(key);
return unlocks.has(key); // konami / gold / pokemon return unlocks.has(key); /* konami / gold / pokemon */
}; };
const unlockedIndices = () => const unlockedIndices = () =>
CAT_MODES.map((_, i) => i).filter(isUnlocked); CAT_MODES.map((_, i) => i).filter(isUnlocked);
@ -492,7 +492,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
oneko.style.filter = c.filter || "none"; oneko.style.filter = c.filter || "none";
}; };
/* ---------- picker overlay (no visible trigger — press C to find it) ---------- */ /* ---- picker overlay (no visible trigger, press C to find it) ---- */
const overlay = document.createElement("div"); const overlay = document.createElement("div");
overlay.className = "cat-picker"; overlay.className = "cat-picker";
overlay.hidden = true; overlay.hidden = true;
@ -526,14 +526,14 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
function renderGrid() { function renderGrid() {
grid.innerHTML = ""; grid.innerHTML = "";
// bucket cat indices by category /* Bucket cat indices by category. */
const byCat = {}; const byCat = {};
CAT_MODES.forEach((c, i) => { CAT_MODES.forEach((c, i) => {
const cat = c.category || "Classics"; const cat = c.category || "Classics";
(byCat[cat] = byCat[cat] || []).push(i); (byCat[cat] = byCat[cat] || []).push(i);
}); });
// known categories first (in order), then any stragglers /* Known categories first, in order, then any stragglers. */
const order = CATEGORY_ORDER.filter((c) => byCat[c]) const order = CATEGORY_ORDER.filter((c) => byCat[c])
.concat(Object.keys(byCat).filter((c) => !CATEGORY_ORDER.includes(c))); .concat(Object.keys(byCat).filter((c) => !CATEGORY_ORDER.includes(c)));
@ -569,7 +569,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
const closePicker = () => (overlay.hidden = true); const closePicker = () => (overlay.hidden = true);
const togglePicker = () => (overlay.hidden ? openPicker() : closePicker()); const togglePicker = () => (overlay.hidden ? openPicker() : closePicker());
// let other scripts (e.g. the theme-bar button) open the cat menu /* Let other scripts (e.g. the theme-bar button) open the cat menu. */
window.toggleCatPicker = togglePicker; window.toggleCatPicker = togglePicker;
overlay overlay
@ -579,7 +579,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
if (e.target === overlay) closePicker(); if (e.target === overlay) closePicker();
}); });
document.addEventListener("keydown", (e) => { document.addEventListener("keydown", (e) => {
// ignore while typing in a field or with modifier keys held /* Ignore while typing in a field or with a modifier key held. */
const typing = /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName || ""); const typing = /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName || "");
if (e.key === "Escape" && !overlay.hidden) { if (e.key === "Escape" && !overlay.hidden) {
closePicker(); closePicker();
@ -596,7 +596,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
} }
}); });
/* ---------- toast ---------- */ /* ---- toast ---- */
let toastEl, toastTimer; let toastEl, toastTimer;
function toast(msg) { function toast(msg) {
if (!toastEl) { if (!toastEl) {
@ -612,22 +612,23 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
toastTimer = setTimeout(() => toastEl.classList.remove("show"), 1700); toastTimer = setTimeout(() => toastEl.classList.remove("show"), 1700);
} }
/* ---------- squeak / boop sound on click ---------- */ /* ---- squeak / boop sound on click ---- */
const boop = new Audio("/assets/oneko/boop.mp3"); const boop = new Audio("/assets/oneko/boop.mp3");
boop.preload = "auto"; boop.preload = "auto";
function playBoop() { function playBoop() {
try { try {
boop.currentTime = 0; // rewind so rapid clicks each squeak boop.currentTime = 0; /* rewind so rapid clicks each squeak */
boop.play().catch(() => { }); // ignore autoplay/missing-file errors boop.play().catch(() => { }); /* ignore autoplay/missing-file errors */
} catch (e) { /* no-op */ } } catch (e) { /* no-op */ }
} }
/* ---------- init + cat click ---------- */ /* ---- init + cat click ---- */
if (!isUnlocked(mode)) mode = 0; // fall back to Classic if current is locked if (!isUnlocked(mode)) mode = 0; /* fall back to Classic if current is locked */
apply(mode); apply(mode);
// Clicking the cat no longer changes its look — it only counts toward /* Clicking the cat no longer changes its look, it only counts toward
// the click-based unlocks (13 / 69 / 420). Pick a cat from the menu. * the click-based unlocks (13 / 69 / 420). Pick a cat from the menu
* instead. */
oneko.addEventListener("click", (e) => { oneko.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -635,20 +636,20 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
clicks += 1; clicks += 1;
ls.setItem("onekoClicks", String(clicks)); ls.setItem("onekoClicks", String(clicks));
// Did this click hit a click-count goal exactly? (13 / 69 / 420) /* Did this click hit a click-count goal exactly? (13 / 69 / 420) */
for (const key in CLICK_GOALS) { for (const key in CLICK_GOALS) {
if (clicks === CLICK_GOALS[key]) { if (clicks === CLICK_GOALS[key]) {
unlocks.add(key); unlocks.add(key);
saveUnlocks(); saveUnlocks();
const idx = CAT_MODES.findIndex((c) => methodOf(c) === key); const idx = CAT_MODES.findIndex((c) => methodOf(c) === key);
const name = idx >= 0 ? CAT_MODES[idx].name : key; const name = idx >= 0 ? CAT_MODES[idx].name : key;
toast(`✨ Unlocked: ${name}! — open the cat menu 🐱`); toast(`✨ Unlocked: ${name}! Open the cat menu 🐱`);
if (!overlay.hidden) renderGrid(); if (!overlay.hidden) renderGrid();
} }
} }
}); });
/* ---------- Konami code → press Enter to confirm ---------- */ /* ---- Konami code, press Enter to confirm ---- */
const KONAMI = ["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown", const KONAMI = ["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown",
"ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "b", "a"]; "ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "b", "a"];
let kProg = 0, kArmed = false; let kProg = 0, kArmed = false;
@ -671,11 +672,11 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
toast("Konami code… press Enter ↵"); toast("Konami code… press Enter ↵");
} }
} else { } else {
kProg = key === KONAMI[0] ? 1 : 0; // allow a fresh start on ↑ kProg = key === KONAMI[0] ? 1 : 0; /* allow a fresh start on ↑ */
} }
}); });
/* ---------- Gold → opened the site while Discord status is Idle ---------- */ /* ---- Gold: opened the site while Discord status is Idle ---- */
const dc = document.getElementById("discord"); const dc = document.getElementById("discord");
if (dc) { if (dc) {
const checkIdle = () => { const checkIdle = () => {
@ -688,7 +689,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
.observe(dc, { attributes: true, attributeFilter: ["data-status"] }); .observe(dc, { attributes: true, attributeFilter: ["data-status"] });
} }
/* ---------- Pokémon → find & click the hidden pokéball ---------- */ /* ---- Pokémon: find and click the hidden pokéball ---- */
const poke = document.getElementById("pokeball-secret"); const poke = document.getElementById("pokeball-secret");
if (poke) { if (poke) {
poke.addEventListener("click", (e) => { poke.addEventListener("click", (e) => {
@ -698,10 +699,10 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
}); });
} }
/* ---------- Timer → keep the page open for a while ---------- */ /* ---- Timer: keep the page open for a while ----
// Counts only while the tab is visible; resets each visit, but once the * Counts only while the tab is visible, and resets each visit, but
// goal is reached the unlock is saved for good. * once the goal is reached the unlock is saved for good. */
const TIMER_GOAL_MS = 5 * 60 * 1000; // 5 minutes const TIMER_GOAL_MS = 5 * 60 * 1000; /* 5 minutes */
if (!unlocks.has("timer")) { if (!unlocks.has("timer")) {
let elapsed = 0; let elapsed = 0;
let last = Date.now(); let last = Date.now();
@ -712,8 +713,8 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
last = now; last = now;
if (elapsed >= TIMER_GOAL_MS) { if (elapsed >= TIMER_GOAL_MS) {
clearInterval(timer); clearInterval(timer);
if (unlockMethod("timer")) toast("✨ Patience pays off timer cats unlocked!"); if (unlockMethod("timer")) toast("✨ Patience pays off, timer cats unlocked!");
} }
}, 1000); }, 1000);
} }
})(); })();

View File

@ -1,7 +1,7 @@
(function () { (function () {
"use strict"; "use strict";
// ====== PASTE YOUR EMBEDDABLE JSON SHARE URLS HERE =================== /* Paste your embeddable JSON share URLs here. */
var WAKATIME = { var WAKATIME = {
codingActivity: "https://wakatime.com/share/@doughmination/9dcc5b5c-ed3d-4896-bfa3-87737fa70930.json", codingActivity: "https://wakatime.com/share/@doughmination/9dcc5b5c-ed3d-4896-bfa3-87737fa70930.json",
languages: "https://wakatime.com/share/@doughmination/8354e3f8-b458-452b-aa06-839f303d4904.json", languages: "https://wakatime.com/share/@doughmination/8354e3f8-b458-452b-aa06-839f303d4904.json",
@ -9,11 +9,10 @@
editors: "https://wakatime.com/share/@doughmination/38dba24b-d2de-4d50-9b09-83642c01c33e.json", editors: "https://wakatime.com/share/@doughmination/38dba24b-d2de-4d50-9b09-83642c01c33e.json",
operatingSystems: "https://wakatime.com/share/@doughmination/a69f00cb-e38e-4de1-aa42-eec71dc6d658.json" operatingSystems: "https://wakatime.com/share/@doughmination/a69f00cb-e38e-4de1-aa42-eec71dc6d658.json"
}; };
// How many rows to show in each ranked list. /* How many rows to show in each ranked list. */
var MAX_ROWS = 8; var MAX_ROWS = 8;
// =====================================================================
// ---- JSONP loader --------------------------------------------------- /* ---- JSONP loader ---- */
var seq = 0; var seq = 0;
function jsonp(url, timeoutMs) { function jsonp(url, timeoutMs) {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
@ -36,7 +35,7 @@
}); });
} }
// ---- helpers -------------------------------------------------------- /* ---- helpers ---- */
function fmt(seconds) { function fmt(seconds) {
seconds = Math.max(0, Math.round(seconds || 0)); seconds = Math.max(0, Math.round(seconds || 0));
var h = Math.floor(seconds / 3600); var h = Math.floor(seconds / 3600);
@ -50,8 +49,8 @@
return (p < 10 ? Math.round(p * 10) / 10 : Math.round(p)) + "%"; return (p < 10 ? Math.round(p * 10) / 10 : Math.round(p)) + "%";
} }
// Value shown at the end of a bar: real time when the embed provides it, /* Value shown at the end of a bar: real time when the embed provides it,
// otherwise the percentage share. * otherwise the percentage share. */
function valueLabel(d, hasSeconds) { function valueLabel(d, hasSeconds) {
if (hasSeconds && (d.total_seconds || 0) > 0) return d.text || fmt(d.total_seconds); if (hasSeconds && (d.total_seconds || 0) > 0) return d.text || fmt(d.total_seconds);
if (typeof d.percent === "number") return pctLabel(d.percent); if (typeof d.percent === "number") return pctLabel(d.percent);
@ -66,15 +65,15 @@
if (m && text) { m.textContent = text; m.hidden = false; } if (m && text) { m.textContent = text; m.hidden = false; }
} }
// Render a ranked list of horizontal bars into a container. /* Render a ranked list of horizontal bars into a container. */
function renderBars(containerId, items) { function renderBars(containerId, items) {
var box = el(containerId); var box = el(containerId);
if (!box) return; if (!box) return;
if (!items || !items.length) { failSection(box, "No data yet."); return; } if (!items || !items.length) { failSection(box, "No data yet."); return; }
// Share embeds for languages/categories/editors/OS often return only /* Share embeds for languages/categories/editors/OS often return only
// {name, percent, color} with no seconds — so fall back to percent for * {name, percent, color} with no seconds, so we fall back to percent
// both the bar width and the value label when time isn't provided. * for both the bar width and the value label when time isn't given. */
var hasSeconds = items.some(function (d) { return d && (d.total_seconds || 0) > 0; }); var hasSeconds = items.some(function (d) { return d && (d.total_seconds || 0) > 0; });
var rows = items var rows = items
@ -122,7 +121,7 @@
showSection(box); showSection(box);
} }
// Render the 7-day vertical bar chart + headline total. /* Render the 7 day vertical bar chart and the headline total. */
function renderWeek(days) { function renderWeek(days) {
var box = el("waka-week"); var box = el("waka-week");
if (!box) return; if (!box) return;
@ -171,12 +170,15 @@
showSection(box); showSection(box);
} }
// ---- shape parsers (defensive: WakaTime embed shapes vary) ---------- /* ---- shape parsers ----
// Categorical embeds (languages/categories/editors/OS) -> data:[{name,total_seconds,percent,color,text}] * Defensive, because WakaTime's embed shapes vary between endpoints. */
/* Categorical embeds (languages/categories/editors/OS) ->
* data: [{name, total_seconds, percent, color, text}] */
function asCategorical(json) { function asCategorical(json) {
var data = json && json.data; var data = json && json.data;
if (!Array.isArray(data)) return []; if (!Array.isArray(data)) return [];
// Some embeds nest under data.<key>; flatten the first array we find. /* Some embeds nest under data.<key>, so flatten the first array we find. */
if (data.length && data[0] && data[0].name === undefined && Array.isArray(data[0])) { if (data.length && data[0] && data[0].name === undefined && Array.isArray(data[0])) {
data = data[0]; data = data[0];
} }
@ -191,7 +193,7 @@
}); });
} }
// Coding-activity embed -> array of {label, short, total} /* Coding activity embed -> array of {label, short, total} */
function asDays(json) { function asDays(json) {
var data = json && json.data; var data = json && json.data;
if (!Array.isArray(data)) return []; if (!Array.isArray(data)) return [];
@ -199,17 +201,18 @@
data.forEach(function (d) { data.forEach(function (d) {
var seconds = 0, dateStr = ""; var seconds = 0, dateStr = "";
if (d.grand_total && typeof d.grand_total.total_seconds === "number") { if (d.grand_total && typeof d.grand_total.total_seconds === "number") {
seconds = d.grand_total.total_seconds; // daily-summaries shape seconds = d.grand_total.total_seconds; /* daily-summaries shape */
} else if (typeof d.total_seconds === "number") { } else if (typeof d.total_seconds === "number") {
seconds = d.total_seconds; // flat shape seconds = d.total_seconds; /* flat shape */
} }
if (d.range && (d.range.date || d.range.text)) { if (d.range && (d.range.date || d.range.text)) {
dateStr = d.range.date || d.range.text; dateStr = d.range.date || d.range.text;
} else if (d.date) { } else if (d.date) {
dateStr = d.date; dateStr = d.date;
} }
// Anchor bare YYYY-MM-DD to local noon so the weekday label doesn't /* Anchor a bare YYYY-MM-DD to local noon so the weekday label doesn't
// slip a day in timezones west of UTC (where it'd parse as UTC midnight). * slip a day in timezones west of UTC, where it would otherwise parse
* as UTC midnight. */
var dateForParse = /^\d{4}-\d{2}-\d{2}$/.test(dateStr) ? dateStr + "T12:00:00" : dateStr; var dateForParse = /^\d{4}-\d{2}-\d{2}$/.test(dateStr) ? dateStr + "T12:00:00" : dateStr;
var dt = dateStr ? new Date(dateForParse) : null; var dt = dateStr ? new Date(dateForParse) : null;
var label = dt && !isNaN(dt) ? dt.toDateString() : (dateStr || ""); var label = dt && !isNaN(dt) ? dt.toDateString() : (dateStr || "");
@ -221,7 +224,7 @@
return out; return out;
} }
// ---- orchestration -------------------------------------------------- /* ---- orchestration ---- */
function load(url, onData, fallbackBoxId) { function load(url, onData, fallbackBoxId) {
if (!url) return Promise.resolve(null); if (!url) return Promise.resolve(null);
return jsonp(url).then(function (json) { return jsonp(url).then(function (json) {
@ -277,7 +280,7 @@
}, "waka-os")); }, "waka-os"));
} }
// If no coding-activity embed was set, hide the headline total card. /* If no coding-activity embed was set, hide the headline total card. */
if (!WAKATIME.codingActivity) { if (!WAKATIME.codingActivity) {
var totEl = el("waka-total"); var totEl = el("waka-total");
if (totEl) totEl.hidden = true; if (totEl) totEl.hidden = true;
@ -292,10 +295,11 @@
init(); init();
} }
// ---- anchor links: open the targeted <details> and scroll to it -------- /* ---- anchor links ----
// Sections are collapsible (and some are populated/un-hidden async), so a * Open the targeted <details> and scroll to it. Sections are collapsible
// plain #hash won't reliably reveal them. Open + scroll on load and on * (and some get populated/unhidden async), so a plain #hash won't reliably
// hashchange, retrying briefly while late content settles in. * reveal them. We open and scroll on load and on hashchange, retrying
* briefly while late content settles in. */
function openFromHash() { function openFromHash() {
var id = (location.hash || "").slice(1); var id = (location.hash || "").slice(1);
if (!id) return; if (!id) return;
@ -316,4 +320,4 @@
openFromHash(); openFromHash();
} }
window.addEventListener("hashchange", openFromHash); window.addEventListener("hashchange", openFromHash);
})(); })();

View File

@ -745,7 +745,6 @@
// ---- data source: Doughmination Restful API (sole source) --------------- // ---- data source: Doughmination Restful API (sole source) ---------------
// Returns presence + full profile (incl. theme_colors + display_name_styles) // Returns presence + full profile (incl. theme_colors + display_name_styles)
// in a single call. Lanyard + dstn.to were removed.
const SELF_BASE = "https://restful.doughmination.uk/v1/users/"; const SELF_BASE = "https://restful.doughmination.uk/v1/users/";
const SELF_POLL_MS = opts.pollMs || 20000; // presence refresh cadence const SELF_POLL_MS = opts.pollMs || 20000; // presence refresh cadence
let selfTimer = null; let selfTimer = null;
@ -864,12 +863,12 @@
(function friends() { (function friends() {
"use strict"; "use strict";
// Each friend is rendered as a full — but smaller — presence card, built by /* Each friend is rendered as a full but smaller presence card, built by
// the shared factory above (window.PresenceCard). Cards pull live * the shared factory above (window.PresenceCard). Cards pull live
// presence (status, activity, badges, banner, bio, connections, wishlist…) * presence (status, activity, badges, banner, bio, connections, wishlist)
// from the same Doughmination Restful API the main card uses. * from the same Doughmination Restful API the main card uses.
// NOTE: now lives in the same file as the factory (formerly discord.js), * NOTE: now lives in the same file as the factory (formerly discord.js),
// so load order is no longer a concern — this IIFE just runs second. * so load order is no longer a concern this IIFE just runs second. */
var FRIENDS = [ var FRIENDS = [
{ {

View File

@ -1,10 +1,9 @@
/* ===================================================================== /* fronting.js
* fronting.js homepage "who's fronting" box.
* *
* Polls the system's PluralKit-style API for the current fronter(s) and * Homepage "who's fronting" box. Polls the system's PluralKit-style API
* renders a small card. Each member links to their page on the system * for the current fronter(s) and renders a small card. Each member links
* site. Refreshes every 30s so switches show without a reload. * to their page on the system site. Refreshes every 30s so switches show
* ===================================================================== */ * up without needing a reload. */
(function fronting() { (function fronting() {
"use strict"; "use strict";
@ -18,12 +17,12 @@
return String(s == null ? "" : s) return String(s == null ? "" : s)
.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
} }
// member.color is a 6-char hex string (no leading #), may be null. /* member.color is a 6-char hex string with no leading #, and may be null. */
function colorHex(c) { function colorHex(c) {
return /^[0-9a-fA-F]{6}$/.test(c || "") ? "#" + c : null; return /^[0-9a-fA-F]{6}$/.test(c || "") ? "#" + c : null;
} }
// ---- build the shell ---------------------------------------------------- /* ---- build the shell ---- */
const card = document.createElement("section"); const card = document.createElement("section");
card.id = "fronting"; card.id = "fronting";
card.className = "fronting-card"; card.className = "fronting-card";
@ -59,7 +58,7 @@
function render(members) { function render(members) {
if (!Array.isArray(members) || !members.length) { if (!Array.isArray(members) || !members.length) {
// No one registered as fronting — keep the box but say so. /* No one registered as fronting, keep the box up but say so. */
membersEl.innerHTML = '<span class="fr-empty">no one is currently fronting</span>'; membersEl.innerHTML = '<span class="fr-empty">no one is currently fronting</span>';
card.hidden = false; card.hidden = false;
return; return;
@ -78,8 +77,9 @@
failed = false; failed = false;
}) })
.catch(function () { .catch(function () {
// On first failure hide the box quietly; if it was already showing, /* On the first failure, hide the box quietly. If it was already
// leave the last-known fronters up rather than flashing an error. * showing, leave the last known fronters up instead of flashing
* an error. */
if (!failed && card.hidden) card.hidden = true; if (!failed && card.hidden) card.hidden = true;
failed = true; failed = true;
}); });
@ -87,9 +87,9 @@
load(); load();
const timer = setInterval(load, POLL_MS); const timer = setInterval(load, POLL_MS);
// Refresh immediately when the tab becomes visible again. /* Refresh immediately when the tab becomes visible again. */
document.addEventListener("visibilitychange", function () { document.addEventListener("visibilitychange", function () {
if (!document.hidden) load(); if (!document.hidden) load();
}); });
window.addEventListener("beforeunload", function () { clearInterval(timer); }); window.addEventListener("beforeunload", function () { clearInterval(timer); });
})(); })();

View File

@ -1,7 +1,7 @@
(function (global) { (function (global) {
"use strict"; "use strict";
// ---- config (from the <script> data-* attributes) ------------------ /* ---- config (from the <script> data-* attributes) ---- */
var script = var script =
document.currentScript || document.currentScript ||
document.querySelector('script[src*="guestbook.js"]'); document.querySelector('script[src*="guestbook.js"]');
@ -10,7 +10,7 @@
var STYLE_ID = "guestbook-styles"; var STYLE_ID = "guestbook-styles";
// ---- styles (injected, mirrors visitor-counter.js pattern) ---------- /* ---- styles (injected, mirrors the visitor-counter.js pattern) ---- */
function injectStyles() { function injectStyles() {
if (document.getElementById(STYLE_ID)) return; if (document.getElementById(STYLE_ID)) return;
var s = document.createElement("style"); var s = document.createElement("style");
@ -49,7 +49,7 @@
".gb-status { font-size: 0.82rem; color: var(--subtext-0); }", ".gb-status { font-size: 0.82rem; color: var(--subtext-0); }",
".gb-status.gb-err { color: var(--red); }", ".gb-status.gb-err { color: var(--red); }",
".gb-status.gb-ok { color: var(--green); }", ".gb-status.gb-ok { color: var(--green); }",
// honeypot: visually hidden but still in the DOM for bots /* honeypot: visually hidden but still in the DOM for bots */
".gb-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }", ".gb-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }",
".gb-entries { display: flex; flex-direction: column; gap: 0.9rem; padding-bottom: 4.5rem; }", ".gb-entries { display: flex; flex-direction: column; gap: 0.9rem; padding-bottom: 4.5rem; }",
".gb-empty { color: var(--subtext-0); text-align: center; font-size: 0.9rem; }", ".gb-empty { color: var(--subtext-0); text-align: center; font-size: 0.9rem; }",
@ -69,7 +69,7 @@
document.head.appendChild(s); document.head.appendChild(s);
} }
// ---- helpers -------------------------------------------------------- /* ---- helpers ---- */
function esc(str) { function esc(str) {
return String(str == null ? "" : str) return String(str == null ? "" : str)
.replace(/&/g, "&amp;") .replace(/&/g, "&amp;")
@ -111,7 +111,7 @@
); );
} }
// ---- rendering ------------------------------------------------------ /* ---- rendering ---- */
var entriesEl, formEl, statusEl, submitEl, counterEl, msgEl; var entriesEl, formEl, statusEl, submitEl, counterEl, msgEl;
function setStatus(text, kind) { function setStatus(text, kind) {
@ -123,7 +123,7 @@
function renderEntries(list) { function renderEntries(list) {
if (!entriesEl) return; if (!entriesEl) return;
if (!list || !list.length) { if (!list || !list.length) {
entriesEl.innerHTML = '<p class="gb-empty">No messages yet be the first to sign!</p>'; entriesEl.innerHTML = '<p class="gb-empty">No messages yet, be the first to sign!</p>';
return; return;
} }
entriesEl.innerHTML = list.map(entryHTML).join(""); entriesEl.innerHTML = list.map(entryHTML).join("");
@ -146,14 +146,14 @@
} }
} }
// ---- Turnstile (optional) ------------------------------------------ /* ---- Turnstile (optional) ---- */
function turnstileToken() { function turnstileToken() {
try { try {
if (global.turnstile && typeof global.turnstile.getResponse === "function") { if (global.turnstile && typeof global.turnstile.getResponse === "function") {
return global.turnstile.getResponse() || ""; return global.turnstile.getResponse() || "";
} }
} catch (_) { } } catch (_) { }
// Fallback: the widget injects a hidden input named cf-turnstile-response /* Fallback: the widget injects a hidden input named cf-turnstile-response. */
var input = document.querySelector('[name="cf-turnstile-response"]'); var input = document.querySelector('[name="cf-turnstile-response"]');
return input ? input.value : ""; return input ? input.value : "";
} }
@ -173,7 +173,7 @@
document.head.appendChild(s); document.head.appendChild(s);
} }
// ---- submit --------------------------------------------------------- /* ---- submit ---- */
async function onSubmit(ev) { async function onSubmit(ev) {
ev.preventDefault(); ev.preventDefault();
if (!API) { if (!API) {
@ -185,7 +185,7 @@
name: formEl.name.value, name: formEl.name.value,
website: formEl.website.value, website: formEl.website.value,
message: formEl.message.value, message: formEl.message.value,
url2: formEl.url2.value, // honeypot url2: formEl.url2.value, /* honeypot */
}; };
if (!payload.name.trim() || !payload.message.trim()) { if (!payload.name.trim() || !payload.message.trim()) {
@ -225,13 +225,13 @@
await loadEntries(); await loadEntries();
} catch (err) { } catch (err) {
console.error("[guestbook] submit failed", err); console.error("[guestbook] submit failed", err);
setStatus("Network error please try again.", "err"); setStatus("Network error, please try again.", "err");
} finally { } finally {
submitEl.disabled = false; submitEl.disabled = false;
} }
} }
// ---- init ----------------------------------------------------------- /* ---- init ---- */
function init() { function init() {
injectStyles(); injectStyles();
entriesEl = document.getElementById("gb-entries"); entriesEl = document.getElementById("gb-entries");
@ -262,4 +262,4 @@
} }
global.Guestbook = { reload: loadEntries }; global.Guestbook = { reload: loadEntries };
})(typeof window !== "undefined" ? window : this); })(typeof window !== "undefined" ? window : this);

View File

@ -96,7 +96,7 @@
} }
/* ---- pride palettes: [empty, level1, level2, level3, level4] ---- */ /* ---- pride palettes: [empty, level1, level2, level3, level4] ---- */
const N = "#20262e"; // shared neutral "empty" so lit cells pop on a dark card const N = "#20262e"; /* shared neutral "empty" so lit cells pop on a dark card */
const THEMES = { const THEMES = {
forest: ["#232a33", "#173f2c", "#1e7349", "#34ab68", "#5ce897"], forest: ["#232a33", "#173f2c", "#1e7349", "#34ab68", "#5ce897"],
rainbow: [N, "#e40303", "#ff8c00", "#ffed00", "#2ecc40"], rainbow: [N, "#e40303", "#ff8c00", "#ffed00", "#2ecc40"],
@ -113,14 +113,14 @@
function resolveTheme(theme) { function resolveTheme(theme) {
if (Array.isArray(theme)) { if (Array.isArray(theme)) {
return theme.length === 4 ? [N].concat(theme) : theme; // allow 4 lit colors return theme.length === 4 ? [N].concat(theme) : theme; /* allow 4 lit colors */
} }
return THEMES[theme] || THEMES.rainbow; return THEMES[theme] || THEMES.rainbow;
} }
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
// track in-flight fetch per container so re-renders cancel cleanly /* Track the in-flight fetch per container so re-renders cancel cleanly. */
const controllers = new WeakMap(); const controllers = new WeakMap();
const observers = new WeakMap(); const observers = new WeakMap();
@ -131,7 +131,7 @@
months: true, weekdays: true, legend: true, count: true, months: true, weekdays: true, legend: true, count: true,
fit: false, minCell: 8, maxCell: 13, gap: 3, fit: false, minCell: 8, maxCell: 13, gap: 3,
}, options); }, options);
opts.cell = opts.cell || opts.maxCell; // initial size before auto-fit kicks in opts.cell = opts.cell || opts.maxCell; /* initial size before auto-fit kicks in */
const root = typeof target === "string" ? document.querySelector(target) : target; const root = typeof target === "string" ? document.querySelector(target) : target;
if (!root) { if (!root) {
@ -141,7 +141,7 @@
injectStyles(); injectStyles();
// cancel a previous render on this element (route change / re-mount) /* Cancel a previous render on this element (route change or re-mount). */
const prev = controllers.get(root); const prev = controllers.get(root);
if (prev) prev.abort(); if (prev) prev.abort();
const controller = new AbortController(); const controller = new AbortController();
@ -150,7 +150,7 @@
const prevObs = observers.get(root); const prevObs = observers.get(root);
if (prevObs) { prevObs.disconnect(); observers.delete(root); } if (prevObs) { prevObs.disconnect(); observers.delete(root); }
// (re)build the shell /* (Re)build the shell. */
root.classList.add("ch-root"); root.classList.add("ch-root");
root.style.setProperty("--ch-cell", opts.cell + "px"); root.style.setProperty("--ch-cell", opts.cell + "px");
root.style.setProperty("--ch-gap", opts.gap + "px"); root.style.setProperty("--ch-gap", opts.gap + "px");
@ -227,8 +227,8 @@
const since = weeks.length ? weeks[0][0].date : null; const since = weeks.length ? weeks[0][0].date : null;
if (countEl) countEl.textContent = `${total} contributions since ${since}`; if (countEl) countEl.textContent = `${total} contributions since ${since}`;
// scale day squares so the full span fits the container width /* Scale the day squares so the full span fits the container width,
// (instead of overflowing past the page's content column) * instead of overflowing past the page's content column. */
if (opts.fit && weeks.length) { if (opts.fit && weeks.length) {
const fitCells = () => { const fitCells = () => {
const w = parseFloat(getComputedStyle(root).getPropertyValue("--ch-weekday-w")) || 0; const w = parseFloat(getComputedStyle(root).getPropertyValue("--ch-weekday-w")) || 0;
@ -248,7 +248,7 @@
return { total, since, weeks }; return { total, since, weeks };
}) })
.catch(err => { .catch(err => {
if (err.name === "AbortError") return null; // superseded by a newer render if (err.name === "AbortError") return null; /* superseded by a newer render */
if (countEl) countEl.textContent = "Couldn't load contributions."; if (countEl) countEl.textContent = "Couldn't load contributions.";
console.error("[heatmap]", err); console.error("[heatmap]", err);
return null; return null;

View File

@ -379,7 +379,7 @@
} }
// ======================================================================= // =======================================================================
// PRESENCE — Doughmination Restful API (replaces the Lanyard socket) // PRESENCE — Doughmination Restful API
// ======================================================================= // =======================================================================
// Same now-playing data, pulled from the self-hosted API the rest of the // Same now-playing data, pulled from the self-hosted API the rest of the
// site uses. It's request/response (not a socket), so we poll; the tick() // site uses. It's request/response (not a socket), so we poll; the tick()

View File

@ -1,11 +1,11 @@
/* ============================================================ /* selfies.js
selfies.js renders the /selfies gallery from a manifest *
Manifest: /assets/selfies/selfies.json * Renders the /selfies gallery from a manifest at
- array of filename strings, or { "src", "alt", "caption" } objects * /assets/selfies/selfies.json. The manifest is either an array of
- shown in list order (newest first = top of the list) * filename strings, or an array of {"src", "alt", "caption"} objects,
"alt" is for screen readers; "caption" (optional) is shown on the page. * and is shown in list order (newest first goes at the top of the list).
Click any thumbnail to open it full-size in a lightbox. * "alt" is for screen readers, "caption" is optional and shown on the
============================================================ */ * page. Click any thumbnail to open it full size in a lightbox. */
(function selfies() { (function selfies() {
"use strict"; "use strict";
@ -18,8 +18,8 @@
"(prefers-reduced-motion: reduce)" "(prefers-reduced-motion: reduce)"
).matches; ).matches;
/* ---------- helpers ---------- */ /* ---- helpers ---- */
// Resolve a manifest src to a usable URL. /* Resolve a manifest src to a usable URL. */
function resolveSrc(s) { function resolveSrc(s) {
if (typeof s !== "string") return ""; if (typeof s !== "string") return "";
s = s.trim(); s = s.trim();
@ -27,7 +27,7 @@
return FOLDER + s.replace(/^\.?\//, ""); return FOLDER + s.replace(/^\.?\//, "");
} }
// Normalise a manifest entry into { src, alt, caption } or null if unusable. /* Normalise a manifest entry into { src, alt, caption }, or null if unusable. */
function normalize(entry, i) { function normalize(entry, i) {
let raw = ""; let raw = "";
let alt = ""; let alt = "";
@ -54,7 +54,7 @@
root.appendChild(p); root.appendChild(p);
} }
/* ---------- lightbox ---------- */ /* ---- lightbox ---- */
let items = []; let items = [];
let current = 0; let current = 0;
let lastFocus = null; let lastFocus = null;
@ -88,7 +88,7 @@
} }
function render(i) { function render(i) {
current = (i + items.length) % items.length; // wrap around current = (i + items.length) % items.length; /* wrap around */
const it = items[current]; const it = items[current];
lbImg.src = it.src; lbImg.src = it.src;
lbImg.alt = it.alt; lbImg.alt = it.alt;
@ -131,7 +131,7 @@
btnClose.addEventListener("click", close); btnClose.addEventListener("click", close);
btnNext.addEventListener("click", next); btnNext.addEventListener("click", next);
btnPrev.addEventListener("click", prev); btnPrev.addEventListener("click", prev);
// Click on the dim backdrop (but not the image/caption or buttons) closes. /* Click on the dim backdrop (but not the image, caption, or buttons) closes. */
lb.addEventListener("click", (e) => { lb.addEventListener("click", (e) => {
if (e.target === lb) close(); if (e.target === lb) close();
}); });
@ -142,7 +142,7 @@
else if (e.key === "ArrowLeft") prev(); else if (e.key === "ArrowLeft") prev();
}); });
/* ---------- grid ---------- */ /* ---- grid ---- */
function buildGrid(list) { function buildGrid(list) {
root.innerHTML = ""; root.innerHTML = "";
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
@ -160,7 +160,7 @@
img.alt = it.alt; img.alt = it.alt;
img.loading = i < 4 ? "eager" : "lazy"; img.loading = i < 4 ? "eager" : "lazy";
img.decoding = "async"; img.decoding = "async";
// If an image fails to load, drop its tile so the grid stays clean. /* If an image fails to load, drop its tile so the grid stays clean. */
img.addEventListener("error", () => fig.remove()); img.addEventListener("error", () => fig.remove());
btn.appendChild(img); btn.appendChild(img);
@ -179,7 +179,7 @@
root.appendChild(frag); root.appendChild(frag);
} }
/* ---------- load ---------- */ /* ---- load ---- */
root.setAttribute("aria-busy", "true"); root.setAttribute("aria-busy", "true");
fetch(MANIFEST, { cache: "no-cache" }) fetch(MANIFEST, { cache: "no-cache" })
.then((r) => { .then((r) => {
@ -190,7 +190,7 @@
if (!Array.isArray(data)) throw new Error("manifest is not an array"); if (!Array.isArray(data)) throw new Error("manifest is not an array");
items = data.map(normalize).filter(Boolean); items = data.map(normalize).filter(Boolean);
if (!items.length) { if (!items.length) {
showMessage("No selfies yet check back soon! 📸"); showMessage("No selfies yet, check back soon! 📸");
return; return;
} }
buildGrid(items); buildGrid(items);
@ -200,4 +200,4 @@
showMessage("Couldn't load the selfies right now."); showMessage("Couldn't load the selfies right now.");
}) })
.finally(() => root.removeAttribute("aria-busy")); .finally(() => root.removeAttribute("aria-busy"));
})(); })();

View File

@ -1,16 +1,15 @@
/* ===================================================================== /* terminal.js
* terminal.js the homepage's interactive terminal.
* *
* Flow: a short boot log streams in, the side chrome fades in alongside * The homepage's interactive terminal. Flow: a short boot log streams
* it, then the banner + a pinned prompt appear. You type a command and * in, the side chrome fades in alongside it, then the banner and a
* the output is appended to the scrollback BELOW the input the input * pinned prompt appear. You type a command and the output gets
* itself never moves. * appended to the scrollback below the input, the input itself never
* ===================================================================== */ * moves. */
(function terminal() { (function terminal() {
const root = document.getElementById("terminal"); const root = document.getElementById("terminal");
if (!root) return; if (!root) return;
// arch.ascii (hyfetch format) is fetched once at startup for `hyfetch`. /* arch.ascii (hyfetch format) is fetched once at startup for `hyfetch`. */
let archLines = null; let archLines = null;
function loadArt() { function loadArt() {
fetch("/arch.ascii").then(function (r) { return r.ok ? r.text() : ""; }).then(function (t) { fetch("/arch.ascii").then(function (r) { return r.ok ? r.text() : ""; }).then(function (t) {
@ -23,7 +22,7 @@
}).catch(function () { }); }).catch(function () { });
} }
// ---- ascii banner ------------------------------------------------------- /* ---- ascii banner ---- */
const BANNER = [ const BANNER = [
" ██████╗██╗ ██████╗ ██╗ ██╗███████╗", " ██████╗██╗ ██████╗ ██╗ ██╗███████╗",
"██╔════╝██║ ██╔═══██╗██║ ██║██╔════╝", "██╔════╝██║ ██╔═══██╗██║ ██║██╔════╝",
@ -33,7 +32,7 @@
" ╚═════╝╚══════╝ ╚═════╝ ╚═══╝ ╚══════╝" " ╚═════╝╚══════╝ ╚═════╝ ╚═══╝ ╚══════╝"
].join("\n"); ].join("\n");
// ---- boot log ----------------------------------------------------------- /* ---- boot log ---- */
const BOOT = [ const BOOT = [
["info", "starting clovesh..."], ["info", "starting clovesh..."],
["info", "mounting /dev/estrogen..."], ["info", "mounting /dev/estrogen..."],
@ -42,15 +41,15 @@
["ok", "modules loaded"], ["ok", "modules loaded"],
["info", "summoning cats..."], ["info", "summoning cats..."],
["ok", "oneko ready"], ["ok", "oneko ready"],
["info", "connecting to discord via lanyard..."], ["info", "connecting to discord via restful..."],
["ok", "presence online"], ["ok", "presence online"],
["info", "mounting button wall..."], ["info", "mounting button wall..."],
["ok", "88x31 buttons hung"], ["ok", "88x31 buttons hung"],
["info", "starting terminal..."], ["info", "starting terminal..."],
["ok", "ready type 'help'"] ["ok", "ready, type 'help'"]
]; ];
// ---- build DOM ---------------------------------------------------------- /* ---- build DOM ---- */
root.innerHTML = root.innerHTML =
'<pre class="t-boot" id="t-boot" aria-hidden="true"></pre>' + '<pre class="t-boot" id="t-boot" aria-hidden="true"></pre>' +
'<div class="t-main" id="t-main" hidden>' + '<div class="t-main" id="t-main" hidden>' +
@ -76,7 +75,7 @@
return new Date().toLocaleTimeString("en-GB", { hour12: false }); return new Date().toLocaleTimeString("en-GB", { hour12: false });
} }
// ---- command handlers --------------------------------------------------- /* ---- command handlers ---- */
const COMMANDS = { const COMMANDS = {
help() { help() {
const rows = [ const rows = [
@ -123,7 +122,7 @@
about() { about() {
return { return {
text: text:
"Clove Twilight fae/faer\n" + "Clove Twilight, fae/faer\n" +
"Transfem developer from Southampton, UK. I make Projects,\n" + "Transfem developer from Southampton, UK. I make Projects,\n" +
"personal-site nonsense, and run a small corner of the internet\n" + "personal-site nonsense, and run a small corner of the internet\n" +
"under the trade mark 'doughmination system'. Big on Linux, Catppuccin, and cats." "under the trade mark 'doughmination system'. Big on Linux, Catppuccin, and cats."
@ -160,7 +159,7 @@
}, },
}; };
// ---- runtime ------------------------------------------------------------ /* ---- runtime ---- */
const startedAt = Date.now(); const startedAt = Date.now();
function uptime() { function uptime() {
let s = Math.floor((Date.now() - startedAt) / 1000); let s = Math.floor((Date.now() - startedAt) / 1000);
@ -213,7 +212,7 @@
showResult({ text: "clovesh: command not found: " + name + "\nType 'help' for a list.", error: true }); showResult({ text: "clovesh: command not found: " + name + "\nType 'help' for a list.", error: true });
} }
// ---- tab-complete + history -------------------------------------------- /* ---- tab-complete + history ---- */
const COMPLETIONS = Object.keys(COMMANDS); const COMPLETIONS = Object.keys(COMMANDS);
function complete(prefix) { function complete(prefix) {
if (!prefix) return null; if (!prefix) return null;
@ -250,7 +249,7 @@
if ((window.getSelection() + "") === "") input.focus(); if ((window.getSelection() + "") === "") input.focus();
}); });
// ---- boot then reveal --------------------------------------------------- /* ---- boot then reveal ---- */
document.body.classList.add("term-booting"); document.body.classList.add("term-booting");
let booted = false; let booted = false;
@ -285,4 +284,4 @@
loadArt(); loadArt();
requestAnimationFrame(() => document.body.classList.add("term-chrome-in")); requestAnimationFrame(() => document.body.classList.add("term-chrome-in"));
streamBoot(0); streamBoot(0);
})(); })();

View File

@ -44,17 +44,16 @@
document.head.appendChild(s); document.head.appendChild(s);
} }
/** /* Returns the cached visitor count for this session, or null if not
* Returns the cached visitor count for this session, or null if not cached. * cached. The cache key is scoped to namespace + key so multiple
* The cache key is scoped to namespace+key so multiple counters don't collide. * counters don't collide. */
*/
function getCached(namespace, key) { function getCached(namespace, key) {
try { try {
const raw = localStorage.getItem("vc:" + namespace + ":" + key); const raw = localStorage.getItem("vc:" + namespace + ":" + key);
if (!raw) return null; if (!raw) return null;
const { count, session } = JSON.parse(raw); const { count, session } = JSON.parse(raw);
// Use sessionStorage token to detect new tabs vs. refreshes. /* Use a sessionStorage token to tell a new tab apart from a refresh.
// A refresh keeps the same sessionStorage; a new tab starts fresh. * A refresh keeps the same sessionStorage, a new tab starts fresh. */
const token = sessionStorage.getItem("vc-session"); const token = sessionStorage.getItem("vc-session");
if (token && token === session) return count; if (token && token === session) return count;
return null; return null;
@ -65,7 +64,7 @@
function setCached(namespace, key, count) { function setCached(namespace, key, count) {
try { try {
// Create (or reuse) a session token so this count is tied to the tab session. /* Create (or reuse) a session token so this count is tied to the tab session. */
let token = sessionStorage.getItem("vc-session"); let token = sessionStorage.getItem("vc-session");
if (!token) { if (!token) {
token = Math.random().toString(36).slice(2); token = Math.random().toString(36).slice(2);
@ -83,7 +82,7 @@
const res = await fetch(url); const res = await fetch(url);
if (!res.ok) throw new Error("Abacus HTTP " + res.status); if (!res.ok) throw new Error("Abacus HTTP " + res.status);
const data = await res.json(); const data = await res.json();
// Abacus returns { value: <number> } /* Abacus returns { value: <number> }. */
if (typeof data.value !== "number") throw new Error("Unexpected response shape"); if (typeof data.value !== "number") throw new Error("Unexpected response shape");
return data.value; return data.value;
} }
@ -135,14 +134,15 @@
root.appendChild(labelEl); root.appendChild(labelEl);
} }
// Try cache first — avoids hitting the API (and incrementing) on refresh /* Try the cache first, this avoids hitting the API (and incrementing
* the count) on every refresh. */
const cached = getCached(opts.namespace, opts.key); const cached = getCached(opts.namespace, opts.key);
if (cached !== null) { if (cached !== null) {
renderDigits(digitsEl, cached, opts.imgPath, opts.imgExt); renderDigits(digitsEl, cached, opts.imgPath, opts.imgExt);
return; return;
} }
// First visit in this session: hit the API /* First visit in this session, so hit the API. */
try { try {
const count = await fetchCount(opts.namespace, opts.key); const count = await fetchCount(opts.namespace, opts.key);
setCached(opts.namespace, opts.key, count); setCached(opts.namespace, opts.key, count);
@ -151,7 +151,7 @@
console.error("[visitor-counter]", err); console.error("[visitor-counter]", err);
const errEl = document.createElement("span"); const errEl = document.createElement("span");
errEl.className = "vc-error"; errEl.className = "vc-error";
errEl.textContent = " visitors"; errEl.textContent = "?? visitors";
digitsEl.replaceWith(errEl); digitsEl.replaceWith(errEl);
if (opts.label && root.querySelector(".vc-label")) { if (opts.label && root.querySelector(".vc-label")) {
root.querySelector(".vc-label").remove(); root.querySelector(".vc-label").remove();
@ -159,7 +159,7 @@
} }
} }
// Auto-init from script tag attributes /* Auto-init from script tag attributes. */
function autoInit() { function autoInit() {
const script = const script =
document.currentScript || document.currentScript ||