Standardize ZXDB UI; add SSR search/tables

Unify the look and feel of all /zxdb pages and minimize client pop-in.

- Make all /zxdb pages full-width to match /explorer
- Convert Languages, Genres, Machine Types, and Labels lists to
  Bootstrap tables with table-striped and table-hover inside
  table-responsive wrappers
- Replace raw FK IDs with linked names via SSR repository joins
- Add scoped search boxes on detail pages (labels, genres, languages,
  machine types) with SSR filtering and pagination that preserves q/tab
- Keep explorer results consistent: show Machine/Language names with
  links, no client lookups required

This improves consistency, readability, and first paint stability across
the ZXDB section while keeping navigation fast and discoverable.

Signed-off-by: Junie@lucy.xalior.com
This commit is contained in:
2025-12-12 16:58:50 +00:00
parent ddbf72ea52
commit 240936a850
18 changed files with 873 additions and 147 deletions

View File

@@ -2,16 +2,20 @@
import Link from "next/link";
import { useMemo, useState } from "react";
import { useRouter } from "next/navigation";
type Label = { id: number; name: string; labeltypeId: string | null };
type Item = { id: number; title: string; isXrated: number; machinetypeId: number | null; languageId: string | null };
type Item = { id: number; title: string; isXrated: number; machinetypeId: number | null; machinetypeName?: string | null; languageId: string | null; languageName?: string | null };
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, initialTab }: { id: number; initial: Payload; initialTab?: "authored" | "published" }) {
export default function LabelDetailClient({ id, initial, initialTab, initialQ }: { id: number; initial: Payload; initialTab?: "authored" | "published"; initialQ?: string }) {
// 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");
const [q, setQ] = useState(initialQ ?? "");
const router = useRouter();
// Names are now delivered by SSR payload to minimize pop-in.
if (!initial || !initial.label) return <div className="alert alert-warning">Not found</div>;
@@ -19,7 +23,7 @@ export default function LabelDetailClient({ id, initial, initialTab }: { id: num
const totalPages = useMemo(() => Math.max(1, Math.ceil(current.total / current.pageSize)), [current]);
return (
<div className="container">
<div>
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2">
<h1 className="mb-0">{initial.label.name}</h1>
<div>
@@ -36,6 +40,15 @@ export default function LabelDetailClient({ id, initial, initialTab }: { id: num
</li>
</ul>
<form className="row gy-2 gx-2 align-items-center mt-2" onSubmit={(e) => { e.preventDefault(); const p = new URLSearchParams(); p.set("tab", tab); if (q) p.set("q", q); p.set("page", "1"); router.push(`/zxdb/labels/${id}?${p.toString()}`); }}>
<div className="col-sm-8 col-md-6 col-lg-4">
<input className="form-control" placeholder={`Search within ${tab}`} value={q} onChange={(e) => setQ(e.target.value)} />
</div>
<div className="col-auto">
<button className="btn btn-primary">Search</button>
</div>
</form>
<div className="mt-3">
{current && current.items.length === 0 && <div className="alert alert-warning">No entries.</div>}
{current && current.items.length > 0 && (
@@ -45,8 +58,8 @@ export default function LabelDetailClient({ id, initial, initialTab }: { id: num
<tr>
<th style={{ width: 80 }}>ID</th>
<th>Title</th>
<th style={{ width: 120 }}>Machine</th>
<th style={{ width: 80 }}>Lang</th>
<th style={{ width: 160 }}>Machine</th>
<th style={{ width: 120 }}>Language</th>
</tr>
</thead>
<tbody>
@@ -54,8 +67,28 @@ export default function LabelDetailClient({ id, initial, initialTab }: { id: num
<tr key={it.id}>
<td>{it.id}</td>
<td><Link href={`/zxdb/entries/${it.id}`}>{it.title}</Link></td>
<td>{it.machinetypeId ?? "-"}</td>
<td>{it.languageId ?? "-"}</td>
<td>
{it.machinetypeId != null ? (
it.machinetypeName ? (
<Link href={`/zxdb/machinetypes/${it.machinetypeId}`}>{it.machinetypeName}</Link>
) : (
<span>{it.machinetypeId}</span>
)
) : (
<span className="text-secondary">-</span>
)}
</td>
<td>
{it.languageId ? (
it.languageName ? (
<Link href={`/zxdb/languages/${it.languageId}`}>{it.languageName}</Link>
) : (
<span>{it.languageId}</span>
)
) : (
<span className="text-secondary">-</span>
)}
</td>
</tr>
))}
</tbody>
@@ -70,14 +103,14 @@ export default function LabelDetailClient({ id, initial, initialTab }: { id: num
<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)}`}
href={`/zxdb/labels/${id}?${(() => { const p = new URLSearchParams(); p.set("tab", tab); if (q) p.set("q", q); p.set("page", String(Math.max(1, current.page - 1))); return p.toString(); })()}`}
>
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)}`}
href={`/zxdb/labels/${id}?${(() => { const p = new URLSearchParams(); p.set("tab", tab); if (q) p.set("q", q); p.set("page", String(Math.min(totalPages, current.page + 1))); return p.toString(); })()}`}
>
Next
</Link>