Files
explorer/src/app/zxdb/genres/GenresSearch.tsx
D. Rimron-Soutter 240936a850 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
2025-12-12 16:58:50 +00:00

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>
);
}