- Remove optional path prefix and prepend the required local string. - Avoid hardcoded 'SC' or 'WoS' subdirectories in path mapping. - Maintain binary state: show local link only if env var is set and file exists. Signed-off: junie@McFiver.local
891 lines
37 KiB
TypeScript
891 lines
37 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import ZxdbBreadcrumbs from "@/app/zxdb/components/ZxdbBreadcrumbs";
|
|
|
|
type Label = { id: number; name: string; labeltypeId: string | null };
|
|
export type EntryDetailData = {
|
|
id: number;
|
|
title: string;
|
|
isXrated: number;
|
|
machinetype: { id: number | null; name: string | null };
|
|
language: { id: string | null; name: string | null };
|
|
genre: { id: number | null; name: string | null };
|
|
authors: Label[];
|
|
publishers: Label[];
|
|
licenses?: {
|
|
id: number;
|
|
name: string;
|
|
type: { id: string; name: string | null };
|
|
isOfficial: boolean;
|
|
linkWikipedia?: string | null;
|
|
linkSite?: string | null;
|
|
comments?: string | null;
|
|
}[];
|
|
relations?: {
|
|
direction: "from" | "to";
|
|
type: { id: string; name: string | null };
|
|
entry: { id: number; title: string | null };
|
|
}[];
|
|
tags?: {
|
|
id: number;
|
|
name: string;
|
|
type: { id: string; name: string | null };
|
|
category: { id: number | null; name: string | null };
|
|
memberSeq: number | null;
|
|
link: string | null;
|
|
comments: string | null;
|
|
}[];
|
|
ports?: {
|
|
id: number;
|
|
title: string | null;
|
|
platform: { id: number; name: string | null };
|
|
isOfficial: boolean;
|
|
linkSystem: string | null;
|
|
}[];
|
|
remakes?: {
|
|
id: number;
|
|
title: string;
|
|
fileLink: string;
|
|
fileDate: string | null;
|
|
fileSize: number | null;
|
|
authors: string | null;
|
|
platforms: string | null;
|
|
remakeYears: string | null;
|
|
remakeStatus: string | null;
|
|
}[];
|
|
scores?: {
|
|
website: { id: number; name: string | null };
|
|
score: number;
|
|
votes: number;
|
|
}[];
|
|
notes?: {
|
|
id: number;
|
|
type: { id: string; name: string | null };
|
|
text: string;
|
|
}[];
|
|
origins?: {
|
|
type: { id: string; name: string | null };
|
|
libraryTitle: string;
|
|
publication: string | null;
|
|
containerId: number | null;
|
|
issueId: number | null;
|
|
issue: { id: number; magazineId: number | null; magazineTitle: string | null } | null;
|
|
date: { year: number | null; month: number | null; day: number | null };
|
|
}[];
|
|
// extra fields for richer details
|
|
maxPlayers?: number;
|
|
availabletypeId?: string | null;
|
|
withoutLoadScreen?: number;
|
|
withoutInlay?: number;
|
|
issueId?: number | null;
|
|
files?: {
|
|
id: number;
|
|
link: string;
|
|
size: number | null;
|
|
md5: string | null;
|
|
comments: string | null;
|
|
type: { id: number; name: string };
|
|
}[];
|
|
// Flat downloads by entry_id
|
|
downloadsFlat?: {
|
|
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;
|
|
releaseSeq: number;
|
|
localLink?: string | null;
|
|
}[];
|
|
releases?: {
|
|
releaseSeq: number;
|
|
type: { id: string | null; name: string | null };
|
|
language: { id: string | null; name: string | null };
|
|
machinetype: { id: number | null; name: string | null };
|
|
year: number | null;
|
|
comments: string | null;
|
|
downloads: {
|
|
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;
|
|
localLink?: string | null;
|
|
}[];
|
|
}[];
|
|
// Additional relationships
|
|
aliases?: { releaseSeq: number; languageId: string; title: string }[];
|
|
webrefs?: { link: string; languageId: string; website: { id: number; name: string; link?: string | null } }[];
|
|
magazineRefs?: {
|
|
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;
|
|
};
|
|
}[];
|
|
};
|
|
|
|
export default function EntryDetailClient({ data }: { data: EntryDetailData | null }) {
|
|
if (!data) return <div className="alert alert-warning">Not found</div>;
|
|
|
|
return (
|
|
<div>
|
|
<ZxdbBreadcrumbs
|
|
items={[
|
|
{ label: "ZXDB", href: "/zxdb" },
|
|
{ label: "Entries", href: "/zxdb/entries" },
|
|
{ label: data.title },
|
|
]}
|
|
/>
|
|
|
|
<div className="d-flex align-items-center gap-2 flex-wrap">
|
|
<h1 className="mb-0">{data.title}</h1>
|
|
{data.genre.name && (
|
|
<Link className="badge text-bg-secondary text-decoration-none" href={`/zxdb/genres/${data.genre.id}`}>
|
|
{data.genre.name}
|
|
</Link>
|
|
)}
|
|
{data.language.name && (
|
|
<Link className="badge text-bg-info text-decoration-none" href={`/zxdb/languages/${data.language.id}`}>
|
|
{data.language.name}
|
|
</Link>
|
|
)}
|
|
{data.machinetype.name && (
|
|
<Link className="badge text-bg-primary text-decoration-none" href={`/zxdb/machinetypes/${data.machinetype.id}`}>
|
|
{data.machinetype.name}
|
|
</Link>
|
|
)}
|
|
{data.isXrated ? <span className="badge text-bg-danger">18+</span> : null}
|
|
</div>
|
|
|
|
<div className="row g-3 mt-2">
|
|
<div className="col-lg-4">
|
|
<div className="card shadow-sm mb-3">
|
|
<div className="card-body">
|
|
<h5 className="card-title">Entry Summary</h5>
|
|
<div className="table-responsive">
|
|
<table className="table table-sm table-striped align-middle mb-0">
|
|
<tbody>
|
|
<tr>
|
|
<th style={{ width: 180 }}>ID</th>
|
|
<td>{data.id}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Title</th>
|
|
<td>{data.title}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Machine</th>
|
|
<td>
|
|
{data.machinetype.id != null ? (
|
|
data.machinetype.name ? (
|
|
<Link href={`/zxdb/machinetypes/${data.machinetype.id}`}>{data.machinetype.name}</Link>
|
|
) : (
|
|
<span>#{data.machinetype.id}</span>
|
|
)
|
|
) : (
|
|
<span className="text-secondary">-</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Language</th>
|
|
<td>
|
|
{data.language.id ? (
|
|
data.language.name ? (
|
|
<Link href={`/zxdb/languages/${data.language.id}`}>{data.language.name}</Link>
|
|
) : (
|
|
<span>{data.language.id}</span>
|
|
)
|
|
) : (
|
|
<span className="text-secondary">-</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Genre</th>
|
|
<td>
|
|
{data.genre.id ? (
|
|
data.genre.name ? (
|
|
<Link href={`/zxdb/genres/${data.genre.id}`}>{data.genre.name}</Link>
|
|
) : (
|
|
<span>#{data.genre.id}</span>
|
|
)
|
|
) : (
|
|
<span className="text-secondary">-</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
{typeof data.maxPlayers !== "undefined" && (
|
|
<tr>
|
|
<th>Max Players</th>
|
|
<td>{data.maxPlayers}</td>
|
|
</tr>
|
|
)}
|
|
{typeof data.availabletypeId !== "undefined" && (
|
|
<tr>
|
|
<th>Available Type</th>
|
|
<td>{data.availabletypeId ?? <span className="text-secondary">-</span>}</td>
|
|
</tr>
|
|
)}
|
|
{typeof data.withoutLoadScreen !== "undefined" && (
|
|
<tr>
|
|
<th>Without Load Screen</th>
|
|
<td>{data.withoutLoadScreen ? "Yes" : "No"}</td>
|
|
</tr>
|
|
)}
|
|
{typeof data.withoutInlay !== "undefined" && (
|
|
<tr>
|
|
<th>Without Inlay</th>
|
|
<td>{data.withoutInlay ? "Yes" : "No"}</td>
|
|
</tr>
|
|
)}
|
|
{typeof data.issueId !== "undefined" && (
|
|
<tr>
|
|
<th>Issue</th>
|
|
<td>{data.issueId ? <span>#{data.issueId}</span> : <span className="text-secondary">-</span>}</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card shadow-sm mb-3">
|
|
<div className="card-body">
|
|
<h5 className="card-title">People</h5>
|
|
<div className="row g-3">
|
|
<div className="col-12">
|
|
<div className="text-secondary small mb-1">Authors</div>
|
|
{data.authors.length === 0 && <div className="text-secondary">Unknown</div>}
|
|
{data.authors.length > 0 && (
|
|
<ul className="list-unstyled mb-0">
|
|
{data.authors.map((a) => (
|
|
<li key={a.id}>
|
|
<Link href={`/zxdb/labels/${a.id}`}>{a.name}</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
<div className="col-12">
|
|
<div className="text-secondary small mb-1">Publishers</div>
|
|
{data.publishers.length === 0 && <div className="text-secondary">Unknown</div>}
|
|
{data.publishers.length > 0 && (
|
|
<ul className="list-unstyled mb-0">
|
|
{data.publishers.map((p) => (
|
|
<li key={p.id}>
|
|
<Link href={`/zxdb/labels/${p.id}`}>{p.name}</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card shadow-sm mb-3">
|
|
<div className="card-body">
|
|
<h5 className="card-title">Magazine References</h5>
|
|
{(!data.magazineRefs || data.magazineRefs.length === 0) && <div className="text-secondary">No magazine references recorded</div>}
|
|
{data.magazineRefs && data.magazineRefs.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table table-sm table-striped align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Magazine</th>
|
|
<th style={{ width: 140 }}>Issue</th>
|
|
<th style={{ width: 140 }}>Type</th>
|
|
<th style={{ width: 120 }}>Page</th>
|
|
<th>Score</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.magazineRefs.map((m) => (
|
|
<tr key={m.id}>
|
|
<td>
|
|
{m.magazineId ? (
|
|
<Link href={`/zxdb/magazines/${m.magazineId}`}>{m.magazineName}</Link>
|
|
) : (
|
|
<span>{m.magazineName}</span>
|
|
)}
|
|
</td>
|
|
<td>
|
|
<Link href={`/zxdb/issues/${m.issueId}`}>
|
|
{m.issue.dateYear ? `${m.issue.dateYear} ` : ""}
|
|
{m.issue.number ? `#${m.issue.number}` : ""}
|
|
{m.issue.special ? ` (${m.issue.special})` : ""}
|
|
</Link>
|
|
</td>
|
|
<td>{m.referencetypeName}</td>
|
|
<td>{m.page > 0 ? m.page : "-"}</td>
|
|
<td>{m.scoreGroup || "-"}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card shadow-sm mb-3">
|
|
<div className="card-body d-flex flex-wrap gap-2">
|
|
<Link className="btn btn-sm btn-outline-secondary" href={`/zxdb/entries/${data.id}`}>Permalink</Link>
|
|
<Link className="btn btn-sm btn-outline-primary" href="/zxdb">Back to Explorer</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-lg-8">
|
|
<div className="card shadow-sm mb-3">
|
|
<div className="card-body">
|
|
<h5 className="card-title">Downloads</h5>
|
|
{(!data.downloadsFlat || data.downloadsFlat.length === 0) && <div className="text-secondary">No downloads</div>}
|
|
{data.downloadsFlat && data.downloadsFlat.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: 260 }}>MD5</th>
|
|
<th>Flags</th>
|
|
<th>Details</th>
|
|
<th>Comments</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.downloadsFlat.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>
|
|
<div className="d-flex flex-column gap-1">
|
|
{isHttp ? (
|
|
<a href={d.link} target="_blank" rel="noopener noreferrer" className="text-break small">{d.link}</a>
|
|
) : (
|
|
<span className="text-break small">{d.link}</span>
|
|
)}
|
|
{d.localLink && (
|
|
<a href={d.localLink} className="btn btn-xs btn-success py-0 px-1" style={{ fontSize: "0.7rem", width: "fit-content" }}>
|
|
Local Mirror
|
|
</a>
|
|
)}
|
|
</div>
|
|
</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>
|
|
)}
|
|
{d.machinetype.name && (
|
|
<Link className="badge text-bg-primary text-decoration-none" href={`/zxdb/machinetypes/${d.machinetype.id}`}>{d.machinetype.name}</Link>
|
|
)}
|
|
{typeof d.year === "number" ? <span className="badge text-bg-dark">{d.year}</span> : null}
|
|
<Link className="badge text-bg-light text-decoration-none" href={`/zxdb/releases/${data.id}/${d.releaseSeq}`}>
|
|
rel #{d.releaseSeq}
|
|
</Link>
|
|
</div>
|
|
</td>
|
|
<td>{d.comments ?? ""}</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card shadow-sm mb-3">
|
|
<div className="card-body">
|
|
<h5 className="card-title">Releases</h5>
|
|
{(!data.releases || data.releases.length === 0) && <div className="text-secondary">No releases recorded</div>}
|
|
{data.releases && data.releases.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table table-sm table-striped align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th style={{ width: 120 }}>Release #</th>
|
|
<th style={{ width: 120 }}>Year</th>
|
|
<th>Downloads</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.releases.map((r) => (
|
|
<tr key={r.releaseSeq}>
|
|
<td>
|
|
<Link href={`/zxdb/releases/${data.id}/${r.releaseSeq}`}>#{r.releaseSeq}</Link>
|
|
</td>
|
|
<td>{r.year ?? <span className="text-secondary">-</span>}</td>
|
|
<td>{r.downloads.length}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card shadow-sm mb-3">
|
|
<div className="card-body">
|
|
<h5 className="card-title">Origins</h5>
|
|
{(!data.origins || data.origins.length === 0) && <div className="text-secondary">No origins recorded</div>}
|
|
{data.origins && data.origins.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table table-sm table-striped align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Type</th>
|
|
<th>Title</th>
|
|
<th>Publication</th>
|
|
<th style={{ width: 200 }}>Issue</th>
|
|
<th style={{ width: 140 }}>Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.origins.map((o, idx) => {
|
|
const dateParts = [o.date.year, o.date.month, o.date.day]
|
|
.filter((v) => typeof v === "number" && Number.isFinite(v))
|
|
.map((v, i) => (i === 0 ? String(v) : String(v).padStart(2, "0")));
|
|
const dateText = dateParts.length ? dateParts.join("/") : "-";
|
|
return (
|
|
<tr key={`${o.type.id}-${idx}`}>
|
|
<td>{o.type.name ?? o.type.id}</td>
|
|
<td>{o.libraryTitle}</td>
|
|
<td>{o.publication ?? <span className="text-secondary">-</span>}</td>
|
|
<td>
|
|
{o.issue ? (
|
|
<div className="d-flex flex-column">
|
|
<Link href={`/zxdb/issues/${o.issue.id}`}>Issue #{o.issue.id}</Link>
|
|
{o.issue.magazineId != null && (
|
|
<Link className="text-secondary small" href={`/zxdb/magazines/${o.issue.magazineId}`}>
|
|
{o.issue.magazineTitle ?? `Magazine #${o.issue.magazineId}`}
|
|
</Link>
|
|
)}
|
|
</div>
|
|
) : o.containerId ? (
|
|
<span>Container #{o.containerId}</span>
|
|
) : (
|
|
<span className="text-secondary">-</span>
|
|
)}
|
|
</td>
|
|
<td>{dateText}</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card shadow-sm mb-3">
|
|
<div className="card-body">
|
|
<h5 className="card-title">Relations</h5>
|
|
{(!data.relations || data.relations.length === 0) && <div className="text-secondary">No relations recorded</div>}
|
|
{data.relations && data.relations.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table table-sm table-striped align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th style={{ width: 90 }}>Direction</th>
|
|
<th style={{ width: 160 }}>Type</th>
|
|
<th>Entry</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.relations.map((r, idx) => (
|
|
<tr key={`${r.entry.id}-${r.type.id}-${idx}`}>
|
|
<td>{r.direction === "from" ? "From" : "To"}</td>
|
|
<td>{r.type.name ?? r.type.id}</td>
|
|
<td>
|
|
<Link href={`/zxdb/entries/${r.entry.id}`}>
|
|
{r.entry.title ?? `Entry #${r.entry.id}`}
|
|
</Link>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card shadow-sm mb-3">
|
|
<div className="card-body">
|
|
<h5 className="card-title">Tags / Members</h5>
|
|
{(!data.tags || data.tags.length === 0) && <div className="text-secondary">No tags recorded</div>}
|
|
{data.tags && data.tags.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table table-sm table-striped align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Tag</th>
|
|
<th style={{ width: 140 }}>Type</th>
|
|
<th style={{ width: 140 }}>Category</th>
|
|
<th style={{ width: 120 }}>Member Seq</th>
|
|
<th>Links</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.tags.map((t) => (
|
|
<tr key={`${t.id}-${t.category.id ?? "none"}`}>
|
|
<td>{t.name}</td>
|
|
<td>{t.type.name ?? t.type.id}</td>
|
|
<td>{t.category.name ?? (t.category.id != null ? `#${t.category.id}` : "-")}</td>
|
|
<td>{t.memberSeq ?? <span className="text-secondary">-</span>}</td>
|
|
<td>
|
|
<div className="d-flex gap-2 flex-wrap">
|
|
{t.link && (
|
|
<a href={t.link} target="_blank" rel="noreferrer">Link</a>
|
|
)}
|
|
{t.comments && <span className="text-secondary">{t.comments}</span>}
|
|
{!t.link && !t.comments && <span className="text-secondary">-</span>}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card shadow-sm mb-3">
|
|
<div className="card-body">
|
|
<h5 className="card-title">Ports</h5>
|
|
{(!data.ports || data.ports.length === 0) && <div className="text-secondary">No ports recorded</div>}
|
|
{data.ports && data.ports.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table table-sm table-striped align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Title</th>
|
|
<th style={{ width: 160 }}>Platform</th>
|
|
<th style={{ width: 120 }}>Official</th>
|
|
<th>Link</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.ports.map((p) => (
|
|
<tr key={p.id}>
|
|
<td>{p.title ?? <span className="text-secondary">-</span>}</td>
|
|
<td>{p.platform.name ?? `#${p.platform.id}`}</td>
|
|
<td>{p.isOfficial ? "Yes" : "No"}</td>
|
|
<td>
|
|
{p.linkSystem ? (
|
|
<a href={p.linkSystem} target="_blank" rel="noreferrer">Link</a>
|
|
) : (
|
|
<span className="text-secondary">-</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card shadow-sm mb-3">
|
|
<div className="card-body">
|
|
<h5 className="card-title">Remakes</h5>
|
|
{(!data.remakes || data.remakes.length === 0) && <div className="text-secondary">No remakes recorded</div>}
|
|
{data.remakes && data.remakes.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table table-sm table-striped align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Title</th>
|
|
<th style={{ width: 160 }}>Platforms</th>
|
|
<th style={{ width: 140 }}>Years</th>
|
|
<th style={{ width: 140 }}>File</th>
|
|
<th>Notes</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.remakes.map((r) => (
|
|
<tr key={r.id}>
|
|
<td>{r.title}</td>
|
|
<td>{r.platforms ?? <span className="text-secondary">-</span>}</td>
|
|
<td>{r.remakeYears ?? <span className="text-secondary">-</span>}</td>
|
|
<td>
|
|
{r.fileLink ? (
|
|
<a href={r.fileLink} target="_blank" rel="noreferrer">File</a>
|
|
) : (
|
|
<span className="text-secondary">-</span>
|
|
)}
|
|
</td>
|
|
<td>{r.remakeStatus ?? r.authors ?? <span className="text-secondary">-</span>}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card shadow-sm mb-3">
|
|
<div className="card-body">
|
|
<h5 className="card-title">Scores</h5>
|
|
{(!data.scores || data.scores.length === 0) && <div className="text-secondary">No scores recorded</div>}
|
|
{data.scores && data.scores.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table table-sm table-striped align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Website</th>
|
|
<th style={{ width: 120 }}>Score</th>
|
|
<th style={{ width: 120 }}>Votes</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.scores.map((s, idx) => (
|
|
<tr key={`${s.website.id}-${idx}`}>
|
|
<td>{s.website.name ?? `#${s.website.id}`}</td>
|
|
<td>{s.score}</td>
|
|
<td>{s.votes}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card shadow-sm mb-3">
|
|
<div className="card-body">
|
|
<h5 className="card-title">Notes</h5>
|
|
{(!data.notes || data.notes.length === 0) && <div className="text-secondary">No notes recorded</div>}
|
|
{data.notes && data.notes.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table table-sm table-striped align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th style={{ width: 140 }}>Type</th>
|
|
<th>Text</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.notes.map((n) => (
|
|
<tr key={n.id}>
|
|
<td>{n.type.name ?? n.type.id}</td>
|
|
<td>{n.text}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card shadow-sm mb-3">
|
|
<div className="card-body">
|
|
<h5 className="card-title">Aliases</h5>
|
|
{(!data.aliases || data.aliases.length === 0) && <div className="text-secondary">No aliases</div>}
|
|
{data.aliases && data.aliases.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table table-sm table-striped align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th style={{ width: 90 }}>Release #</th>
|
|
<th style={{ width: 120 }}>Language</th>
|
|
<th>Title</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.aliases.map((a, idx) => (
|
|
<tr key={`${a.releaseSeq}-${a.languageId}-${idx}`}>
|
|
<td>
|
|
<Link href={`/zxdb/releases/${data.id}/${a.releaseSeq}`}>#{a.releaseSeq}</Link>
|
|
</td>
|
|
<td>{a.languageId}</td>
|
|
<td>{a.title}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card shadow-sm mb-3">
|
|
<div className="card-body">
|
|
<h5 className="card-title">Licenses</h5>
|
|
{(!data.licenses || data.licenses.length === 0) && <div className="text-secondary">No licenses linked</div>}
|
|
{data.licenses && data.licenses.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table table-sm table-striped align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th style={{ width: 140 }}>Type</th>
|
|
<th style={{ width: 120 }}>Official</th>
|
|
<th>Links</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.licenses.map((l) => (
|
|
<tr key={l.id}>
|
|
<td>{l.name}</td>
|
|
<td>{l.type.name ?? l.type.id}</td>
|
|
<td>{l.isOfficial ? "Yes" : "No"}</td>
|
|
<td>
|
|
<div className="d-flex gap-2 flex-wrap">
|
|
{l.linkWikipedia && (
|
|
<a href={l.linkWikipedia} target="_blank" rel="noreferrer">Wikipedia</a>
|
|
)}
|
|
{l.linkSite && (
|
|
<a href={l.linkSite} target="_blank" rel="noreferrer">Site</a>
|
|
)}
|
|
{!l.linkWikipedia && !l.linkSite && <span className="text-secondary">-</span>}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card shadow-sm mb-3">
|
|
<div className="card-body">
|
|
<h5 className="card-title">Web links</h5>
|
|
{(!data.webrefs || data.webrefs.length === 0) && <div className="text-secondary">No web links</div>}
|
|
{data.webrefs && data.webrefs.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table table-sm table-striped align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Website</th>
|
|
<th style={{ width: 120 }}>Language</th>
|
|
<th>URL</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.webrefs.map((w, idx) => (
|
|
<tr key={`${w.website.id}-${idx}`}>
|
|
<td>
|
|
{w.website.link ? (
|
|
<a href={w.website.link} target="_blank" rel="noopener noreferrer">{w.website.name}</a>
|
|
) : (
|
|
<span>{w.website.name}</span>
|
|
)}
|
|
</td>
|
|
<td>{w.languageId}</td>
|
|
<td>
|
|
<a href={w.link} target="_blank" rel="noopener noreferrer">{w.link}</a>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card shadow-sm">
|
|
<div className="card-body">
|
|
<h5 className="card-title">Files</h5>
|
|
{(!data.files || data.files.length === 0) && <div className="text-secondary">No files linked</div>}
|
|
{data.files && 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: 260 }}>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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|