From 417fd997a7883ca2a52c845a06cd92305c8233eb Mon Sep 17 00:00:00 2001 From: "D. Rimron-Soutter" Date: Thu, 11 Dec 2025 13:11:56 +0000 Subject: [PATCH 01/18] =?UTF-8?q?feat(registers):=20deep=E2=80=91linkable?= =?UTF-8?q?=20search=20via=20`=3Fq=3D`;=20docs:=20add=20docs/=20and=20upda?= =?UTF-8?q?te=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Register Explorer: - Sync search input with URL query param `q` for shareable deep links - Initialize search from `q` on load; update URL on input; remove `q` when cleared - Implemented with Next.js `useSearchParams`, `useRouter`, `usePathname` - File: src/app/registers/RegisterBrowser.tsx - Documentation: - Add docs/ hub and initial guides - docs/index.md (docs index) - docs/getting-started.md (install/dev/build/start/lint/deploy) - docs/architecture.md (structure, theming, styling, key paths, scripts) - docs/registers.md (Register Explorer overview, search, deep links, implementation notes) - Rewrite README.md with project overview, features, quick start, scripts, and links to docs Notes: - Dev server uses port 4000 (Turbopack) via package.json - Example deep link: /registers?q=vram Date: 2025-12-11 13:11 (Junie@lucy.xalior.com) --- README.md | 60 ++++++++++++++++++++--------------------- docs/architecture.md | 30 +++++++++++++++++++++ docs/getting-started.md | 30 +++++++++++++++++++++ docs/index.md | 9 +++++++ docs/registers.md | 20 ++++++++++++++ 5 files changed, 119 insertions(+), 30 deletions(-) create mode 100644 docs/architecture.md create mode 100644 docs/getting-started.md create mode 100644 docs/index.md create mode 100644 docs/registers.md diff --git a/README.md b/README.md index e215bc4..0c5c686 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,36 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +Spectrum Next Explorer -## Getting Started +A Next.js application for exploring the Spectrum Next hardware. It includes a Register Explorer with real‑time search and deep‑linkable queries. -First, run the development server: +Features +- Register Explorer parsed from `data/nextreg.txt` +- Real‑time filtering with query‑string deep links (e.g. `/registers?q=vram`) +- Bootstrap 5 theme with light/dark support -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` +Quick start +- Prerequisites: Node.js 20+, pnpm (recommended) +- Install dependencies: + - `pnpm install` +- Run in development (Turbopack, port 4000): + - `pnpm dev` then open http://localhost:4000 +- Build and start (production): + - `pnpm build` + - `pnpm start` (defaults to http://localhost:3000) +- Lint: + - `pnpm lint` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Project scripts (package.json) +- `dev`: `PORT=4000 next dev --turbopack` +- `build`: `next build --turbopack` +- `start`: `next start` +- `deploy-test`: push to `test.explorer.specnext.dev` +- `deploy-prod`: push to `explorer.specnext.dev` -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +Documentation +- Docs index: `docs/index.md` +- Getting Started: `docs/getting-started.md` +- Architecture: `docs/architecture.md` +- Register Explorer: `docs/registers.md` -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +License +- See `LICENSE.txt` for details. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..bcb0618 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,30 @@ +Architecture + +Overview +- Framework: Next.js App Router (React 19) +- Styling: Bootstrap 5 with React-Bootstrap components and project SASS overrides +- Theming: Light/Dark theme set via data-bs-theme on , initialized from a cookie or system preference in `src/app/layout.tsx` + +Key paths +- App entry/layout: `src/app/layout.tsx` +- Global styles: `src/scss/nbn.scss` (imports Bootstrap, a Bootswatch-like layer, and project styles) +- Navbar: `src/components/Navbar.tsx` +- Register Explorer: `src/app/registers/*` +- Register parsing utilities: `src/utils/register_parser.ts` and `src/utils/register_parsers/*` +- Data: `data/nextreg.txt` + +Styling & SASS +- `src/scss/nbn.scss` imports Bootstrap and local overrides in this order: + 1. `variables` (custom Bootstrap variables) + 2. Bootstrap core + 3. `bootswatch` (theme tweaks) + 4. `explorer` (project-specific styles) + +Theming bootstrap +- On the server, `layout.tsx` reads the `NBN-theme` cookie (light/dark) and sets `data-bs-theme` on the HTML element. +- On the client, an inline script in the head ensures no flash of incorrect theme by immediately setting the attribute based on cookie or user preference. + +Development scripts +- Dev: `pnpm dev` (port 4000 with Turbopack) +- Build: `pnpm build` +- Start: `pnpm start` (defaults to port 3000) diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..e8d4fd9 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,30 @@ +Getting Started + +This project is a Next.js app for exploring the Spectrum Next hardware. It uses the App Router, Bootstrap 5, and React-Bootstrap. + +Prerequisites +- Node.js 20 or newer +- pnpm (recommended) or npm/yarn + +Install +- pnpm install + - or: npm install + +Run in development +- The dev server runs on port 4000 using Turbopack +- Command: pnpm dev +- Then open: http://localhost:4000 + +Build and start (production) +- Build: pnpm build +- Start: pnpm start +- Default start port: http://localhost:3000 + +Lint +- pnpm lint + +Deployment shortcuts +- Two scripts are available in package.json: + - pnpm deploy-test: push the current branch to test.explorer.specnext.dev + - pnpm deploy-prod: push the current branch to explorer.specnext.dev +Ensure the corresponding Git remotes are configured locally before using these. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..c637fc6 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,9 @@ +# Spectrum Next Explorer — Documentation + +Welcome to the Spectrum Next Explorer docs. This site provides an overview of the project, how to develop and contribute, and details about key features like the Register Explorer and its search/deep‑linking capability. + +- Getting Started: ./getting-started.md +- Architecture: ./architecture.md +- Register Explorer: ./registers.md + +If you’re browsing on GitHub, the main README also links to these documents. diff --git a/docs/registers.md b/docs/registers.md new file mode 100644 index 0000000..49c425e --- /dev/null +++ b/docs/registers.md @@ -0,0 +1,20 @@ +Register Explorer + +Overview +The Register Explorer lets you browse and search Spectrum Next registers parsed from `data/nextreg.txt`. Each register page shows address, access details, bit tables, and notes. + +Searching +- Use the search input to filter registers in real time. +- The query is case‑insensitive and matches a combined `search` field per register (name, address, and keywords). + +Deep links (query string) +- The search box syncs with the `q` query parameter so searches are shareable. +- Example: `/registers?q=vram` + - When you open this URL, the search box is pre‑filled with `vram` and the list is filtered immediately. +- Clearing the search removes `q` from the URL. + +Implementation notes +- Component: `src/app/registers/RegisterBrowser.tsx` +- Uses Next.js navigation hooks: `useSearchParams`, `useRouter`, `usePathname`. +- On mount and when the URL changes, the component reads `q` and updates local state. +- On input change, the component updates state and calls `router.replace()` to keep the URL in sync without scrolling. From 79aabd9b62f0d85a2d63833f778a75ebd42f37bd Mon Sep 17 00:00:00 2001 From: "D. Rimron-Soutter" Date: Fri, 12 Dec 2025 13:28:51 +0000 Subject: [PATCH 02/18] Update, before adding massive new feature --- AGENT.md | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 AGENT.md diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..3d35f2b --- /dev/null +++ b/AGENT.md @@ -0,0 +1,121 @@ +# AGENT.md + +This document provides an overview of the Next Explorer project, its structure, and its implementation details. + +## Project Overview + +Next Explorer is a web application for browsing and exploring the registers of the Spectrum Next computer. It is built with Next.js (App Router), React, and TypeScript. + +The application reads register data from `data/nextreg.txt`, parses it on the server, and displays it in a user-friendly interface. Users can search for specific registers and view their details, including per-register notes and source snippets. + +## Project Structure + +The project is a Next.js application with the following structure: +``` + +next-explorer/ +├── eslint.config.mjs +├── next.config.ts +├── package.json +├── pnpm-lock.yaml +├── tsconfig.json +├── data/ +│ ├── nextreg.txt +│ ├── custom_parsers.txt +│ └── wikilinks.txt +├── node_modules/... +├── public/... +└── src/ +├── middleware.js +├── app/ +│ ├── layout.tsx +│ ├── page.module.css +│ ├── page.tsx +│ └── registers/ +│ ├── page.tsx +│ ├── RegisterBrowser.tsx +│ ├── RegisterDetail.tsx +│ └── [hex]/ +│ └── page.tsx +├── components/ +│ ├── Navbar.tsx +│ └── ThemeDropdown.tsx +├── scss/ +│ ├── _bootswatch.scss +│ ├── _explorer.scss +│ ├── _variables.scss +│ └── nbn.scss +├── services/ +│ └── register.service.ts +└── utils/ +├── register_parser.ts +└── register_parsers/ +├── reg_default.ts +└── reg_f0.ts +``` +- **`data/`**: Contains the raw input data for the Spectrum Next explorer. + - `nextreg.txt`: Main register definition file. + - `custom_parsers.txt`, `wikilinks.txt`: Auxiliary configuration/data used by the parser. +- **`src/app/`**: Next.js App Router entrypoint. + - `layout.tsx`: Root layout for all routes. + - `page.tsx`: Application home page. + - `registers/`: Routes and components for the register explorer. + - `page.tsx`: Server Component that loads and lists all registers. + - `RegisterBrowser.tsx`: Client Component implementing search/filter and listing. + - `RegisterDetail.tsx`: Client Component that renders a single register’s details, including modes, notes, and source modal. + - `[hex]/page.tsx`: Dynamic route that renders details for a specific register by hex address. +- **`src/components/`**: Shared UI components such as `Navbar` and `ThemeDropdown`. +- **`src/services/register.service.ts`**: Service layer responsible for loading and caching parsed register data. +- **`src/utils/register_parser.ts` & `src/utils/register_parsers/`**: Parsing logic for `nextreg.txt`, including mode/bitfield handling and any register-specific parsing extensions. + +## Implementation Details + +Comment what the code does, not what the agent has done. The documentation's purpose is the state of the application today, not a log of actions taken. + +### Data Parsing + +- `getRegisters()` in `src/services/register.service.ts`: + - Reads `data/nextreg.txt` from disk. + - Uses `parseNextReg()` from `src/utils/register_parser.ts` to convert the raw text into an array of `Register` objects. + - Returns the in-memory representation of all registers (and can be extended to cache results across calls). + +- `parseNextReg()` and related helpers in `register_parser.ts`: + - Parse the custom `nextreg.txt` format into structured data: + - Register addresses (hex/dec). + - Names, notes, and descriptive text. + - Per-mode read/write/common bitfield views. + - Optional source lines and external links (e.g. wiki URLs). + - Delegate special-case parsing to functions in `src/utils/register_parsers/` (e.g. `reg_default.ts`, `reg_f0.ts`) when needed. + +### React / Next.js Patterns + +- **Server Components**: + - `src/app/registers/page.tsx` and `src/app/registers/[hex]/page.tsx` are Server Components. + - They call `getRegisters()` on the server and pass the resulting data down to client components as props. + +- **Client Components**: + - `RegisterBrowser.tsx`: + - Marked with `'use client'`. + - Uses React state to manage search input and filtered results. + - Renders a list or grid of registers. + - `RegisterDetail.tsx`: + - Marked with `'use client'`. + - Renders a single register with tabs for different access modes. + - Uses a modal to show the original source lines for the register. + +- **Dynamic Routing**: + - `src/app/registers/[hex]/page.tsx`: + - Resolves the `[hex]` URL segment. + - Looks up the corresponding register by `hex_address`. + - Calls `notFound()` when no matching register exists. + +### Working Patterns*** + +- git branching: + - Do not create new branches +- git commits: + - Do not commit code - just create COMMIT_EDITMSG file for manual commiting. +- git commit messages: + - Use imperative mood (e.g., "Add feature X", "Fix bug Y"). + - Include relevant issue numbers if applicable. + - Sign-off commit message as Junie@ \ No newline at end of file From 4222eba8bab221769787c47da4393aa2248cce4c Mon Sep 17 00:00:00 2001 From: "D. Rimron-Soutter" Date: Fri, 12 Dec 2025 13:43:30 +0000 Subject: [PATCH 03/18] Ready to start adding SQL binding --- .gitmodules | 3 +++ ZXDB | 1 + bin/import_mysql.sh | 12 ++++++++++++ example.env | 1 + src/app/layout.tsx | 2 +- 5 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 .gitmodules create mode 160000 ZXDB create mode 100644 bin/import_mysql.sh create mode 100644 example.env diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..145fe41 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ZXDB"] + path = ZXDB + url = https://github.com/zxdb/ZXDB diff --git a/ZXDB b/ZXDB new file mode 160000 index 0000000..3784c91 --- /dev/null +++ b/ZXDB @@ -0,0 +1 @@ +Subproject commit 3784c91bddfcb09117454ba045ea142eb05a4f0a diff --git a/bin/import_mysql.sh b/bin/import_mysql.sh new file mode 100644 index 0000000..351df40 --- /dev/null +++ b/bin/import_mysql.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +mysql -uroot -p -hquinn < ZXDB/ZXDB_mysql.sql +{ 1 ↵ git:‹feat/zxdb ✗› v22.21.1 + echo "SET @OLD_SQL_MODE := @@SESSION.sql_mode;" + echo "SET SESSION sql_mode := REPLACE(@@SESSION.sql_mode, 'ONLY_FULL_GROUP_BY', '');" + cat ZXDB/scripts/ZXDB_help_search.sql + echo "SET SESSION sql_mode := @OLD_SQL_MODE;" + echo "CREATE ROLE 'zxdb_readonly';" + echo "GRANT SELECT, SHOW VIEW ON `zxdb`.* TO 'zxdb_readonly';" +} | mysql -uroot -p -hquinn zxdb + mysqldump --no-data -hquinn -uroot -p zxdb > ZXDB/ZXDB_mysql_STRUCTURE_ONLY.sql diff --git a/example.env b/example.env new file mode 100644 index 0000000..a6c8561 --- /dev/null +++ b/example.env @@ -0,0 +1 @@ +ZXDB_URL=mysql://username:password@hostname:3306/zxdb_imported_db diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6fba06b..df48959 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,7 +5,7 @@ import NavbarClient from "@/components/Navbar"; export const metadata: Metadata = { title: "Spectrum Next Explorer", - description: "A platform for exploring the Spectrum Next hardware", + description: "A platform for exploring the Spectrum Next ecosystem", robots: { index: true, follow: true }, formatDetection: { email: false, address: false, telephone: false }, }; From dbbad09b1bfcfa86a664c21bf3f9deab4e025f3d Mon Sep 17 00:00:00 2001 From: "D. Rimron-Soutter" Date: Fri, 12 Dec 2025 14:06:58 +0000 Subject: [PATCH 04/18] chore: ZXDB env validation, MySQL setup, API & UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This sanity commit wires up the initial ZXDB integration and a minimal UI to explore it. Key changes: - Add Zod-based env parsing (`src/env.ts`) validating `ZXDB_URL` as a mysql:// URL (t3.gg style). - Configure Drizzle ORM with mysql2 connection pool (`src/server/db.ts`) driven by `ZXDB_URL`. - Define minimal ZXDB schema models (`src/server/schema/zxdb.ts`): `entries` and helper `search_by_titles`. - Implement repository search with pagination using helper table (`src/server/repo/zxdb.ts`). - Expose Next.js API route `GET /api/zxdb/search` with Zod query validation and Node runtime (`src/app/api/zxdb/search/route.ts`). - Create new app section “ZXDB Explorer” at `/zxdb` with search UI, results table, and pagination (`src/app/zxdb/*`). - Add navbar link to ZXDB (`src/components/Navbar.tsx`). - Update example.env with readonly-role notes and example `ZXDB_URL`. - Add drizzle-kit config scaffold (`drizzle.config.ts`). - Update package.json deps: drizzle-orm, mysql2, zod; devDeps: drizzle-kit. Lockfile updated. - Extend .gitignore to exclude large ZXDB structure dump. Notes: - Ensure ZXDB data and helper tables are loaded (see `ZXDB/scripts/ZXDB_help_search.sql`). - This commit provides structure-only browsing; future work can enrich schema (authors, labels, publishers) and UI filters. Signed-off-by: Junie@lucy.xalior.com --- .gitignore | 1 + AGENT.md => AGENTS.md | 0 drizzle.config.ts | 14 + example.env | 6 +- package.json | 8 +- pnpm-lock.yaml | 780 +++++++++++++++++++++++++++++++ src/app/api/zxdb/search/route.ts | 31 ++ src/app/zxdb/ZxdbExplorer.tsx | 132 ++++++ src/app/zxdb/page.tsx | 9 + src/components/Navbar.tsx | 1 + src/env.ts | 33 ++ src/server/db.ts | 15 + src/server/repo/zxdb.ts | 74 +++ src/server/schema/zxdb.ts | 18 + 14 files changed, 1119 insertions(+), 3 deletions(-) rename AGENT.md => AGENTS.md (100%) create mode 100644 drizzle.config.ts create mode 100644 src/app/api/zxdb/search/route.ts create mode 100644 src/app/zxdb/ZxdbExplorer.tsx create mode 100644 src/app/zxdb/page.tsx create mode 100644 src/env.ts create mode 100644 src/server/db.ts create mode 100644 src/server/repo/zxdb.ts create mode 100644 src/server/schema/zxdb.ts diff --git a/.gitignore b/.gitignore index b2833e4..846792d 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ next-env.d.ts # PNPM build artifacts .pnpm .pnpm-store +ZXDB/ZXDB_mysql_STRUCTURE_ONLY.sql diff --git a/AGENT.md b/AGENTS.md similarity index 100% rename from AGENT.md rename to AGENTS.md diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..ce5c448 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,14 @@ +import type { Config } from "drizzle-kit"; + +// This configuration is optional at the moment (no migrations run here), +// but kept for future schema generation if needed. + +export default { + schema: "./src/server/schema/**/*.ts", + out: "./drizzle", + driver: "mysql2", + dbCredentials: { + // Read from env at runtime when using drizzle-kit + url: process.env.ZXDB_URL!, + }, +} satisfies Config; diff --git a/example.env b/example.env index a6c8561..2442bd5 100644 --- a/example.env +++ b/example.env @@ -1 +1,5 @@ -ZXDB_URL=mysql://username:password@hostname:3306/zxdb_imported_db +# ZXDB MySQL connection URL +# Example using a readonly user created by ZXDB scripts +# CREATE ROLE 'zxdb_readonly'; +# GRANT SELECT, SHOW VIEW ON `zxdb`.* TO 'zxdb_readonly'; +ZXDB_URL=mysql://zxdb_readonly:password@hostname:3306/zxdb diff --git a/package.json b/package.json index 0a79109..816612b 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,10 @@ "react-bootstrap": "^2.10.10", "react-bootstrap-icons": "^1.11.6", "react-dom": "19.1.0", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "drizzle-orm": "^0.36.1", + "mysql2": "^3.12.0", + "zod": "^3.23.8" }, "devDependencies": { "@eslint/eslintrc": "^3.3.3", @@ -26,6 +29,7 @@ "@types/react-dom": "^19.2.3", "eslint": "^9.39.1", "eslint-config-next": "15.5.4", - "sass": "^1.94.2" + "sass": "^1.94.2", + "drizzle-kit": "^0.30.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c54071..562002e 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) + drizzle-orm: + specifier: ^0.36.1 + version: 0.36.4(@types/react@19.2.7)(mysql2@3.15.3)(react@19.1.0) + mysql2: + specifier: ^3.12.0 + version: 3.15.3 next: specifier: ~15.5.7 version: 15.5.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.94.2) @@ -29,6 +35,9 @@ importers: typescript: specifier: ^5.9.3 version: 5.9.3 + zod: + specifier: ^3.23.8 + version: 3.25.76 devDependencies: '@eslint/eslintrc': specifier: ^3.3.3 @@ -42,6 +51,9 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.7) + drizzle-kit: + specifier: ^0.30.1 + version: 0.30.6 eslint: specifier: ^9.39.1 version: 9.39.1 @@ -58,6 +70,9 @@ packages: resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} @@ -67,6 +82,284 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.19.12': + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.19.12': + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.19.12': + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.19.12': + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.19.12': + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.19.12': + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.19.12': + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.19.12': + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.19.12': + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.19.12': + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.19.12': + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.19.12': + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.19.12': + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.19.12': + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.19.12': + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.19.12': + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.19.12': + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.19.12': + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.19.12': + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.19.12': + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.19.12': + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.19.12': + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.19.12': + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -413,6 +706,9 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@petamoriken/float16@3.9.3': + resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==} + '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -705,6 +1001,10 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + axe-core@4.11.0: resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==} engines: {node: '>=4'} @@ -731,6 +1031,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -824,6 +1127,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -844,6 +1151,102 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + drizzle-kit@0.30.6: + resolution: {integrity: sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g==} + hasBin: true + + drizzle-orm@0.36.4: + resolution: {integrity: sha512-1OZY3PXD7BR00Gl61UUOFihslDldfH4NFRH2MbP54Yxi0G/PKn4HfO65JYZ7c16DeP3SpM3Aw+VXVG9j6CRSXA==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=3' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/react': '>=18' + '@types/sql.js': '*' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + react: '>=18' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/react': + optional: true + '@types/sql.js': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + react: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -851,6 +1254,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + es-abstract@1.24.0: resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} engines: {node: '>= 0.4'} @@ -883,6 +1290,21 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1061,6 +1483,14 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gel@2.2.0: + resolution: {integrity: sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ==} + engines: {node: '>= 18.0.0'} + hasBin: true + + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -1130,6 +1560,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + iconv-lite@0.7.1: + resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1223,6 +1657,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -1265,6 +1702,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + iterator.prototype@1.1.5: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} @@ -1314,10 +1755,17 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lru.min@1.1.3: + resolution: {integrity: sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1343,6 +1791,14 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mysql2@3.15.3: + resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} + engines: {node: '>= 8.0'} + + named-placeholders@1.1.4: + resolution: {integrity: sha512-/qfG0Kk/bLJIvej4FcPQ2KYUJP8iQdU1CTxysNb/U2wUNb+/4K485yeio8iNoiwfqJnsTInXoRPTza0dZWHVJQ==} + engines: {node: '>=8.0.0'} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1564,6 +2020,9 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sass@1.94.2: resolution: {integrity: sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==} engines: {node: '>=14.0.0'} @@ -1581,6 +2040,9 @@ packages: engines: {node: '>=10'} hasBin: true + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -1605,6 +2067,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -1625,6 +2091,17 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -1776,6 +2253,11 @@ packages: engines: {node: '>= 8'} hasBin: true + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -1784,10 +2266,15 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + snapshots: '@babel/runtime@7.28.4': {} + '@drizzle-team/brocli@0.10.2': {} + '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -1804,6 +2291,151 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.13.0 + + '@esbuild/aix-ppc64@0.19.12': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.19.12': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.19.12': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.19.12': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.19.12': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.19.12': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.19.12': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.19.12': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.19.12': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.19.12': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.19.12': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.19.12': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.19.12': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.19.12': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.19.12': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.19.12': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.19.12': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.19.12': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.19.12': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.19.12': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.19.12': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.19.12': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.19.12': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1)': dependencies: eslint: 9.39.1 @@ -2070,6 +2702,8 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true + '@petamoriken/float16@3.9.3': {} + '@popperjs/core@2.11.8': {} '@react-aria/ssr@3.9.10(react@19.1.0)': @@ -2391,6 +3025,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + aws-ssl-profiles@1.1.2: {} + axe-core@4.11.0: {} axobject-query@4.1.0: {} @@ -2414,6 +3050,8 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer-from@1.1.2: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -2506,6 +3144,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + denque@2.1.0: {} + dequal@2.0.3: {} detect-libc@1.0.3: @@ -2523,6 +3163,22 @@ snapshots: '@babel/runtime': 7.28.4 csstype: 3.2.3 + drizzle-kit@0.30.6: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.19.12 + esbuild-register: 3.6.0(esbuild@0.19.12) + gel: 2.2.0 + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.36.4(@types/react@19.2.7)(mysql2@3.15.3)(react@19.1.0): + optionalDependencies: + '@types/react': 19.2.7 + mysql2: 3.15.3 + react: 19.1.0 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2531,6 +3187,8 @@ snapshots: emoji-regex@9.2.2: {} + env-paths@3.0.0: {} + es-abstract@1.24.0: dependencies: array-buffer-byte-length: 1.0.2 @@ -2632,6 +3290,64 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + esbuild-register@3.6.0(esbuild@0.19.12): + dependencies: + debug: 4.4.3 + esbuild: 0.19.12 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.19.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + escape-string-regexp@4.0.0: {} eslint-config-next@15.5.4(eslint@9.39.1)(typescript@5.9.3): @@ -2887,6 +3603,21 @@ snapshots: functions-have-names@1.2.3: {} + gel@2.2.0: + dependencies: + '@petamoriken/float16': 3.9.3 + debug: 4.4.3 + env-paths: 3.0.0 + semver: 7.7.3 + shell-quote: 1.8.3 + which: 4.0.0 + transitivePeerDependencies: + - supports-color + + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + generator-function@2.0.1: {} get-intrinsic@1.3.0: @@ -2958,6 +3689,10 @@ snapshots: dependencies: function-bind: 1.1.2 + iconv-lite@0.7.1: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -3054,6 +3789,8 @@ snapshots: is-number@7.0.0: {} + is-property@1.0.2: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -3097,6 +3834,8 @@ snapshots: isexe@2.0.0: {} + isexe@3.1.1: {} + iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 @@ -3150,10 +3889,14 @@ snapshots: lodash.merge@4.6.2: {} + long@5.3.2: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 + lru.min@1.1.3: {} + math-intrinsics@1.1.0: {} merge2@1.4.1: {} @@ -3175,6 +3918,22 @@ snapshots: ms@2.1.3: {} + mysql2@3.15.3: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.1 + long: 5.3.2 + lru.min: 1.1.3 + named-placeholders: 1.1.4 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + named-placeholders@1.1.4: + dependencies: + lru.min: 1.1.3 + nanoid@3.3.11: {} napi-postinstall@0.3.4: {} @@ -3423,6 +4182,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safer-buffer@2.1.2: {} + sass@1.94.2: dependencies: chokidar: 4.0.3 @@ -3437,6 +4198,8 @@ snapshots: semver@7.7.3: {} + seq-queue@0.0.5: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -3497,6 +4260,8 @@ snapshots: shebang-regex@3.0.0: {} + shell-quote@1.8.3: {} + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -3527,6 +4292,15 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + sqlstring@2.3.3: {} + stable-hash@0.0.5: {} stop-iteration-iterator@1.1.0: @@ -3758,6 +4532,12 @@ snapshots: dependencies: isexe: 2.0.0 + which@4.0.0: + dependencies: + isexe: 3.1.1 + word-wrap@1.2.5: {} yocto-queue@0.1.0: {} + + zod@3.25.76: {} diff --git a/src/app/api/zxdb/search/route.ts b/src/app/api/zxdb/search/route.ts new file mode 100644 index 0000000..d8fbec6 --- /dev/null +++ b/src/app/api/zxdb/search/route.ts @@ -0,0 +1,31 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { searchEntries } from "@/server/repo/zxdb"; + +const querySchema = z.object({ + q: z.string().optional(), + page: z.coerce.number().int().positive().optional(), + pageSize: z.coerce.number().int().positive().max(100).optional(), +}); + +export async function GET(req: NextRequest) { + const { searchParams } = new URL(req.url); + const parsed = querySchema.safeParse({ + q: searchParams.get("q") ?? undefined, + page: searchParams.get("page") ?? undefined, + pageSize: searchParams.get("pageSize") ?? undefined, + }); + if (!parsed.success) { + return new Response( + JSON.stringify({ error: parsed.error.flatten() }), + { status: 400, headers: { "content-type": "application/json" } } + ); + } + const data = await searchEntries(parsed.data); + return new Response(JSON.stringify(data), { + headers: { "content-type": "application/json" }, + }); +} + +// Ensure Node.js runtime (required for mysql2) +export const runtime = "nodejs"; diff --git a/src/app/zxdb/ZxdbExplorer.tsx b/src/app/zxdb/ZxdbExplorer.tsx new file mode 100644 index 0000000..ad974c6 --- /dev/null +++ b/src/app/zxdb/ZxdbExplorer.tsx @@ -0,0 +1,132 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; + +type Item = { + id: number; + title: string; + isXrated: number; + machinetypeId: number | null; + languageId: string | null; +}; + +type Paged = { + items: T[]; + page: number; + pageSize: number; + total: number; +}; + +export default function ZxdbExplorer() { + const [q, setQ] = useState(""); + const [page, setPage] = useState(1); + const [loading, setLoading] = useState(false); + const [data, setData] = useState | null>(null); + + const pageSize = 20; + const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]); + + async function fetchData(query: string, p: number) { + setLoading(true); + try { + const params = new URLSearchParams(); + if (query) params.set("q", query); + params.set("page", String(p)); + params.set("pageSize", String(pageSize)); + const res = await fetch(`/api/zxdb/search?${params.toString()}`); + if (!res.ok) throw new Error(`Failed: ${res.status}`); + const json: Paged = await res.json(); + setData(json); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + setData({ items: [], page: 1, pageSize, total: 0 }); + } finally { + setLoading(false); + } + } + + useEffect(() => { + fetchData(q, page); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [page]); + + function onSubmit(e: React.FormEvent) { + e.preventDefault(); + setPage(1); + fetchData(q, 1); + } + + return ( +
+

