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

@@ -1,30 +1,60 @@
import ZxdbExplorer from "./ZxdbExplorer";
import { searchEntries, listGenres, listLanguages, listMachinetypes } from "@/server/repo/zxdb";
import Link from "next/link";
export const metadata = {
title: "ZXDB Explorer",
};
// This page depends on searchParams (?page=, filters in future). Force dynamic
// rendering so ISR doesnt cache a single HTML for all query strings.
export const dynamic = "force-dynamic";
export const revalidate = 3600;
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);
// Server-render initial page based on URL to avoid first client fetch and keep counter in sync
const [initial, genres, langs, machines] = await Promise.all([
searchEntries({ page, pageSize: 20, sort: "id_desc" }),
listGenres(),
listLanguages(),
listMachinetypes(),
]);
export default async function Page() {
return (
<ZxdbExplorer
initial={initial as any}
initialGenres={genres as any}
initialLanguages={langs as any}
initialMachines={machines as any}
/>
<div>
<h1 className="mb-3">ZXDB Explorer</h1>
<p className="text-secondary">Choose what you want to explore.</p>
<div className="row g-3">
<div className="col-sm-6 col-lg-4">
<Link href="/zxdb/entries" className="text-decoration-none">
<div className="card h-100 shadow-sm">
<div className="card-body d-flex align-items-center">
<div className="me-3" aria-hidden>
<span className="bi bi-collection" style={{ fontSize: 28 }} />
</div>
<div>
<h5 className="card-title mb-1">Entries</h5>
<div className="card-text text-secondary">Browse software entries with filters</div>
</div>
</div>
</div>
</Link>
</div>
<div className="col-sm-6 col-lg-4">
<Link href="/zxdb/releases" className="text-decoration-none">
<div className="card h-100 shadow-sm">
<div className="card-body d-flex align-items-center">
<div className="me-3" aria-hidden>
<span className="bi bi-box-arrow-down" style={{ fontSize: 28 }} />
</div>
<div>
<h5 className="card-title mb-1">Releases</h5>
<div className="card-text text-secondary">Drill into releases and downloads</div>
</div>
</div>
</div>
</Link>
</div>
</div>
<div className="mt-4">
<h2 className="h5 mb-2">Categories</h2>
<div className="d-flex flex-wrap gap-2">
<Link className="btn btn-outline-secondary btn-sm" href="/zxdb/genres">Genres</Link>
<Link className="btn btn-outline-secondary btn-sm" href="/zxdb/languages">Languages</Link>
<Link className="btn btn-outline-secondary btn-sm" href="/zxdb/machinetypes">Machine Types</Link>
<Link className="btn btn-outline-secondary btn-sm" href="/zxdb/labels">Labels</Link>
</div>
</div>
</div>
);
}