From a1a04a89cf4b45fa7296ee984f7685c8f9a431a1 Mon Sep 17 00:00:00 2001 From: "D. Rimron-Soutter" Date: Thu, 18 Dec 2025 13:10:58 +0000 Subject: [PATCH] Adding the first stubs of the magazine browser --- .output.txt | 225 --------------------------- COMMIT_EDITMSG | 1 - example.env | 11 ++ package.json | 4 +- pnpm-lock.yaml | 26 ++++ src/app/zxdb/issues/[id]/page.tsx | 79 ++++++++++ src/app/zxdb/magazines/[id]/page.tsx | 85 ++++++++++ src/app/zxdb/magazines/page.tsx | 81 ++++++++++ src/app/zxdb/page.tsx | 16 ++ src/server/repo/zxdb.ts | 202 ++++++++++++++++++++++++ src/server/schema/zxdb.ts | 130 +++++++++++++++- 11 files changed, 632 insertions(+), 228 deletions(-) delete mode 100644 .output.txt delete mode 100644 COMMIT_EDITMSG create mode 100644 src/app/zxdb/issues/[id]/page.tsx create mode 100644 src/app/zxdb/magazines/[id]/page.tsx create mode 100644 src/app/zxdb/magazines/page.tsx diff --git a/.output.txt b/.output.txt deleted file mode 100644 index a78c4f1..0000000 --- a/.output.txt +++ /dev/null @@ -1,225 +0,0 @@ - ▲ Next.js 15.5.9 (Turbopack) - - Environments: .env - Creating an optimized production build ... - ✓ Finished writing to disk in 48ms -Turbopack build encountered 21 warnings: -./src/scss/nbn.scss -Issue while running loader -SassWarning: 311 repetitive deprecation warnings omitted. -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 0, column 8 of file:///Volumes/McFiver/u/GIT/next-explorer/node_modules/bootstrap/scss/bootstrap.scss:0:8: -Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0. -More info and automated migrator: https://sass-lang.com/d/import -0 | @import "mixins/banner"; -node_modules/bootstrap/scss/bootstrap.scss 1:9 @import -src/scss/nbn.scss 9:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 10, column 29 of file:///Volumes/McFiver/u/GIT/next-explorer/node_modules/bootstrap/scss/_functions.scss:10:29: -Global built-in functions are deprecated and will be removed in Dart Sass 3.0.0. -Use math.unit instead. -More info and automated migrator: https://sass-lang.com/d/import -10 | @if $prev-num == null or unit($num) == "%" or unit($prev-num) == "%" { -node_modules/bootstrap/scss/_functions.scss 11:30 -assert-ascending() -node_modules/bootstrap/scss/_variables.scss 494:1 @import -node_modules/bootstrap/scss/bootstrap.scss 8:9 @import -src/scss/nbn.scss 9:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 10, column 50 of file:///Volumes/McFiver/u/GIT/next-explorer/node_modules/bootstrap/scss/_functions.scss:10:50: -Global built-in functions are deprecated and will be removed in Dart Sass 3.0.0. -Use math.unit instead. -More info and automated migrator: https://sass-lang.com/d/import -10 | @if $prev-num == null or unit($num) == "%" or unit($prev-num) == "%" { -node_modules/bootstrap/scss/_functions.scss 11:51 -assert-ascending() -node_modules/bootstrap/scss/_variables.scss 494:1 @import -node_modules/bootstrap/scss/bootstrap.scss 8:9 @import -src/scss/nbn.scss 9:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 10, column 8 of file:///Volumes/McFiver/u/GIT/next-explorer/src/scss/nbn.scss:10:8: -Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0. -More info and automated migrator: https://sass-lang.com/d/import -10 | @import "bootswatch"; -src/scss/nbn.scss 11:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 12, column 8 of file:///Volumes/McFiver/u/GIT/next-explorer/src/scss/nbn.scss:12:8: -Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0. -More info and automated migrator: https://sass-lang.com/d/import -12 | @import "explorer"; -src/scss/nbn.scss 13:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 176, column 10 of file:///Volumes/McFiver/u/GIT/next-explorer/node_modules/bootstrap/scss/_functions.scss:176:10: -The Sass if() syntax is deprecated in favor of the modern CSS syntax. -Suggestion: if(sass($l1 > $l2): divide($l1 + 0.05, $l2 + 0.05); else: divide($l2 + 0.05, $l1 + 0.05)) -More info: https://sass-lang.com/d/if-function -176 | @return if($l1 > $l2, divide($l1 + .05, $l2 + .05), divide($l2 + .05, $l1 + .05)); -node_modules/bootstrap/scss/_functions.scss 177:11 @import -node_modules/bootstrap/scss/bootstrap.scss 7:9 @import -src/scss/nbn.scss 9:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 184, column 9 of file:///Volumes/McFiver/u/GIT/next-explorer/node_modules/bootstrap/scss/_functions.scss:184:9: -red() is deprecated. Suggestion: -color.channel($color, "red", $space: rgb) -More info: https://sass-lang.com/d/color-functions -184 | "r": red($color), -node_modules/bootstrap/scss/_functions.scss 185:10 luminance() -node_modules/bootstrap/scss/_functions.scss 174:8 contrast-ratio() -node_modules/bootstrap/scss/_functions.scss 159:22 color-contrast() -node_modules/bootstrap/scss/_variables.scss 846:42 @import -node_modules/bootstrap/scss/bootstrap.scss 8:9 @import -src/scss/nbn.scss 9:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 185, column 9 of file:///Volumes/McFiver/u/GIT/next-explorer/node_modules/bootstrap/scss/_functions.scss:185:9: -green() is deprecated. Suggestion: -color.channel($color, "green", $space: rgb) -More info: https://sass-lang.com/d/color-functions -185 | "g": green($color), -node_modules/bootstrap/scss/_functions.scss 186:10 luminance() -node_modules/bootstrap/scss/_functions.scss 174:8 contrast-ratio() -node_modules/bootstrap/scss/_functions.scss 159:22 color-contrast() -node_modules/bootstrap/scss/_variables.scss 846:42 @import -node_modules/bootstrap/scss/bootstrap.scss 8:9 @import -src/scss/nbn.scss 9:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 186, column 9 of file:///Volumes/McFiver/u/GIT/next-explorer/node_modules/bootstrap/scss/_functions.scss:186:9: -blue() is deprecated. Suggestion: -color.channel($color, "blue", $space: rgb) -More info: https://sass-lang.com/d/color-functions -186 | "b": blue($color) -node_modules/bootstrap/scss/_functions.scss 187:10 luminance() -node_modules/bootstrap/scss/_functions.scss 174:8 contrast-ratio() -node_modules/bootstrap/scss/_functions.scss 159:22 color-contrast() -node_modules/bootstrap/scss/_variables.scss 846:42 @import -node_modules/bootstrap/scss/bootstrap.scss 8:9 @import -src/scss/nbn.scss 9:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 190, column 12 of file:///Volumes/McFiver/u/GIT/next-explorer/node_modules/bootstrap/scss/_functions.scss:190:12: -The Sass if() syntax is deprecated in favor of the modern CSS syntax. -Suggestion: if(sass(divide($value, 255) < 0.04045): divide(divide($value, 255), 12.92); else: nth($_luminance-list, $value + 1)) -More info: https://sass-lang.com/d/if-function -190 | $value: if(divide($value, 255) < .04045, divide(divide($value, 255), 12.92), nth($_luminance-list, $value + 1)); -node_modules/bootstrap/scss/_functions.scss 191:13 @import -node_modules/bootstrap/scss/bootstrap.scss 7:9 @import -src/scss/nbn.scss 9:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 206, column 10 of file:///Volumes/McFiver/u/GIT/next-explorer/node_modules/bootstrap/scss/_functions.scss:206:10: -Global built-in functions are deprecated and will be removed in Dart Sass 3.0.0. -Use color.mix instead. -More info and automated migrator: https://sass-lang.com/d/import -206 | @return mix(white, $color, $weight); -node_modules/bootstrap/scss/_functions.scss 207:11 tint-color() -node_modules/bootstrap/scss/_variables.scss 79:12 @import -node_modules/bootstrap/scss/bootstrap.scss 8:9 @import -src/scss/nbn.scss 9:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 211, column 10 of file:///Volumes/McFiver/u/GIT/next-explorer/node_modules/bootstrap/scss/_functions.scss:211:10: -Global built-in functions are deprecated and will be removed in Dart Sass 3.0.0. -Use color.mix instead. -More info and automated migrator: https://sass-lang.com/d/import -211 | @return mix(black, $color, $weight); -node_modules/bootstrap/scss/_functions.scss 212:11 shade-color() -node_modules/bootstrap/scss/_variables.scss 84:12 @import -node_modules/bootstrap/scss/bootstrap.scss 8:9 @import -src/scss/nbn.scss 9:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 216, column 10 of file:///Volumes/McFiver/u/GIT/next-explorer/node_modules/bootstrap/scss/_functions.scss:216:10: -The Sass if() syntax is deprecated in favor of the modern CSS syntax. -Suggestion: if(sass($weight > 0): shade-color($color, $weight); else: tint-color($color, -$weight)) -More info: https://sass-lang.com/d/if-function -216 | @return if($weight > 0, shade-color($color, $weight), tint-color($color, -$weight)); -node_modules/bootstrap/scss/_functions.scss 217:11 @import -node_modules/bootstrap/scss/bootstrap.scss 7:9 @import -src/scss/nbn.scss 9:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 341, column 26 of file:///Volumes/McFiver/u/GIT/next-explorer/node_modules/bootstrap/scss/_variables.scss:341:26: -Global built-in functions are deprecated and will be removed in Dart Sass 3.0.0. -Use color.mix instead. -More info and automated migrator: https://sass-lang.com/d/import -341 | $light-bg-subtle: mix($gray-100, $white) !default; -node_modules/bootstrap/scss/_variables.scss 342:27 @import -node_modules/bootstrap/scss/bootstrap.scss 8:9 @import -src/scss/nbn.scss 9:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 36, column 10 of file:///Volumes/McFiver/u/GIT/next-explorer/node_modules/bootstrap/scss/_functions.scss:36:10: -red() is deprecated. Suggestion: -color.channel($color, "red", $space: rgb) -More info: https://sass-lang.com/d/color-functions -36 | @return red($value), green($value), blue($value); -node_modules/bootstrap/scss/_functions.scss 37:11 to-rgb() -node_modules/bootstrap/scss/_variables.scss 846:31 @import -node_modules/bootstrap/scss/bootstrap.scss 8:9 @import -src/scss/nbn.scss 9:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 36, column 23 of file:///Volumes/McFiver/u/GIT/next-explorer/node_modules/bootstrap/scss/_functions.scss:36:23: -green() is deprecated. Suggestion: -color.channel($color, "green", $space: rgb) -More info: https://sass-lang.com/d/color-functions -36 | @return red($value), green($value), blue($value); -node_modules/bootstrap/scss/_functions.scss 37:24 to-rgb() -node_modules/bootstrap/scss/_variables.scss 846:31 @import -node_modules/bootstrap/scss/bootstrap.scss 8:9 @import -src/scss/nbn.scss 9:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 57, column 29 of file:///Volumes/McFiver/u/GIT/next-explorer/node_modules/bootstrap/scss/_functions.scss:57:29: -The Sass if() syntax is deprecated in favor of the modern CSS syntax. -Suggestion: if(sass($arg == "$key"): $key; else: if($arg == "$value", $value, $arg)) -More info: https://sass-lang.com/d/if-function -57 | $_args: append($_args, if($arg == "$key", $key, if($arg == "$value", $value, $arg))); -node_modules/bootstrap/scss/_functions.scss 58:30 @import -node_modules/bootstrap/scss/bootstrap.scss 7:9 @import -src/scss/nbn.scss 9:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 57, column 54 of file:///Volumes/McFiver/u/GIT/next-explorer/node_modules/bootstrap/scss/_functions.scss:57:54: -The Sass if() syntax is deprecated in favor of the modern CSS syntax. -Suggestion: if(sass($arg == "$value"): $value; else: $arg) -More info: https://sass-lang.com/d/if-function -57 | $_args: append($_args, if($arg == "$key", $key, if($arg == "$value", $value, $arg))); -node_modules/bootstrap/scss/_functions.scss 58:55 @import -node_modules/bootstrap/scss/bootstrap.scss 7:9 @import -src/scss/nbn.scss 9:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 6, column 8 of file:///Volumes/McFiver/u/GIT/next-explorer/src/scss/nbn.scss:6:8: -Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0. -More info and automated migrator: https://sass-lang.com/d/import -6 | @import "variables"; -src/scss/nbn.scss 7:9 root stylesheet -./src/scss/nbn.scss -Issue while running loader -SassWarning: Deprecation Warning on line 8, column 8 of file:///Volumes/McFiver/u/GIT/next-explorer/src/scss/nbn.scss:8:8: -Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0. -More info and automated migrator: https://sass-lang.com/d/import -8 | @import "../../node_modules/bootstrap/scss/bootstrap"; -src/scss/nbn.scss 9:9 root stylesheet - ✓ Compiled successfully in 3.7s -./src/app/zxdb/releases/ReleasesExplorer.tsx -142:6 Warning: React Hook useEffect has missing dependencies: 'fetchData', 'initial', 'initialUrlState?.casetypeId', 'initialUrlState?.dLanguageId', 'initialUrlState?.dMachinetypeId', 'initialUrlState?.filetypeId', 'initialUrlState?.isDemo', 'initialUrlState?.q', 'initialUrlState?.schemetypeId', 'initialUrlState?.sort', 'initialUrlState?.sourcetypeId', 'initialUrlState?.year', 'q', and 'updateUrl'. Either include them or remove the dependency array. react-hooks/exhaustive-deps -info - Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/app/api-reference/config/eslint#disabling-rules -Failed to compile. -./src/server/repo/zxdb.ts:491:17 -Type error: Argument of type 'Name' is not assignable to parameter of type 'SQL | Column, object, object> | Aliased'. - Type 'Name' is missing the following properties from type 'Aliased': sql, fieldAlias, _ - 489 | .select({ total: sql`count(distinct ${sql.identifier("label_id")})` }) - 490 | .from(sql`search_by_names`) -> 491 | .where(like(sql.identifier("label_name"), pattern)); - | ^ - 492 | const total = Number(countRows[0]?.total ?? 0); - 493 | - 494 | const items = await db -Next.js build worker exited with code: 1 and signal: null diff --git a/COMMIT_EDITMSG b/COMMIT_EDITMSG deleted file mode 100644 index 9161201..0000000 --- a/COMMIT_EDITMSG +++ /dev/null @@ -1 +0,0 @@ -Link entry_id across UI; surface aliases/webrefs on Entry\n\n- Add EntryLink component for /zxdb/entries/[id]\n- Use EntryLink in Entries, Releases, and Label detail tables\n- Extend Entry detail with Aliases and Web links sections\n- Add Drizzle schema for aliases, webrefs, websites; fetch in repo\n\nSigned-off-by: Junie@lucy\n \ No newline at end of file diff --git a/example.env b/example.env index 1f25098..e757c95 100644 --- a/example.env +++ b/example.env @@ -5,3 +5,14 @@ # See docs/ZXDB.md for full setup instructions (DB import, helper tables, # readonly role, and environment validation notes). ZXDB_URL=mysql://zxdb_readonly:password@hostname:3306/zxdb + +# Base HTTP locations for CDN sources used by downloads.file_link +# When file_link starts with /zxdb, it will be fetched from ZXDB_FILEPATH +ZXDB_FILEPATH=https://zxdbfiles.com/ + +# When file_link starts with /public, it will be fetched from WOS_FILEPATH +# Note: Example uses the Internet Archive WoS mirror; keep the trailing slash +WOS_FILEPATH=https://archive.org/download/World_of_Spectrum_June_2017_Mirror/World%20of%20Spectrum%20June%202017%20Mirror.zip/World%20of%20Spectrum%20June%202017%20Mirror/ + +# Local cache root where files will be mirrored (without the leading slash) +CDN_CACHE=/mnt/files/zxfiles \ No newline at end of file diff --git a/package.json b/package.json index ef37bce..84dc229 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ }, "dependencies": { "bootstrap": "^5.3.8", + "dotenv": "^17.2.3", + "dotenv-expand": "^11.0.7", "drizzle-orm": "^0.36.4", "mysql2": "^3.16.0", "next": "~15.5.9", @@ -27,9 +29,9 @@ "@types/node": "^20.19.27", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", + "drizzle-kit": "^0.30.6", "eslint": "^9.39.2", "eslint-config-next": "15.5.4", - "drizzle-kit": "^0.30.6", "sass": "^1.97.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f1c4df..8d1e931 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: bootstrap: specifier: ^5.3.8 version: 5.3.8(@popperjs/core@2.11.8) + dotenv: + specifier: ^17.2.3 + version: 17.2.3 + dotenv-expand: + specifier: ^11.0.7 + version: 11.0.7 drizzle-orm: specifier: ^0.36.4 version: 0.36.4(@types/react@19.2.7)(mysql2@3.16.0)(react@19.1.0) @@ -1151,6 +1157,18 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dotenv-expand@11.0.7: + resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + drizzle-kit@0.30.6: resolution: {integrity: sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g==} hasBin: true @@ -3159,6 +3177,14 @@ snapshots: '@babel/runtime': 7.28.4 csstype: 3.2.3 + dotenv-expand@11.0.7: + dependencies: + dotenv: 16.6.1 + + dotenv@16.6.1: {} + + dotenv@17.2.3: {} + drizzle-kit@0.30.6: dependencies: '@drizzle-team/brocli': 0.10.2 diff --git a/src/app/zxdb/issues/[id]/page.tsx b/src/app/zxdb/issues/[id]/page.tsx new file mode 100644 index 0000000..67650a1 --- /dev/null +++ b/src/app/zxdb/issues/[id]/page.tsx @@ -0,0 +1,79 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { getIssue } from "@/server/repo/zxdb"; +import EntryLink from "@/app/zxdb/components/EntryLink"; + +export const metadata = { title: "ZXDB Issue" }; +export const revalidate = 3600; + +export default async function Page({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const issueId = Number(id); + if (!Number.isFinite(issueId) || issueId <= 0) return notFound(); + + const issue = await getIssue(issueId); + if (!issue) return notFound(); + + const ym = [issue.dateYear ?? "", issue.dateMonth ? String(issue.dateMonth).padStart(2, "0") : ""].filter(Boolean).join("/"); + + return ( +
+
+ ← Back to magazine + All magazines + {issue.linkMask && ( + Issue link + )} + {issue.archiveMask && ( + Archive + )} +
+ +

