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:
@@ -1,51 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
type Label = { id: number; name: string; labeltypeId: string | null };
|
||||
type Paged<T> = { items: T[]; page: number; pageSize: number; total: number };
|
||||
|
||||
export default function LabelsSearch({ initial }: { initial?: Paged<Label> }) {
|
||||
const [q, setQ] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
export default function LabelsSearch({ initial, initialQ }: { initial?: Paged<Label>; initialQ?: string }) {
|
||||
const router = useRouter();
|
||||
const [q, setQ] = useState(initialQ ?? "");
|
||||
const [data, setData] = useState<Paged<Label> | null>(initial ?? null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const firstLoadSkipped = useRef(false);
|
||||
const pageSize = 20;
|
||||
const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]);
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (q) params.set("q", q);
|
||||
params.set("page", String(page));
|
||||
params.set("pageSize", String(pageSize));
|
||||
const res = await fetch(`/api/zxdb/labels/search?${params.toString()}`, { cache: "no-store" });
|
||||
const json = (await res.json()) as Paged<Label>;
|
||||
setData(json);
|
||||
} catch (e) {
|
||||
setData({ items: [], page: 1, pageSize, total: 0 });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Sync incoming SSR payload on navigation (e.g., when clicking Prev/Next Links)
|
||||
useEffect(() => {
|
||||
// If server provided initial data for first page of empty search, skip first fetch
|
||||
if (!firstLoadSkipped.current && initial && !q && page === 1) {
|
||||
firstLoadSkipped.current = true;
|
||||
return;
|
||||
}
|
||||
load();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page]);
|
||||
if (initial) setData(initial);
|
||||
}, [initial]);
|
||||
|
||||
// Keep input in sync with URL q on navigation
|
||||
useEffect(() => {
|
||||
setQ(initialQ ?? "");
|
||||
}, [initialQ]);
|
||||
|
||||
function submit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setPage(1);
|
||||
load();
|
||||
const params = new URLSearchParams();
|
||||
if (q) params.set("q", q);
|
||||
params.set("page", "1");
|
||||
router.push(`/zxdb/labels?${params.toString()}`);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -56,13 +39,12 @@ export default function LabelsSearch({ initial }: { initial?: Paged<Label> }) {
|
||||
<input className="form-control" placeholder="Search labels…" value={q} onChange={(e) => setQ(e.target.value)} />
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<button className="btn btn-primary" disabled={loading}>Search</button>
|
||||
<button className="btn btn-primary">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-3">
|
||||
{loading && <div>Loading…</div>}
|
||||
{data && data.items.length === 0 && !loading && <div className="alert alert-warning">No labels found.</div>}
|
||||
{data && data.items.length === 0 && <div className="alert alert-warning">No labels found.</div>}
|
||||
{data && data.items.length > 0 && (
|
||||
<ul className="list-group">
|
||||
{data.items.map((l) => (
|
||||
@@ -76,15 +58,23 @@ export default function LabelsSearch({ initial }: { initial?: Paged<Label> }) {
|
||||
</div>
|
||||
|
||||
<div className="d-flex align-items-center gap-2 mt-2">
|
||||
<button className="btn btn-outline-secondary" onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={loading || page <= 1}>
|
||||
Prev
|
||||
</button>
|
||||
<span>
|
||||
Page {data?.page ?? page} / {totalPages}
|
||||
</span>
|
||||
<button className="btn btn-outline-secondary" onClick={() => setPage((p) => p + 1)} disabled={loading || (data ? data.page >= totalPages : false)}>
|
||||
Next
|
||||
</button>
|
||||
<span>Page {data?.page ?? 1} / {totalPages}</span>
|
||||
<div className="ms-auto d-flex gap-2">
|
||||
<Link
|
||||
className={`btn btn-outline-secondary ${!data || (data.page <= 1) ? "disabled" : ""}`}
|
||||
aria-disabled={!data || data.page <= 1}
|
||||
href={`/zxdb/labels?${(() => { const p = new URLSearchParams(); if (q) p.set("q", q); p.set("page", String(Math.max(1, (data?.page ?? 1) - 1))); return p.toString(); })()}`}
|
||||
>
|
||||
Prev
|
||||
</Link>
|
||||
<Link
|
||||
className={`btn btn-outline-secondary ${!data || (data.page >= totalPages) ? "disabled" : ""}`}
|
||||
aria-disabled={!data || data.page >= totalPages}
|
||||
href={`/zxdb/labels?${(() => { const p = new URLSearchParams(); if (q) p.set("q", q); p.set("page", String(Math.min(totalPages, (data?.page ?? 1) + 1))); return p.toString(); })()}`}
|
||||
>
|
||||
Next
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user