pushes on tangled
sites.wisp.place/zzstoatzz.io/punch
fun
tangled
1// punch frontend — renders leaderboard.json, theme toggle, filter.
2
3const fmt = new Intl.NumberFormat("en-US");
4const relTime = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
5
6// zero-pad a count to six digits with commas: 3060 → "003,060", classic high-score board
7function padScore(n) {
8 const s = String(n).padStart(6, "0");
9 return `${s.slice(0, 3)},${s.slice(3)}`;
10}
11
12const boardEl = document.getElementById("board");
13const statusEl = document.getElementById("status");
14const stampEl = document.getElementById("stamp");
15const qEl = document.getElementById("q");
16const themeEl = document.getElementById("theme");
17
18const statEls = {
19 pushes: document.getElementById("stat-pushes"),
20 actors: document.getElementById("stat-actors"),
21 knots: document.getElementById("stat-knots"),
22};
23
24let rows = [];
25
26// ---- theme ----
27
28const THEME_KEY = "punch:theme";
29const THEMES = ["dark", "light", "auto"];
30const THEME_GLYPH = { dark: "◑", light: "◐", auto: "◒" };
31
32function applyTheme(t) {
33 const theme = THEMES.includes(t) ? t : "dark";
34 document.documentElement.dataset.theme = theme;
35 themeEl.textContent = THEME_GLYPH[theme];
36 themeEl.setAttribute("title", `theme: ${theme}`);
37 localStorage.setItem(THEME_KEY, theme);
38}
39
40themeEl.addEventListener("click", () => {
41 const cur = document.documentElement.dataset.theme ?? "dark";
42 const next = THEMES[(THEMES.indexOf(cur) + 1) % THEMES.length];
43 applyTheme(next);
44});
45
46applyTheme(localStorage.getItem(THEME_KEY) ?? "dark");
47
48// ---- data ----
49
50// the CF Worker serves the leaderboard straight from KV so we sidestep
51// wisp's edge cache (max-age=600 meant 10-min staleness).
52const LEADERBOARD_URL = "https://punch-indexer.n8-3e9.workers.dev/leaderboard.json";
53
54async function load() {
55 try {
56 const r = await fetch(LEADERBOARD_URL, { cache: "no-store" });
57 if (!r.ok) throw new Error(`http ${r.status}`);
58 const payload = await r.json();
59 rows = payload.rows ?? [];
60 render();
61 updateHeader(payload);
62 } catch (err) {
63 statusEl.hidden = false;
64 statusEl.textContent = `error loading leaderboard: ${err.message}`;
65 }
66}
67
68function updateHeader(data) {
69 statEls.pushes.textContent = fmt.format(data.totalPushes ?? 0);
70 statEls.actors.textContent = fmt.format(rows.length);
71 statEls.knots.textContent = fmt.format(data.knots?.length ?? 0);
72
73 if (data.status) {
74 statusEl.hidden = false;
75 statusEl.textContent = data.status;
76 } else {
77 statusEl.hidden = true;
78 }
79
80 if (data.updatedAt) {
81 const when = new Date(data.updatedAt);
82 const diff = Date.now() - when.getTime();
83 const mins = Math.round(diff / 60000);
84 stampEl.textContent = mins < 1
85 ? "just now"
86 : relTime.format(-mins, "minute");
87 stampEl.title = when.toLocaleString();
88 }
89}
90
91function render() {
92 const query = qEl.value.trim().toLowerCase();
93
94 rows.forEach((r, i) => {
95 r._originalRank = i + 1;
96 });
97
98 const filtered = query
99 ? rows.filter((r) => {
100 const h = (r.handle ?? "").toLowerCase();
101 const d = (r.did ?? "").toLowerCase();
102 return h.includes(query) || d.includes(query);
103 })
104 : rows;
105
106 boardEl.replaceChildren();
107
108 if (filtered.length === 0) {
109 const li = document.createElement("li");
110 li.className = "empty";
111 li.textContent = rows.length === 0 ? "no data yet" : "no matches";
112 boardEl.append(li);
113 return;
114 }
115
116 const frag = document.createDocumentFragment();
117 for (const r of filtered) frag.append(rowEl(r));
118 boardEl.append(frag);
119}
120
121function rowEl(r) {
122 const li = document.createElement("li");
123 li.className = "row";
124
125 // whole row is one link — clicking rank, avatar, handle, or count all go to
126 // the committer's tangled profile (where their own punchcard graph lives).
127 const a = document.createElement("a");
128 a.className = "row-link";
129 a.href = r.handle ? `https://tangled.org/${r.handle}` : "https://tangled.org/";
130 a.target = "_blank";
131 a.rel = "noopener";
132 a.title = r.did;
133
134 const rank = document.createElement("span");
135 rank.className = "rank";
136 rank.textContent = String(r._originalRank).padStart(2, "0");
137
138 const who = document.createElement("div");
139 who.className = "who";
140
141 const av = document.createElement("div");
142 av.className = "avatar";
143 if (r.avatar) av.style.backgroundImage = `url(${r.avatar})`;
144
145 const handle = document.createElement("span");
146 handle.className = "handle";
147 handle.textContent = r.handle ? r.handle : shortDid(r.did);
148
149 who.append(av, handle);
150
151 const count = document.createElement("span");
152 count.className = "count";
153 count.innerHTML = `${padScore(r.pushes)}<span class="unit">pushes</span>`;
154
155 a.append(rank, who, count);
156 li.append(a);
157 return li;
158}
159
160function shortDid(did) {
161 if (!did) return "?";
162 if (did.length <= 20) return did;
163 return `${did.slice(0, 10)}…${did.slice(-6)}`;
164}
165
166qEl.addEventListener("input", render);
167
168load();
169setInterval(load, 30_000);