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
92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
type Genre = { id: number; name: string };
|
|
type Paged<T> = { items: T[]; page: number; pageSize: number; total: number };
|
|
|
|
export default function GenresSearch({ initial, initialQ }: { initial?: Paged<Genre>; initialQ?: string }) {
|
|
const router = useRouter();
|
|
const [q, setQ] = useState(initialQ ?? "");
|
|
const [data, setData] = useState<Paged<Genre> | null>(initial ?? null);
|
|
const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]);
|
|
|
|
useEffect(() => {
|
|
if (initial) setData(initial);
|
|
}, [initial]);
|
|
|
|
useEffect(() => {
|
|
setQ(initialQ ?? "");
|
|
}, [initialQ]);
|
|
|
|
function submit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
const params = new URLSearchParams();
|
|
if (q) params.set("q", q);
|
|
params.set("page", "1");
|
|
router.push(`/zxdb/genres?${params.toString()}`);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<h1>Genres</h1>
|
|
<form className="row gy-2 gx-2 align-items-center" onSubmit={submit}>
|
|
<div className="col-sm-8 col-md-6 col-lg-4">
|
|
<input className="form-control" placeholder="Search genres…" 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">
|
|
{data && data.items.length === 0 && <div className="alert alert-warning">No genres found.</div>}
|
|
{data && data.items.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table table-striped table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th style={{ width: 120 }}>ID</th>
|
|
<th>Name</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.items.map((g) => (
|
|
<tr key={g.id}>
|
|
<td><span className="badge text-bg-light">#{g.id}</span></td>
|
|
<td>
|
|
<Link href={`/zxdb/genres/${g.id}`}>{g.name}</Link>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="d-flex align-items-center gap-2 mt-2">
|
|
<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/genres?${(() => { 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/genres?${(() => { 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>
|
|
);
|
|
}
|