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")]);
|
link("history \u2192", "?history")]);
|
||||||
showSec("On hold", hold, hold.length ? [link("history \u2192", "?history")] : []);
|
showSec("On hold", hold, hold.length ? [link("history \u2192", "?history")] : []);
|
||||||
showSec("Recently finished", done, done.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 ----------
|
// ---------- search ----------
|
||||||
@@ -343,23 +360,7 @@ async function showHistory() {
|
|||||||
main.innerHTML = "";
|
main.innerHTML = "";
|
||||||
main.append(el("h2", null, "History"));
|
main.append(el("h2", null, "History"));
|
||||||
if (!items.length) { main.append(el("p", "muted", "Nothing here.")); return; }
|
if (!items.length) { main.append(el("p", "muted", "Nothing here.")); return; }
|
||||||
const labels = {all: "all", watching: "watching", done: "completed", hold: "on hold"};
|
main.append(...histBoard(items));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function histCard(x) {
|
function histCard(x) {
|
||||||
@@ -415,13 +416,9 @@ const animeMeta = a => {
|
|||||||
return bits.join(" \u00b7 ");
|
return bits.join(" \u00b7 ");
|
||||||
};
|
};
|
||||||
const animeLine = a => `${animeMeta(a)} \u00b7 ${statusLabel(a)}`;
|
const animeLine = a => `${animeMeta(a)} \u00b7 ${statusLabel(a)}`;
|
||||||
async function showStats() {
|
// ---------- stats + history renderers (shared by pages and home widgets) ----------
|
||||||
main.innerHTML = "";
|
function statsCards(d) {
|
||||||
main.append(el("h2", null, "Stats \u2014 last 30 days"));
|
const out = el("div", null);
|
||||||
skel(5);
|
|
||||||
const d = await api("/api/stats?days=30");
|
|
||||||
main.innerHTML = "";
|
|
||||||
main.append(el("h2", null, "Stats \u2014 last 30 days"));
|
|
||||||
const cards = el("div", "grid stats");
|
const cards = el("div", "grid stats");
|
||||||
cards.append(
|
cards.append(
|
||||||
statCard("watch time", fmtMin(d.watch_minutes)),
|
statCard("watch time", fmtMin(d.watch_minutes)),
|
||||||
@@ -430,24 +427,16 @@ async function showStats() {
|
|||||||
statCard("on hold", `${d.on_hold} anime`),
|
statCard("on hold", `${d.on_hold} anime`),
|
||||||
statCard("best streak", `${d.best_streak} days`),
|
statCard("best streak", `${d.best_streak} days`),
|
||||||
statCard("sub \u00b7 dub plays", `${d.sub_dub.jpn} \u00b7 ${d.sub_dub.eng}`));
|
statCard("sub \u00b7 dub plays", `${d.sub_dub.jpn} \u00b7 ${d.sub_dub.eng}`));
|
||||||
main.append(cards);
|
out.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`));
|
if (d.errors > 0) out.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"];
|
return out;
|
||||||
const mx = Math.max(1, ...d.by_weekday);
|
}
|
||||||
const wk = el("div", "wk");
|
function topAnime(d, n = 0) {
|
||||||
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 order = {ongoing: 0, hold: 1, finished: 2};
|
const order = {ongoing: 0, hold: 1, finished: 2};
|
||||||
const list = [...d.top_anime].sort((x, y) =>
|
const list = [...d.top_anime].sort((x, y) =>
|
||||||
order[x.status] - order[y.status] || y.minutes - x.minutes);
|
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);
|
const ta = el("section", null);
|
||||||
ta.append(el("h2", null, "Most watched"));
|
ta.append(el("h2", null, "Most watched"));
|
||||||
for (const a of list) {
|
for (const a of list) {
|
||||||
@@ -470,8 +459,49 @@ async function showStats() {
|
|||||||
row.append(b);
|
row.append(b);
|
||||||
ta.append(row);
|
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) {
|
if (d.top_searches.length) {
|
||||||
main.append(el("section", null).append(
|
main.append(el("section", null).append(
|
||||||
el("h2", null, "Top searches"),
|
el("h2", null, "Top searches"),
|
||||||
|
|||||||
Reference in new issue
Block a user