UI / react-bootstrap: Migrate client components to react-bootstrap (Card, Table, Form, Alert, Badge, Nav, Button, Spinner, Row, Col): the ZXDB explorers and detail pages (Labels, Genres, Languages, MachineTypes, Releases, Entries), TapeIdentifier, home page, Navbar and ThemeDropdown. Server components (home, zxdb hub, magazines, issues) keep raw HTML+className — react-bootstrap barrel imports resolve to undefined under Turbopack in server components. Replace bi bi-* CSS icons with react-bootstrap-icons. Add aria-labels to search inputs and visually-hidden captions to data tables. Code-review remediation (docs/todo.md): - FileViewer: replace useState-as-effect with a proper useEffect. - register.service: restore request-level caching of parsed registers. - middleware: convert .js to .ts, dev-only request logging. - Extract shared types to src/types/zxdb.ts; add src/server/repo barrel for incremental per-domain splitting. - Extract helpers: parseIdList (params.ts), serialize (serialize.ts), buildRegisterSummary/isInfoLine (register_helpers.ts). - Add loading.tsx skeletons for dynamic ZXDB detail routes. - generateMetadata + notFound() on entry/release/label detail pages. - opengraph-image: stable keys; ThemeDropdown: drop hardcoded cookie domain; remove unused page.module.css. Register parser & data: - Update data/nextreg.txt from upstream tbblue (SpectrumNext FPGA): 0x04/0x0A/0x0F/0x80/0x81 bit changes, new Issue 5 board id, 0x43 renamed "Palette Control", 0xF0/0xF8/0xF9/0xFA now "Issues 4 and 5 Only". - Add reg_44 custom parser for 0x44 (Palette Value 9-bit): the two consecutive writes render as separate "1st write" / "2nd write" modes. - Skip commented-out register headers so the disabled 0xA3 block no longer leaks a phantom register. - Add detailHasContent guard so body-less registers (0xC7/0xCB/0xCF/ 0xFF) and 0xF0's leading blank no longer emit empty tab strips. - Capture 0xF0's leading "Issues 4 and 5 Only" line as register text. - Add isIssueRestricted (case-sensitive) to detect the issue badge across rewording without flagging per-bit "(issue 5 only)" notes; update badge label to "Issues 4 & 5 Only". claude-opus-4-8@lucy
97 lines
3.6 KiB
TypeScript
97 lines
3.6 KiB
TypeScript
import Link from "next/link";
|
|
import { notFound } from "next/navigation";
|
|
import { getMagazine } from "@/server/repo";
|
|
import { Link45deg, Archive } from "react-bootstrap-icons";
|
|
import ZxdbBreadcrumbs from "@/app/zxdb/components/ZxdbBreadcrumbs";
|
|
|
|
export const metadata = { title: "ZXDB Magazine" };
|
|
export const revalidate = 3600;
|
|
|
|
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
|
|
const { id } = await params;
|
|
const magazineId = Number(id);
|
|
if (!Number.isFinite(magazineId) || magazineId <= 0) return notFound();
|
|
|
|
const mag = await getMagazine(magazineId);
|
|
if (!mag) return notFound();
|
|
|
|
return (
|
|
<div>
|
|
<ZxdbBreadcrumbs
|
|
items={[
|
|
{ label: "ZXDB", href: "/zxdb" },
|
|
{ label: "Magazines", href: "/zxdb/magazines" },
|
|
{ label: mag.title },
|
|
]}
|
|
/>
|
|
|
|
<h1 className="mb-1">{mag.title}</h1>
|
|
<div className="text-secondary mb-3">Language: {mag.languageId}</div>
|
|
|
|
<div className="mb-3 d-flex gap-2 flex-wrap">
|
|
<Link className="btn btn-outline-secondary btn-sm" href="/zxdb/magazines">← Back to list</Link>
|
|
{mag.linkSite && (
|
|
<a className="btn btn-outline-secondary btn-sm" href={mag.linkSite} target="_blank" rel="noreferrer">
|
|
Official site
|
|
</a>
|
|
)}
|
|
</div>
|
|
|
|
<h2 className="h5 mt-4">Issues</h2>
|
|
{mag.issues.length === 0 ? (
|
|
<div className="text-secondary">No issues found.</div>
|
|
) : (
|
|
<div className="table-responsive">
|
|
<table className="table table-sm align-middle">
|
|
<caption className="visually-hidden">Magazine issues</caption>
|
|
<thead>
|
|
<tr>
|
|
<th style={{ width: 200 }}>Issue</th>
|
|
<th style={{ width: 100 }}>Volume</th>
|
|
<th style={{ width: 100 }}>Number</th>
|
|
<th>Special</th>
|
|
<th>Supplement</th>
|
|
<th style={{ width: 100 }}>Links</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{mag.issues.map((i) => (
|
|
<tr key={i.id}>
|
|
<td>
|
|
<Link href={`/zxdb/issues/${i.id}`} className="link-underline link-underline-opacity-0">
|
|
{i.dateYear ?? ""}
|
|
{i.dateMonth ? `/${String(i.dateMonth).padStart(2, "0")}` : ""}
|
|
{" "}
|
|
<span className="text-secondary">(open issue)</span>
|
|
</Link>
|
|
</td>
|
|
<td>{i.volume ?? ""}</td>
|
|
<td>{i.number ?? ""}</td>
|
|
<td>{i.special ?? ""}</td>
|
|
<td>{i.supplement ?? ""}</td>
|
|
<td>
|
|
<div className="d-flex gap-2">
|
|
{i.linkMask && (
|
|
<a className="btn btn-outline-secondary btn-sm" href={i.linkMask} target="_blank" rel="noreferrer" title="Link">
|
|
<Link45deg aria-hidden />
|
|
<span className="visually-hidden">Link</span>
|
|
</a>
|
|
)}
|
|
{i.archiveMask && (
|
|
<a className="btn btn-outline-secondary btn-sm" href={i.archiveMask} target="_blank" rel="noreferrer" title="Archive">
|
|
<Archive aria-hidden />
|
|
<span className="visually-hidden">Archive</span>
|
|
</a>
|
|
)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|