Files
explorer/src/app/zxdb/genres/GenresSearch.tsx
D. Rimron-Soutter e803274af4 react-bootstrap migration, review & parser fixes
UI / react-bootstrap:
Migrate client components to react-bootstrap (Card, Table, Form,
Alert, Badge, Nav, Button, Spinner, Row, Col): the ZXDB explorers
and detail pages (Labels, Genres, Languages, MachineTypes, Releases,
Entries), TapeIdentifier, home page, Navbar and ThemeDropdown.
Server components (home, zxdb hub, magazines, issues) keep raw
HTML+className — react-bootstrap barrel imports resolve to undefined
under Turbopack in server components. Replace bi bi-* CSS icons with
react-bootstrap-icons. Add aria-labels to search inputs and
visually-hidden captions to data tables.

Code-review remediation (docs/todo.md):
- FileViewer: replace useState-as-effect with a proper useEffect.
- register.service: restore request-level caching of parsed registers.
- middleware: convert .js to .ts, dev-only request logging.
- Extract shared types to src/types/zxdb.ts; add src/server/repo
  barrel for incremental per-domain splitting.
- Extract helpers: parseIdList (params.ts), serialize (serialize.ts),
  buildRegisterSummary/isInfoLine (register_helpers.ts).
- Add loading.tsx skeletons for dynamic ZXDB detail routes.
- generateMetadata + notFound() on entry/release/label detail pages.
- opengraph-image: stable keys; ThemeDropdown: drop hardcoded cookie
  domain; remove unused page.module.css.

Register parser & data:
- Update data/nextreg.txt from upstream tbblue (SpectrumNext FPGA):
  0x04/0x0A/0x0F/0x80/0x81 bit changes, new Issue 5 board id, 0x43
  renamed "Palette Control", 0xF0/0xF8/0xF9/0xFA now "Issues 4 and 5
  Only".
- Add reg_44 custom parser for 0x44 (Palette Value 9-bit): the two
  consecutive writes render as separate "1st write" / "2nd write"
  modes.
- Skip commented-out register headers so the disabled 0xA3 block no
  longer leaks a phantom register.
- Add detailHasContent guard so body-less registers (0xC7/0xCB/0xCF/
  0xFF) and 0xF0's leading blank no longer emit empty tab strips.
- Capture 0xF0's leading "Issues 4 and 5 Only" line as register text.
- Add isIssueRestricted (case-sensitive) to detect the issue badge
  across rewording without flagging per-bit "(issue 5 only)" notes;
  update badge label to "Issues 4 & 5 Only".

claude-opus-4-8@lucy
2026-06-08 22:47:37 +01:00

111 lines
3.5 KiB
TypeScript

"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Row, Col, Card, Form, Button, Alert, Table, Badge } from "react-bootstrap";
import ZxdbBreadcrumbs from "@/app/zxdb/components/ZxdbBreadcrumbs";
import Pagination from "@/components/explorer/Pagination";
import type { PagedResult } from "@/types/zxdb";
type Genre = { id: number; name: string };
export default function GenresSearch({ initial, initialQ }: { initial?: PagedResult<Genre>; initialQ?: string }) {
const router = useRouter();
const [q, setQ] = useState(initialQ ?? "");
const [data, setData] = useState<PagedResult<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()}`);
}
const buildHref = useCallback((p: number) => {
const params = new URLSearchParams();
if (q) params.set("q", q);
params.set("page", String(p));
return `/zxdb/genres?${params.toString()}`;
}, [q]);
return (
<div>
<ZxdbBreadcrumbs
items={[
{ label: "ZXDB", href: "/zxdb" },
{ label: "Genres" },
]}
/>
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
<div>
<h1 className="mb-1">Genres</h1>
<div className="text-secondary">{data?.total.toLocaleString() ?? "0"} results</div>
</div>
</div>
<Row className="g-3">
<Col lg={3}>
<Card className="shadow-sm">
<Card.Body>
<Form className="d-flex flex-column gap-2" onSubmit={submit}>
<Form.Group>
<Form.Label className="small text-secondary">Search</Form.Label>
<Form.Control placeholder="Search genres..." value={q} onChange={(e) => setQ(e.target.value)} />
</Form.Group>
<div className="d-grid">
<Button variant="primary" type="submit">Search</Button>
</div>
</Form>
</Card.Body>
</Card>
</Col>
<Col lg={9}>
{data && data.items.length === 0 && <Alert variant="warning">No genres found.</Alert>}
{data && data.items.length > 0 && (
<Table striped hover className="align-middle">
<caption className="visually-hidden">Genres search results</caption>
<thead>
<tr>
<th style={{ width: 120 }}>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
{data.items.map((g) => (
<tr key={g.id}>
<td><Badge bg="light" text="dark">#{g.id}</Badge></td>
<td>
<Link href={`/zxdb/genres/${g.id}`}>{g.name}</Link>
</td>
</tr>
))}
</tbody>
</Table>
)}
</Col>
</Row>
<Pagination
page={data?.page ?? 1}
totalPages={totalPages}
buildHref={buildHref}
onPageChange={(p) => router.push(buildHref(p))}
/>
</div>
);
}