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:
1 parent
bb825a3acb
commit
7d9fab6bd9
1 file changed
+70
-40
+70
-40
@@ -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"),
|
||||
|
||||
Reference in new issue
Block a user