Home: status board instead of a feed

- replace 'Recently played' with one card per anime grouped by state:
  Continue watching (<=6, progress + ~Xm left), On hold (<=4, dimmed,
  days ago), Recently finished (<=6, check)
- hero button says 'Next ep' when the last ep is >=95% watched
- empty home shows recent searches as clickable chips (race-guarded:
  only appended while the empty state is still on screen)
- helpers: byShow (latest row per anime), homeCard, showSec, link
This commit is contained in:
jozamudi committed 2026-08-24 20:09:39 +00:00
1 parent 1de58629a0
commit bb825a3acb
2 files changed
+63 -35

No files matched your search

+62 -35
View File
@@ -85,10 +85,60 @@ const bar = (x, cls) => {
return b;
};
const histHref = x => `?anime=${x.slug_id}&season=${x.season_id}&ep=${x.ep_id}&lang=${x.lang}`;
// one entry per anime (latest row wins), newest first
const byShow = () => {
const m = new Map();
for (const x of history) {
const k = x.slug_id + ":" + x.season_id;
if (!m.has(k) || x.t > m.get(k).t) m.set(k, x);
}
return [...m.values()].sort((a, b) => b.t - a.t);
};
const homeCard = x => {
const done = histClass(x) === "done", hold = histClass(x) === "hold";
const c = el("a", "card" + (hold ? " hold" : ""));
c.href = histHref(x);
if (x.poster) {
const i = el("img"); i.src = x.poster; i.alt = "";
c.append(i);
}
let meta = `E${x.ep} \u00b7 ${x.lang === "eng" ? "dub" : "sub"}`;
if (done) meta += " \u00b7 \u2713";
else if (x.dur > 0 && x.pos < x.dur * 0.95) meta += ` \u00b7 ${fmtMin((x.dur - x.pos) / 60)} left`;
else if (hold) meta += ` \u00b7 ${Math.round(Date.now() / 1000 / 86400 - x.t / 86400)}d ago`;
const info = el("div", "mini-info");
info.append(el("div", "card-name", x.title), el("div", "mini-meta", meta));
const pb = bar(x, "small");
if (pb) info.append(pb);
c.append(info);
return c;
};
const showSec = (title, items, extra) => {
if (!items.length) return;
const head = el("div", "sec-head");
head.append(el("h2", null, title));
for (const a of extra) head.append(a);
const grid = el("div", "grid mini");
for (const x of items) grid.append(homeCard(x));
main.append(head, grid);
};
const link = (text, href) => Object.assign(el("a", null, text), {href: BASE + href});
function showHome() {
main.innerHTML = "";
if (!history.length) { main.append(el("p", "muted", "Search for an anime to get started.")); return; }
if (!history.length) {
main.append(el("p", "muted empty", "Search for an anime to get started."));
api("/api/searches").then(rs => {
if (!rs.length || !main.querySelector("p.empty")) return;
const d = el("div", null);
d.append(el("div", "muted", "recent searches"));
const chips = el("div", "chips");
for (const q of rs) chips.append(link(q, "?q=" + encodeURIComponent(q)));
d.append(chips);
main.append(d);
}).catch(() => {});
return;
}
const hero = history[0];
// 1. continue-watching hero
const sec = el("section", "hero-sec");
@@ -107,44 +157,21 @@ function showHome() {
` · ${timeago(hero.t)}`));
const p = bar(hero);
if (p) body.append(p);
const go = el("button", "btn resume", "▶ Resume");
const heroDone = hero.dur > 0 && hero.pos >= hero.dur * 0.95;
const go = el("button", "btn resume", heroDone ? "\u25b6 Next ep" : "\u25b6 Resume");
go.onclick = () => location.href = histHref(hero);
right.append(body, go);
sec.append(card, right);
main.append(sec);
// 2-4. the rest as mini poster cards with progress + time-ago
if (history.length > 1) {
const head = el("div", "sec-head");
head.append(el("h2", null, "Recently played"));
const st = el("a", null, "stats \u2192");
st.href = BASE + "?stats";
head.append(st);
if (history.length >= 5) {
const va = el("a", null, "view all \u2192");
va.href = BASE + "?history";
head.append(va);
}
main.append(head);
const grid = el("div", "grid mini");
for (const x of history.slice(1, 6)) {
const c = el("a", "card");
c.href = histHref(x);
if (x.poster) {
const i = el("img"); i.src = x.poster; i.alt = "";
c.append(i);
}
const info = el("div", "mini-info");
info.append(el("div", "card-name", x.title));
const meta = el("div", "mini-meta");
meta.textContent = `E${x.ep} · ${x.lang === "eng" ? "dub" : "sub"} · ${timeago(x.t)}`;
info.append(meta);
const pb = bar(x, "small");
if (pb) info.append(pb);
c.append(info);
grid.append(c);
}
main.append(grid);
}
// 2-4. status board: one card per anime, grouped by state
const shows = byShow().filter(x => x !== hero);
const cont = shows.filter(x => histClass(x) === "watching").slice(0, 6);
const hold = shows.filter(x => histClass(x) === "hold").slice(0, 4);
const done = shows.filter(x => histClass(x) === "done").slice(0, 6);
showSec("Continue watching", cont, [link("stats \u2192", "?stats"),
link("history \u2192", "?history")]);
showSec("On hold", hold, hold.length ? [link("history \u2192", "?history")] : []);
showSec("Recently finished", done, done.length ? [link("history \u2192", "?history")] : []);
}
// ---------- search ----------
+1
View File
@@ -9,6 +9,7 @@ header form { flex: 1; max-width: 420px; }
button.tiny, a.tiny { padding: .2rem .55rem; font-size: .8rem; border-radius: 6px;
border: 1px solid var(--bd); background: var(--card); color: var(--fg); cursor: pointer; }
a.tiny:hover { border-color: var(--acc); }
a.chip:hover { border-color: var(--acc); }
button.tiny.danger { border-color: #7f1d1d; color: #f87171; }
#menubox { position: relative; }
#menu-items { display: none; position: absolute; right: 0; top: calc(100% + 4px);