From dce0fb9b663d7618919f0d631b70942f2540343a Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Sun, 26 Apr 2026 15:10:18 -0700 Subject: [PATCH] news-digest: live-recompute source + desk counts; hide empty sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server-rendered .source-count / .desk-count badges were correct at render time but went stale the moment the user hid anything — "r/HOMELAB (4)" stayed at 4 even after all 4 items were hidden. Worse, the entire source header still rendered with a (0) badge once every item underneath was gone. app.js gains a refreshCounts() pass that walks every .source and .desk, recomputes the visible (non-.is-hidden) child count, updates the badge text, and toggles an .is-empty class. CSS rule for .source.is-empty and .desk.is-empty sets display:none so empty groups collapse out entirely. Hooked into hideItem, restoreItem, and the initial-paint hidden-set application. --- stacks/news-digest/templates/app.js | 27 ++++++++++++++++++++++++++ stacks/news-digest/templates/style.css | 8 ++++++++ 2 files changed, 35 insertions(+) diff --git a/stacks/news-digest/templates/app.js b/stacks/news-digest/templates/app.js index 8f54efd..d089941 100644 --- a/stacks/news-digest/templates/app.js +++ b/stacks/news-digest/templates/app.js @@ -61,6 +61,28 @@ return r.json(); } + /* ── live counts ───────────────────────────────────────────────── + The server-rendered .source-count and .desk-count badges are + accurate at render time, but they fall stale as soon as the user + hides anything. Recompute from the DOM whenever the visible set + changes. Empty sources / desks get an .is-empty class that hides + them entirely (no point showing "r/homelab (0)"). */ + + function refreshCounts() { + document.querySelectorAll(".source").forEach((src) => { + const visible = src.querySelectorAll(".item:not(.is-hidden)").length; + const badge = src.querySelector(".source-count"); + if (badge) badge.textContent = String(visible); + src.classList.toggle("is-empty", visible === 0); + }); + document.querySelectorAll(".desk").forEach((desk) => { + const visibleItems = desk.querySelectorAll(".item:not(.is-hidden)").length; + const badge = desk.querySelector(".desk-count"); + if (badge) badge.textContent = `${visibleItems} items`; + desk.classList.toggle("is-empty", visibleItems === 0); + }); + } + /* ── tray rendering ─────────────────────────────────────────────── */ function refreshTrayVisibility() { @@ -122,6 +144,7 @@ // Optimistic — flip class first, talk to server next. el.classList.add(HIDE_CLASS); addTrayRow(id, titleOf(el)); + refreshCounts(); try { await apiHide(id, true); @@ -129,6 +152,7 @@ console.warn("[digest] hide failed, rolling back:", e); el.classList.remove(HIDE_CLASS); removeTrayRow(id); + refreshCounts(); } } @@ -144,12 +168,14 @@ } el.classList.remove(HIDE_CLASS); removeTrayRow(id); + refreshCounts(); try { await apiHide(id, false); } catch (e) { console.warn("[digest] restore failed, re-hiding:", e); el.classList.add(HIDE_CLASS); addTrayRow(id, titleOf(el)); + refreshCounts(); } } @@ -187,5 +213,6 @@ } }); if (trayHadAdditions) refreshTrayVisibility(); + refreshCounts(); }); })(); diff --git a/stacks/news-digest/templates/style.css b/stacks/news-digest/templates/style.css index e72462c..4cf47c2 100644 --- a/stacks/news-digest/templates/style.css +++ b/stacks/news-digest/templates/style.css @@ -632,6 +632,14 @@ a:hover { color: var(--accent); } display: none; } +/* When every item in a .source (or every source in a .desk) is hidden, + collapse the whole header out — no point rendering "r/homelab (0)". + app.js applies .is-empty when the visible-count drops to zero. */ +.source.is-empty, +.desk.is-empty { + display: none; +} + .hidden-tray { margin-top: 24px; padding: 14px 18px;