Home: stats + history widgets inline

- extract shared renderers: statsCards(), topAnime(d, n), histBoard(items, limit)
- home now embeds a compact stats widget (summary cards + top 3 most
  watched, async with loading placeholder) and a history widget
  (filter chips + first 6 cards, data already in memory)
- full ?stats and ?history pages remain as drill-downs, now built on
  the same renderers; showStats also loses a duplicated header it
  rendered twice around the await
This commit is contained in:
jozamudi committed 2026-08-24 22:18:23 +00:00
1 parent bb825a3acb
commit 7d9fab6bd9
1 file changed
+70 -40
+70 -40
View File
@@ -172,6 +172,23 @@ function showHome() {
link("history \u2192", "?history")]);
showSec("On hold", hold, hold.length ? [link("history \u2192", "?history")] : []);
showSec("Recently finished", done, done.length ? [link("history \u2192", "?history")] : []);
// 5. stats widget (compact, drill-down to ?stats)
const sw = el("div", null);
const sh = el("div", "sec-head");
sh.append(el("h2", null, "Stats \u00b7 30d"), link("see all \u2192", "?stats"));
const load = el("p", "muted", "loading\u2026");
sw.append(sh, load);
main.append(sw);
api("/api/stats?days=30").then(d => {
load.remove();
sw.append(statsCards(d));
const ta = topAnime(d, 3);
if (ta) sw.append(ta);
}).catch(() => load.remove());
// 6. history widget (data already loaded, capped at 6)
const hh = el("div", "sec-head");
hh.append(el("h2", null, "History"), link(`all ${history.length} \u2192`, "?history"));
main.append(hh, ...histBoard(history, 6));
}
// ---------- search ----------
@@ -343,23 +360,7 @@ async function showHistory() {
main.innerHTML = "";
main.append(el("h2", null, "History"));
if (!items.length) { main.append(el("p", "muted", "Nothing here.")); return; }
const labels = {all: "all", watching: "watching", done: "completed", hold: "on hold"};
let active = "all";
const chips = el("div", "chips");
const grid = el("div", "grid");
const draw = () => {
grid.replaceChildren(...items.filter(x => active === "all" || histClass(x) === active).map(histCard));
for (const c of chips.children) c.classList.toggle("on", c.dataset.f === active);
};
for (const f of Object.keys(labels)) {
const b = el("button", "chip", labels[f]);
b.dataset.f = f;
b.onclick = () => { active = f; draw(); };
chips.append(b);
}
main.append(chips);
draw();
main.append(grid);
main.append(...histBoard(items));
}
function histCard(x) {
@@ -415,13 +416,9 @@ const animeMeta = a => {
return bits.join(" \u00b7 ");
};
const animeLine = a => `${animeMeta(a)} \u00b7 ${statusLabel(a)}`;
async function showStats() {
main.innerHTML = "";
main.append(el("h2", null, "Stats \u2014 last 30 days"));
skel(5);
const d = await api("/api/stats?days=30");
main.innerHTML = "";
main.append(el("h2", null, "Stats \u2014 last 30 days"));
// ---------- stats + history renderers (shared by pages and home widgets) ----------
function statsCards(d) {
const out = el("div", null);
const cards = el("div", "grid stats");
cards.append(
statCard("watch time", fmtMin(d.watch_minutes)),
@@ -430,24 +427,16 @@ async function showStats() {
statCard("on hold", `${d.on_hold} anime`),
statCard("best streak", `${d.best_streak} days`),
statCard("sub \u00b7 dub plays", `${d.sub_dub.jpn} \u00b7 ${d.sub_dub.eng}`));
main.append(cards);
if (d.errors > 0) main.append(el("p", "muted", `${d.errors} playback error${d.errors > 1 ? "s" : ""} in the last ${d.days} days`));
const WD = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const mx = Math.max(1, ...d.by_weekday);
const wk = el("div", "wk");
d.by_weekday.forEach((s, i) => {
const col = el("div", "wk-col");
const b = el("div", "wk-bar");
b.style.height = Math.max(3, Math.round(s / mx * 50)) + "px";
b.title = fmtMin(s / 60);
col.append(b, el("div", "wk-day", WD[i]));
wk.append(col);
});
main.append(wk);
out.append(cards);
if (d.errors > 0) out.append(el("p", "muted", `${d.errors} playback error${d.errors > 1 ? "s" : ""} in the last ${d.days} days`));
return out;
}
function topAnime(d, n = 0) {
const order = {ongoing: 0, hold: 1, finished: 2};
const list = [...d.top_anime].sort((x, y) =>
order[x.status] - order[y.status] || y.minutes - x.minutes);
if (list.length) {
if (n) list.length = Math.min(n, list.length);
if (!list.length) return null;
const ta = el("section", null);
ta.append(el("h2", null, "Most watched"));
for (const a of list) {
@@ -470,8 +459,49 @@ async function showStats() {
row.append(b);
ta.append(row);
}
main.append(ta);
return ta;
}
function histBoard(items, limit = 0) {
const labels = {all: "all", watching: "watching", done: "completed", hold: "on hold"};
let active = "all";
const chips = el("div", "chips");
const grid = el("div", "grid");
const draw = () => {
let f = items.filter(x => active === "all" || histClass(x) === active);
if (limit) f = f.slice(0, limit);
grid.replaceChildren(...f.map(histCard));
for (const c of chips.children) c.classList.toggle("on", c.dataset.f === active);
};
for (const f of Object.keys(labels)) {
const b = el("button", "chip", labels[f]);
b.dataset.f = f;
b.onclick = () => { active = f; draw(); };
chips.append(b);
}
draw();
return [chips, grid];
}
async function showStats() {
skel(5);
const d = await api("/api/stats?days=30");
main.innerHTML = "";
main.append(el("h2", null, "Stats \u2014 last 30 days"));
main.append(statsCards(d));
const WD = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const mx = Math.max(1, ...d.by_weekday);
const wk = el("div", "wk");
d.by_weekday.forEach((s, i) => {
const col = el("div", "wk-col");
const b = el("div", "wk-bar");
b.style.height = Math.max(3, Math.round(s / mx * 50)) + "px";
b.title = fmtMin(s / 60);
col.append(b, el("div", "wk-day", WD[i]));
wk.append(col);
});
main.append(wk);
const ta = topAnime(d);
if (ta) main.append(ta);
if (d.top_searches.length) {
main.append(el("section", null).append(
el("h2", null, "Top searches"),