Unify ZXDB list layouts
Apply sidebar filter layout to label/genre/language/machine lists and restructure release detail into a two-column view. Signed-off-by: codex@lucy.xalior.com
This commit is contained in:
@@ -39,40 +39,55 @@ export default function GenresSearch({ initial, initialQ }: { initial?: Paged<Ge
|
||||
]}
|
||||
/>
|
||||
|
||||
<h1>Genres</h1>
|
||||
<form className="row gy-2 gx-2 align-items-center" onSubmit={submit}>
|
||||
<div className="col-sm-8 col-md-6 col-lg-4">
|
||||
<input className="form-control" placeholder="Search genres…" value={q} onChange={(e) => setQ(e.target.value)} />
|
||||
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
|
||||
<div>
|
||||
<h1 className="mb-1">Genres</h1>
|
||||
<div className="text-secondary">{data?.total.toLocaleString() ?? "0"} results</div>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<button className="btn btn-primary">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
{data && data.items.length === 0 && <div className="alert alert-warning">No genres found.</div>}
|
||||
{data && data.items.length > 0 && (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 120 }}>ID</th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.items.map((g) => (
|
||||
<tr key={g.id}>
|
||||
<td><span className="badge text-bg-light">#{g.id}</span></td>
|
||||
<td>
|
||||
<Link href={`/zxdb/genres/${g.id}`}>{g.name}</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="row g-3">
|
||||
<div className="col-lg-3">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-body">
|
||||
<form className="d-flex flex-column gap-2" onSubmit={submit}>
|
||||
<div>
|
||||
<label className="form-label small text-secondary">Search</label>
|
||||
<input className="form-control" placeholder="Search genres…" value={q} onChange={(e) => setQ(e.target.value)} />
|
||||
</div>
|
||||
<div className="d-grid">
|
||||
<button className="btn btn-primary">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-lg-9">
|
||||
{data && data.items.length === 0 && <div className="alert alert-warning">No genres found.</div>}
|
||||
{data && data.items.length > 0 && (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 120 }}>ID</th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.items.map((g) => (
|
||||
<tr key={g.id}>
|
||||
<td><span className="badge text-bg-light">#{g.id}</span></td>
|
||||
<td>
|
||||
<Link href={`/zxdb/genres/${g.id}`}>{g.name}</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex align-items-center gap-2 mt-2">
|
||||
|
||||
@@ -41,44 +41,59 @@ export default function LabelsSearch({ initial, initialQ }: { initial?: Paged<La
|
||||
]}
|
||||
/>
|
||||
|
||||
<h1>Labels</h1>
|
||||
<form className="row gy-2 gx-2 align-items-center" onSubmit={submit}>
|
||||
<div className="col-sm-8 col-md-6 col-lg-4">
|
||||
<input className="form-control" placeholder="Search labels…" value={q} onChange={(e) => setQ(e.target.value)} />
|
||||
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
|
||||
<div>
|
||||
<h1 className="mb-1">Labels</h1>
|
||||
<div className="text-secondary">{data?.total.toLocaleString() ?? "0"} results</div>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<button className="btn btn-primary">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
{data && data.items.length === 0 && <div className="alert alert-warning">No labels found.</div>}
|
||||
{data && data.items.length > 0 && (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 100 }}>ID</th>
|
||||
<th>Name</th>
|
||||
<th style={{ width: 120 }}>Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.items.map((l) => (
|
||||
<tr key={l.id}>
|
||||
<td>#{l.id}</td>
|
||||
<td>
|
||||
<Link href={`/zxdb/labels/${l.id}`}>{l.name}</Link>
|
||||
</td>
|
||||
<td>
|
||||
<span className="badge text-bg-light">{l.labeltypeId ?? "?"}</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="row g-3">
|
||||
<div className="col-lg-3">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-body">
|
||||
<form className="d-flex flex-column gap-2" onSubmit={submit}>
|
||||
<div>
|
||||
<label className="form-label small text-secondary">Search</label>
|
||||
<input className="form-control" placeholder="Search labels…" value={q} onChange={(e) => setQ(e.target.value)} />
|
||||
</div>
|
||||
<div className="d-grid">
|
||||
<button className="btn btn-primary">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-lg-9">
|
||||
{data && data.items.length === 0 && <div className="alert alert-warning">No labels found.</div>}
|
||||
{data && data.items.length > 0 && (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 100 }}>ID</th>
|
||||
<th>Name</th>
|
||||
<th style={{ width: 120 }}>Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.items.map((l) => (
|
||||
<tr key={l.id}>
|
||||
<td>#{l.id}</td>
|
||||
<td>
|
||||
<Link href={`/zxdb/labels/${l.id}`}>{l.name}</Link>
|
||||
</td>
|
||||
<td>
|
||||
<span className="badge text-bg-light">{l.labeltypeId ?? "?"}</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex align-items-center gap-2 mt-2">
|
||||
|
||||
@@ -39,40 +39,55 @@ export default function LanguagesSearch({ initial, initialQ }: { initial?: Paged
|
||||
]}
|
||||
/>
|
||||
|
||||
<h1>Languages</h1>
|
||||
<form className="row gy-2 gx-2 align-items-center" onSubmit={submit}>
|
||||
<div className="col-sm-8 col-md-6 col-lg-4">
|
||||
<input className="form-control" placeholder="Search languages…" value={q} onChange={(e) => setQ(e.target.value)} />
|
||||
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
|
||||
<div>
|
||||
<h1 className="mb-1">Languages</h1>
|
||||
<div className="text-secondary">{data?.total.toLocaleString() ?? "0"} results</div>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<button className="btn btn-primary">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
{data && data.items.length === 0 && <div className="alert alert-warning">No languages found.</div>}
|
||||
{data && data.items.length > 0 && (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 120 }}>Code</th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.items.map((l) => (
|
||||
<tr key={l.id}>
|
||||
<td><span className="badge text-bg-light">{l.id}</span></td>
|
||||
<td>
|
||||
<Link href={`/zxdb/languages/${l.id}`}>{l.name}</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="row g-3">
|
||||
<div className="col-lg-3">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-body">
|
||||
<form className="d-flex flex-column gap-2" onSubmit={submit}>
|
||||
<div>
|
||||
<label className="form-label small text-secondary">Search</label>
|
||||
<input className="form-control" placeholder="Search languages…" value={q} onChange={(e) => setQ(e.target.value)} />
|
||||
</div>
|
||||
<div className="d-grid">
|
||||
<button className="btn btn-primary">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-lg-9">
|
||||
{data && data.items.length === 0 && <div className="alert alert-warning">No languages found.</div>}
|
||||
{data && data.items.length > 0 && (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 120 }}>Code</th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.items.map((l) => (
|
||||
<tr key={l.id}>
|
||||
<td><span className="badge text-bg-light">{l.id}</span></td>
|
||||
<td>
|
||||
<Link href={`/zxdb/languages/${l.id}`}>{l.name}</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex align-items-center gap-2 mt-2">
|
||||
|
||||
@@ -41,40 +41,55 @@ export default function MachineTypesSearch({ initial, initialQ }: { initial?: Pa
|
||||
]}
|
||||
/>
|
||||
|
||||
<h1>Machine Types</h1>
|
||||
<form className="row gy-2 gx-2 align-items-center" onSubmit={submit}>
|
||||
<div className="col-sm-8 col-md-6 col-lg-4">
|
||||
<input className="form-control" placeholder="Search machine types…" value={q} onChange={(e) => setQ(e.target.value)} />
|
||||
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
|
||||
<div>
|
||||
<h1 className="mb-1">Machine Types</h1>
|
||||
<div className="text-secondary">{data?.total.toLocaleString() ?? "0"} results</div>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<button className="btn btn-primary">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
{data && data.items.length === 0 && <div className="alert alert-warning">No machine types found.</div>}
|
||||
{data && data.items.length > 0 && (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 120 }}>ID</th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.items.map((m) => (
|
||||
<tr key={m.id}>
|
||||
<td><span className="badge text-bg-light">#{m.id}</span></td>
|
||||
<td>
|
||||
<Link href={`/zxdb/machinetypes/${m.id}`}>{m.name}</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="row g-3">
|
||||
<div className="col-lg-3">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-body">
|
||||
<form className="d-flex flex-column gap-2" onSubmit={submit}>
|
||||
<div>
|
||||
<label className="form-label small text-secondary">Search</label>
|
||||
<input className="form-control" placeholder="Search machine types…" value={q} onChange={(e) => setQ(e.target.value)} />
|
||||
</div>
|
||||
<div className="d-grid">
|
||||
<button className="btn btn-primary">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-lg-9">
|
||||
{data && data.items.length === 0 && <div className="alert alert-warning">No machine types found.</div>}
|
||||
{data && data.items.length > 0 && (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 120 }}>ID</th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.items.map((m) => (
|
||||
<tr key={m.id}>
|
||||
<td><span className="badge text-bg-light">#{m.id}</span></td>
|
||||
<td>
|
||||
<Link href={`/zxdb/machinetypes/${m.id}`}>{m.name}</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex align-items-center gap-2 mt-2">
|
||||
|
||||
@@ -191,336 +191,337 @@ export default function ReleaseDetailClient({ data }: { data: ReleaseDetailData
|
||||
</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>Other Releases</h5>
|
||||
{otherReleases.length === 0 && <div className="text-secondary">No other releases</div>}
|
||||
{otherReleases.length > 0 && (
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
{otherReleases.map((r) => (
|
||||
<Link
|
||||
key={r.releaseSeq}
|
||||
className="badge text-bg-light text-decoration-none"
|
||||
href={`/zxdb/releases/${data.entry.id}/${r.releaseSeq}`}
|
||||
>
|
||||
#{r.releaseSeq}{r.year != null ? ` · ${r.year}` : ""}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<h5>Places (Magazines)</h5>
|
||||
{magazineGroups.length === 0 && <div className="text-secondary">No magazine references</div>}
|
||||
{magazineGroups.length > 0 && (
|
||||
<div className="d-flex flex-column gap-3">
|
||||
{magazineGroups.map((group) => (
|
||||
<div key={group.magazineId ?? "unknown"}>
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<div className="fw-semibold">
|
||||
{group.magazineId != null ? (
|
||||
<Link href={`/zxdb/magazines/${group.magazineId}`}>
|
||||
{group.magazineName ?? `Magazine #${group.magazineId}`}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-secondary">Unknown magazine</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-secondary small">{group.items.length} reference{group.items.length === 1 ? "" : "s"}</div>
|
||||
</div>
|
||||
{groupIssueRefs(group.items).map((issueGroup) => (
|
||||
<div key={issueGroup.issueId} className="mt-2">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<Link href={`/zxdb/issues/${issueGroup.issueId}`}>Issue #{issueGroup.issueId}</Link>
|
||||
<div className="text-secondary small">{formatIssue(issueGroup.issue) || "-"}</div>
|
||||
</div>
|
||||
<div className="text-secondary small">
|
||||
{issueGroup.items.length} reference{issueGroup.items.length === 1 ? "" : "s"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="table-responsive mt-2">
|
||||
<table className="table table-sm table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 80 }}>Page</th>
|
||||
<th style={{ width: 120 }}>Type</th>
|
||||
<th style={{ width: 100 }}>Original</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{issueGroup.items.map((m) => (
|
||||
<tr key={m.id}>
|
||||
<td>{m.page}</td>
|
||||
<td>{m.referencetypeName ?? `#${m.referencetypeId}`}</td>
|
||||
<td>{m.isOriginal ? "Yes" : "No"}</td>
|
||||
<td>{m.scoreGroup || "-"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</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>
|
||||
<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">Release Summary</h5>
|
||||
<div className="table-responsive">
|
||||
<table className="table table-sm table-striped align-middle mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style={{ width: 160 }}>Entry</th>
|
||||
<td>
|
||||
{isHttp ? (
|
||||
<a href={d.link} target="_blank" rel="noopener noreferrer">{d.link}</a>
|
||||
) : (
|
||||
<span>{d.link}</span>
|
||||
)}
|
||||
<Link href={`/zxdb/entries/${data.entry.id}`}>#{data.entry.id}</Link>
|
||||
</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>
|
||||
<tr>
|
||||
<th>Release Sequence</th>
|
||||
<td>#{data.release.releaseSeq}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Release Date</th>
|
||||
<td>
|
||||
{s.link ? (
|
||||
isHttp ? (
|
||||
<a href={s.link} target="_blank" rel="noopener noreferrer">{s.link}</a>
|
||||
) : (
|
||||
<span>{s.link}</span>
|
||||
)
|
||||
{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>
|
||||
<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>
|
||||
<tr>
|
||||
<th>Currency</th>
|
||||
<td>
|
||||
{isHttp ? (
|
||||
<a href={f.link} target="_blank" rel="noopener noreferrer">{f.link}</a>
|
||||
{data.release.currency.id ? (
|
||||
<span>{data.release.currency.id} {data.release.currency.name ? `(${data.release.currency.name})` : ""}</span>
|
||||
) : (
|
||||
<span>{f.link}</span>
|
||||
<span className="text-secondary">-</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>
|
||||
<tr>
|
||||
<th>Price</th>
|
||||
<td>{formatCurrency(data.release.prices.release, data.release.currency)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Budget Price</th>
|
||||
<td>{formatCurrency(data.release.prices.budget, data.release.currency)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Microdrive Price</th>
|
||||
<td>{formatCurrency(data.release.prices.microdrive, data.release.currency)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Disk Price</th>
|
||||
<td>{formatCurrency(data.release.prices.disk, data.release.currency)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Cartridge Price</th>
|
||||
<td>{formatCurrency(data.release.prices.cartridge, data.release.currency)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Book ISBN</th>
|
||||
<td>{data.release.book.isbn ?? <span className="text-secondary">-</span>}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Book Pages</th>
|
||||
<td>{data.release.book.pages ?? <span className="text-secondary">-</span>}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">Other Releases</h5>
|
||||
{otherReleases.length === 0 && <div className="text-secondary">No other releases</div>}
|
||||
{otherReleases.length > 0 && (
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
{otherReleases.map((r) => (
|
||||
<Link
|
||||
key={r.releaseSeq}
|
||||
className="badge text-bg-light text-decoration-none"
|
||||
href={`/zxdb/releases/${data.entry.id}/${r.releaseSeq}`}
|
||||
>
|
||||
#{r.releaseSeq}{r.year != null ? ` · ${r.year}` : ""}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-lg-8">
|
||||
<div className="card shadow-sm mb-3">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">Places (Magazines)</h5>
|
||||
{magazineGroups.length === 0 && <div className="text-secondary">No magazine references</div>}
|
||||
{magazineGroups.length > 0 && (
|
||||
<div className="d-flex flex-column gap-3">
|
||||
{magazineGroups.map((group) => (
|
||||
<div key={group.magazineId ?? "unknown"}>
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<div className="fw-semibold">
|
||||
{group.magazineId != null ? (
|
||||
<Link href={`/zxdb/magazines/${group.magazineId}`}>
|
||||
{group.magazineName ?? `Magazine #${group.magazineId}`}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-secondary">Unknown magazine</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-secondary small">{group.items.length} reference{group.items.length === 1 ? "" : "s"}</div>
|
||||
</div>
|
||||
{groupIssueRefs(group.items).map((issueGroup) => (
|
||||
<div key={issueGroup.issueId} className="mt-2">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<Link href={`/zxdb/issues/${issueGroup.issueId}`}>Issue #{issueGroup.issueId}</Link>
|
||||
<div className="text-secondary small">{formatIssue(issueGroup.issue) || "-"}</div>
|
||||
</div>
|
||||
<div className="text-secondary small">
|
||||
{issueGroup.items.length} reference{issueGroup.items.length === 1 ? "" : "s"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="table-responsive mt-2">
|
||||
<table className="table table-sm table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 80 }}>Page</th>
|
||||
<th style={{ width: 120 }}>Type</th>
|
||||
<th style={{ width: 100 }}>Original</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{issueGroup.items.map((m) => (
|
||||
<tr key={m.id}>
|
||||
<td>{m.page}</td>
|
||||
<td>{m.referencetypeName ?? `#${m.referencetypeId}`}</td>
|
||||
<td>{m.isOriginal ? "Yes" : "No"}</td>
|
||||
<td>{m.scoreGroup || "-"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card shadow-sm mb-3">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">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>
|
||||
</div>
|
||||
|
||||
<div className="card shadow-sm mb-3">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">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>
|
||||
</div>
|
||||
|
||||
<div className="card shadow-sm mb-3">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user