Fix ZXDB pagination counters and navigation

Implement URL-driven pagination and correct total counts across ZXDB:
- Root /zxdb: SSR reads ?page; client syncs to SSR; Prev/Next as Links.
- Sub-index pages (genres, languages, machinetypes): parse ?page on server; use SSR props in clients; Prev/Next via Links.
- Labels browse (/zxdb/labels): dynamic SSR, reads ?q & ?page; typed count(*); client syncs to SSR; Prev/Next preserve q.
- Label detail (/zxdb/labels/[id]): tab-aware Prev/Next Links; counters from server.
- Repo: replace raw counts with typed Drizzle count(*) for reliable totals.

Signed-off-by: Junie <Junie@lucy.xalior.com>
This commit is contained in:
2025-12-12 16:11:12 +00:00
parent 54cfe4f175
commit 3ef3a16bc0
13 changed files with 238 additions and 143 deletions

View File

@@ -9,22 +9,21 @@ type Paged<T> = { items: T[]; page: number; pageSize: number; total: number };
type Payload = { label: Label | null; authored: Paged<Item>; published: Paged<Item> };
export default function LabelDetailClient({ id, initial }: { id: number; initial: Payload }) {
const [data] = useState<Payload>(initial);
const [tab, setTab] = useState<"authored" | "published">("authored");
const [page] = useState(1);
export default function LabelDetailClient({ id, initial, initialTab }: { id: number; initial: Payload; initialTab?: "authored" | "published" }) {
// Keep only interactive UI state (tab). Data should come directly from SSR props so it updates on navigation.
const [tab, setTab] = useState<"authored" | "published">(initialTab ?? "authored");
if (!data || !data.label) return <div className="alert alert-warning">Not found</div>;
if (!initial || !initial.label) return <div className="alert alert-warning">Not found</div>;
const current = useMemo(() => (tab === "authored" ? data.authored : data.published), [data, tab]);
const current = useMemo(() => (tab === "authored" ? initial.authored : initial.published), [initial, tab]);
const totalPages = useMemo(() => Math.max(1, Math.ceil(current.total / current.pageSize)), [current]);
return (
<div className="container">
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2">
<h1 className="mb-0">{data.label.name}</h1>
<h1 className="mb-0">{initial.label.name}</h1>
<div>
<span className="badge text-bg-light">{data.label.labeltypeId ?? "?"}</span>
<span className="badge text-bg-light">{initial.label.labeltypeId ?? "?"}</span>
</div>
</div>
@@ -66,7 +65,23 @@ export default function LabelDetailClient({ id, initial }: { id: number; initial
</div>
<div className="d-flex align-items-center gap-2 mt-2">
<span>Page {page} / {totalPages}</span>
<span>Page {current.page} / {totalPages}</span>
<div className="ms-auto d-flex gap-2">
<Link
className={`btn btn-sm btn-outline-secondary ${current.page <= 1 ? "disabled" : ""}`}
aria-disabled={current.page <= 1}
href={`/zxdb/labels/${id}?tab=${tab}&page=${Math.max(1, current.page - 1)}`}
>
Prev
</Link>
<Link
className={`btn btn-sm btn-outline-secondary ${current.page >= totalPages ? "disabled" : ""}`}
aria-disabled={current.page >= totalPages}
href={`/zxdb/labels/${id}?tab=${tab}&page=${Math.min(totalPages, current.page + 1)}`}
>
Next
</Link>
</div>
</div>
</div>
);