// punch frontend — renders leaderboard.json, theme toggle, filter. const fmt = new Intl.NumberFormat("en-US"); const relTime = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); // zero-pad a count to six digits with commas: 3060 → "003,060", classic high-score board function padScore(n) { const s = String(n).padStart(6, "0"); return `${s.slice(0, 3)},${s.slice(3)}`; } const boardEl = document.getElementById("board"); const statusEl = document.getElementById("status"); const stampEl = document.getElementById("stamp"); const qEl = document.getElementById("q"); const themeEl = document.getElementById("theme"); const statEls = { pushes: document.getElementById("stat-pushes"), actors: document.getElementById("stat-actors"), knots: document.getElementById("stat-knots"), }; let rows = []; // ---- theme ---- const THEME_KEY = "punch:theme"; const THEMES = ["dark", "light", "auto"]; const THEME_GLYPH = { dark: "◑", light: "◐", auto: "◒" }; function applyTheme(t) { const theme = THEMES.includes(t) ? t : "dark"; document.documentElement.dataset.theme = theme; themeEl.textContent = THEME_GLYPH[theme]; themeEl.setAttribute("title", `theme: ${theme}`); localStorage.setItem(THEME_KEY, theme); } themeEl.addEventListener("click", () => { const cur = document.documentElement.dataset.theme ?? "dark"; const next = THEMES[(THEMES.indexOf(cur) + 1) % THEMES.length]; applyTheme(next); }); applyTheme(localStorage.getItem(THEME_KEY) ?? "dark"); // ---- data ---- // the CF Worker serves the leaderboard straight from KV so we sidestep // wisp's edge cache (max-age=600 meant 10-min staleness). const LEADERBOARD_URL = "https://punch-indexer.n8-3e9.workers.dev/leaderboard.json"; async function load() { try { const r = await fetch(LEADERBOARD_URL, { cache: "no-store" }); if (!r.ok) throw new Error(`http ${r.status}`); const payload = await r.json(); rows = payload.rows ?? []; render(); updateHeader(payload); } catch (err) { statusEl.hidden = false; statusEl.textContent = `error loading leaderboard: ${err.message}`; } } function updateHeader(data) { statEls.pushes.textContent = fmt.format(data.totalPushes ?? 0); statEls.actors.textContent = fmt.format(rows.length); statEls.knots.textContent = fmt.format(data.knots?.length ?? 0); if (data.status) { statusEl.hidden = false; statusEl.textContent = data.status; } else { statusEl.hidden = true; } if (data.updatedAt) { const when = new Date(data.updatedAt); const diff = Date.now() - when.getTime(); const mins = Math.round(diff / 60000); stampEl.textContent = mins < 1 ? "just now" : relTime.format(-mins, "minute"); stampEl.title = when.toLocaleString(); } } function render() { const query = qEl.value.trim().toLowerCase(); rows.forEach((r, i) => { r._originalRank = i + 1; }); const filtered = query ? rows.filter((r) => { const h = (r.handle ?? "").toLowerCase(); const d = (r.did ?? "").toLowerCase(); return h.includes(query) || d.includes(query); }) : rows; boardEl.replaceChildren(); if (filtered.length === 0) { const li = document.createElement("li"); li.className = "empty"; li.textContent = rows.length === 0 ? "no data yet" : "no matches"; boardEl.append(li); return; } const frag = document.createDocumentFragment(); for (const r of filtered) frag.append(rowEl(r)); boardEl.append(frag); } function rowEl(r) { const li = document.createElement("li"); li.className = "row"; // whole row is one link — clicking rank, avatar, handle, or count all go to // the committer's tangled profile (where their own punchcard graph lives). const a = document.createElement("a"); a.className = "row-link"; a.href = r.handle ? `https://tangled.org/${r.handle}` : "https://tangled.org/"; a.target = "_blank"; a.rel = "noopener"; a.title = r.did; const rank = document.createElement("span"); rank.className = "rank"; rank.textContent = String(r._originalRank).padStart(2, "0"); const who = document.createElement("div"); who.className = "who"; const av = document.createElement("div"); av.className = "avatar"; if (r.avatar) av.style.backgroundImage = `url(${r.avatar})`; const handle = document.createElement("span"); handle.className = "handle"; handle.textContent = r.handle ? r.handle : shortDid(r.did); who.append(av, handle); const count = document.createElement("span"); count.className = "count"; count.innerHTML = `${padScore(r.pushes)}pushes`; a.append(rank, who, count); li.append(a); return li; } function shortDid(did) { if (!did) return "?"; if (did.length <= 20) return did; return `${did.slice(0, 10)}…${did.slice(-6)}`; } qEl.addEventListener("input", render); load(); setInterval(load, 30_000);