news-digest: live-recompute source + desk counts; hide empty sections

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.
This commit is contained in:
vh
2026-04-26 15:10:18 -07:00
parent 0f03c6c9f6
commit dce0fb9b66
2 changed files with 35 additions and 0 deletions
+27
View File
@@ -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();
});
})();
+8
View File
@@ -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;