@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import EntryLink from "../components/EntryLink";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
@@ -22,6 +22,8 @@ type Paged<T> = {
|
||||
export default function ReleasesExplorer({
|
||||
initial,
|
||||
initialUrlState,
|
||||
initialUrlHasParams,
|
||||
initialLists,
|
||||
}: {
|
||||
initial?: Paged<Item>;
|
||||
initialUrlState?: {
|
||||
@@ -37,6 +39,15 @@ export default function ReleasesExplorer({
|
||||
casetypeId?: string;
|
||||
isDemo?: string; // "1" or "true"
|
||||
};
|
||||
initialUrlHasParams?: boolean;
|
||||
initialLists?: {
|
||||
languages: { id: string; name: string }[];
|
||||
machinetypes: { id: number; name: string }[];
|
||||
filetypes: { id: number; name: string }[];
|
||||
schemetypes: { id: string; name: string }[];
|
||||
sourcetypes: { id: string; name: string }[];
|
||||
casetypes: { id: string; name: string }[];
|
||||
};
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@@ -57,12 +68,13 @@ export default function ReleasesExplorer({
|
||||
const [casetypeId, setCasetypeId] = useState<string>(initialUrlState?.casetypeId ?? "");
|
||||
const [isDemo, setIsDemo] = useState<boolean>(!!(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 [langs, setLangs] = useState<{ id: string; name: string }[]>(initialLists?.languages ?? []);
|
||||
const [machines, setMachines] = useState<{ id: number; name: string }[]>(initialLists?.machinetypes ?? []);
|
||||
const [filetypes, setFiletypes] = useState<{ id: number; name: string }[]>(initialLists?.filetypes ?? []);
|
||||
const [schemes, setSchemes] = useState<{ id: string; name: string }[]>(initialLists?.schemetypes ?? []);
|
||||
const [sources, setSources] = useState<{ id: string; name: string }[]>(initialLists?.sourcetypes ?? []);
|
||||
const [cases, setCases] = useState<{ id: string; name: string }[]>(initialLists?.casetypes ?? []);
|
||||
const initialLoad = useRef(true);
|
||||
|
||||
const pageSize = 20;
|
||||
const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]);
|
||||
@@ -135,9 +147,17 @@ export default function ReleasesExplorer({
|
||||
(initialUrlState?.casetypeId ?? "") === casetypeId &&
|
||||
(!!initialUrlState?.isDemo === isDemo)
|
||||
) {
|
||||
if (initialLoad.current) {
|
||||
initialLoad.current = false;
|
||||
return;
|
||||
}
|
||||
updateUrl(page);
|
||||
return;
|
||||
}
|
||||
if (initialLoad.current) {
|
||||
initialLoad.current = false;
|
||||
if (initial && !initialUrlHasParams) return;
|
||||
}
|
||||
updateUrl(page);
|
||||
fetchData(q, page);
|
||||
}, [page, year, sort, dLanguageId, dMachinetypeId, filetypeId, schemetypeId, sourcetypeId, casetypeId, isDemo]);
|
||||
@@ -152,6 +172,7 @@ export default function ReleasesExplorer({
|
||||
// Load filter option lists on mount
|
||||
useEffect(() => {
|
||||
async function loadLists() {
|
||||
if (langs.length || machines.length || filetypes.length || schemes.length || sources.length || cases.length) return;
|
||||
try {
|
||||
const [l, m, ft, sc, so, ca] = await Promise.all([
|
||||
fetch("/api/zxdb/languages", { cache: "force-cache" }).then((r) => r.json()),
|
||||
|
||||
443
src/app/zxdb/releases/[entryId]/[releaseSeq]/ReleaseDetail.tsx
Normal file
443
src/app/zxdb/releases/[entryId]/[releaseSeq]/ReleaseDetail.tsx
Normal file
@@ -0,0 +1,443 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
type ReleaseDetailData = {
|
||||
entry: {
|
||||
id: number;
|
||||
title: string;
|
||||
issueId: number | null;
|
||||
};
|
||||
release: {
|
||||
entryId: number;
|
||||
releaseSeq: number;
|
||||
year: number | null;
|
||||
month: number | null;
|
||||
day: number | null;
|
||||
currency: { id: string | null; name: string | null; symbol: string | null; prefix: number | null };
|
||||
prices: {
|
||||
release: number | null;
|
||||
budget: number | null;
|
||||
microdrive: number | null;
|
||||
disk: number | null;
|
||||
cartridge: number | null;
|
||||
};
|
||||
book: { isbn: string | null; pages: number | null };
|
||||
};
|
||||
downloads: Array<{
|
||||
id: number;
|
||||
link: string;
|
||||
size: number | null;
|
||||
md5: string | null;
|
||||
comments: string | null;
|
||||
isDemo: boolean;
|
||||
type: { id: number; name: string };
|
||||
language: { id: string | null; name: string | null };
|
||||
machinetype: { id: number | null; name: string | null };
|
||||
scheme: { id: string | null; name: string | null };
|
||||
source: { id: string | null; name: string | null };
|
||||
case: { id: string | null; name: string | null };
|
||||
year: number | null;
|
||||
}>;
|
||||
scraps: Array<{
|
||||
id: number;
|
||||
link: string | null;
|
||||
size: number | null;
|
||||
comments: string | null;
|
||||
rationale: string;
|
||||
isDemo: boolean;
|
||||
type: { id: number; name: string };
|
||||
language: { id: string | null; name: string | null };
|
||||
machinetype: { id: number | null; name: string | null };
|
||||
scheme: { id: string | null; name: string | null };
|
||||
source: { id: string | null; name: string | null };
|
||||
case: { id: string | null; name: string | null };
|
||||
year: number | null;
|
||||
}>;
|
||||
files: Array<{
|
||||
id: number;
|
||||
link: string;
|
||||
size: number | null;
|
||||
md5: string | null;
|
||||
comments: string | null;
|
||||
type: { id: number; name: string };
|
||||
}>;
|
||||
magazineRefs: Array<{
|
||||
id: number;
|
||||
issueId: number;
|
||||
magazineId: number | null;
|
||||
magazineName: string | null;
|
||||
referencetypeId: number;
|
||||
referencetypeName: string | null;
|
||||
page: number;
|
||||
isOriginal: number;
|
||||
scoreGroup: string;
|
||||
issue: {
|
||||
dateYear: number | null;
|
||||
dateMonth: number | null;
|
||||
dateDay: number | null;
|
||||
volume: number | null;
|
||||
number: number | null;
|
||||
special: string | null;
|
||||
supplement: string | null;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
function formatIssue(issue: ReleaseDetailData["magazineRefs"][number]["issue"]) {
|
||||
const parts: string[] = [];
|
||||
if (issue.volume != null) parts.push(`v.${issue.volume}`);
|
||||
if (issue.number != null) parts.push(`#${issue.number}`);
|
||||
if (issue.dateYear != null) {
|
||||
let date = `${issue.dateYear}`;
|
||||
if (issue.dateMonth != null) {
|
||||
const mm = String(issue.dateMonth).padStart(2, "0");
|
||||
date += `/${mm}`;
|
||||
if (issue.dateDay != null) {
|
||||
const dd = String(issue.dateDay).padStart(2, "0");
|
||||
date += `/${dd}`;
|
||||
}
|
||||
}
|
||||
parts.push(date);
|
||||
}
|
||||
if (issue.special) parts.push(`special "${issue.special}"`);
|
||||
if (issue.supplement) parts.push(`supplement "${issue.supplement}"`);
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
function formatCurrency(value: number | null, currency: ReleaseDetailData["release"]["currency"]) {
|
||||
if (value == null) return "-";
|
||||
if (currency.symbol) {
|
||||
return currency.prefix ? `${currency.symbol}${value}` : `${value}${currency.symbol}`;
|
||||
}
|
||||
if (currency.name) return `${value} ${currency.name}`;
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export default function ReleaseDetailClient({ data }: { data: ReleaseDetailData | null }) {
|
||||
if (!data) return <div className="alert alert-warning">Not found</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol className="breadcrumb">
|
||||
<li className="breadcrumb-item">
|
||||
<Link href="/zxdb">ZXDB</Link>
|
||||
</li>
|
||||
<li className="breadcrumb-item">
|
||||
<Link href="/zxdb/releases">Releases</Link>
|
||||
</li>
|
||||
<li className="breadcrumb-item">
|
||||
<Link href={`/zxdb/entries/${data.entry.id}`}>{data.entry.title}</Link>
|
||||
</li>
|
||||
<li className="breadcrumb-item active" aria-current="page">Release #{data.release.releaseSeq}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div className="d-flex align-items-center gap-2 flex-wrap">
|
||||
<h1 className="mb-0">Release #{data.release.releaseSeq}</h1>
|
||||
<Link className="badge text-bg-secondary text-decoration-none" href={`/zxdb/entries/${data.entry.id}`}>
|
||||
{data.entry.title}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 220 }}>Field</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Entry</td>
|
||||
<td>
|
||||
<Link href={`/zxdb/entries/${data.entry.id}`}>#{data.entry.id}</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Release Sequence</td>
|
||||
<td>#{data.release.releaseSeq}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Release Date</td>
|
||||
<td>
|
||||
{data.release.year != null ? (
|
||||
<span>
|
||||
{data.release.year}
|
||||
{data.release.month != null ? `/${String(data.release.month).padStart(2, "0")}` : ""}
|
||||
{data.release.day != null ? `/${String(data.release.day).padStart(2, "0")}` : ""}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-secondary">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Currency</td>
|
||||
<td>
|
||||
{data.release.currency.id ? (
|
||||
<span>{data.release.currency.id} {data.release.currency.name ? `(${data.release.currency.name})` : ""}</span>
|
||||
) : (
|
||||
<span className="text-secondary">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Price</td>
|
||||
<td>{formatCurrency(data.release.prices.release, data.release.currency)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Budget Price</td>
|
||||
<td>{formatCurrency(data.release.prices.budget, data.release.currency)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microdrive Price</td>
|
||||
<td>{formatCurrency(data.release.prices.microdrive, data.release.currency)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Disk Price</td>
|
||||
<td>{formatCurrency(data.release.prices.disk, data.release.currency)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cartridge Price</td>
|
||||
<td>{formatCurrency(data.release.prices.cartridge, data.release.currency)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Book ISBN</td>
|
||||
<td>{data.release.book.isbn ?? <span className="text-secondary">-</span>}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Book Pages</td>
|
||||
<td>{data.release.book.pages ?? <span className="text-secondary">-</span>}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<h5>Magazine References</h5>
|
||||
{data.magazineRefs.length === 0 && <div className="text-secondary">No magazine references</div>}
|
||||
{data.magazineRefs.length > 0 && (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-sm table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Magazine</th>
|
||||
<th>Issue</th>
|
||||
<th style={{ width: 120 }}>Type</th>
|
||||
<th style={{ width: 80 }}>Page</th>
|
||||
<th style={{ width: 100 }}>Original</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.magazineRefs.map((m) => (
|
||||
<tr key={m.id}>
|
||||
<td>
|
||||
{m.magazineId != null ? (
|
||||
<Link href={`/zxdb/magazines/${m.magazineId}`}>{m.magazineName ?? `#${m.magazineId}`}</Link>
|
||||
) : (
|
||||
<span className="text-secondary">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<Link href={`/zxdb/issues/${m.issueId}`}>#{m.issueId}</Link>
|
||||
<div className="text-secondary small">{formatIssue(m.issue) || "-"}</div>
|
||||
</td>
|
||||
<td>{m.referencetypeName ?? `#${m.referencetypeId}`}</td>
|
||||
<td>{m.page}</td>
|
||||
<td>{m.isOriginal ? "Yes" : "No"}</td>
|
||||
<td>{m.scoreGroup || "-"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<h5>Downloads</h5>
|
||||
{data.downloads.length === 0 && <div className="text-secondary">No downloads</div>}
|
||||
{data.downloads.length > 0 && (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-sm table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Link</th>
|
||||
<th style={{ width: 120 }} className="text-end">Size</th>
|
||||
<th style={{ width: 240 }}>MD5</th>
|
||||
<th>Flags</th>
|
||||
<th>Details</th>
|
||||
<th>Comments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.downloads.map((d) => {
|
||||
const isHttp = d.link.startsWith("http://") || d.link.startsWith("https://");
|
||||
return (
|
||||
<tr key={d.id}>
|
||||
<td><span className="badge text-bg-secondary">{d.type.name}</span></td>
|
||||
<td>
|
||||
{isHttp ? (
|
||||
<a href={d.link} target="_blank" rel="noopener noreferrer">{d.link}</a>
|
||||
) : (
|
||||
<span>{d.link}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-end">{typeof d.size === "number" ? d.size.toLocaleString() : "-"}</td>
|
||||
<td><code>{d.md5 ?? "-"}</code></td>
|
||||
<td>
|
||||
<div className="d-flex gap-1 flex-wrap">
|
||||
{d.isDemo ? <span className="badge text-bg-warning">Demo</span> : null}
|
||||
{d.scheme.name ? <span className="badge text-bg-info">{d.scheme.name}</span> : null}
|
||||
{d.source.name ? <span className="badge text-bg-light border">{d.source.name}</span> : null}
|
||||
{d.case.name ? <span className="badge text-bg-secondary">{d.case.name}</span> : null}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="d-flex gap-2 flex-wrap align-items-center">
|
||||
{d.language.name ? (
|
||||
<Link className="badge text-bg-info text-decoration-none" href={`/zxdb/languages/${d.language.id}`}>{d.language.name}</Link>
|
||||
) : null}
|
||||
{d.machinetype.name ? (
|
||||
<Link className="badge text-bg-primary text-decoration-none" href={`/zxdb/machinetypes/${d.machinetype.id}`}>{d.machinetype.name}</Link>
|
||||
) : null}
|
||||
{typeof d.year === "number" ? <span className="badge text-bg-dark">{d.year}</span> : null}
|
||||
</div>
|
||||
</td>
|
||||
<td>{d.comments ?? ""}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<h5>Scraps / Media</h5>
|
||||
{data.scraps.length === 0 && <div className="text-secondary">No scraps</div>}
|
||||
{data.scraps.length > 0 && (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-sm table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Link</th>
|
||||
<th style={{ width: 120 }} className="text-end">Size</th>
|
||||
<th>Flags</th>
|
||||
<th>Details</th>
|
||||
<th>Rationale</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.scraps.map((s) => {
|
||||
const isHttp = s.link?.startsWith("http://") || s.link?.startsWith("https://");
|
||||
return (
|
||||
<tr key={s.id}>
|
||||
<td><span className="badge text-bg-secondary">{s.type.name}</span></td>
|
||||
<td>
|
||||
{s.link ? (
|
||||
isHttp ? (
|
||||
<a href={s.link} target="_blank" rel="noopener noreferrer">{s.link}</a>
|
||||
) : (
|
||||
<span>{s.link}</span>
|
||||
)
|
||||
) : (
|
||||
<span className="text-secondary">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-end">{typeof s.size === "number" ? s.size.toLocaleString() : "-"}</td>
|
||||
<td>
|
||||
<div className="d-flex gap-1 flex-wrap">
|
||||
{s.isDemo ? <span className="badge text-bg-warning">Demo</span> : null}
|
||||
{s.scheme.name ? <span className="badge text-bg-info">{s.scheme.name}</span> : null}
|
||||
{s.source.name ? <span className="badge text-bg-light border">{s.source.name}</span> : null}
|
||||
{s.case.name ? <span className="badge text-bg-secondary">{s.case.name}</span> : null}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="d-flex gap-2 flex-wrap align-items-center">
|
||||
{s.language.name ? (
|
||||
<Link className="badge text-bg-info text-decoration-none" href={`/zxdb/languages/${s.language.id}`}>{s.language.name}</Link>
|
||||
) : null}
|
||||
{s.machinetype.name ? (
|
||||
<Link className="badge text-bg-primary text-decoration-none" href={`/zxdb/machinetypes/${s.machinetype.id}`}>{s.machinetype.name}</Link>
|
||||
) : null}
|
||||
{typeof s.year === "number" ? <span className="badge text-bg-dark">{s.year}</span> : null}
|
||||
</div>
|
||||
</td>
|
||||
<td>{s.rationale}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<h5>Issue Files</h5>
|
||||
{data.files.length === 0 && <div className="text-secondary">No files linked</div>}
|
||||
{data.files.length > 0 && (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-sm table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Link</th>
|
||||
<th style={{ width: 120 }} className="text-end">Size</th>
|
||||
<th style={{ width: 240 }}>MD5</th>
|
||||
<th>Comments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.files.map((f) => {
|
||||
const isHttp = f.link.startsWith("http://") || f.link.startsWith("https://");
|
||||
return (
|
||||
<tr key={f.id}>
|
||||
<td><span className="badge text-bg-secondary">{f.type.name}</span></td>
|
||||
<td>
|
||||
{isHttp ? (
|
||||
<a href={f.link} target="_blank" rel="noopener noreferrer">{f.link}</a>
|
||||
) : (
|
||||
<span>{f.link}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-end">{f.size != null ? new Intl.NumberFormat().format(f.size) : "-"}</td>
|
||||
<td><code>{f.md5 ?? "-"}</code></td>
|
||||
<td>{f.comments ?? ""}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<Link className="btn btn-sm btn-outline-secondary" href={`/zxdb/releases/${data.entry.id}/${data.release.releaseSeq}`}>Permalink</Link>
|
||||
<Link className="btn btn-sm btn-outline-primary" href="/zxdb/releases">Back to Releases</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
src/app/zxdb/releases/[entryId]/[releaseSeq]/page.tsx
Normal file
16
src/app/zxdb/releases/[entryId]/[releaseSeq]/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import ReleaseDetailClient from "./ReleaseDetail";
|
||||
import { getReleaseDetail } from "@/server/repo/zxdb";
|
||||
|
||||
export const metadata = {
|
||||
title: "ZXDB Release",
|
||||
};
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
export default async function Page({ params }: { params: Promise<{ entryId: string; releaseSeq: string }> }) {
|
||||
const { entryId, releaseSeq } = await params;
|
||||
const entryIdNum = Number(entryId);
|
||||
const releaseSeqNum = Number(releaseSeq);
|
||||
const data = await getReleaseDetail(entryIdNum, releaseSeqNum);
|
||||
return <ReleaseDetailClient data={data} />;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import ReleasesExplorer from "./ReleasesExplorer";
|
||||
import { searchReleases } from "@/server/repo/zxdb";
|
||||
import { listCasetypes, listFiletypes, listLanguages, listMachinetypes, listSchemetypes, listSourcetypes, searchReleases } from "@/server/repo/zxdb";
|
||||
|
||||
export const metadata = {
|
||||
title: "ZXDB Releases",
|
||||
@@ -9,6 +9,7 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function Page({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) {
|
||||
const sp = await searchParams;
|
||||
const hasParams = Object.values(sp).some((value) => value !== undefined);
|
||||
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) ?? "";
|
||||
@@ -25,8 +26,14 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
|
||||
const isDemoStr = (Array.isArray(sp.isDemo) ? sp.isDemo[0] : sp.isDemo) ?? "";
|
||||
const isDemo = isDemoStr ? (isDemoStr === "true" || isDemoStr === "1") : undefined;
|
||||
|
||||
const [initial] = await Promise.all([
|
||||
const [initial, langs, machines, filetypes, schemes, sources, cases] = 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 }),
|
||||
listLanguages(),
|
||||
listMachinetypes(),
|
||||
listFiletypes(),
|
||||
listSchemetypes(),
|
||||
listSourcetypes(),
|
||||
listCasetypes(),
|
||||
]);
|
||||
|
||||
// Ensure the object passed to a Client Component is a plain JSON value
|
||||
@@ -35,7 +42,16 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
|
||||
return (
|
||||
<ReleasesExplorer
|
||||
initial={initialPlain}
|
||||
initialLists={{
|
||||
languages: JSON.parse(JSON.stringify(langs)),
|
||||
machinetypes: JSON.parse(JSON.stringify(machines)),
|
||||
filetypes: JSON.parse(JSON.stringify(filetypes)),
|
||||
schemetypes: JSON.parse(JSON.stringify(schemes)),
|
||||
sourcetypes: JSON.parse(JSON.stringify(sources)),
|
||||
casetypes: JSON.parse(JSON.stringify(cases)),
|
||||
}}
|
||||
initialUrlState={{ q, page, year: yearStr, sort, dLanguageId, dMachinetypeId: dMachinetypeIdStr, filetypeId: filetypeIdStr, schemetypeId, sourcetypeId, casetypeId, isDemo: isDemoStr }}
|
||||
initialUrlHasParams={hasParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { and, desc, eq, like, sql, asc } from "drizzle-orm";
|
||||
import { cache } from "react";
|
||||
// import { alias } from "drizzle-orm/mysql-core";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
@@ -731,15 +732,9 @@ export async function getLabelPublishedEntries(labelId: number, params: LabelCon
|
||||
|
||||
// ----- Lookups lists and category browsing -----
|
||||
|
||||
export async function listGenres() {
|
||||
return db.select().from(genretypes).orderBy(genretypes.name);
|
||||
}
|
||||
export async function listLanguages() {
|
||||
return db.select().from(languages).orderBy(languages.name);
|
||||
}
|
||||
export async function listMachinetypes() {
|
||||
return db.select().from(machinetypes).orderBy(machinetypes.name);
|
||||
}
|
||||
export const listGenres = cache(async () => db.select().from(genretypes).orderBy(genretypes.name));
|
||||
export const listLanguages = cache(async () => db.select().from(languages).orderBy(languages.name));
|
||||
export const listMachinetypes = cache(async () => db.select().from(machinetypes).orderBy(machinetypes.name));
|
||||
|
||||
// Note: ZXDB structure in this project does not include a `releasetypes` table.
|
||||
// Do not attempt to query it here.
|
||||
@@ -1546,35 +1541,22 @@ export async function getReleaseDetail(entryId: number, releaseSeq: number): Pro
|
||||
}
|
||||
|
||||
// ----- Download/lookups simple lists -----
|
||||
export async function listFiletypes() {
|
||||
return db.select().from(filetypes).orderBy(filetypes.name);
|
||||
}
|
||||
export async function listSchemetypes() {
|
||||
return db.select().from(schemetypes).orderBy(schemetypes.name);
|
||||
}
|
||||
export async function listSourcetypes() {
|
||||
return db.select().from(sourcetypes).orderBy(sourcetypes.name);
|
||||
}
|
||||
export async function listCasetypes() {
|
||||
return db.select().from(casetypes).orderBy(casetypes.name);
|
||||
}
|
||||
export const listFiletypes = cache(async () => db.select().from(filetypes).orderBy(filetypes.name));
|
||||
export const listSchemetypes = cache(async () => db.select().from(schemetypes).orderBy(schemetypes.name));
|
||||
export const listSourcetypes = cache(async () => db.select().from(sourcetypes).orderBy(sourcetypes.name));
|
||||
export const listCasetypes = cache(async () => db.select().from(casetypes).orderBy(casetypes.name));
|
||||
|
||||
// Newly exposed lookups
|
||||
export async function listAvailabletypes() {
|
||||
return db.select().from(availabletypes).orderBy(availabletypes.name);
|
||||
}
|
||||
export const listAvailabletypes = cache(async () => db.select().from(availabletypes).orderBy(availabletypes.name));
|
||||
|
||||
export async function listCurrencies() {
|
||||
// Preserve full fields for UI needs
|
||||
return db
|
||||
export const listCurrencies = cache(async () =>
|
||||
db
|
||||
.select({ id: currencies.id, name: currencies.name, symbol: currencies.symbol, prefix: currencies.prefix })
|
||||
.from(currencies)
|
||||
.orderBy(currencies.name);
|
||||
}
|
||||
.orderBy(currencies.name)
|
||||
);
|
||||
|
||||
export async function listRoletypes() {
|
||||
return db.select().from(roletypes).orderBy(roletypes.name);
|
||||
}
|
||||
export const listRoletypes = cache(async () => db.select().from(roletypes).orderBy(roletypes.name));
|
||||
|
||||
export async function listMagazines(params: { q?: string; page?: number; pageSize?: number }): Promise<PagedResult<MagazineListItem>> {
|
||||
const q = (params.q ?? "").trim();
|
||||
|
||||
Reference in New Issue
Block a user