feat(web): history view helpers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-15 11:31:51 +02:00
1 parent 2293d2810f
commit 01c2253051
2 files changed
+47

No files matched your search

+26
View File
@@ -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<SearchSummary, "fresh" | "present">): string =>
`${s.fresh} new${s.present ? ` · ${s.present} on MangaBaka` : ""}`;
+21
View File
@@ -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");
});