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
94 lines
4.1 KiB
TypeScript
94 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useMemo, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
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 };
|
|
|
|
export default function LanguageDetailClient({ id, initial, initialQ }: { id: string; initial: Paged<Item>; initialQ?: string }) {
|
|
const router = useRouter();
|
|
const [q, setQ] = useState(initialQ ?? "");
|
|
const totalPages = useMemo(() => Math.max(1, Math.ceil(initial.total / initial.pageSize)), [initial]);
|
|
|
|
return (
|
|
<div>
|
|
<h1 className="mb-0">Language <span className="badge text-bg-light">{id}</span></h1>
|
|
<form className="row gy-2 gx-2 align-items-center mt-2" onSubmit={(e) => { e.preventDefault(); const p = new URLSearchParams(); if (q) p.set("q", q); p.set("page", "1"); router.push(`/zxdb/languages/${id}?${p.toString()}`); }}>
|
|
<div className="col-sm-8 col-md-6 col-lg-4">
|
|
<input className="form-control" placeholder="Search within this language…" value={q} onChange={(e) => setQ(e.target.value)} />
|
|
</div>
|
|
<div className="col-auto">
|
|
<button className="btn btn-primary">Search</button>
|
|
</div>
|
|
</form>
|
|
{initial && initial.items.length === 0 && <div className="alert alert-warning">No entries.</div>}
|
|
{initial && initial.items.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table table-striped table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th style={{ width: 80 }}>ID</th>
|
|
<th>Title</th>
|
|
<th style={{ width: 160 }}>Machine</th>
|
|
<th style={{ width: 120 }}>Language</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{initial.items.map((it) => (
|
|
<tr key={it.id}>
|
|
<td>{it.id}</td>
|
|
<td><Link href={`/zxdb/entries/${it.id}`}>{it.title}</Link></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>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
<div className="d-flex align-items-center gap-2 mt-2">
|
|
<span>Page {initial.page} / {totalPages}</span>
|
|
<div className="ms-auto d-flex gap-2">
|
|
<Link
|
|
className={`btn btn-sm btn-outline-secondary ${initial.page <= 1 ? "disabled" : ""}`}
|
|
aria-disabled={initial.page <= 1}
|
|
href={`/zxdb/languages/${id}?${(() => { const p = new URLSearchParams(); if (q) p.set("q", q); p.set("page", String(Math.max(1, initial.page - 1))); return p.toString(); })()}`}
|
|
>
|
|
Prev
|
|
</Link>
|
|
<Link
|
|
className={`btn btn-sm btn-outline-secondary ${initial.page >= totalPages ? "disabled" : ""}`}
|
|
aria-disabled={initial.page >= totalPages}
|
|
href={`/zxdb/languages/${id}?${(() => { const p = new URLSearchParams(); if (q) p.set("q", q); p.set("page", String(Math.min(totalPages, initial.page + 1))); return p.toString(); })()}`}
|
|
>
|
|
Next
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|