{issue.magazine.title}

+
+ Issue: {ym || issue.id}{issue.volume != null ? ` · Vol ${issue.volume}` : ""}{issue.number != null ? ` · No ${issue.number}` : ""} +
+ + {(issue.special || issue.supplement) && ( +
+ {issue.special &&
Special: {issue.special}
} + {issue.supplement &&
Supplement: {issue.supplement}
} +
+ )} + +

References

+ {issue.refs.length === 0 ? ( +
No references recorded.
+ ) : ( +
+ + + + + + + + + + {issue.refs.map((r) => ( + + + + + + ))} + +
PageTypeReference
{r.page}{r.typeName} + {r.entryId ? ( + + ) : r.labelId ? ( + {r.labelName ?? r.labelId} + ) : ( + + )} +
+
+ )} +
+ ); +} diff --git a/src/app/zxdb/magazines/[id]/page.tsx b/src/app/zxdb/magazines/[id]/page.tsx new file mode 100644 index 0000000..ef52a28 --- /dev/null +++ b/src/app/zxdb/magazines/[id]/page.tsx @@ -0,0 +1,85 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { getMagazine } from "@/server/repo/zxdb"; + +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 ( +
+

{mag.title}

+
Language: {mag.languageId}
+ +
+ ← Back to list + {mag.linkSite && ( + + Official site + + )} +
+ +

Issues

+ {mag.issues.length === 0 ? ( +
No issues found.
+ ) : ( +
+ + + + + + + + + + + + + {mag.issues.map((i) => ( + + + + + + + + + ))} + +
IssueVolumeNumberSpecialSupplementLinks
+ + {i.dateYear ?? ""} + {i.dateMonth ? `/${String(i.dateMonth).padStart(2, "0")}` : ""} + {" "} + (open issue) + + {i.volume ?? ""}{i.number ?? ""}{i.special ?? ""}{i.supplement ?? ""} +
+ {i.linkMask && ( + + + Link + + )} + {i.archiveMask && ( + + + Archive + + )} +
+
+
+ )} +
+ ); +} diff --git a/src/app/zxdb/magazines/page.tsx b/src/app/zxdb/magazines/page.tsx new file mode 100644 index 0000000..ddbb49b --- /dev/null +++ b/src/app/zxdb/magazines/page.tsx @@ -0,0 +1,81 @@ +import Link from "next/link"; +import { listMagazines } from "@/server/repo/zxdb"; + +export const metadata = { title: "ZXDB Magazines" }; + +// Depends on searchParams (?q=, ?page=). Force dynamic so each request renders correctly. +export const dynamic = "force-dynamic"; + +export default async function Page({ + searchParams, +}: { + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +}) { + const sp = await searchParams; + const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? ""; + const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1); + + const data = await listMagazines({ q, page, pageSize: 20 }); + + return ( +
+

