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 -->
<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="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
// Professional boob lover
// girls kissing,,,
/* Ari was here uwu
* Professional boob lover
* girls kissing,,, */
console.log(`
@ -35,10 +35,10 @@ console.log(`
`);
// mmmmmmmmmmmmmmmmm girls kissing,,,,,
/* mmmmmmmmmmmmmmmmm girls kissing,,,,, */
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("tabindex")) el.setAttribute("tabindex", "0");
@ -59,7 +59,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
/* ========================== flavors.js ========================= */
(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 = {
mocha: { label: "Mocha", dot: "#f5c2e7" },
macchiato: { label: "Macchiato", dot: "#f5bde6" },
@ -73,33 +73,33 @@ document.querySelectorAll("[data-href]").forEach((el) => {
function apply(name) {
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"]');
if (meta) meta.setAttribute("content", f.dot);
}
let current = ls.getItem("ctpFlavor");
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");
bar.className = "beta-bar";
bar.innerHTML = `
<button class="beta-btn" id="flavor-btn" type="button">
<img class="beta-icon" alt="">
</button>`;
// Group the single-item widgets (discord + theme toggle) into one
// top bar. On mobile they sit side by side; on desktop both stay
// position:fixed, so this wrapper is zero-size and invisible.
/* Group the single-item widgets (discord + theme toggle) into one top
* bar. On mobile they sit side by side, on desktop both stay
* position:fixed, so this wrapper ends up zero-size and invisible. */
let topbar = document.querySelector(".topbar");
if (!topbar) {
topbar = document.createElement("div");
topbar.className = "topbar";
document.body.insertBefore(topbar, document.body.firstChild);
const dc = document.getElementById("discord");
// Don't hijack the presence card when it's the centerpiece of the
// dedicated /discord page (it lives inside .presence-stage there).
/* Don't hijack the presence card when it's the centerpiece of the
* dedicated /discord page (it lives inside .presence-stage there). */
if (dc && !dc.closest(".presence-stage")) topbar.appendChild(dc);
}
topbar.appendChild(bar);
@ -109,7 +109,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
function paintBtn() {
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;
btn.title = `Theme: ${f.label} (click to cycle)`;
}
@ -122,7 +122,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
paintBtn();
});
// ---- cat collection button (sits next to the theme button) ----
/* ---- cat collection button (sits next to the theme button) ---- */
const catBtn = document.createElement("button");
catBtn.className = "beta-btn";
catBtn.id = "cat-btn";
@ -138,7 +138,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
})();
/* ===================== cat.js (oneko.js) ======================= */
// oneko.js: https://github.com/adryd325/oneko.js
/* oneko.js: https://github.com/adryd325/oneko.js */
(function oneko() {
const isReducedMotion =
@ -296,7 +296,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
let lastFrameTimestamp;
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) {
return;
}
@ -323,7 +323,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
function idle() {
idleTime += 1;
// every ~ 20 seconds
/* Roughly every 20 seconds. */
if (
idleTime > 10 &&
Math.floor(Math.random() * 200) == 0 &&
@ -392,7 +392,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
if (idleTime > 1) {
setSprite("alert", 0);
// count down after being alerted before moving
/* Count down after being alerted, before moving. */
idleTime = Math.min(idleTime, 7);
idleTime -= 1;
return;
@ -422,23 +422,23 @@ const BASE_SPRITE = "/assets/oneko/classics/classic.png";
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"];
// 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 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 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 spriteFor = (c) => c.sprite || BASE_SPRITE;
(async function catModes() {
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) => {
if (!r.ok) throw new Error(`index.json (${r.status})`);
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(
index.map((name) =>
fetch(`/js/on/${name}.json`)
@ -455,19 +455,19 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
if (!oneko) return;
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;
let clicks = parseInt(ls.getItem("onekoClicks") || "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;
try { unlocks = new Set(JSON.parse(ls.getItem("onekoUnlocks") || "[]")); }
catch (e) { unlocks = new Set(); }
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) {
if (unlocks.has(key)) return false;
unlocks.add(key);
@ -481,7 +481,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
const key = methodOf(CAT_MODES[i]);
if (key === "gay") return true;
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 = () =>
CAT_MODES.map((_, i) => i).filter(isUnlocked);
@ -492,7 +492,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
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");
overlay.className = "cat-picker";
overlay.hidden = true;
@ -526,14 +526,14 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
function renderGrid() {
grid.innerHTML = "";
// bucket cat indices by category
/* Bucket cat indices by category. */
const byCat = {};
CAT_MODES.forEach((c, i) => {
const cat = c.category || "Classics";
(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])
.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 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;
overlay
@ -579,7 +579,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
if (e.target === overlay) closePicker();
});
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 || "");
if (e.key === "Escape" && !overlay.hidden) {
closePicker();
@ -596,7 +596,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
}
});
/* ---------- toast ---------- */
/* ---- toast ---- */
let toastEl, toastTimer;
function toast(msg) {
if (!toastEl) {
@ -612,22 +612,23 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
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");
boop.preload = "auto";
function playBoop() {
try {
boop.currentTime = 0; // rewind so rapid clicks each squeak
boop.play().catch(() => { }); // ignore autoplay/missing-file errors
boop.currentTime = 0; /* rewind so rapid clicks each squeak */
boop.play().catch(() => { }); /* ignore autoplay/missing-file errors */
} catch (e) { /* no-op */ }
}
/* ---------- init + cat click ---------- */
if (!isUnlocked(mode)) mode = 0; // fall back to Classic if current is locked
/* ---- init + cat click ---- */
if (!isUnlocked(mode)) mode = 0; /* fall back to Classic if current is locked */
apply(mode);
// 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.
/* 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
* instead. */
oneko.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
@ -635,20 +636,20 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
clicks += 1;
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) {
if (clicks === CLICK_GOALS[key]) {
unlocks.add(key);
saveUnlocks();
const idx = CAT_MODES.findIndex((c) => methodOf(c) === 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();
}
}
});
/* ---------- Konami code → press Enter to confirm ---------- */
/* ---- Konami code, press Enter to confirm ---- */
const KONAMI = ["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown",
"ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "b", "a"];
let kProg = 0, kArmed = false;
@ -671,11 +672,11 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
toast("Konami code… press Enter ↵");
}
} 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");
if (dc) {
const checkIdle = () => {
@ -688,7 +689,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
.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");
if (poke) {
poke.addEventListener("click", (e) => {
@ -698,10 +699,10 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
});
}
/* ---------- Timer → keep the page open for a while ---------- */
// Counts only while the tab is visible; resets each visit, but once the
// goal is reached the unlock is saved for good.
const TIMER_GOAL_MS = 5 * 60 * 1000; // 5 minutes
/* ---- Timer: keep the page open for a while ----
* Counts only while the tab is visible, and resets each visit, but
* once the goal is reached the unlock is saved for good. */
const TIMER_GOAL_MS = 5 * 60 * 1000; /* 5 minutes */
if (!unlocks.has("timer")) {
let elapsed = 0;
let last = Date.now();
@ -712,7 +713,7 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
last = now;
if (elapsed >= TIMER_GOAL_MS) {
clearInterval(timer);
if (unlockMethod("timer")) toast("✨ Patience pays off timer cats unlocked!");
if (unlockMethod("timer")) toast("✨ Patience pays off, timer cats unlocked!");
}
}, 1000);
}

View File

@ -1,7 +1,7 @@
(function () {
"use strict";
// ====== PASTE YOUR EMBEDDABLE JSON SHARE URLS HERE ===================
/* Paste your embeddable JSON share URLs here. */
var WAKATIME = {
codingActivity: "https://wakatime.com/share/@doughmination/9dcc5b5c-ed3d-4896-bfa3-87737fa70930.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",
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;
// =====================================================================
// ---- JSONP loader ---------------------------------------------------
/* ---- JSONP loader ---- */
var seq = 0;
function jsonp(url, timeoutMs) {
return new Promise(function (resolve, reject) {
@ -36,7 +35,7 @@
});
}
// ---- helpers --------------------------------------------------------
/* ---- helpers ---- */
function fmt(seconds) {
seconds = Math.max(0, Math.round(seconds || 0));
var h = Math.floor(seconds / 3600);
@ -50,8 +49,8 @@
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,
// otherwise the percentage share.
/* Value shown at the end of a bar: real time when the embed provides it,
* otherwise the percentage share. */
function valueLabel(d, hasSeconds) {
if (hasSeconds && (d.total_seconds || 0) > 0) return d.text || fmt(d.total_seconds);
if (typeof d.percent === "number") return pctLabel(d.percent);
@ -66,15 +65,15 @@
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) {
var box = el(containerId);
if (!box) return;
if (!items || !items.length) { failSection(box, "No data yet."); return; }
// Share embeds for languages/categories/editors/OS often return only
// {name, percent, color} with no seconds — so fall back to percent for
// both the bar width and the value label when time isn't provided.
/* Share embeds for languages/categories/editors/OS often return only
* {name, percent, color} with no seconds, so we fall back to percent
* 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 rows = items
@ -122,7 +121,7 @@
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) {
var box = el("waka-week");
if (!box) return;
@ -171,12 +170,15 @@
showSection(box);
}
// ---- shape parsers (defensive: WakaTime embed shapes vary) ----------
// Categorical embeds (languages/categories/editors/OS) -> data:[{name,total_seconds,percent,color,text}]
/* ---- shape parsers ----
* 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) {
var data = json && json.data;
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])) {
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) {
var data = json && json.data;
if (!Array.isArray(data)) return [];
@ -199,17 +201,18 @@
data.forEach(function (d) {
var seconds = 0, dateStr = "";
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") {
seconds = d.total_seconds; // flat shape
seconds = d.total_seconds; /* flat shape */
}
if (d.range && (d.range.date || d.range.text)) {
dateStr = d.range.date || d.range.text;
} else if (d.date) {
dateStr = d.date;
}
// Anchor 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).
/* 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 would otherwise parse
* as UTC midnight. */
var dateForParse = /^\d{4}-\d{2}-\d{2}$/.test(dateStr) ? dateStr + "T12:00:00" : dateStr;
var dt = dateStr ? new Date(dateForParse) : null;
var label = dt && !isNaN(dt) ? dt.toDateString() : (dateStr || "");
@ -221,7 +224,7 @@
return out;
}
// ---- orchestration --------------------------------------------------
/* ---- orchestration ---- */
function load(url, onData, fallbackBoxId) {
if (!url) return Promise.resolve(null);
return jsonp(url).then(function (json) {
@ -277,7 +280,7 @@
}, "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) {
var totEl = el("waka-total");
if (totEl) totEl.hidden = true;
@ -292,10 +295,11 @@
init();
}
// ---- anchor links: open the targeted <details> and scroll to it --------
// Sections are collapsible (and some are populated/un-hidden async), so a
// plain #hash won't reliably reveal them. Open + scroll on load and on
// hashchange, retrying briefly while late content settles in.
/* ---- anchor links ----
* Open the targeted <details> and scroll to it. Sections are collapsible
* (and some get populated/unhidden async), so a plain #hash won't reliably
* reveal them. We open and scroll on load and on hashchange, retrying
* briefly while late content settles in. */
function openFromHash() {
var id = (location.hash || "").slice(1);
if (!id) return;

View File

@ -745,7 +745,6 @@
// ---- data source: Doughmination Restful API (sole source) ---------------
// 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_POLL_MS = opts.pollMs || 20000; // presence refresh cadence
let selfTimer = null;
@ -864,12 +863,12 @@
(function friends() {
"use strict";
// Each friend is rendered as a full — but smaller — presence card, built by
// the shared factory above (window.PresenceCard). Cards pull live
// presence (status, activity, badges, banner, bio, connections, wishlist…)
// from the same Doughmination Restful API the main card uses.
// 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.
/* Each friend is rendered as a full but smaller presence card, built by
* the shared factory above (window.PresenceCard). Cards pull live
* presence (status, activity, badges, banner, bio, connections, wishlist)
* from the same Doughmination Restful API the main card uses.
* 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. */
var FRIENDS = [
{

View File

@ -1,10 +1,9 @@
/* =====================================================================
* fronting.js homepage "who's fronting" box.
/* fronting.js
*
* 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.
* ===================================================================== */
* 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
* up without needing a reload. */
(function fronting() {
"use strict";
@ -18,12 +17,12 @@
return String(s == null ? "" : s)
.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) {
return /^[0-9a-fA-F]{6}$/.test(c || "") ? "#" + c : null;
}
// ---- build the shell ----------------------------------------------------
/* ---- build the shell ---- */
const card = document.createElement("section");
card.id = "fronting";
card.className = "fronting-card";
@ -59,7 +58,7 @@
function render(members) {
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>';
card.hidden = false;
return;
@ -78,8 +77,9 @@
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.
/* On the first failure, hide the box quietly. If it was already
* showing, leave the last known fronters up instead of flashing
* an error. */
if (!failed && card.hidden) card.hidden = true;
failed = true;
});
@ -87,7 +87,7 @@
load();
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 () {
if (!document.hidden) load();
});

View File

@ -1,7 +1,7 @@
(function (global) {
"use strict";
// ---- config (from the <script> data-* attributes) ------------------
/* ---- config (from the <script> data-* attributes) ---- */
var script =
document.currentScript ||
document.querySelector('script[src*="guestbook.js"]');
@ -10,7 +10,7 @@
var STYLE_ID = "guestbook-styles";
// ---- styles (injected, mirrors visitor-counter.js pattern) ----------
/* ---- styles (injected, mirrors the visitor-counter.js pattern) ---- */
function injectStyles() {
if (document.getElementById(STYLE_ID)) return;
var s = document.createElement("style");
@ -49,7 +49,7 @@
".gb-status { font-size: 0.82rem; color: var(--subtext-0); }",
".gb-status.gb-err { color: var(--red); }",
".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-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; }",
@ -69,7 +69,7 @@
document.head.appendChild(s);
}
// ---- helpers --------------------------------------------------------
/* ---- helpers ---- */
function esc(str) {
return String(str == null ? "" : str)
.replace(/&/g, "&amp;")
@ -111,7 +111,7 @@
);
}
// ---- rendering ------------------------------------------------------
/* ---- rendering ---- */
var entriesEl, formEl, statusEl, submitEl, counterEl, msgEl;
function setStatus(text, kind) {
@ -123,7 +123,7 @@
function renderEntries(list) {
if (!entriesEl) return;
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;
}
entriesEl.innerHTML = list.map(entryHTML).join("");
@ -146,14 +146,14 @@
}
}
// ---- Turnstile (optional) ------------------------------------------
/* ---- Turnstile (optional) ---- */
function turnstileToken() {
try {
if (global.turnstile && typeof global.turnstile.getResponse === "function") {
return global.turnstile.getResponse() || "";
}
} 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"]');
return input ? input.value : "";
}
@ -173,7 +173,7 @@
document.head.appendChild(s);
}
// ---- submit ---------------------------------------------------------
/* ---- submit ---- */
async function onSubmit(ev) {
ev.preventDefault();
if (!API) {
@ -185,7 +185,7 @@
name: formEl.name.value,
website: formEl.website.value,
message: formEl.message.value,
url2: formEl.url2.value, // honeypot
url2: formEl.url2.value, /* honeypot */
};
if (!payload.name.trim() || !payload.message.trim()) {
@ -225,13 +225,13 @@
await loadEntries();
} catch (err) {
console.error("[guestbook] submit failed", err);
setStatus("Network error please try again.", "err");
setStatus("Network error, please try again.", "err");
} finally {
submitEl.disabled = false;
}
}
// ---- init -----------------------------------------------------------
/* ---- init ---- */
function init() {
injectStyles();
entriesEl = document.getElementById("gb-entries");

View File

@ -96,7 +96,7 @@
}
/* ---- 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 = {
forest: ["#232a33", "#173f2c", "#1e7349", "#34ab68", "#5ce897"],
rainbow: [N, "#e40303", "#ff8c00", "#ffed00", "#2ecc40"],
@ -113,14 +113,14 @@
function resolveTheme(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;
}
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 observers = new WeakMap();
@ -131,7 +131,7 @@
months: true, weekdays: true, legend: true, count: true,
fit: false, minCell: 8, maxCell: 13, gap: 3,
}, 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;
if (!root) {
@ -141,7 +141,7 @@
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);
if (prev) prev.abort();
const controller = new AbortController();
@ -150,7 +150,7 @@
const prevObs = observers.get(root);
if (prevObs) { prevObs.disconnect(); observers.delete(root); }
// (re)build the shell
/* (Re)build the shell. */
root.classList.add("ch-root");
root.style.setProperty("--ch-cell", opts.cell + "px");
root.style.setProperty("--ch-gap", opts.gap + "px");
@ -227,8 +227,8 @@
const since = weeks.length ? weeks[0][0].date : null;
if (countEl) countEl.textContent = `${total} contributions since ${since}`;
// scale day squares so the full span fits the container width
// (instead of overflowing past the page's content column)
/* Scale the day squares so the full span fits the container width,
* instead of overflowing past the page's content column. */
if (opts.fit && weeks.length) {
const fitCells = () => {
const w = parseFloat(getComputedStyle(root).getPropertyValue("--ch-weekday-w")) || 0;
@ -248,7 +248,7 @@
return { total, since, weeks };
})
.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.";
console.error("[heatmap]", err);
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
// site uses. It's request/response (not a socket), so we poll; the tick()

View File

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

View File

@ -1,16 +1,15 @@
/* =====================================================================
* terminal.js the homepage's interactive terminal.
/* terminal.js
*
* Flow: a short boot log streams in, the side chrome fades in alongside
* it, then the banner + a pinned prompt appear. You type a command and
* the output is appended to the scrollback BELOW the input the input
* itself never moves.
* ===================================================================== */
* The homepage's interactive terminal. Flow: a short boot log streams
* in, the side chrome fades in alongside it, then the banner and a
* pinned prompt appear. You type a command and the output gets
* appended to the scrollback below the input, the input itself never
* moves. */
(function terminal() {
const root = document.getElementById("terminal");
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;
function loadArt() {
fetch("/arch.ascii").then(function (r) { return r.ok ? r.text() : ""; }).then(function (t) {
@ -23,7 +22,7 @@
}).catch(function () { });
}
// ---- ascii banner -------------------------------------------------------
/* ---- ascii banner ---- */
const BANNER = [
" ██████╗██╗ ██████╗ ██╗ ██╗███████╗",
"██╔════╝██║ ██╔═══██╗██║ ██║██╔════╝",
@ -33,7 +32,7 @@
" ╚═════╝╚══════╝ ╚═════╝ ╚═══╝ ╚══════╝"
].join("\n");
// ---- boot log -----------------------------------------------------------
/* ---- boot log ---- */
const BOOT = [
["info", "starting clovesh..."],
["info", "mounting /dev/estrogen..."],
@ -42,15 +41,15 @@
["ok", "modules loaded"],
["info", "summoning cats..."],
["ok", "oneko ready"],
["info", "connecting to discord via lanyard..."],
["info", "connecting to discord via restful..."],
["ok", "presence online"],
["info", "mounting button wall..."],
["ok", "88x31 buttons hung"],
["info", "starting terminal..."],
["ok", "ready type 'help'"]
["ok", "ready, type 'help'"]
];
// ---- build DOM ----------------------------------------------------------
/* ---- build DOM ---- */
root.innerHTML =
'<pre class="t-boot" id="t-boot" aria-hidden="true"></pre>' +
'<div class="t-main" id="t-main" hidden>' +
@ -76,7 +75,7 @@
return new Date().toLocaleTimeString("en-GB", { hour12: false });
}
// ---- command handlers ---------------------------------------------------
/* ---- command handlers ---- */
const COMMANDS = {
help() {
const rows = [
@ -123,7 +122,7 @@
about() {
return {
text:
"Clove Twilight fae/faer\n" +
"Clove Twilight, fae/faer\n" +
"Transfem developer from Southampton, UK. I make Projects,\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."
@ -160,7 +159,7 @@
},
};
// ---- runtime ------------------------------------------------------------
/* ---- runtime ---- */
const startedAt = Date.now();
function uptime() {
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 });
}
// ---- tab-complete + history --------------------------------------------
/* ---- tab-complete + history ---- */
const COMPLETIONS = Object.keys(COMMANDS);
function complete(prefix) {
if (!prefix) return null;
@ -250,7 +249,7 @@
if ((window.getSelection() + "") === "") input.focus();
});
// ---- boot then reveal ---------------------------------------------------
/* ---- boot then reveal ---- */
document.body.classList.add("term-booting");
let booted = false;

View File

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