From 01c22530516dc400418914e035d0f00605da4f5d Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Thu, 15 Oct 2026 11:31:51 +0200 Subject: [PATCH] feat(web): history view helpers Co-Authored-By: Claude Opus 5.5 --- src/web/view-model.ts | 26 ++++++++++++++++++++++++++ test/view-model.test.ts | 21 +++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/web/view-model.ts b/src/web/view-model.ts index aac7ed8..5a19cb0 100644 --- a/src/web/view-model.ts +++ b/src/web/view-model.ts @@ -3,6 +3,7 @@ import type { HostId, HostOptions } from "../core/hosting/host"; import { HOSTS } from "../core/hosting/registry"; import type { Granularity, InspectedCover } from "../core/model"; import type { Stage } from "../core/pipeline"; +import type { Marks, SearchSummary } from "../core/saved"; import type { ReviewCard } from "../core/session"; export interface ServerConfig { @@ -120,3 +121,28 @@ export function timelineGroups(cards: readonly ReviewCard[], now = new Date()): } return [...groups].map(([year, list]) => ({ year, cards: list })); } + +/** History list times: recent ones relative, older ones as a date. */ +export function relativeTime(iso: string, now = new Date()): string { + const ms = now.getTime() - new Date(iso).getTime(); + const min = Math.floor(ms / 60_000); + if (min < 1) return "just now"; + if (min < 60) return `${min} min ago`; + const h = Math.floor(min / 60); + if (h < 24) return `${h} h ago`; + const days = Math.floor(h / 24); + if (days === 1) return "yesterday"; + if (days < 7) return `${days} days ago`; + return iso.slice(0, 10); +} + +export function markText(m: Marks): string[] { + return [ + ...(m.downloaded ? ["downloaded"] : []), + ...(m.rehosted ? [`rehosted on ${m.rehosted.host}`] : []), + ...(m.filed ? [`filed #${m.filed.submissionId}`] : []), + ]; +} + +export const summaryLine = (s: Pick): string => + `${s.fresh} new${s.present ? ` · ${s.present} on MangaBaka` : ""}`; diff --git a/test/view-model.test.ts b/test/view-model.test.ts index ff47895..8fa431f 100644 --- a/test/view-model.test.ts +++ b/test/view-model.test.ts @@ -71,3 +71,24 @@ describe("seen ranges and the timeline", () => { expect(groups[1]!.cards).toEqual([mid]); }); }); + +test("relative time for the history list", async () => { + const { relativeTime } = await import("../src/web/view-model"); + const now = new Date("2026-10-15T12:00:00Z"); + const ago = (ms: number) => new Date(now.getTime() - ms).toISOString(); + expect(relativeTime(ago(20_000), now)).toBe("just now"); + expect(relativeTime(ago(5 * 60_000), now)).toBe("5 min ago"); + expect(relativeTime(ago(3 * 3_600_000), now)).toBe("3 h ago"); + expect(relativeTime(ago(30 * 3_600_000), now)).toBe("yesterday"); + expect(relativeTime(ago(4 * 86_400_000), now)).toBe("4 days ago"); + expect(relativeTime(ago(40 * 86_400_000), now)).toBe("2026-09-05"); +}); + +test("history marks and summary line", async () => { + const { markText, summaryLine } = await import("../src/web/view-model"); + expect(markText({})).toEqual([]); + expect(markText({ downloaded: "x", rehosted: { at: "x", host: "litterbox" }, filed: { at: "x", submissionId: 77 } })) + .toEqual(["downloaded", "rehosted on litterbox", "filed #77"]); + expect(summaryLine({ fresh: 14, present: 3 })).toBe("14 new · 3 on MangaBaka"); + expect(summaryLine({ fresh: 1, present: 0 })).toBe("1 new"); +});