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
56 lines
2.1 KiB
TypeScript
56 lines
2.1 KiB
TypeScript
import { NextRequest } from "next/server";
|
|
import { z } from "zod";
|
|
import { searchEntries, getEntryFacets } from "@/server/repo";
|
|
import { parseIdList } from "@/utils/params";
|
|
|
|
const querySchema = z.object({
|
|
q: z.string().optional(),
|
|
page: z.coerce.number().int().positive().optional(),
|
|
pageSize: z.coerce.number().int().positive().max(100).optional(),
|
|
genreId: z.coerce.number().int().positive().optional(),
|
|
languageId: z
|
|
.string()
|
|
.trim()
|
|
.length(2, "languageId must be a 2-char code")
|
|
.optional(),
|
|
machinetypeId: z.string().optional(),
|
|
year: z.coerce.number().int().optional(),
|
|
sort: z.enum(["title", "id_desc"]).optional(),
|
|
scope: z.enum(["title", "title_aliases", "title_aliases_origins"]).optional(),
|
|
facets: z.coerce.boolean().optional(),
|
|
});
|
|
|
|
export async function GET(req: NextRequest) {
|
|
const { searchParams } = new URL(req.url);
|
|
const parsed = querySchema.safeParse({
|
|
q: searchParams.get("q") ?? undefined,
|
|
page: searchParams.get("page") ?? undefined,
|
|
pageSize: searchParams.get("pageSize") ?? undefined,
|
|
genreId: searchParams.get("genreId") ?? undefined,
|
|
languageId: searchParams.get("languageId") ?? undefined,
|
|
machinetypeId: searchParams.get("machinetypeId") ?? undefined,
|
|
year: searchParams.get("year") ?? undefined,
|
|
sort: searchParams.get("sort") ?? undefined,
|
|
scope: searchParams.get("scope") ?? undefined,
|
|
facets: searchParams.get("facets") ?? undefined,
|
|
});
|
|
if (!parsed.success) {
|
|
return new Response(
|
|
JSON.stringify({ error: parsed.error.flatten() }),
|
|
{ status: 400, headers: { "content-type": "application/json" } }
|
|
);
|
|
}
|
|
const machinetypeId = parseIdList(parsed.data.machinetypeId);
|
|
const searchParamsParsed = { ...parsed.data, machinetypeId };
|
|
const data = await searchEntries(searchParamsParsed);
|
|
const body = parsed.data.facets
|
|
? { ...data, facets: await getEntryFacets(searchParamsParsed) }
|
|
: data;
|
|
return new Response(JSON.stringify(body), {
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
|
|
// Ensure Node.js runtime (required for mysql2)
|
|
export const runtime = "nodejs";
|