AAAAAAAAAAA
This commit is contained in:
parent
fc619033b3
commit
4592f76d14
|
|
@ -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">
|
||||
|
||||
|
|
|
|||
111
js/core.js
111
js/core.js
|
|
@ -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,8 +713,8 @@ 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);
|
||||
}
|
||||
})();
|
||||
})();
|
||||
|
|
@ -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;
|
||||
|
|
@ -316,4 +320,4 @@
|
|||
openFromHash();
|
||||
}
|
||||
window.addEventListener("hashchange", openFromHash);
|
||||
})();
|
||||
})();
|
||||
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
// 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,9 +87,9 @@
|
|||
|
||||
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();
|
||||
});
|
||||
window.addEventListener("beforeunload", function () { clearInterval(timer); });
|
||||
})();
|
||||
})();
|
||||
|
|
@ -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, "&")
|
||||
|
|
@ -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");
|
||||
|
|
@ -262,4 +262,4 @@
|
|||
}
|
||||
|
||||
global.Guestbook = { reload: loadEntries };
|
||||
})(typeof window !== "undefined" ? window : this);
|
||||
})(typeof window !== "undefined" ? window : this);
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -200,4 +200,4 @@
|
|||
showMessage("Couldn't load the selfies right now.");
|
||||
})
|
||||
.finally(() => root.removeAttribute("aria-busy"));
|
||||
})();
|
||||
})();
|
||||
|
|
@ -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;
|
||||
|
|
@ -285,4 +284,4 @@
|
|||
loadArt();
|
||||
requestAnimationFrame(() => document.body.classList.add("term-chrome-in"));
|
||||
streamBoot(0);
|
||||
})();
|
||||
})();
|
||||
|
|
@ -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 ||
|
||||
|
|
|
|||
Loading…
Reference in New Issue