pushes on tangled sites.wisp.place/zzstoatzz.io/punch
fun tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

punch / public / app.js
4.9 kB 169 lines
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);