ZXDB: Releases browser filters, schema lists, and fixes

- UI: Add /zxdb hub cards for Entries and Releases; implement Releases browser
  with URL‑synced filters (q, year, sort, DL language/machine, file/scheme/source/case, demo)
  and a paginated table (Entry ID, Title, Release #, Year).
- API: Add GET /api/zxdb/releases/search (Zod‑validated, Node runtime) supporting
  title, year, sort, and downloads‑based filters; return paged JSON.
- Repo: Rewrite searchReleases to Drizzle QB; correct ORDER BY on releases.release_year;
  implement EXISTS on downloads using explicit "from downloads as d"; return JSON‑safe rows.
- Schema: Align Drizzle models with ZXDB for releases/downloads; add lookups
  availabletypes, currencies, roletypes, and roles relation.
- API (lookups): Add GET /api/zxdb/{availabletypes,currencies,roletypes} for dropdowns.
- Stability: JSON‑clone SSR payloads before passing to Client Components to avoid
  RowDataPacket serialization errors.

Signed-off-by: Junie@lucy.xalior.com
This commit is contained in:
2025-12-16 23:00:38 +00:00
parent fd4c0f8963
commit f563b41792
16 changed files with 1147 additions and 62 deletions

View File

@@ -0,0 +1,41 @@
import ReleasesExplorer from "./ReleasesExplorer";
import { searchReleases } from "@/server/repo/zxdb";
export const metadata = {
title: "ZXDB Releases",
};
export const dynamic = "force-dynamic";
export default async function Page({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) {
const sp = await searchParams;
const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1);
const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? "";
const yearStr = (Array.isArray(sp.year) ? sp.year[0] : sp.year) ?? "";
const year = yearStr ? Number(yearStr) : undefined;
const sort = ((Array.isArray(sp.sort) ? sp.sort[0] : sp.sort) as any) ?? "year_desc";
const dLanguageId = (Array.isArray(sp.dLanguageId) ? sp.dLanguageId[0] : sp.dLanguageId) ?? "";
const dMachinetypeIdStr = (Array.isArray(sp.dMachinetypeId) ? sp.dMachinetypeId[0] : sp.dMachinetypeId) ?? "";
const dMachinetypeId = dMachinetypeIdStr ? Number(dMachinetypeIdStr) : undefined;
const filetypeIdStr = (Array.isArray(sp.filetypeId) ? sp.filetypeId[0] : sp.filetypeId) ?? "";
const filetypeId = filetypeIdStr ? Number(filetypeIdStr) : undefined;
const schemetypeId = (Array.isArray(sp.schemetypeId) ? sp.schemetypeId[0] : sp.schemetypeId) ?? "";
const sourcetypeId = (Array.isArray(sp.sourcetypeId) ? sp.sourcetypeId[0] : sp.sourcetypeId) ?? "";
const casetypeId = (Array.isArray(sp.casetypeId) ? sp.casetypeId[0] : sp.casetypeId) ?? "";
const isDemoStr = (Array.isArray(sp.isDemo) ? sp.isDemo[0] : sp.isDemo) ?? "";
const isDemo = isDemoStr ? (isDemoStr === "true" || isDemoStr === "1") : undefined;
const [initial] = await Promise.all([
searchReleases({ page, pageSize: 20, q, year, sort, dLanguageId: dLanguageId || undefined, dMachinetypeId, filetypeId, schemetypeId: schemetypeId || undefined, sourcetypeId: sourcetypeId || undefined, casetypeId: casetypeId || undefined, isDemo }),
]);
// Ensure the object passed to a Client Component is a plain JSON value
const initialPlain = JSON.parse(JSON.stringify(initial));
return (
<ReleasesExplorer
initial={initialPlain as any}
initialUrlState={{ q, page, year: yearStr, sort, dLanguageId, dMachinetypeId: dMachinetypeIdStr, filetypeId: filetypeIdStr, schemetypeId, sourcetypeId, casetypeId, isDemo: isDemoStr }}
/>
);
}