Magazines

+ +
+
+ + +
+
+ +
+ {data.items.map((m) => ( + + + {m.title} + ({m.languageId}) + + + {m.issueCount} + + + ))} +
+ + +
+ ); +} + +function Pagination({ page, pageSize, total, q }: { page: number; pageSize: number; total: number; q: string }) { + const totalPages = Math.max(1, Math.ceil(total / pageSize)); + if (totalPages <= 1) return null; + const makeHref = (p: number) => { + const params = new URLSearchParams(); + if (q) params.set("q", q); + params.set("page", String(p)); + return `/zxdb/magazines?${params.toString()}`; + }; + return ( + + ); +} diff --git a/src/app/zxdb/page.tsx b/src/app/zxdb/page.tsx index fd4608a..a7b1724 100644 --- a/src/app/zxdb/page.tsx +++ b/src/app/zxdb/page.tsx @@ -44,6 +44,22 @@ export default async function Page() { + +
+ +
+
+
+ +
+
+
Magazines
+
Browse magazines and their issues
+
+
+
+ +
diff --git a/src/server/repo/zxdb.ts b/src/server/repo/zxdb.ts index 9f60e30..0cc722a 100644 --- a/src/server/repo/zxdb.ts +++ b/src/server/repo/zxdb.ts @@ -23,6 +23,10 @@ import { aliases, webrefs, websites, + magazines, + issues, + magrefs, + referencetypes, } from "@/server/schema/zxdb"; export interface SearchParams { @@ -66,6 +70,58 @@ export interface EntryFacets { machinetypes: FacetItem[]; } +export interface MagazineListItem { + id: number; + title: string; + languageId: string; + issueCount: number; +} + +export interface MagazineDetail { + id: number; + title: string; + languageId: string; + linkSite?: string | null; + linkMask?: string | null; + archiveMask?: string | null; + issues: Array<{ + id: number; + dateYear: number | null; + dateMonth: number | null; + number: number | null; + volume: number | null; + special: string | null; + supplement: string | null; + linkMask?: string | null; + archiveMask?: string | null; + }>; +} + +export interface IssueDetail { + id: number; + magazine: { id: number; title: string }; + dateYear: number | null; + dateMonth: number | null; + number: number | null; + volume: number | null; + special: string | null; + supplement: string | null; + linkMask?: string | null; + archiveMask?: string | null; + refs: Array<{ + id: number; + page: number; + typeId: number; + typeName: string; + entryId: number | null; + entryTitle: string | null; + labelId: number | null; + labelName: string | null; + isOriginal: number; + scoreGroup: string; + }> +} + export async function searchEntries(params: SearchParams): Promise> { const q = (params.q ?? "").trim(); const pageSize = Math.max(1, Math.min(params.pageSize ?? 20, 100)); @@ -1180,3 +1236,149 @@ export async function listCurrencies() { export async function listRoletypes() { return db.select().from(roletypes).orderBy(roletypes.name); } + +export async function listMagazines(params: { q?: string; page?: number; pageSize?: number }): Promise> { + const q = (params.q ?? "").trim(); + const pageSize = Math.max(1, Math.min(params.pageSize ?? 20, 100)); + const page = Math.max(1, params.page ?? 1); + const offset = (page - 1) * pageSize; + + const whereExpr = q ? like(magazines.name, `%${q}%`) : sql`true`; + + const [items, totalRows] = await Promise.all([ + db + .select({ + id: magazines.id, + // Expose as `title` to UI while DB column is `name` + title: magazines.name, + languageId: magazines.languageId, + issueCount: sql`count(${issues.id})`, + }) + .from(magazines) + .leftJoin(issues, eq(issues.magazineId, magazines.id)) + .where(whereExpr) + .groupBy(magazines.id) + .orderBy(asc(magazines.name)) + .limit(pageSize) + .offset(offset), + db + .select({ cnt: sql`count(*)` }) + .from(magazines) + .where(whereExpr), + ]); + + return { + items, + page, + pageSize, + total: totalRows[0]?.cnt ?? 0, + }; +} + +export async function getMagazine(id: number): Promise { + const rows = await db + .select({ + id: magazines.id, + // Alias DB `name` as `title` for UI shape + title: magazines.name, + languageId: magazines.languageId, + linkSite: magazines.linkSite, + linkMask: magazines.linkMask, + archiveMask: magazines.archiveMask, + }) + .from(magazines) + .where(eq(magazines.id, id)); + if (rows.length === 0) return null; + const mag = rows[0]; + + const iss = await db + .select({ + id: issues.id, + dateYear: issues.dateYear, + dateMonth: issues.dateMonth, + number: issues.number, + volume: issues.volume, + special: issues.special, + supplement: issues.supplement, + linkMask: issues.linkMask, + archiveMask: issues.archiveMask, + }) + .from(issues) + .where(eq(issues.magazineId, id)) + .orderBy( + asc(issues.dateYear), + asc(issues.dateMonth), + asc(issues.volume), + asc(issues.number), + asc(issues.id), + ); + + return { ...mag, issues: iss }; +} + +export async function getIssue(id: number): Promise { + const rows = await db + .select({ + id: issues.id, + magazineId: issues.magazineId, + magazineTitle: magazines.name, + dateYear: issues.dateYear, + dateMonth: issues.dateMonth, + number: issues.number, + volume: issues.volume, + special: issues.special, + supplement: issues.supplement, + linkMask: issues.linkMask, + archiveMask: issues.archiveMask, + }) + .from(issues) + .leftJoin(magazines, eq(magazines.id, issues.magazineId)) + .where(eq(issues.id, id)); + const base = rows[0]; + if (!base) return null; + + const refs = await db + .select({ + id: magrefs.id, + page: magrefs.page, + typeId: magrefs.referencetypeId, + typeName: referencetypes.name, + entryId: magrefs.entryId, + entryTitle: entries.title, + labelId: magrefs.labelId, + labelName: labels.name, + isOriginal: magrefs.isOriginal, + scoreGroup: magrefs.scoreGroup, + }) + .from(magrefs) + .leftJoin(referencetypes, eq(referencetypes.id, magrefs.referencetypeId)) + .leftJoin(entries, eq(entries.id, magrefs.entryId)) + .leftJoin(labels, eq(labels.id, magrefs.labelId)) + .where(eq(magrefs.issueId, id)) + .orderBy(asc(magrefs.page), asc(magrefs.id)); + + return { + id: base.id, + magazine: { id: Number(base.magazineId), title: base.magazineTitle ?? "" }, + dateYear: base.dateYear, + dateMonth: base.dateMonth, + number: base.number, + volume: base.volume, + special: base.special, + supplement: base.supplement, + linkMask: base.linkMask, + archiveMask: base.archiveMask, + refs: refs.map((r) => ({ + id: r.id, + page: r.page, + typeId: Number(r.typeId), + typeName: r.typeName ?? "", + entryId: r.entryId ?? null, + entryTitle: r.entryTitle ?? null, + labelId: r.labelId ?? null, + labelName: r.labelName ?? null, + isOriginal: Number(r.isOriginal), + scoreGroup: r.scoreGroup, + })), + }; +} diff --git a/src/server/schema/zxdb.ts b/src/server/schema/zxdb.ts index d632de4..b0a7481 100644 --- a/src/server/schema/zxdb.ts +++ b/src/server/schema/zxdb.ts @@ -7,9 +7,10 @@ export const entries = mysqlTable("entries", { isXrated: tinyint("is_xrated").notNull(), machinetypeId: tinyint("machinetype_id"), maxPlayers: tinyint("max_players").notNull().default(1), + // DB allows NULLs on many of these languageId: char("language_id", { length: 2 }), - genretypeSpotId: tinyint("spot_genretype_id"), genretypeId: tinyint("genretype_id"), + genretypeSpotId: tinyint("spot_genretype_id"), availabletypeId: char("availabletype_id", { length: 1 }), withoutLoadScreen: tinyint("without_load_screen").notNull(), withoutInlay: tinyint("without_inlay").notNull(), @@ -28,6 +29,14 @@ export type Entry = typeof entries.$inferSelect; export const labels = mysqlTable("labels", { id: int("id").notNull().primaryKey(), name: varchar("name", { length: 100 }).notNull(), + countryId: char("country_id", { length: 2 }), + country2Id: char("country2_id", { length: 2 }), + fromId: int("from_id"), + ownerId: int("owner_id"), + wasRenamed: tinyint("was_renamed").notNull().default(0), + deceased: varchar("deceased", { length: 200 }), + linkWikipedia: varchar("link_wikipedia", { length: 200 }), + linkSite: varchar("link_site", { length: 200 }), labeltypeId: char("labeltype_id", { length: 1 }), }); @@ -54,6 +63,8 @@ export const authors = mysqlTable("authors", { entryId: int("entry_id").notNull(), labelId: int("label_id").notNull(), teamId: int("team_id"), + // Present in schema; sequence of the author for a given entry + authorSeq: smallint("author_seq").notNull().default(1), }); export const publishers = mysqlTable("publishers", { @@ -143,6 +154,35 @@ export const hosts = mysqlTable("hosts", { magazineId: smallint("magazine_id"), }); +// ---- Magazines and Issues (subset used by the app) ---- +export const magazines = mysqlTable("magazines", { + id: smallint("id").notNull().primaryKey(), + // ZXDB column is `name` + name: varchar("name", { length: 100 }).notNull(), + countryId: char("country_id", { length: 2 }).notNull(), + languageId: char("language_id", { length: 2 }).notNull(), + linkSite: varchar("link_site", { length: 200 }), + magtypeId: char("magtype_id", { length: 1 }).notNull(), + topicId: int("topic_id"), + linkMask: varchar("link_mask", { length: 250 }), + archiveMask: varchar("archive_mask", { length: 250 }), + translationMask: varchar("translation_mask", { length: 250 }), +}); + +export const issues = mysqlTable("issues", { + id: int("id").notNull().primaryKey(), + magazineId: smallint("magazine_id").notNull(), + dateYear: smallint("date_year"), + dateMonth: smallint("date_month"), + dateDay: smallint("date_day"), + volume: smallint("volume"), + number: smallint("number"), + special: varchar("special", { length: 100 }), + supplement: varchar("supplement", { length: 100 }), + linkMask: varchar("link_mask", { length: 250 }), + archiveMask: varchar("archive_mask", { length: 250 }), +}); + // ---- Aliases (alternative titles per entry/release/language) export const aliases = mysqlTable("aliases", { entryId: int("entry_id").notNull(), @@ -214,3 +254,91 @@ export const roles = mysqlTable("roles", { labelId: int("label_id").notNull(), roletypeId: char("roletype_id", { length: 1 }).notNull(), }); + +// ---- Additional ZXDB schema coverage (lookups and content) ---- + +export const articletypes = mysqlTable("articletypes", { + id: char("id", { length: 1 }).notNull().primaryKey(), + name: varchar("text", { length: 50 }).notNull(), +}); + +export const articles = mysqlTable("articles", { + labelId: int("label_id").notNull(), + link: varchar("link", { length: 200 }).notNull(), + articletypeId: char("articletype_id", { length: 1 }).notNull(), + title: varchar("title", { length: 200 }), + languageId: char("language_id", { length: 2 }).notNull(), + writer: varchar("writer", { length: 200 }), + dateYear: smallint("date_year"), +}); + +export const categories = mysqlTable("categories", { + id: smallint("id").notNull().primaryKey(), + // DB column `text` + name: varchar("text", { length: 50 }).notNull(), +}); + +export const contenttypes = mysqlTable("contenttypes", { + id: char("id", { length: 1 }).notNull().primaryKey(), + name: varchar("text", { length: 50 }).notNull(), +}); + +export const contents = mysqlTable("contents", { + // ZXDB contents table does not have its own `id`; natural key is (issue_id, page_from, page_to, label_id, entry_id) + entryId: int("entry_id").notNull(), + labelId: int("label_id"), + issueId: int("issue_id").notNull(), + contenttypeId: char("contenttype_id", { length: 1 }).notNull(), + pageFrom: smallint("page_from"), + pageTo: smallint("page_to"), + title: varchar("title", { length: 200 }), + dateYear: smallint("date_year"), + rating: tinyint("rating"), + comments: varchar("comments", { length: 250 }), +}); + +export const extensions = mysqlTable("extensions", { + ext: varchar("ext", { length: 15 }).notNull().primaryKey(), + name: varchar("text", { length: 50 }).notNull(), +}); + +export const features = mysqlTable("features", { + id: int("id").notNull().primaryKey(), + name: varchar("name", { length: 150 }).notNull(), + version: tinyint("version").notNull().default(0), + labelId: int("label_id"), + label2Id: int("label2_id"), +}); + +export const tooltypes = mysqlTable("tooltypes", { + id: char("id", { length: 1 }).notNull().primaryKey(), + name: varchar("text", { length: 50 }).notNull(), +}); + +export const tools = mysqlTable("tools", { + id: int("id").notNull().primaryKey(), + title: varchar("title", { length: 200 }).notNull(), + languageId: char("language_id", { length: 2 }), + tooltypeId: char("tooltype_id", { length: 1 }), + link: varchar("link", { length: 200 }), +}); + +// ---- Magazine references (per-issue references to entries/labels/topics) ---- +export const referencetypes = mysqlTable("referencetypes", { + id: tinyint("id").notNull().primaryKey(), + name: varchar("text", { length: 50 }).notNull(), +}); + +export const magrefs = mysqlTable("magrefs", { + id: int("id").notNull().primaryKey(), + referencetypeId: tinyint("referencetype_id").notNull(), + entryId: int("entry_id"), + labelId: int("label_id"), + topicId: int("topic_id"), + issueId: int("issue_id").notNull(), + page: smallint("page").notNull().default(0), + isOriginal: tinyint("is_original").notNull().default(0), + scoreGroup: varchar("score_group", { length: 100 }).notNull().default(""), + reviewId: int("review_id"), + awardId: tinyint("award_id"), +});