From 0594b34c625977285a9f6d9665fa059acae7eeb0 Mon Sep 17 00:00:00 2001 From: "D. Rimron-Soutter" Date: Sat, 10 Jan 2026 17:35:36 +0000 Subject: [PATCH] Add ZXDB breadcrumbs and release places Add ZXDB breadcrumbs on list/detail pages and group release magazine references by issue for clearer Places view. Signed-off-by: codex@lucy.xalior.com --- src/app/zxdb/components/ZxdbBreadcrumbs.tsx | 27 +++ src/app/zxdb/entries/EntriesExplorer.tsx | 8 + src/app/zxdb/entries/[id]/EntryDetail.tsx | 17 +- src/app/zxdb/genres/GenresSearch.tsx | 8 + src/app/zxdb/issues/[id]/page.tsx | 10 ++ src/app/zxdb/labels/LabelsSearch.tsx | 8 + src/app/zxdb/languages/LanguagesSearch.tsx | 8 + .../zxdb/machinetypes/MachineTypesSearch.tsx | 8 + src/app/zxdb/magazines/[id]/page.tsx | 9 + src/app/zxdb/magazines/page.tsx | 8 + src/app/zxdb/releases/ReleasesExplorer.tsx | 8 + .../[entryId]/[releaseSeq]/ReleaseDetail.tsx | 163 ++++++++++++------ 12 files changed, 230 insertions(+), 52 deletions(-) create mode 100644 src/app/zxdb/components/ZxdbBreadcrumbs.tsx diff --git a/src/app/zxdb/components/ZxdbBreadcrumbs.tsx b/src/app/zxdb/components/ZxdbBreadcrumbs.tsx new file mode 100644 index 0000000..bbc1ba4 --- /dev/null +++ b/src/app/zxdb/components/ZxdbBreadcrumbs.tsx @@ -0,0 +1,27 @@ +import Link from "next/link"; + +type Crumb = { + label: string; + href?: string; +}; + +export default function ZxdbBreadcrumbs({ items }: { items: Crumb[] }) { + if (items.length === 0) return null; + + const lastIndex = items.length - 1; + + return ( + + ); +} diff --git a/src/app/zxdb/entries/EntriesExplorer.tsx b/src/app/zxdb/entries/EntriesExplorer.tsx index 50a5ead..1ccebaa 100644 --- a/src/app/zxdb/entries/EntriesExplorer.tsx +++ b/src/app/zxdb/entries/EntriesExplorer.tsx @@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; import EntryLink from "../components/EntryLink"; import { usePathname, useRouter } from "next/navigation"; +import ZxdbBreadcrumbs from "@/app/zxdb/components/ZxdbBreadcrumbs"; type Item = { id: number; @@ -178,6 +179,13 @@ export default function EntriesExplorer({ return (
+ +

Entries

diff --git a/src/app/zxdb/entries/[id]/EntryDetail.tsx b/src/app/zxdb/entries/[id]/EntryDetail.tsx index af2d156..81cd217 100644 --- a/src/app/zxdb/entries/[id]/EntryDetail.tsx +++ b/src/app/zxdb/entries/[id]/EntryDetail.tsx @@ -1,6 +1,7 @@ "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 = { @@ -76,6 +77,14 @@ export default function EntryDetailClient({ data }: { data: EntryDetailData | nu return (
+ +

{data.title}

{data.genre.name && ( @@ -243,7 +252,9 @@ export default function EntryDetailClient({ data }: { data: EntryDetailData | nu {d.machinetype.name} )} {typeof d.year === "number" ? {d.year} : null} - rel #{d.releaseSeq} + + rel #{d.releaseSeq} +
{d.comments ?? ""} @@ -306,7 +317,9 @@ export default function EntryDetailClient({ data }: { data: EntryDetailData | nu {data.aliases.map((a, idx) => ( - #{a.releaseSeq} + + #{a.releaseSeq} + {a.languageId} {a.title} diff --git a/src/app/zxdb/genres/GenresSearch.tsx b/src/app/zxdb/genres/GenresSearch.tsx index 7afa726..d996344 100644 --- a/src/app/zxdb/genres/GenresSearch.tsx +++ b/src/app/zxdb/genres/GenresSearch.tsx @@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; +import ZxdbBreadcrumbs from "@/app/zxdb/components/ZxdbBreadcrumbs"; type Genre = { id: number; name: string }; type Paged = { items: T[]; page: number; pageSize: number; total: number }; @@ -31,6 +32,13 @@ export default function GenresSearch({ initial, initialQ }: { initial?: Paged + +

Genres

diff --git a/src/app/zxdb/issues/[id]/page.tsx b/src/app/zxdb/issues/[id]/page.tsx index 67650a1..1e49578 100644 --- a/src/app/zxdb/issues/[id]/page.tsx +++ b/src/app/zxdb/issues/[id]/page.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { notFound } from "next/navigation"; import { getIssue } from "@/server/repo/zxdb"; import EntryLink from "@/app/zxdb/components/EntryLink"; +import ZxdbBreadcrumbs from "@/app/zxdb/components/ZxdbBreadcrumbs"; export const metadata = { title: "ZXDB Issue" }; export const revalidate = 3600; @@ -18,6 +19,15 @@ export default async function Page({ params }: { params: Promise<{ id: string }> return (
+ +
← Back to magazine All magazines diff --git a/src/app/zxdb/labels/LabelsSearch.tsx b/src/app/zxdb/labels/LabelsSearch.tsx index 9911fb6..1a081ba 100644 --- a/src/app/zxdb/labels/LabelsSearch.tsx +++ b/src/app/zxdb/labels/LabelsSearch.tsx @@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; +import ZxdbBreadcrumbs from "@/app/zxdb/components/ZxdbBreadcrumbs"; type Label = { id: number; name: string; labeltypeId: string | null }; type Paged = { items: T[]; page: number; pageSize: number; total: number }; @@ -33,6 +34,13 @@ export default function LabelsSearch({ initial, initialQ }: { initial?: Paged + +

Labels

diff --git a/src/app/zxdb/languages/LanguagesSearch.tsx b/src/app/zxdb/languages/LanguagesSearch.tsx index 1ec3043..8ff5cc8 100644 --- a/src/app/zxdb/languages/LanguagesSearch.tsx +++ b/src/app/zxdb/languages/LanguagesSearch.tsx @@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; +import ZxdbBreadcrumbs from "@/app/zxdb/components/ZxdbBreadcrumbs"; type Language = { id: string; name: string }; type Paged = { items: T[]; page: number; pageSize: number; total: number }; @@ -31,6 +32,13 @@ export default function LanguagesSearch({ initial, initialQ }: { initial?: Paged return (
+ +

Languages

diff --git a/src/app/zxdb/machinetypes/MachineTypesSearch.tsx b/src/app/zxdb/machinetypes/MachineTypesSearch.tsx index d865b9f..a1a7b7d 100644 --- a/src/app/zxdb/machinetypes/MachineTypesSearch.tsx +++ b/src/app/zxdb/machinetypes/MachineTypesSearch.tsx @@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; +import ZxdbBreadcrumbs from "@/app/zxdb/components/ZxdbBreadcrumbs"; type MT = { id: number; name: string }; type Paged = { items: T[]; page: number; pageSize: number; total: number }; @@ -33,6 +34,13 @@ export default function MachineTypesSearch({ initial, initialQ }: { initial?: Pa return (
+ +

Machine Types

diff --git a/src/app/zxdb/magazines/[id]/page.tsx b/src/app/zxdb/magazines/[id]/page.tsx index ef52a28..5c6f512 100644 --- a/src/app/zxdb/magazines/[id]/page.tsx +++ b/src/app/zxdb/magazines/[id]/page.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import { notFound } from "next/navigation"; import { getMagazine } from "@/server/repo/zxdb"; +import ZxdbBreadcrumbs from "@/app/zxdb/components/ZxdbBreadcrumbs"; export const metadata = { title: "ZXDB Magazine" }; export const revalidate = 3600; @@ -15,6 +16,14 @@ export default async function Page({ params }: { params: Promise<{ id: string }> return (
+ +

{mag.title}

Language: {mag.languageId}
diff --git a/src/app/zxdb/magazines/page.tsx b/src/app/zxdb/magazines/page.tsx index ddbb49b..6027b73 100644 --- a/src/app/zxdb/magazines/page.tsx +++ b/src/app/zxdb/magazines/page.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import { listMagazines } from "@/server/repo/zxdb"; +import ZxdbBreadcrumbs from "@/app/zxdb/components/ZxdbBreadcrumbs"; export const metadata = { title: "ZXDB Magazines" }; @@ -19,6 +20,13 @@ export default async function Page({ return (
+ +

Magazines

diff --git a/src/app/zxdb/releases/ReleasesExplorer.tsx b/src/app/zxdb/releases/ReleasesExplorer.tsx index 84b0356..a9e0084 100644 --- a/src/app/zxdb/releases/ReleasesExplorer.tsx +++ b/src/app/zxdb/releases/ReleasesExplorer.tsx @@ -4,6 +4,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; import EntryLink from "../components/EntryLink"; import { usePathname, useRouter } from "next/navigation"; +import ZxdbBreadcrumbs from "@/app/zxdb/components/ZxdbBreadcrumbs"; type Item = { entryId: number; @@ -229,6 +230,13 @@ export default function ReleasesExplorer({ return (
+ +

Releases

diff --git a/src/app/zxdb/releases/[entryId]/[releaseSeq]/ReleaseDetail.tsx b/src/app/zxdb/releases/[entryId]/[releaseSeq]/ReleaseDetail.tsx index ed499de..d2dedbb 100644 --- a/src/app/zxdb/releases/[entryId]/[releaseSeq]/ReleaseDetail.tsx +++ b/src/app/zxdb/releases/[entryId]/[releaseSeq]/ReleaseDetail.tsx @@ -1,6 +1,7 @@ "use client"; import Link from "next/link"; +import ZxdbBreadcrumbs from "@/app/zxdb/components/ZxdbBreadcrumbs"; type ReleaseDetailData = { entry: { @@ -114,25 +115,69 @@ function formatCurrency(value: number | null, currency: ReleaseDetailData["relea return String(value); } +type MagazineGroup = { + magazineId: number | null; + magazineName: string | null; + items: ReleaseDetailData["magazineRefs"]; +}; + +type IssueGroup = { + issueId: number; + issue: ReleaseDetailData["magazineRefs"][number]["issue"]; + items: ReleaseDetailData["magazineRefs"]; +}; + +function groupMagazineRefs(refs: ReleaseDetailData["magazineRefs"]) { + const groups: MagazineGroup[] = []; + const lookup = new Map(); + + for (const ref of refs) { + const key = ref.magazineId != null ? `mag:${ref.magazineId}` : "mag:unknown"; + let group = lookup.get(key); + if (!group) { + group = { magazineId: ref.magazineId, magazineName: ref.magazineName, items: [] }; + lookup.set(key, group); + groups.push(group); + } + group.items.push(ref); + } + + return groups; +} + +function groupIssueRefs(refs: ReleaseDetailData["magazineRefs"]) { + const groups: IssueGroup[] = []; + const lookup = new Map(); + + for (const ref of refs) { + const key = ref.issueId; + let group = lookup.get(key); + if (!group) { + group = { issueId: ref.issueId, issue: ref.issue, items: [] }; + lookup.set(key, group); + groups.push(group); + } + group.items.push(ref); + } + + return groups; +} + export default function ReleaseDetailClient({ data }: { data: ReleaseDetailData | null }) { if (!data) return
Not found
; + const magazineGroups = groupMagazineRefs(data.magazineRefs); + return (
- +

Release #{data.release.releaseSeq}

@@ -221,43 +266,61 @@ export default function ReleaseDetailClient({ data }: { data: ReleaseDetailData
-
Magazine References
- {data.magazineRefs.length === 0 &&
No magazine references
} - {data.magazineRefs.length > 0 && ( -
- - - - - - - - - - - - - {data.magazineRefs.map((m) => ( - - - - - - - - +
Places (Magazines)
+ {magazineGroups.length === 0 &&
No magazine references
} + {magazineGroups.length > 0 && ( +
+ {magazineGroups.map((group) => ( +
+
+
+ {group.magazineId != null ? ( + + {group.magazineName ?? `Magazine #${group.magazineId}`} + + ) : ( + Unknown magazine + )} +
+
{group.items.length} reference{group.items.length === 1 ? "" : "s"}
+
+ {groupIssueRefs(group.items).map((issueGroup) => ( +
+
+
+ Issue #{issueGroup.issueId} +
{formatIssue(issueGroup.issue) || "-"}
+
+
+ {issueGroup.items.length} reference{issueGroup.items.length === 1 ? "" : "s"} +
+
+
+
MagazineIssueTypePageOriginalNotes
- {m.magazineId != null ? ( - {m.magazineName ?? `#${m.magazineId}`} - ) : ( - - - )} - - #{m.issueId} -
{formatIssue(m.issue) || "-"}
-
{m.referencetypeName ?? `#${m.referencetypeId}`}{m.page}{m.isOriginal ? "Yes" : "No"}{m.scoreGroup || "-"}
+ + + + + + + + + + {issueGroup.items.map((m) => ( + + + + + + + ))} + +
PageTypeOriginalNotes
{m.page}{m.referencetypeName ?? `#${m.referencetypeId}`}{m.isOriginal ? "Yes" : "No"}{m.scoreGroup || "-"}
+
+
))} - - +
+ ))}
)}