reading: split the book tracker into books, papers and articles (kind column + migration classifying 20 papers), segmented tabs with counts, article cards that open the link, owner + add link with server-side unfurl, kind switch on detail pages, /trackers/reading alias
All checks were successful
Deploy production / deploy (push) Successful in 1m8s

This commit is contained in:
2026-10-25 13:42:51 +00:00
parent 7b5faf6e74
commit e31e1be73f
20 changed files with 27592 additions and 247 deletions

View File

@@ -1,4 +1,18 @@
// Shared (client + server) types and helpers for the book tracker.
export const bookKinds = ['book', 'paper', 'article'] as const;
export type BookKind = (typeof bookKinds)[number];
export const isBookKind = (v: unknown): v is BookKind =>
typeof v === 'string' && (bookKinds as readonly string[]).includes(v);
/** Tab labels and the `?kind=` value for each (plural, as in the URL). */
export const kindTabs: Record<BookKind, { label: string; param: string }> = {
book: { label: 'books', param: 'books' },
paper: { label: 'papers', param: 'papers' },
article: { label: 'articles', param: 'articles' }
};
export function kindFromParam(param: string | null): BookKind {
const hit = bookKinds.find((k) => kindTabs[k].param === param || k === param);
return hit ?? 'book';
}
export const readingStatuses = ['want_to_read', 'reading', 'finished', 'abandoned'] as const;
export type ReadingStatus = (typeof readingStatuses)[number];
@@ -24,6 +38,11 @@ export const formatPreference = ['epub', 'azw3', 'mobi', 'azw', 'fb2', 'pdf', 'c
/** One card in the library. Public fields only, unless noted. */
export type BookCard = {
id: number;
kind: BookKind;
url: string | null; // articles/papers on the web
site: string | null;
publishedAt: string | null;
excerpt: string | null; // articles only (short description)
title: string;
subtitle: string | null;
authors: string[];