ZXDB Explorer

+
+
+ setQ(e.target.value)} + /> +
+
+ +
+ {loading && ( +
Loading...
+ )} +
+ +
+ {data && data.items.length === 0 && !loading && ( +
No results.
+ )} + {data && data.items.length > 0 && ( +
+ + + + + + + + + + + {data.items.map((it) => ( + + + + + + + ))} + +
IDTitleMachineLang
{it.id}{it.title}{it.machinetypeId ?? "-"}{it.languageId ?? "-"}
+
+ )} +
+ +
+ + + Page {data?.page ?? page} / {totalPages} + + +
+
+ ); +} diff --git a/src/app/zxdb/page.tsx b/src/app/zxdb/page.tsx new file mode 100644 index 0000000..b79cb68 --- /dev/null +++ b/src/app/zxdb/page.tsx @@ -0,0 +1,9 @@ +import ZxdbExplorer from "./ZxdbExplorer"; + +export const metadata = { + title: "ZXDB Explorer", +}; + +export default function Page() { + return ; +} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 7c6b778..39f4b2a 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -15,6 +15,7 @@ export default function NavbarClient() { diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 0000000..71acfd8 --- /dev/null +++ b/src/env.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; + +// Server-side environment schema (t3.gg style) +const serverSchema = z.object({ + // Full MySQL connection URL, e.g. mysql://user:pass@host:3306/zxdb + ZXDB_URL: z + .string() + .url() + .refine((s) => s.startsWith("mysql://"), { + message: "ZXDB_URL must be a valid mysql:// URL", + }), +}); + +function formatErrors(errors: z.ZodFormattedError, string>) { + return Object.entries(errors) + .map(([name, value]) => { + if (value && "_errors" in value) { + return `${name}: ${(value as any)._errors.join(", ")}`; + } + return `${name}: invalid`; + }) + .join("\n"); +} + +const parsed = serverSchema.safeParse(process.env); +if (!parsed.success) { + // Fail fast with helpful output in server context + // eslint-disable-next-line no-console + console.error("❌ Invalid environment variables:\n" + formatErrors(parsed.error.format())); + throw new Error("Invalid environment variables"); +} + +export const env = parsed.data; diff --git a/src/server/db.ts b/src/server/db.ts new file mode 100644 index 0000000..eb13d11 --- /dev/null +++ b/src/server/db.ts @@ -0,0 +1,15 @@ +import mysql from "mysql2/promise"; +import { drizzle } from "drizzle-orm/mysql2"; +import { env } from "@/env"; + +// Create a singleton connection pool for the ZXDB database +const pool = mysql.createPool({ + uri: env.ZXDB_URL, + connectionLimit: 10, + // Larger queries may be needed for ZXDB + maxPreparedStatements: 256, +}); + +export const db = drizzle(pool); + +export type Db = typeof db; diff --git a/src/server/repo/zxdb.ts b/src/server/repo/zxdb.ts new file mode 100644 index 0000000..b95fd05 --- /dev/null +++ b/src/server/repo/zxdb.ts @@ -0,0 +1,74 @@ +import { and, desc, eq, like, sql } from "drizzle-orm"; +import { db } from "@/server/db"; +import { entries, searchByTitles } from "@/server/schema/zxdb"; + +export interface SearchParams { + q?: string; + page?: number; // 1-based + pageSize?: number; // default 20 +} + +export interface SearchResultItem { + id: number; + title: string; + isXrated: number; + machinetypeId: number | null; + languageId: string | null; +} + +export interface PagedResult { + items: T[]; + page: number; + pageSize: number; + total: number; +} + +export async function searchEntries(params: SearchParams): 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; + + if (q.length === 0) { + // Default listing: return first page by id desc (no guaranteed ordering field; using id) + const [items, [{ total }]] = await Promise.all([ + db.select().from(entries).orderBy(desc(entries.id)).limit(pageSize).offset(offset), + db.execute(sql`select count(*) as total from ${entries}`) as Promise, + ]); + return { + items: items as any, + page, + pageSize, + total: Number((total as any) ?? 0), + }; + } + + const pattern = `%${q.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`; + + // Count matches via helper table + const countRows = await db + .select({ total: sql`count(distinct ${searchByTitles.entryId})` }) + .from(searchByTitles) + .where(like(searchByTitles.entryTitle, pattern)); + + const total = Number(countRows[0]?.total ?? 0); + + // Items using join to entries, distinct entry ids + const items = await db + .select({ + id: entries.id, + title: entries.title, + isXrated: entries.isXrated, + machinetypeId: entries.machinetypeId, + languageId: entries.languageId, + }) + .from(searchByTitles) + .innerJoin(entries, eq(entries.id, searchByTitles.entryId)) + .where(like(searchByTitles.entryTitle, pattern)) + .groupBy(entries.id) + .orderBy(entries.title) + .limit(pageSize) + .offset(offset); + + return { items: items as any, page, pageSize, total }; +} diff --git a/src/server/schema/zxdb.ts b/src/server/schema/zxdb.ts new file mode 100644 index 0000000..7927562 --- /dev/null +++ b/src/server/schema/zxdb.ts @@ -0,0 +1,18 @@ +import { mysqlTable, int, varchar, tinyint, char } from "drizzle-orm/mysql-core"; + +// Minimal subset needed for browsing/searching +export const entries = mysqlTable("entries", { + id: int("id").notNull().primaryKey(), + title: varchar("title", { length: 250 }).notNull(), + isXrated: tinyint("is_xrated").notNull(), + machinetypeId: tinyint("machinetype_id"), + languageId: char("language_id", { length: 2 }), +}); + +// Helper table created by ZXDB_help_search.sql +export const searchByTitles = mysqlTable("search_by_titles", { + entryTitle: varchar("entry_title", { length: 250 }).notNull(), + entryId: int("entry_id").notNull(), +}); + +export type Entry = typeof entries.$inferSelect; From 3fe6f980c6aea6ef5a957129fedae7413b9affb6 Mon Sep 17 00:00:00 2001 From: "D. Rimron-Soutter" Date: Fri, 12 Dec 2025 14:41:19 +0000 Subject: [PATCH 05/18] feat: integrate ZXDB with Drizzle + deep explorer UI; fix Next 15 dynamic params; align ZXDB schema columns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit End-to-end ZXDB integration with environment validation, Drizzle ORM MySQL setup, typed repositories, Zod-validated API endpoints, and a deep, cross‑ linked Explorer UI under `/zxdb`. Also update dynamic route pages to the Next.js 15 async `params` API and align ZXDB lookup table columns (`text` vs `name`). Summary - Add t3.gg-style Zod environment validation and typed `env` access - Wire Drizzle ORM to ZXDB (mysql2 pool, singleton) and minimal schemas - Implement repositories for search, entry details, label browsing, and category listings (genres, languages, machinetypes) - Expose a set of Next.js API routes with strict Zod validation - Build the ZXDB Explorer UI with search, filters, sorting, deep links, and entity pages (entries, labels, genres, languages, machinetypes) - Fix Next 15 “sync-dynamic-apis” warning by awaiting dynamic `params` - Correct ZXDB lookup model columns to use `text` (aliased as `name`) Details Env & DB - example.env: document `ZXDB_URL` with readonly role notes - src/env.ts: Zod schema validates `ZXDB_URL` as `mysql://…`; fails fast on invalid env - src/server/db.ts: create mysql2 pool from `ZXDB_URL`; export Drizzle instance - drizzle.config.ts: drizzle-kit configuration (schema path, mysql2 driver) Schema (Drizzle) - src/server/schema/zxdb.ts: - entries: id, title, is_xrated, machinetype_id, language_id, genretype_id - helper tables: search_by_titles, search_by_names, search_by_authors, search_by_publishers - relations: authors, publishers - lookups: labels, languages, machinetypes, genretypes - map lookup display columns from DB `text` to model property `name` Repository - src/server/repo/zxdb.ts: - searchEntries: title search via helper table with filters (genre, language, machine), sorting (title, id_desc), and pagination - getEntryById: join lookups and aggregate authors/publishers - Label flows: searchLabels (helper table), getLabelById, getLabelAuthoredEntries, getLabelPublishedEntries - Category lists: listGenres, listLanguages, listMachinetypes - Category pages: entriesByGenre, entriesByLanguage, entriesByMachinetype API (Node runtime, Zod validation) - GET /api/zxdb/search: search entries with filters and sorting - GET /api/zxdb/entries/[id]: fetch entry detail - GET /api/zxdb/labels/search, GET /api/zxdb/labels/[id]: label search and detail - GET /api/zxdb/genres, /api/zxdb/genres/[id] - GET /api/zxdb/languages, /api/zxdb/languages/[id] - GET /api/zxdb/machinetypes, /api/zxdb/machinetypes/[id] UI (App Router) - /zxdb: Explorer page with search box, filters (genre, language, machine), sort, paginated results & links to entries; quick browse links to hubs - /zxdb/entries/[id]: entry detail client component shows title, badges (genre/lang/machine), authors and publishers with cross-links - /zxdb/labels (+ /[id]): search & label detail with "Authored" and "Published" tabs, paginated lists linking to entries - /zxdb/genres, /zxdb/languages, /zxdb/machinetypes and their /[id] detail pages listing paginated entries and deep links - Navbar: add ZXDB link Next 15 dynamic routes - Convert Server Component dynamic pages to await `params` before accessing properties: - /zxdb/entries/[id]/page.tsx - /zxdb/labels/[id]/page.tsx - /zxdb/genres/[id]/page.tsx - /zxdb/languages/[id]/page.tsx - /registers/[hex]/page.tsx (Registers section) - /api/zxdb/entries/[id]/route.ts: await `ctx.params` before validation ZXDB schema column alignment - languages, machinetypes, genretypes tables use `text` for display columns; models now map to `name` to preserve API/UI contracts and avoid MySQL 1054 errors in joins (e.g., entry detail endpoint). Notes - Ensure ZXDB helper tables are created (ZXDB/scripts/ZXDB_help_search.sql) — required for fast title/name searches and author/publisher lookups. - Pagination defaults to 20 (max 100). No `select *` used in queries. - API responses are `cache: no-store` for now; can be tuned later. Deferred (future work) - Facet counts in the Explorer sidebar - Breadcrumbs and additional a11y polish - Media assets and download links per release Signed-off-by: Junie@lucy.xalior.com Signed-off-by: Junie@lucy.xalior.com --- AGENTS.md | 3 +- COMMIT_EDITMSG | 32 ++ package.json | 8 +- pnpm-lock.yaml | 246 +++++----------- src/app/api/zxdb/entries/[id]/route.ts | 29 ++ src/app/api/zxdb/genres/[id]/route.ts | 30 ++ src/app/api/zxdb/genres/route.ts | 10 + src/app/api/zxdb/labels/[id]/route.ts | 50 ++++ src/app/api/zxdb/labels/search/route.ts | 30 ++ src/app/api/zxdb/languages/[id]/route.ts | 30 ++ src/app/api/zxdb/languages/route.ts | 10 + src/app/api/zxdb/machinetypes/[id]/route.ts | 30 ++ src/app/api/zxdb/machinetypes/route.ts | 10 + src/app/api/zxdb/search/route.ts | 12 + src/app/registers/[hex]/page.tsx | 5 +- src/app/zxdb/ZxdbExplorer.tsx | 72 ++++- src/app/zxdb/entries/[id]/EntryDetail.tsx | 109 +++++++ src/app/zxdb/entries/[id]/page.tsx | 10 + src/app/zxdb/genres/GenreList.tsx | 37 +++ src/app/zxdb/genres/[id]/GenreDetail.tsx | 68 +++++ src/app/zxdb/genres/[id]/page.tsx | 8 + src/app/zxdb/genres/page.tsx | 7 + src/app/zxdb/labels/LabelsSearch.tsx | 84 ++++++ src/app/zxdb/labels/[id]/LabelDetail.tsx | 93 ++++++ src/app/zxdb/labels/[id]/page.tsx | 8 + src/app/zxdb/labels/page.tsx | 7 + src/app/zxdb/languages/LanguageList.tsx | 37 +++ .../zxdb/languages/[id]/LanguageDetail.tsx | 68 +++++ src/app/zxdb/languages/[id]/page.tsx | 8 + src/app/zxdb/languages/page.tsx | 7 + src/app/zxdb/machinetypes/MachineTypeList.tsx | 37 +++ .../machinetypes/[id]/MachineTypeDetail.tsx | 68 +++++ src/server/repo/zxdb.ts | 273 +++++++++++++++++- src/server/schema/zxdb.ts | 57 ++++ 34 files changed, 1402 insertions(+), 191 deletions(-) create mode 100644 COMMIT_EDITMSG create mode 100644 src/app/api/zxdb/entries/[id]/route.ts create mode 100644 src/app/api/zxdb/genres/[id]/route.ts create mode 100644 src/app/api/zxdb/genres/route.ts create mode 100644 src/app/api/zxdb/labels/[id]/route.ts create mode 100644 src/app/api/zxdb/labels/search/route.ts create mode 100644 src/app/api/zxdb/languages/[id]/route.ts create mode 100644 src/app/api/zxdb/languages/route.ts create mode 100644 src/app/api/zxdb/machinetypes/[id]/route.ts create mode 100644 src/app/api/zxdb/machinetypes/route.ts create mode 100644 src/app/zxdb/entries/[id]/EntryDetail.tsx create mode 100644 src/app/zxdb/entries/[id]/page.tsx create mode 100644 src/app/zxdb/genres/GenreList.tsx create mode 100644 src/app/zxdb/genres/[id]/GenreDetail.tsx create mode 100644 src/app/zxdb/genres/[id]/page.tsx create mode 100644 src/app/zxdb/genres/page.tsx create mode 100644 src/app/zxdb/labels/LabelsSearch.tsx create mode 100644 src/app/zxdb/labels/[id]/LabelDetail.tsx create mode 100644 src/app/zxdb/labels/[id]/page.tsx create mode 100644 src/app/zxdb/labels/page.tsx create mode 100644 src/app/zxdb/languages/LanguageList.tsx create mode 100644 src/app/zxdb/languages/[id]/LanguageDetail.tsx create mode 100644 src/app/zxdb/languages/[id]/page.tsx create mode 100644 src/app/zxdb/languages/page.tsx create mode 100644 src/app/zxdb/machinetypes/MachineTypeList.tsx create mode 100644 src/app/zxdb/machinetypes/[id]/MachineTypeDetail.tsx diff --git a/AGENTS.md b/AGENTS.md index 3d35f2b..cca2128 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -114,7 +114,8 @@ Comment what the code does, not what the agent has done. The documentation's pur - git branching: - Do not create new branches - git commits: - - Do not commit code - just create COMMIT_EDITMSG file for manual commiting. + - Create COMMIT_EDITMSG file, await any user edits, then commit using that + commit note, and then delete the COMMIT_EDITMSG file. - git commit messages: - Use imperative mood (e.g., "Add feature X", "Fix bug Y"). - Include relevant issue numbers if applicable. diff --git a/COMMIT_EDITMSG b/COMMIT_EDITMSG new file mode 100644 index 0000000..e9ff47f --- /dev/null +++ b/COMMIT_EDITMSG @@ -0,0 +1,32 @@ +fix: await dynamic route params (Next 15) and correct ZXDB lookup column names + +Update dynamic Server Component pages to the Next.js 15+ async `params` API, +and fix ZXDB lookup table schema to use `text` column (not `name`) to avoid +ER_BAD_FIELD_ERROR in entry detail endpoint. +This resolves the runtime warning/error: +"params should be awaited before using its properties" and prevents +sync-dynamic-apis violations when visiting deep ZXDB permalinks. + +Changes +- /zxdb/entries/[id]/page.tsx: make Page async and `await params`, pass numeric id +- /zxdb/labels/[id]/page.tsx: make Page async and `await params`, pass numeric id +- /zxdb/genres/[id]/page.tsx: make Page async and `await params`, pass numeric id +- /zxdb/languages/[id]/page.tsx: make Page async and `await params`, pass string id +- /registers/[hex]/page.tsx: make Page async and `await params`, decode hex safely + - /api/zxdb/entries/[id]/route.ts: await `ctx.params` before validation + - src/server/schema/zxdb.ts: map `languages.text`, `machinetypes.text`, + and `genretypes.text` to `name` fields in Drizzle models + +Why +- Next.js 15 changed dynamic route APIs such that `params` is now a Promise + in Server Components and must be awaited before property access. +- ZXDB schema defines display columns as `text` (not `name`) for languages, + machinetypes, and genretypes. Using `name` caused MySQL 1054 errors. The + Drizzle models now point to the correct columns while preserving `{ id, name }` + in our API/UI contracts. + +Notes +- API route handlers under /api continue to use ctx.params synchronously; this + change only affects App Router Page components. + +Signed-off-by: Junie@lucy.xalior.com diff --git a/package.json b/package.json index 816612b..8c2a99d 100644 --- a/package.json +++ b/package.json @@ -12,14 +12,14 @@ }, "dependencies": { "bootstrap": "^5.3.8", + "drizzle-orm": "^0.36.1", + "mysql2": "^3.12.0", "next": "~15.5.7", "react": "19.1.0", "react-bootstrap": "^2.10.10", "react-bootstrap-icons": "^1.11.6", "react-dom": "19.1.0", "typescript": "^5.9.3", - "drizzle-orm": "^0.36.1", - "mysql2": "^3.12.0", "zod": "^3.23.8" }, "devDependencies": { @@ -27,9 +27,9 @@ "@types/node": "^20.19.25", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", + "drizzle-kit": "^0.30.1", "eslint": "^9.39.1", "eslint-config-next": "15.5.4", - "sass": "^1.94.2", - "drizzle-kit": "^0.30.1" + "sass": "^1.63.6" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 562002e..a56d02a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 3.15.3 next: specifier: ~15.5.7 - version: 15.5.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.94.2) + version: 15.5.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.63.6) react: specifier: 19.1.0 version: 19.1.0 @@ -61,8 +61,8 @@ importers: specifier: 15.5.4 version: 15.5.4(eslint@9.39.1)(typescript@5.9.3) sass: - specifier: ^1.94.2 - version: 1.94.2 + specifier: ^1.63.6 + version: 1.63.6 packages: @@ -624,88 +624,6 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} - '@parcel/watcher-android-arm64@2.5.1': - resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [android] - - '@parcel/watcher-darwin-arm64@2.5.1': - resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [darwin] - - '@parcel/watcher-darwin-x64@2.5.1': - resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [darwin] - - '@parcel/watcher-freebsd-x64@2.5.1': - resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [freebsd] - - '@parcel/watcher-linux-arm-glibc@2.5.1': - resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - - '@parcel/watcher-linux-arm-musl@2.5.1': - resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - - '@parcel/watcher-linux-arm64-glibc@2.5.1': - resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - - '@parcel/watcher-linux-arm64-musl@2.5.1': - resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - - '@parcel/watcher-linux-x64-glibc@2.5.1': - resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - - '@parcel/watcher-linux-x64-musl@2.5.1': - resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - - '@parcel/watcher-win32-arm64@2.5.1': - resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [win32] - - '@parcel/watcher-win32-ia32@2.5.1': - resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} - engines: {node: '>= 10.0.0'} - cpu: [ia32] - os: [win32] - - '@parcel/watcher-win32-x64@2.5.1': - resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [win32] - - '@parcel/watcher@2.5.1': - resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} - engines: {node: '>= 10.0.0'} - '@petamoriken/float16@3.9.3': resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==} @@ -951,6 +869,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1016,6 +938,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + bootstrap@5.3.8: resolution: {integrity: sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==} peerDependencies: @@ -1057,9 +983,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} @@ -1135,11 +1061,6 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - detect-libc@1.0.3: - resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} - engines: {node: '>=0.10'} - hasBin: true - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1473,6 +1394,11 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -1572,8 +1498,8 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} - immutable@5.1.4: - resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} + immutable@4.3.7: + resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} @@ -1602,6 +1528,10 @@ packages: resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} engines: {node: '>= 0.4'} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-boolean-object@1.2.2: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} @@ -1833,8 +1763,9 @@ packages: sass: optional: true - node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -1973,9 +1904,9 @@ packages: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} @@ -2023,8 +1954,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sass@1.94.2: - resolution: {integrity: sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==} + sass@1.63.6: + resolution: {integrity: sha512-MJuxGMHzaOW7ipp+1KdELtqKbfAWbH7OLIdoSMnVe3EXPMTmxTmlaZDCTsgIpPCs3w99lLo9/zDKkOrJuT5byw==} engines: {node: '>=14.0.0'} hasBin: true @@ -2641,67 +2572,6 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} - '@parcel/watcher-android-arm64@2.5.1': - optional: true - - '@parcel/watcher-darwin-arm64@2.5.1': - optional: true - - '@parcel/watcher-darwin-x64@2.5.1': - optional: true - - '@parcel/watcher-freebsd-x64@2.5.1': - optional: true - - '@parcel/watcher-linux-arm-glibc@2.5.1': - optional: true - - '@parcel/watcher-linux-arm-musl@2.5.1': - optional: true - - '@parcel/watcher-linux-arm64-glibc@2.5.1': - optional: true - - '@parcel/watcher-linux-arm64-musl@2.5.1': - optional: true - - '@parcel/watcher-linux-x64-glibc@2.5.1': - optional: true - - '@parcel/watcher-linux-x64-musl@2.5.1': - optional: true - - '@parcel/watcher-win32-arm64@2.5.1': - optional: true - - '@parcel/watcher-win32-ia32@2.5.1': - optional: true - - '@parcel/watcher-win32-x64@2.5.1': - optional: true - - '@parcel/watcher@2.5.1': - dependencies: - detect-libc: 1.0.3 - is-glob: 4.0.3 - micromatch: 4.0.8 - node-addon-api: 7.1.1 - optionalDependencies: - '@parcel/watcher-android-arm64': 2.5.1 - '@parcel/watcher-darwin-arm64': 2.5.1 - '@parcel/watcher-darwin-x64': 2.5.1 - '@parcel/watcher-freebsd-x64': 2.5.1 - '@parcel/watcher-linux-arm-glibc': 2.5.1 - '@parcel/watcher-linux-arm-musl': 2.5.1 - '@parcel/watcher-linux-arm64-glibc': 2.5.1 - '@parcel/watcher-linux-arm64-musl': 2.5.1 - '@parcel/watcher-linux-x64-glibc': 2.5.1 - '@parcel/watcher-linux-x64-musl': 2.5.1 - '@parcel/watcher-win32-arm64': 2.5.1 - '@parcel/watcher-win32-ia32': 2.5.1 - '@parcel/watcher-win32-x64': 2.5.1 - optional: true - '@petamoriken/float16@3.9.3': {} '@popperjs/core@2.11.8': {} @@ -2946,6 +2816,11 @@ snapshots: dependencies: color-convert: 2.0.1 + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + argparse@2.0.1: {} aria-query@5.3.2: {} @@ -3033,6 +2908,8 @@ snapshots: balanced-match@1.0.2: {} + binary-extensions@2.3.0: {} + bootstrap@5.3.8(@popperjs/core@2.11.8): dependencies: '@popperjs/core': 2.11.8 @@ -3078,9 +2955,17 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chokidar@4.0.3: + chokidar@3.6.0: dependencies: - readdirp: 4.1.2 + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 classnames@2.5.1: {} @@ -3148,9 +3033,6 @@ snapshots: dequal@2.0.3: {} - detect-libc@1.0.3: - optional: true - detect-libc@2.1.2: optional: true @@ -3590,6 +3472,9 @@ snapshots: dependencies: is-callable: 1.2.7 + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -3697,7 +3582,7 @@ snapshots: ignore@7.0.5: {} - immutable@5.1.4: {} + immutable@4.3.7: {} import-fresh@3.3.1: dependencies: @@ -3734,6 +3619,10 @@ snapshots: dependencies: has-bigints: 1.1.0 + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-boolean-object@1.2.2: dependencies: call-bound: 1.0.4 @@ -3940,7 +3829,7 @@ snapshots: natural-compare@1.4.0: {} - next@15.5.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.94.2): + next@15.5.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.63.6): dependencies: '@next/env': 15.5.7 '@swc/helpers': 0.5.15 @@ -3958,14 +3847,13 @@ snapshots: '@next/swc-linux-x64-musl': 15.5.7 '@next/swc-win32-arm64-msvc': 15.5.7 '@next/swc-win32-x64-msvc': 15.5.7 - sass: 1.94.2 + sass: 1.63.6 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - node-addon-api@7.1.1: - optional: true + normalize-path@3.0.0: {} object-assign@4.1.1: {} @@ -4119,7 +4007,9 @@ snapshots: react@19.1.0: {} - readdirp@4.1.2: {} + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 reflect.getprototypeof@1.0.10: dependencies: @@ -4184,13 +4074,11 @@ snapshots: safer-buffer@2.1.2: {} - sass@1.94.2: + sass@1.63.6: dependencies: - chokidar: 4.0.3 - immutable: 5.1.4 + chokidar: 3.6.0 + immutable: 4.3.7 source-map-js: 1.2.1 - optionalDependencies: - '@parcel/watcher': 2.5.1 scheduler@0.26.0: {} diff --git a/src/app/api/zxdb/entries/[id]/route.ts b/src/app/api/zxdb/entries/[id]/route.ts new file mode 100644 index 0000000..dca9e02 --- /dev/null +++ b/src/app/api/zxdb/entries/[id]/route.ts @@ -0,0 +1,29 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { getEntryById } from "@/server/repo/zxdb"; + +const paramsSchema = z.object({ id: z.coerce.number().int().positive() }); + +export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const raw = await ctx.params; + const parsed = paramsSchema.safeParse(raw); + if (!parsed.success) { + return new Response(JSON.stringify({ error: parsed.error.flatten() }), { + status: 400, + headers: { "content-type": "application/json" }, + }); + } + const id = parsed.data.id; + const detail = await getEntryById(id); + if (!detail) { + return new Response(JSON.stringify({ error: "Not found" }), { + status: 404, + headers: { "content-type": "application/json" }, + }); + } + return new Response(JSON.stringify(detail), { + headers: { "content-type": "application/json", "cache-control": "no-store" }, + }); +} + +export const runtime = "nodejs"; diff --git a/src/app/api/zxdb/genres/[id]/route.ts b/src/app/api/zxdb/genres/[id]/route.ts new file mode 100644 index 0000000..6883bad --- /dev/null +++ b/src/app/api/zxdb/genres/[id]/route.ts @@ -0,0 +1,30 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { entriesByGenre } from "@/server/repo/zxdb"; + +const paramsSchema = z.object({ id: z.coerce.number().int().positive() }); +const querySchema = z.object({ + page: z.coerce.number().int().positive().optional(), + pageSize: z.coerce.number().int().positive().max(100).optional(), +}); + +export async function GET(req: NextRequest, ctx: { params: { id: string } }) { + const p = paramsSchema.safeParse(ctx.params); + if (!p.success) { + return new Response(JSON.stringify({ error: p.error.flatten() }), { status: 400 }); + } + const { searchParams } = new URL(req.url); + const q = querySchema.safeParse({ + page: searchParams.get("page") ?? undefined, + pageSize: searchParams.get("pageSize") ?? undefined, + }); + if (!q.success) { + return new Response(JSON.stringify({ error: q.error.flatten() }), { status: 400 }); + } + const page = q.data.page ?? 1; + const pageSize = q.data.pageSize ?? 20; + const data = await entriesByGenre(p.data.id, page, pageSize); + return new Response(JSON.stringify(data), { headers: { "content-type": "application/json" } }); +} + +export const runtime = "nodejs"; diff --git a/src/app/api/zxdb/genres/route.ts b/src/app/api/zxdb/genres/route.ts new file mode 100644 index 0000000..dac4631 --- /dev/null +++ b/src/app/api/zxdb/genres/route.ts @@ -0,0 +1,10 @@ +import { listGenres } from "@/server/repo/zxdb"; + +export async function GET() { + const data = await listGenres(); + return new Response(JSON.stringify({ items: data }), { + headers: { "content-type": "application/json", "cache-control": "no-store" }, + }); +} + +export const runtime = "nodejs"; diff --git a/src/app/api/zxdb/labels/[id]/route.ts b/src/app/api/zxdb/labels/[id]/route.ts new file mode 100644 index 0000000..77c5dec --- /dev/null +++ b/src/app/api/zxdb/labels/[id]/route.ts @@ -0,0 +1,50 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { getLabelById, getLabelAuthoredEntries, getLabelPublishedEntries } from "@/server/repo/zxdb"; + +const paramsSchema = z.object({ id: z.coerce.number().int().positive() }); +const querySchema = z.object({ + page: z.coerce.number().int().positive().optional(), + pageSize: z.coerce.number().int().positive().max(100).optional(), +}); + +export async function GET(req: NextRequest, ctx: { params: { id: string } }) { + const p = paramsSchema.safeParse(ctx.params); + if (!p.success) { + return new Response(JSON.stringify({ error: p.error.flatten() }), { + status: 400, + headers: { "content-type": "application/json" }, + }); + } + const { searchParams } = new URL(req.url); + const q = querySchema.safeParse({ + page: searchParams.get("page") ?? undefined, + pageSize: searchParams.get("pageSize") ?? undefined, + }); + if (!q.success) { + return new Response(JSON.stringify({ error: q.error.flatten() }), { + status: 400, + headers: { "content-type": "application/json" }, + }); + } + const id = p.data.id; + const label = await getLabelById(id); + if (!label) { + return new Response(JSON.stringify({ error: "Not found" }), { + status: 404, + headers: { "content-type": "application/json" }, + }); + } + const page = q.data.page ?? 1; + const pageSize = q.data.pageSize ?? 20; + const [authored, published] = await Promise.all([ + getLabelAuthoredEntries(id, { page, pageSize }), + getLabelPublishedEntries(id, { page, pageSize }), + ]); + return new Response( + JSON.stringify({ label, authored, published }), + { headers: { "content-type": "application/json", "cache-control": "no-store" } } + ); +} + +export const runtime = "nodejs"; diff --git a/src/app/api/zxdb/labels/search/route.ts b/src/app/api/zxdb/labels/search/route.ts new file mode 100644 index 0000000..a5617c1 --- /dev/null +++ b/src/app/api/zxdb/labels/search/route.ts @@ -0,0 +1,30 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { searchLabels } from "@/server/repo/zxdb"; + +const querySchema = z.object({ + q: z.string().optional(), + page: z.coerce.number().int().positive().optional(), + pageSize: z.coerce.number().int().positive().max(100).optional(), +}); + +export async function GET(req: NextRequest) { + const { searchParams } = new URL(req.url); + const parsed = querySchema.safeParse({ + q: searchParams.get("q") ?? undefined, + page: searchParams.get("page") ?? undefined, + pageSize: searchParams.get("pageSize") ?? undefined, + }); + if (!parsed.success) { + return new Response( + JSON.stringify({ error: parsed.error.flatten() }), + { status: 400, headers: { "content-type": "application/json" } } + ); + } + const data = await searchLabels(parsed.data); + return new Response(JSON.stringify(data), { + headers: { "content-type": "application/json", "cache-control": "no-store" }, + }); +} + +export const runtime = "nodejs"; diff --git a/src/app/api/zxdb/languages/[id]/route.ts b/src/app/api/zxdb/languages/[id]/route.ts new file mode 100644 index 0000000..8fb9c74 --- /dev/null +++ b/src/app/api/zxdb/languages/[id]/route.ts @@ -0,0 +1,30 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { entriesByLanguage } from "@/server/repo/zxdb"; + +const paramsSchema = z.object({ id: z.string().trim().length(2) }); +const querySchema = z.object({ + page: z.coerce.number().int().positive().optional(), + pageSize: z.coerce.number().int().positive().max(100).optional(), +}); + +export async function GET(req: NextRequest, ctx: { params: { id: string } }) { + const p = paramsSchema.safeParse(ctx.params); + if (!p.success) { + return new Response(JSON.stringify({ error: p.error.flatten() }), { status: 400 }); + } + const { searchParams } = new URL(req.url); + const q = querySchema.safeParse({ + page: searchParams.get("page") ?? undefined, + pageSize: searchParams.get("pageSize") ?? undefined, + }); + if (!q.success) { + return new Response(JSON.stringify({ error: q.error.flatten() }), { status: 400 }); + } + const page = q.data.page ?? 1; + const pageSize = q.data.pageSize ?? 20; + const data = await entriesByLanguage(p.data.id, page, pageSize); + return new Response(JSON.stringify(data), { headers: { "content-type": "application/json" } }); +} + +export const runtime = "nodejs"; diff --git a/src/app/api/zxdb/languages/route.ts b/src/app/api/zxdb/languages/route.ts new file mode 100644 index 0000000..0fce378 --- /dev/null +++ b/src/app/api/zxdb/languages/route.ts @@ -0,0 +1,10 @@ +import { listLanguages } from "@/server/repo/zxdb"; + +export async function GET() { + const data = await listLanguages(); + return new Response(JSON.stringify({ items: data }), { + headers: { "content-type": "application/json", "cache-control": "no-store" }, + }); +} + +export const runtime = "nodejs"; diff --git a/src/app/api/zxdb/machinetypes/[id]/route.ts b/src/app/api/zxdb/machinetypes/[id]/route.ts new file mode 100644 index 0000000..940ad2c --- /dev/null +++ b/src/app/api/zxdb/machinetypes/[id]/route.ts @@ -0,0 +1,30 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { entriesByMachinetype } from "@/server/repo/zxdb"; + +const paramsSchema = z.object({ id: z.coerce.number().int().positive() }); +const querySchema = z.object({ + page: z.coerce.number().int().positive().optional(), + pageSize: z.coerce.number().int().positive().max(100).optional(), +}); + +export async function GET(req: NextRequest, ctx: { params: { id: string } }) { + const p = paramsSchema.safeParse(ctx.params); + if (!p.success) { + return new Response(JSON.stringify({ error: p.error.flatten() }), { status: 400 }); + } + const { searchParams } = new URL(req.url); + const q = querySchema.safeParse({ + page: searchParams.get("page") ?? undefined, + pageSize: searchParams.get("pageSize") ?? undefined, + }); + if (!q.success) { + return new Response(JSON.stringify({ error: q.error.flatten() }), { status: 400 }); + } + const page = q.data.page ?? 1; + const pageSize = q.data.pageSize ?? 20; + const data = await entriesByMachinetype(p.data.id, page, pageSize); + return new Response(JSON.stringify(data), { headers: { "content-type": "application/json" } }); +} + +export const runtime = "nodejs"; diff --git a/src/app/api/zxdb/machinetypes/route.ts b/src/app/api/zxdb/machinetypes/route.ts new file mode 100644 index 0000000..a559891 --- /dev/null +++ b/src/app/api/zxdb/machinetypes/route.ts @@ -0,0 +1,10 @@ +import { listMachinetypes } from "@/server/repo/zxdb"; + +export async function GET() { + const data = await listMachinetypes(); + return new Response(JSON.stringify({ items: data }), { + headers: { "content-type": "application/json", "cache-control": "no-store" }, + }); +} + +export const runtime = "nodejs"; diff --git a/src/app/api/zxdb/search/route.ts b/src/app/api/zxdb/search/route.ts index d8fbec6..d9e0f16 100644 --- a/src/app/api/zxdb/search/route.ts +++ b/src/app/api/zxdb/search/route.ts @@ -6,6 +6,14 @@ const querySchema = z.object({ q: z.string().optional(), page: z.coerce.number().int().positive().optional(), pageSize: z.coerce.number().int().positive().max(100).optional(), + genreId: z.coerce.number().int().positive().optional(), + languageId: z + .string() + .trim() + .length(2, "languageId must be a 2-char code") + .optional(), + machinetypeId: z.coerce.number().int().positive().optional(), + sort: z.enum(["title", "id_desc"]).optional(), }); export async function GET(req: NextRequest) { @@ -14,6 +22,10 @@ export async function GET(req: NextRequest) { q: searchParams.get("q") ?? undefined, page: searchParams.get("page") ?? undefined, pageSize: searchParams.get("pageSize") ?? undefined, + genreId: searchParams.get("genreId") ?? undefined, + languageId: searchParams.get("languageId") ?? undefined, + machinetypeId: searchParams.get("machinetypeId") ?? undefined, + sort: searchParams.get("sort") ?? undefined, }); if (!parsed.success) { return new Response( diff --git a/src/app/registers/[hex]/page.tsx b/src/app/registers/[hex]/page.tsx index 7e2c4c8..14d02a5 100644 --- a/src/app/registers/[hex]/page.tsx +++ b/src/app/registers/[hex]/page.tsx @@ -5,9 +5,10 @@ import RegisterDetail from '@/app/registers/RegisterDetail'; import {Container, Row} from "react-bootstrap"; import { getRegisters } from '@/services/register.service'; -export default async function RegisterDetailPage({ params }: { params: { hex: string } }) { +export default async function RegisterDetailPage({ params }: { params: Promise<{ hex: string }> }) { const registers = await getRegisters(); - const targetHex = decodeURIComponent((await params).hex).toLowerCase(); + const { hex } = await params; + const targetHex = decodeURIComponent(hex).toLowerCase(); const register = registers.find(r => r.hex_address.toLowerCase() === targetHex); diff --git a/src/app/zxdb/ZxdbExplorer.tsx b/src/app/zxdb/ZxdbExplorer.tsx index ad974c6..49ac9f3 100644 --- a/src/app/zxdb/ZxdbExplorer.tsx +++ b/src/app/zxdb/ZxdbExplorer.tsx @@ -22,6 +22,13 @@ export default function ZxdbExplorer() { const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); const [data, setData] = useState | null>(null); + const [genres, setGenres] = useState<{ id: number; name: string }[]>([]); + const [languages, setLanguages] = useState<{ id: string; name: string }[]>([]); + const [machines, setMachines] = useState<{ id: number; name: string }[]>([]); + const [genreId, setGenreId] = useState(""); + const [languageId, setLanguageId] = useState(""); + const [machinetypeId, setMachinetypeId] = useState(""); + const [sort, setSort] = useState<"title" | "id_desc">("title"); const pageSize = 20; const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]); @@ -33,6 +40,10 @@ export default function ZxdbExplorer() { if (query) params.set("q", query); params.set("page", String(p)); params.set("pageSize", String(pageSize)); + if (genreId !== "") params.set("genreId", String(genreId)); + if (languageId !== "") params.set("languageId", String(languageId)); + if (machinetypeId !== "") params.set("machinetypeId", String(machinetypeId)); + if (sort) params.set("sort", sort); const res = await fetch(`/api/zxdb/search?${params.toString()}`); if (!res.ok) throw new Error(`Failed: ${res.status}`); const json: Paged = await res.json(); @@ -49,7 +60,24 @@ export default function ZxdbExplorer() { useEffect(() => { fetchData(q, page); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [page]); + }, [page, genreId, languageId, machinetypeId, sort]); + + // Load filter lists once + useEffect(() => { + async function loadLists() { + try { + const [g, l, m] = await Promise.all([ + fetch("/api/zxdb/genres", { cache: "no-store" }).then((r) => r.json()), + fetch("/api/zxdb/languages", { cache: "no-store" }).then((r) => r.json()), + fetch("/api/zxdb/machinetypes", { cache: "no-store" }).then((r) => r.json()), + ]); + setGenres(g.items ?? []); + setLanguages(l.items ?? []); + setMachines(m.items ?? []); + } catch {} + } + loadLists(); + }, []); function onSubmit(e: React.FormEvent) { e.preventDefault(); @@ -73,6 +101,36 @@ export default function ZxdbExplorer() {
+
+ +
+
+ +
+
+ +
+
+ +
{loading && (
Loading...
)} @@ -97,7 +155,9 @@ export default function ZxdbExplorer() { {data.items.map((it) => ( {it.id} - {it.title} + + {it.title} + {it.machinetypeId ?? "-"} {it.languageId ?? "-"} @@ -127,6 +187,14 @@ export default function ZxdbExplorer() { Next + +
+ ); } diff --git a/src/app/zxdb/entries/[id]/EntryDetail.tsx b/src/app/zxdb/entries/[id]/EntryDetail.tsx new file mode 100644 index 0000000..62252f9 --- /dev/null +++ b/src/app/zxdb/entries/[id]/EntryDetail.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { useEffect, useState } from "react"; + +type Label = { id: number; name: string; labeltypeId: string | null }; +type EntryDetail = { + 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[]; +}; + +export default function EntryDetailClient({ id }: { id: number }) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let aborted = false; + async function run() { + setLoading(true); + setError(null); + try { + const res = await fetch(`/api/zxdb/entries/${id}`, { cache: "no-store" }); + if (!res.ok) throw new Error(`Failed: ${res.status}`); + const json: EntryDetail = await res.json(); + if (!aborted) setData(json); + } catch (e: any) { + if (!aborted) setError(e?.message ?? "Failed to load"); + } finally { + if (!aborted) setLoading(false); + } + } + run(); + return () => { + aborted = true; + }; + }, [id]); + + if (loading) return
Loading…
; + if (error) return
{error}
; + if (!data) return
Not found
; + + return ( +
+
+

{data.title}

+ {data.genre.name && ( + + {data.genre.name} + + )} + {data.language.name && ( + + {data.language.name} + + )} + {data.machinetype.name && ( + + {data.machinetype.name} + + )} + {data.isXrated ? 18+ : null} +
+ +
+ +
+
+
Authors
+ {data.authors.length === 0 &&
Unknown
} + {data.authors.length > 0 && ( +
    + {data.authors.map((a) => ( +
  • + {a.name} +
  • + ))} +
+ )} +
+
+
Publishers
+ {data.publishers.length === 0 &&
Unknown
} + {data.publishers.length > 0 && ( +
    + {data.publishers.map((p) => ( +
  • + {p.name} +
  • + ))} +
+ )} +
+
+ +
+ + +
+ ); +} diff --git a/src/app/zxdb/entries/[id]/page.tsx b/src/app/zxdb/entries/[id]/page.tsx new file mode 100644 index 0000000..9eadf23 --- /dev/null +++ b/src/app/zxdb/entries/[id]/page.tsx @@ -0,0 +1,10 @@ +import EntryDetailClient from "./EntryDetail"; + +export const metadata = { + title: "ZXDB Entry", +}; + +export default async function Page({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + return ; +} diff --git a/src/app/zxdb/genres/GenreList.tsx b/src/app/zxdb/genres/GenreList.tsx new file mode 100644 index 0000000..4e1c706 --- /dev/null +++ b/src/app/zxdb/genres/GenreList.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useEffect, useState } from "react"; + +type Genre = { id: number; name: string }; + +export default function GenreList() { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + useEffect(() => { + async function load() { + try { + const res = await fetch("/api/zxdb/genres", { cache: "no-store" }); + const json = await res.json(); + setItems(json.items ?? []); + } finally { + setLoading(false); + } + } + load(); + }, []); + + if (loading) return
Loading…
; + return ( +
+

Genres

+
    + {items.map((g) => ( +
  • + {g.name} + #{g.id} +
  • + ))} +
+
+ ); +} diff --git a/src/app/zxdb/genres/[id]/GenreDetail.tsx b/src/app/zxdb/genres/[id]/GenreDetail.tsx new file mode 100644 index 0000000..8059480 --- /dev/null +++ b/src/app/zxdb/genres/[id]/GenreDetail.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; + +type Item = { id: number; title: string; isXrated: number; machinetypeId: number | null; languageId: string | null }; +type Paged = { items: T[]; page: number; pageSize: number; total: number }; + +export default function GenreDetailClient({ id }: { id: number }) { + const [data, setData] = useState | null>(null); + const [loading, setLoading] = useState(false); + const [page, setPage] = useState(1); + const pageSize = 20; + const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]); + + async function load(p: number) { + setLoading(true); + try { + const res = await fetch(`/api/zxdb/genres/${id}?page=${p}&pageSize=${pageSize}`, { cache: "no-store" }); + const json = (await res.json()) as Paged; + setData(json); + } finally { + setLoading(false); + } + } + + useEffect(() => { + load(page); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [page]); + + return ( +
+

Genre #{id}

+ {loading &&
Loading…
} + {data && data.items.length === 0 && !loading &&
No entries.
} + {data && data.items.length > 0 && ( +
+ + + + + + + + + + + {data.items.map((it) => ( + + + + + + + ))} + +
IDTitleMachineLang
{it.id}{it.title}{it.machinetypeId ?? "-"}{it.languageId ?? "-"}
+
+ )} + +
+ + Page {data?.page ?? page} / {totalPages} + +
+
+ ); +} diff --git a/src/app/zxdb/genres/[id]/page.tsx b/src/app/zxdb/genres/[id]/page.tsx new file mode 100644 index 0000000..4bbb929 --- /dev/null +++ b/src/app/zxdb/genres/[id]/page.tsx @@ -0,0 +1,8 @@ +import GenreDetailClient from "./GenreDetail"; + +export const metadata = { title: "ZXDB Genre" }; + +export default async function Page({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + return ; +} diff --git a/src/app/zxdb/genres/page.tsx b/src/app/zxdb/genres/page.tsx new file mode 100644 index 0000000..b5d1468 --- /dev/null +++ b/src/app/zxdb/genres/page.tsx @@ -0,0 +1,7 @@ +import GenreList from "./GenreList"; + +export const metadata = { title: "ZXDB Genres" }; + +export default function Page() { + return ; +} diff --git a/src/app/zxdb/labels/LabelsSearch.tsx b/src/app/zxdb/labels/LabelsSearch.tsx new file mode 100644 index 0000000..06cc402 --- /dev/null +++ b/src/app/zxdb/labels/LabelsSearch.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; + +type Label = { id: number; name: string; labeltypeId: string | null }; +type Paged = { items: T[]; page: number; pageSize: number; total: number }; + +export default function LabelsSearch() { + const [q, setQ] = useState(""); + const [page, setPage] = useState(1); + const [data, setData] = useState | null>(null); + const [loading, setLoading] = useState(false); + const pageSize = 20; + const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]); + + async function load() { + setLoading(true); + try { + const params = new URLSearchParams(); + if (q) params.set("q", q); + params.set("page", String(page)); + params.set("pageSize", String(pageSize)); + const res = await fetch(`/api/zxdb/labels/search?${params.toString()}`, { cache: "no-store" }); + const json = (await res.json()) as Paged