@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import EntryLink from "../components/EntryLink";
|
import EntryLink from "../components/EntryLink";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
@@ -22,6 +22,8 @@ type Paged<T> = {
|
|||||||
export default function ReleasesExplorer({
|
export default function ReleasesExplorer({
|
||||||
initial,
|
initial,
|
||||||
initialUrlState,
|
initialUrlState,
|
||||||
|
initialUrlHasParams,
|
||||||
|
initialLists,
|
||||||
}: {
|
}: {
|
||||||
initial?: Paged<Item>;
|
initial?: Paged<Item>;
|
||||||
initialUrlState?: {
|
initialUrlState?: {
|
||||||
@@ -37,6 +39,15 @@ export default function ReleasesExplorer({
|
|||||||
casetypeId?: string;
|
casetypeId?: string;
|
||||||
isDemo?: string; // "1" or "true"
|
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 router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -57,12 +68,13 @@ export default function ReleasesExplorer({
|
|||||||
const [casetypeId, setCasetypeId] = useState<string>(initialUrlState?.casetypeId ?? "");
|
const [casetypeId, setCasetypeId] = useState<string>(initialUrlState?.casetypeId ?? "");
|
||||||
const [isDemo, setIsDemo] = useState<boolean>(!!(initialUrlState?.isDemo && (initialUrlState.isDemo === "1" || initialUrlState.isDemo === "true")));
|
const [isDemo, setIsDemo] = useState<boolean>(!!(initialUrlState?.isDemo && (initialUrlState.isDemo === "1" || initialUrlState.isDemo === "true")));
|
||||||
|
|
||||||
const [langs, setLangs] = useState<{ id: string; name: string }[]>([]);
|
const [langs, setLangs] = useState<{ id: string; name: string }[]>(initialLists?.languages ?? []);
|
||||||
const [machines, setMachines] = useState<{ id: number; name: string }[]>([]);
|
const [machines, setMachines] = useState<{ id: number; name: string }[]>(initialLists?.machinetypes ?? []);
|
||||||
const [filetypes, setFiletypes] = useState<{ id: number; name: string }[]>([]);
|
const [filetypes, setFiletypes] = useState<{ id: number; name: string }[]>(initialLists?.filetypes ?? []);
|
||||||
const [schemes, setSchemes] = useState<{ id: string; name: string }[]>([]);
|
const [schemes, setSchemes] = useState<{ id: string; name: string }[]>(initialLists?.schemetypes ?? []);
|
||||||
const [sources, setSources] = useState<{ id: string; name: string }[]>([]);
|
const [sources, setSources] = useState<{ id: string; name: string }[]>(initialLists?.sourcetypes ?? []);
|
||||||
const [cases, setCases] = useState<{ id: string; name: string }[]>([]);
|
const [cases, setCases] = useState<{ id: string; name: string }[]>(initialLists?.casetypes ?? []);
|
||||||
|
const initialLoad = useRef(true);
|
||||||
|
|
||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]);
|
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?.casetypeId ?? "") === casetypeId &&
|
||||||
(!!initialUrlState?.isDemo === isDemo)
|
(!!initialUrlState?.isDemo === isDemo)
|
||||||
) {
|
) {
|
||||||
|
if (initialLoad.current) {
|
||||||
|
initialLoad.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
updateUrl(page);
|
updateUrl(page);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (initialLoad.current) {
|
||||||
|
initialLoad.current = false;
|
||||||
|
if (initial && !initialUrlHasParams) return;
|
||||||
|
}
|
||||||
updateUrl(page);
|
updateUrl(page);
|
||||||
fetchData(q, page);
|
fetchData(q, page);
|
||||||
}, [page, year, sort, dLanguageId, dMachinetypeId, filetypeId, schemetypeId, sourcetypeId, casetypeId, isDemo]);
|
}, [page, year, sort, dLanguageId, dMachinetypeId, filetypeId, schemetypeId, sourcetypeId, casetypeId, isDemo]);
|
||||||
@@ -152,6 +172,7 @@ export default function ReleasesExplorer({
|
|||||||
// Load filter option lists on mount
|
// Load filter option lists on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function loadLists() {
|
async function loadLists() {
|
||||||
|
if (langs.length || machines.length || filetypes.length || schemes.length || sources.length || cases.length) return;
|
||||||
try {
|
try {
|
||||||
const [l, m, ft, sc, so, ca] = await Promise.all([
|
const [l, m, ft, sc, so, ca] = await Promise.all([
|
||||||
fetch("/api/zxdb/languages", { cache: "force-cache" }).then((r) => r.json()),
|
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 ReleasesExplorer from "./ReleasesExplorer";
|
||||||
import { searchReleases } from "@/server/repo/zxdb";
|
import { listCasetypes, listFiletypes, listLanguages, listMachinetypes, listSchemetypes, listSourcetypes, searchReleases } from "@/server/repo/zxdb";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "ZXDB Releases",
|
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 }> }) {
|
export default async function Page({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) {
|
||||||
const sp = await searchParams;
|
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 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 q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? "";
|
||||||
const yearStr = (Array.isArray(sp.year) ? sp.year[0] : sp.year) ?? "";
|
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 isDemoStr = (Array.isArray(sp.isDemo) ? sp.isDemo[0] : sp.isDemo) ?? "";
|
||||||
const isDemo = isDemoStr ? (isDemoStr === "true" || isDemoStr === "1") : undefined;
|
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 }),
|
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
|
// 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 (
|
return (
|
||||||
<ReleasesExplorer
|
<ReleasesExplorer
|
||||||
initial={initialPlain}
|
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 }}
|
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 { and, desc, eq, like, sql, asc } from "drizzle-orm";
|
||||||
|
import { cache } from "react";
|
||||||
// import { alias } from "drizzle-orm/mysql-core";
|
// import { alias } from "drizzle-orm/mysql-core";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import {
|
import {
|
||||||
@@ -731,15 +732,9 @@ export async function getLabelPublishedEntries(labelId: number, params: LabelCon
|
|||||||
|
|
||||||
// ----- Lookups lists and category browsing -----
|
// ----- Lookups lists and category browsing -----
|
||||||
|
|
||||||
export async function listGenres() {
|
export const listGenres = cache(async () => db.select().from(genretypes).orderBy(genretypes.name));
|
||||||
return 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));
|
||||||
export async function listLanguages() {
|
|
||||||
return db.select().from(languages).orderBy(languages.name);
|
|
||||||
}
|
|
||||||
export async function listMachinetypes() {
|
|
||||||
return db.select().from(machinetypes).orderBy(machinetypes.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: ZXDB structure in this project does not include a `releasetypes` table.
|
// Note: ZXDB structure in this project does not include a `releasetypes` table.
|
||||||
// Do not attempt to query it here.
|
// Do not attempt to query it here.
|
||||||
@@ -1546,35 +1541,22 @@ export async function getReleaseDetail(entryId: number, releaseSeq: number): Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ----- Download/lookups simple lists -----
|
// ----- Download/lookups simple lists -----
|
||||||
export async function listFiletypes() {
|
export const listFiletypes = cache(async () => db.select().from(filetypes).orderBy(filetypes.name));
|
||||||
return 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 async function listSchemetypes() {
|
export const listCasetypes = cache(async () => db.select().from(casetypes).orderBy(casetypes.name));
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Newly exposed lookups
|
// Newly exposed lookups
|
||||||
export async function listAvailabletypes() {
|
export const listAvailabletypes = cache(async () => db.select().from(availabletypes).orderBy(availabletypes.name));
|
||||||
return db.select().from(availabletypes).orderBy(availabletypes.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listCurrencies() {
|
export const listCurrencies = cache(async () =>
|
||||||
// Preserve full fields for UI needs
|
db
|
||||||
return db
|
|
||||||
.select({ id: currencies.id, name: currencies.name, symbol: currencies.symbol, prefix: currencies.prefix })
|
.select({ id: currencies.id, name: currencies.name, symbol: currencies.symbol, prefix: currencies.prefix })
|
||||||
.from(currencies)
|
.from(currencies)
|
||||||
.orderBy(currencies.name);
|
.orderBy(currencies.name)
|
||||||
}
|
);
|
||||||
|
|
||||||
export async function listRoletypes() {
|
export const listRoletypes = cache(async () => db.select().from(roletypes).orderBy(roletypes.name));
|
||||||
return db.select().from(roletypes).orderBy(roletypes.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listMagazines(params: { q?: string; page?: number; pageSize?: number }): Promise<PagedResult<MagazineListItem>> {
|
export async function listMagazines(params: { q?: string; page?: number; pageSize?: number }): Promise<PagedResult<MagazineListItem>> {
|
||||||
const q = (params.q ?? "").trim();
|
const q = (params.q ?? "").trim();
|
||||||
|
|||||||
Reference in New Issue
Block a user