"use client"; import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; import EntryLink from "../components/EntryLink"; import { usePathname, useRouter } from "next/navigation"; type Item = { entryId: number; releaseSeq: number; entryTitle: string; year: number | null; }; type Paged = { items: T[]; page: number; pageSize: number; total: number; }; export default function ReleasesExplorer({ initial, initialUrlState, }: { initial?: Paged; initialUrlState?: { q: string; page: number; year: string; sort: "year_desc" | "year_asc" | "title" | "entry_id_desc"; dLanguageId?: string; dMachinetypeId?: string; // keep as string for URL/state consistency filetypeId?: string; schemetypeId?: string; sourcetypeId?: string; casetypeId?: string; isDemo?: string; // "1" or "true" }; }) { const router = useRouter(); const pathname = usePathname(); const [q, setQ] = useState(initialUrlState?.q ?? ""); const [page, setPage] = useState(initial?.page ?? initialUrlState?.page ?? 1); const [loading, setLoading] = useState(false); const [data, setData] = useState | null>(initial ?? null); const [year, setYear] = useState(initialUrlState?.year ?? ""); const [sort, setSort] = useState<"year_desc" | "year_asc" | "title" | "entry_id_desc">(initialUrlState?.sort ?? "year_desc"); // Download-based filters and their option lists const [dLanguageId, setDLanguageId] = useState(initialUrlState?.dLanguageId ?? ""); const [dMachinetypeId, setDMachinetypeId] = useState(initialUrlState?.dMachinetypeId ?? ""); const [filetypeId, setFiletypeId] = useState(initialUrlState?.filetypeId ?? ""); const [schemetypeId, setSchemetypeId] = useState(initialUrlState?.schemetypeId ?? ""); const [sourcetypeId, setSourcetypeId] = useState(initialUrlState?.sourcetypeId ?? ""); const [casetypeId, setCasetypeId] = useState(initialUrlState?.casetypeId ?? ""); const [isDemo, setIsDemo] = useState(!!(initialUrlState?.isDemo && (initialUrlState.isDemo === "1" || initialUrlState.isDemo === "true"))); const [langs, setLangs] = useState<{ id: string; name: string }[]>([]); const [machines, setMachines] = useState<{ id: number; name: string }[]>([]); const [filetypes, setFiletypes] = useState<{ id: number; name: string }[]>([]); const [schemes, setSchemes] = useState<{ id: string; name: string }[]>([]); const [sources, setSources] = useState<{ id: string; name: string }[]>([]); const [cases, setCases] = useState<{ id: string; name: string }[]>([]); const pageSize = 20; const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]); function updateUrl(nextPage = page) { const params = new URLSearchParams(); if (q) params.set("q", q); params.set("page", String(nextPage)); if (year) params.set("year", year); if (sort) params.set("sort", sort); if (dLanguageId) params.set("dLanguageId", dLanguageId); if (dMachinetypeId) params.set("dMachinetypeId", dMachinetypeId); if (filetypeId) params.set("filetypeId", filetypeId); if (schemetypeId) params.set("schemetypeId", schemetypeId); if (sourcetypeId) params.set("sourcetypeId", sourcetypeId); if (casetypeId) params.set("casetypeId", casetypeId); if (isDemo) params.set("isDemo", "1"); const qs = params.toString(); router.replace(qs ? `${pathname}?${qs}` : pathname); } async function fetchData(query: string, p: number) { setLoading(true); try { const params = new URLSearchParams(); if (query) params.set("q", query); params.set("page", String(p)); params.set("pageSize", String(pageSize)); if (year) params.set("year", String(Number(year))); if (sort) params.set("sort", sort); if (dLanguageId) params.set("dLanguageId", dLanguageId); if (dMachinetypeId) params.set("dMachinetypeId", dMachinetypeId); if (filetypeId) params.set("filetypeId", filetypeId); if (schemetypeId) params.set("schemetypeId", schemetypeId); if (sourcetypeId) params.set("sourcetypeId", sourcetypeId); if (casetypeId) params.set("casetypeId", casetypeId); if (isDemo) params.set("isDemo", "1"); const res = await fetch(`/api/zxdb/releases/search?${params.toString()}`); if (!res.ok) throw new Error(`Failed: ${res.status}`); const json: Paged = await res.json(); setData(json); } catch (e) { console.error(e); setData({ items: [], page: 1, pageSize, total: 0 }); } finally { setLoading(false); } } useEffect(() => { if (initial) { setData(initial); setPage(initial.page); } }, [initial]); useEffect(() => { const initialPage = initial?.page ?? 1; if ( initial && page === initialPage && (initialUrlState?.q ?? "") === q && (initialUrlState?.year ?? "") === (year ?? "") && sort === (initialUrlState?.sort ?? "year_desc") && (initialUrlState?.dLanguageId ?? "") === dLanguageId && (initialUrlState?.dMachinetypeId ?? "") === dMachinetypeId && (initialUrlState?.filetypeId ?? "") === filetypeId && (initialUrlState?.schemetypeId ?? "") === schemetypeId && (initialUrlState?.sourcetypeId ?? "") === sourcetypeId && (initialUrlState?.casetypeId ?? "") === casetypeId && (!!initialUrlState?.isDemo === isDemo) ) { updateUrl(page); return; } updateUrl(page); fetchData(q, page); }, [page, year, sort, dLanguageId, dMachinetypeId, filetypeId, schemetypeId, sourcetypeId, casetypeId, isDemo]); function onSubmit(e: React.FormEvent) { e.preventDefault(); setPage(1); updateUrl(1); fetchData(q, 1); } // Load filter option lists on mount useEffect(() => { async function loadLists() { try { const [l, m, ft, sc, so, ca] = await Promise.all([ fetch("/api/zxdb/languages", { cache: "force-cache" }).then((r) => r.json()), fetch("/api/zxdb/machinetypes", { cache: "force-cache" }).then((r) => r.json()), fetch("/api/zxdb/filetypes", { cache: "force-cache" }).then((r) => r.json()), fetch("/api/zxdb/schemetypes", { cache: "force-cache" }).then((r) => r.json()), fetch("/api/zxdb/sourcetypes", { cache: "force-cache" }).then((r) => r.json()), fetch("/api/zxdb/casetypes", { cache: "force-cache" }).then((r) => r.json()), ]); setLangs(l.items ?? []); setMachines(m.items ?? []); setFiletypes(ft.items ?? []); setSchemes(sc.items ?? []); setSources(so.items ?? []); setCases(ca.items ?? []); } catch { // ignore } } loadLists(); }, []); const prevHref = useMemo(() => { const params = new URLSearchParams(); if (q) params.set("q", q); params.set("page", String(Math.max(1, (data?.page ?? 1) - 1))); if (year) params.set("year", year); if (sort) params.set("sort", sort); if (dLanguageId) params.set("dLanguageId", dLanguageId); if (dMachinetypeId) params.set("dMachinetypeId", dMachinetypeId); if (filetypeId) params.set("filetypeId", filetypeId); if (schemetypeId) params.set("schemetypeId", schemetypeId); if (sourcetypeId) params.set("sourcetypeId", sourcetypeId); if (casetypeId) params.set("casetypeId", casetypeId); if (isDemo) params.set("isDemo", "1"); return `/zxdb/releases?${params.toString()}`; }, [q, data?.page, year, sort, dLanguageId, dMachinetypeId, filetypeId, schemetypeId, sourcetypeId, casetypeId, isDemo]); const nextHref = useMemo(() => { const params = new URLSearchParams(); if (q) params.set("q", q); params.set("page", String(Math.max(1, (data?.page ?? 1) + 1))); if (year) params.set("year", year); if (sort) params.set("sort", sort); if (dLanguageId) params.set("dLanguageId", dLanguageId); if (dMachinetypeId) params.set("dMachinetypeId", dMachinetypeId); if (filetypeId) params.set("filetypeId", filetypeId); if (schemetypeId) params.set("schemetypeId", schemetypeId); if (sourcetypeId) params.set("sourcetypeId", sourcetypeId); if (casetypeId) params.set("casetypeId", casetypeId); if (isDemo) params.set("isDemo", "1"); return `/zxdb/releases?${params.toString()}`; }, [q, data?.page, year, sort, dLanguageId, dMachinetypeId, filetypeId, schemetypeId, sourcetypeId, casetypeId, isDemo]); return (

Releases

setQ(e.target.value)} />
{ setYear(e.target.value); setPage(1); }} />
{ setIsDemo(e.target.checked); setPage(1); }} />
{loading && (
Loading...
)}
{data && data.items.length === 0 && !loading && (
No results.
)} {data && data.items.length > 0 && (
{data.items.map((it) => ( ))}
Entry ID Title Release # Year
#{it.releaseSeq} {it.year ?? -}
)}
Page {data?.page ?? 1} / {totalPages}
{ if (!data || data.page <= 1) return; e.preventDefault(); setPage((p) => Math.max(1, p - 1)); }} > Prev = totalPages) ? "disabled" : ""}`} aria-disabled={!data || data.page >= totalPages} href={nextHref} onClick={(e) => { if (!data || data.page >= totalPages) return; e.preventDefault(); setPage((p) => Math.min(totalPages, p + 1)); }} > Next
); }