diff --git a/.beans/nuzlocke-tracker-2zc9--add-zizmor-and-actionlint-to-ci.md b/.beans/nuzlocke-tracker-2zc9--add-zizmor-and-actionlint-to-ci.md new file mode 100644 index 0000000..732ddd9 --- /dev/null +++ b/.beans/nuzlocke-tracker-2zc9--add-zizmor-and-actionlint-to-ci.md @@ -0,0 +1,17 @@ +--- +# nuzlocke-tracker-2zc9 +title: Add zizmor and actionlint to CI +status: completed +type: task +priority: normal +created_at: 2026-02-16T19:13:50Z +updated_at: 2026-02-16T19:26:23Z +parent: nuzlocke-tracker-a5es +--- + +Global standards require scanning GitHub Actions workflows with zizmor (security audit) and actionlint (linter). + +## Checklist +- [ ] Add actionlint check to CI +- [ ] Add zizmor scan to CI +- [ ] Fix any issues found \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-44ps--replace-eslintprettier-with-oxlintoxfmt.md b/.beans/nuzlocke-tracker-44ps--replace-eslintprettier-with-oxlintoxfmt.md new file mode 100644 index 0000000..b97a62b --- /dev/null +++ b/.beans/nuzlocke-tracker-44ps--replace-eslintprettier-with-oxlintoxfmt.md @@ -0,0 +1,24 @@ +--- +# nuzlocke-tracker-44ps +title: Replace ESLint/Prettier with oxlint/oxfmt +status: completed +type: task +priority: normal +created_at: 2026-02-16T19:13:49Z +updated_at: 2026-02-16T19:24:15Z +parent: nuzlocke-tracker-a5es +--- + +Global standards require oxlint and oxfmt instead of ESLint and Prettier. This involves: + +## Checklist +- [ ] Install oxlint as devDependency +- [ ] Configure oxlint with typescript, import, unicorn plugins +- [ ] Install oxfmt as devDependency (or use oxlint --fix for formatting) +- [ ] Remove ESLint and all ESLint plugins/configs +- [ ] Remove Prettier and eslint-config-prettier +- [ ] Update package.json scripts +- [ ] Update pre-commit hooks +- [ ] Update CI workflow + +Note: oxfmt may not be stable yet — check current status before proceeding. \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-9vny--add-dependabot-config.md b/.beans/nuzlocke-tracker-9vny--add-dependabot-config.md new file mode 100644 index 0000000..78ed9ef --- /dev/null +++ b/.beans/nuzlocke-tracker-9vny--add-dependabot-config.md @@ -0,0 +1,18 @@ +--- +# nuzlocke-tracker-9vny +title: Add Dependabot config +status: completed +type: task +priority: normal +created_at: 2026-02-16T19:13:50Z +updated_at: 2026-02-16T19:24:15Z +parent: nuzlocke-tracker-a5es +--- + +No .github/dependabot.yml exists. Global standards require Dependabot with 7-day cooldowns and grouped updates. + +## Checklist +- [ ] Create .github/dependabot.yml +- [ ] Configure for npm (frontend), pip (backend), and github-actions +- [ ] Set 7-day schedule intervals +- [ ] Group minor/patch updates \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-a5es--align-repo-config-with-global-dev-standards.md b/.beans/nuzlocke-tracker-a5es--align-repo-config-with-global-dev-standards.md new file mode 100644 index 0000000..32e2e5a --- /dev/null +++ b/.beans/nuzlocke-tracker-a5es--align-repo-config-with-global-dev-standards.md @@ -0,0 +1,11 @@ +--- +# nuzlocke-tracker-a5es +title: Align repo config with global dev standards +status: completed +type: epic +priority: normal +created_at: 2026-02-16T19:13:24Z +updated_at: 2026-02-16T19:26:23Z +--- + +Audit found multiple deviations from the global CLAUDE.md development standards. This epic tracks all the fixes needed. \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-ecij--add-vitest-for-frontend-testing.md b/.beans/nuzlocke-tracker-ecij--add-vitest-for-frontend-testing.md new file mode 100644 index 0000000..d38c8e0 --- /dev/null +++ b/.beans/nuzlocke-tracker-ecij--add-vitest-for-frontend-testing.md @@ -0,0 +1,18 @@ +--- +# nuzlocke-tracker-ecij +title: Add vitest for frontend testing +status: completed +type: task +priority: normal +created_at: 2026-02-16T19:13:50Z +updated_at: 2026-02-16T19:26:23Z +parent: nuzlocke-tracker-a5es +--- + +No frontend test runner is configured. Global standards require vitest. + +## Checklist +- [ ] Install vitest as devDependency +- [ ] Create vitest.config.ts +- [ ] Add test script to package.json +- [ ] Add test step to CI workflow \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-mn8d--pin-github-actions-to-sha-hashes-and-add-persist-c.md b/.beans/nuzlocke-tracker-mn8d--pin-github-actions-to-sha-hashes-and-add-persist-c.md new file mode 100644 index 0000000..4a5dcaa --- /dev/null +++ b/.beans/nuzlocke-tracker-mn8d--pin-github-actions-to-sha-hashes-and-add-persist-c.md @@ -0,0 +1,18 @@ +--- +# nuzlocke-tracker-mn8d +title: 'Pin GitHub Actions to SHA hashes and add persist-credentials: false' +status: completed +type: task +priority: normal +created_at: 2026-02-16T19:13:49Z +updated_at: 2026-02-16T19:24:15Z +parent: nuzlocke-tracker-a5es +--- + +All GitHub Actions use tag references (@v4, @v5) instead of SHA hashes with version comments. Also missing persist-credentials: false on checkout steps. + +## Checklist +- [ ] Pin actions/checkout to SHA with version comment +- [ ] Pin actions/setup-python to SHA with version comment +- [ ] Pin actions/setup-node to SHA with version comment +- [ ] Add persist-credentials: false to all checkout steps \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-o1ek--fix-ci-python-version-mismatch-and-ruff-target-ver.md b/.beans/nuzlocke-tracker-o1ek--fix-ci-python-version-mismatch-and-ruff-target-ver.md new file mode 100644 index 0000000..fc2184f --- /dev/null +++ b/.beans/nuzlocke-tracker-o1ek--fix-ci-python-version-mismatch-and-ruff-target-ver.md @@ -0,0 +1,17 @@ +--- +# nuzlocke-tracker-o1ek +title: Fix CI Python version mismatch and ruff target-version +status: completed +type: bug +priority: normal +created_at: 2026-02-16T19:13:49Z +updated_at: 2026-02-16T19:24:15Z +parent: nuzlocke-tracker-a5es +--- + +CI uses python-version 3.12 but .tool-versions and Docker use 3.14. Also, ruff target-version in pyproject.toml is py312 but should be py314. + +## Checklist +- [ ] Update ci.yml python-version from 3.12 to 3.14 +- [ ] Update pyproject.toml ruff target-version from py312 to py314 +- [ ] Update requires-python to >=3.14 \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-sqb9--pin-frontend-dependencies-to-exact-versions.md b/.beans/nuzlocke-tracker-sqb9--pin-frontend-dependencies-to-exact-versions.md new file mode 100644 index 0000000..b350411 --- /dev/null +++ b/.beans/nuzlocke-tracker-sqb9--pin-frontend-dependencies-to-exact-versions.md @@ -0,0 +1,14 @@ +--- +# nuzlocke-tracker-sqb9 +title: Pin frontend dependencies to exact versions +status: completed +type: task +priority: normal +created_at: 2026-02-16T19:13:49Z +updated_at: 2026-02-16T19:24:15Z +parent: nuzlocke-tracker-a5es +--- + +All frontend dependencies in package.json use ^ or ~ ranges. Global standards require exact pinning (no ^ or ~). + +Pin all dependencies and devDependencies to their currently installed exact versions. \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-w5vu--pin-backend-python-dependencies-to-exact-versions.md b/.beans/nuzlocke-tracker-w5vu--pin-backend-python-dependencies-to-exact-versions.md new file mode 100644 index 0000000..614fdf3 --- /dev/null +++ b/.beans/nuzlocke-tracker-w5vu--pin-backend-python-dependencies-to-exact-versions.md @@ -0,0 +1,14 @@ +--- +# nuzlocke-tracker-w5vu +title: Pin backend Python dependencies to exact versions +status: completed +type: task +priority: normal +created_at: 2026-02-16T19:13:49Z +updated_at: 2026-02-16T19:24:15Z +parent: nuzlocke-tracker-a5es +--- + +All backend dependencies in pyproject.toml use >= ranges. Global standards require exact pins (== not >=). + +Pin all dependencies and dev dependencies to their currently installed exact versions. \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-yyce--add-ty-for-python-type-checking.md b/.beans/nuzlocke-tracker-yyce--add-ty-for-python-type-checking.md new file mode 100644 index 0000000..636a27f --- /dev/null +++ b/.beans/nuzlocke-tracker-yyce--add-ty-for-python-type-checking.md @@ -0,0 +1,18 @@ +--- +# nuzlocke-tracker-yyce +title: Add ty for Python type checking +status: completed +type: task +priority: normal +created_at: 2026-02-16T19:13:50Z +updated_at: 2026-02-16T19:26:23Z +parent: nuzlocke-tracker-a5es +--- + +Global standards require ty check for static type analysis. Currently not configured. + +## Checklist +- [ ] Add ty to dev dependencies +- [ ] Configure ty rules in pyproject.toml +- [ ] Add ty check step to CI workflow +- [ ] Fix any type errors surfaced \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-zom1--add-missing-tsconfig-strictness-flags.md b/.beans/nuzlocke-tracker-zom1--add-missing-tsconfig-strictness-flags.md new file mode 100644 index 0000000..2ca2622 --- /dev/null +++ b/.beans/nuzlocke-tracker-zom1--add-missing-tsconfig-strictness-flags.md @@ -0,0 +1,19 @@ +--- +# nuzlocke-tracker-zom1 +title: Add missing tsconfig strictness flags +status: completed +type: task +priority: normal +created_at: 2026-02-16T19:13:49Z +updated_at: 2026-02-16T19:24:15Z +parent: nuzlocke-tracker-a5es +--- + +tsconfig.app.json is missing 4 required strict flags from the global standards: + +- noUncheckedIndexedAccess: true +- exactOptionalPropertyTypes: true +- noImplicitOverride: true +- noPropertyAccessFromIndexSignature: true + +These need to be added and any resulting type errors fixed. \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ba2a624 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,31 @@ +version: 2 + +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + actions: + patterns: + - "*" + + - package-ecosystem: "npm" + directory: "/frontend" + schedule: + interval: "weekly" + groups: + minor-and-patch: + update-types: + - "minor" + - "patch" + + - package-ecosystem: "pip" + directory: "/backend" + schedule: + interval: "weekly" + groups: + minor-and-patch: + update-types: + - "minor" + - "patch" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f79340..8a95e8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,21 +22,45 @@ jobs: backend-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: - python-version: "3.12" - - run: pip install ruff + persist-credentials: false + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.14" + - run: pip install ruff ty - name: Check linting run: ruff check backend/ - name: Check formatting run: ruff format --check backend/ + - name: Type check + run: ty check backend/src/ + continue-on-error: true + + actions-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + - name: Install actionlint + run: | + curl -sL https://github.com/rhysd/actionlint/releases/latest/download/actionlint_linux_amd64.tar.gz | tar xz + sudo mv actionlint /usr/local/bin/ + - name: Lint GitHub Actions + run: actionlint + - name: Install zizmor + run: pip install zizmor + - name: Audit GitHub Actions security + run: zizmor .github/workflows/ frontend-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "24" - name: Install dependencies @@ -46,7 +70,7 @@ jobs: run: npm run lint working-directory: frontend - name: Check formatting - run: npx prettier --check "src/**/*.{ts,tsx,css,json}" + run: npx oxfmt --check "src/" working-directory: frontend - name: Type check run: npx tsc -b diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cd4a9c2..8b08735 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -8,7 +8,9 @@ jobs: runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false - name: Login to Gitea registry run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login gitea.nerdboden.de -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin diff --git a/.gitignore b/.gitignore index 495a352..61d59d6 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,28 @@ temp/ # Local config overrides *.local + +# Claude Code sandbox artifacts +/HEAD +/config +/hooks +/objects +/refs +/frontend/HEAD +/frontend/config +/frontend/hooks +/frontend/objects +/frontend/refs +/.gitmodules +/.gitconfig +/.bash_profile +/.bashrc +/.profile +/.zprofile +/.zshrc +/.ripgreprc +/.mcp.json +/.claude/agents +/.claude/commands +/.claude/skills +/frontend/.claude/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2640044..372260e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,18 +12,18 @@ repos: # Frontend (TypeScript/React) — local hooks using project node_modules - repo: local hooks: - - id: eslint - name: eslint - entry: npx eslint + - id: oxlint + name: oxlint + entry: npx oxlint -c frontend/.oxlintrc.json language: system files: ^frontend/src/.*\.(ts|tsx)$ pass_filenames: true - - id: prettier - name: prettier - entry: npx prettier --check + - id: oxfmt + name: oxfmt + entry: npx oxfmt --check --config frontend/.oxfmtrc.json language: system - files: ^frontend/src/.*\.(ts|tsx|css|json)$ + files: ^frontend/src/.*\.(ts|tsx)$ pass_filenames: true - id: tsc diff --git a/CLAUDE.md b/CLAUDE.md index 5b90c79..30d3390 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ This project uses [pre-commit](https://pre-commit.com/) to run linting and forma **Hooks configured:** - **Backend:** `ruff check --fix` and `ruff format` on Python files under `backend/` -- **Frontend:** `eslint`, `prettier --check`, and `tsc -b` on files under `frontend/` +- **Frontend:** `oxlint`, `oxfmt --check`, and `tsc -b` on files under `frontend/` Frontend hooks require `npm ci` in `frontend/` first (they use `npx` to run from local `node_modules`). diff --git a/backend/pyproject.toml b/backend/pyproject.toml index e4069a2..10d7d9a 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -3,25 +3,26 @@ name = "nuzlocke-tracker-api" version = "0.1.0" description = "Backend API for Nuzlocke Tracker" readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.14" dependencies = [ - "fastapi>=0.115.0", - "uvicorn[standard]>=0.34.0", - "pydantic>=2.10.0", - "pydantic-settings>=2.7.0", - "python-dotenv>=1.0.0", - "sqlalchemy[asyncio]>=2.0.0", - "asyncpg>=0.30.0", - "alembic>=1.14.0", + "fastapi==0.128.4", + "uvicorn[standard]==0.40.0", + "pydantic==2.12.5", + "pydantic-settings==2.12.0", + "python-dotenv==1.2.1", + "sqlalchemy[asyncio]==2.0.46", + "asyncpg==0.31.0", + "alembic==1.18.3", ] [project.optional-dependencies] dev = [ - "ruff>=0.9.0", - "pre-commit>=4.0.0", - "pytest>=8.0.0", - "pytest-asyncio>=0.25.0", - "httpx>=0.28.0", + "ruff==0.15.0", + "ty==0.0.17", + "pre-commit==4.5.1", + "pytest==9.0.2", + "pytest-asyncio==1.3.0", + "httpx==0.28.1", ] [build-system] @@ -32,7 +33,7 @@ build-backend = "hatchling.build" packages = ["src/app"] [tool.ruff] -target-version = "py312" +target-version = "py314" line-length = 88 [tool.ruff.lint] @@ -57,6 +58,12 @@ ignore = [ [tool.ruff.lint.isort] known-first-party = ["app"] +[tool.ty.environment] +python-version = "3.14" + +[tool.ty.src] +root = "src" + [tool.pytest.ini_options] asyncio_mode = "auto" testpaths = ["tests"] diff --git a/frontend/.prettierrc b/frontend/.oxfmtrc.json similarity index 100% rename from frontend/.prettierrc rename to frontend/.oxfmtrc.json diff --git a/frontend/.oxlintrc.json b/frontend/.oxlintrc.json new file mode 100644 index 0000000..9a671f4 --- /dev/null +++ b/frontend/.oxlintrc.json @@ -0,0 +1,9 @@ +{ + "plugins": ["typescript", "import", "unicorn", "react"], + "rules": { + "react/exhaustive-deps": "warn", + "react/rules-of-hooks": "error", + "unicorn/no-null": "off" + }, + "ignorePatterns": ["dist"] +} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js deleted file mode 100644 index 52e80ee..0000000 --- a/frontend/eslint.config.js +++ /dev/null @@ -1,33 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import eslintConfigPrettier from 'eslint-config-prettier' -import { defineConfig, globalIgnores } from 'eslint/config' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - eslintConfigPrettier, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - rules: { - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - 'react-hooks/set-state-in-effect': 'off', - 'react-hooks/preserve-manual-memoization': 'off', - }, - }, -]) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 686b17f..b786304 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,32 +8,27 @@ "name": "nuzlocke-tracker-frontend", "version": "0.0.0", "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@tanstack/react-query": "^5.90.20", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "react-router-dom": "^7.13.0", - "sonner": "^2.0.7" + "@dnd-kit/core": "6.3.1", + "@dnd-kit/sortable": "10.0.0", + "@dnd-kit/utilities": "3.2.2", + "@tanstack/react-query": "5.90.20", + "react": "19.2.4", + "react-dom": "19.2.4", + "react-router-dom": "7.13.0", + "sonner": "2.0.7" }, "devDependencies": { - "@eslint/js": "^9.39.1", - "@tailwindcss/vite": "^4.1.18", - "@types/node": "^24.10.1", - "@types/react": "^19.2.5", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", - "eslint": "^9.39.1", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", - "prettier": "^3.8.1", - "tailwindcss": "^4.1.18", - "typescript": "~5.9.3", - "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" + "@tailwindcss/vite": "4.1.18", + "@types/node": "24.10.10", + "@types/react": "19.2.11", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "5.1.3", + "oxfmt": "0.33.0", + "oxlint": "1.48.0", + "tailwindcss": "4.1.18", + "typescript": "5.9.3", + "vite": "7.3.1", + "vitest": "^4.0.18" } }, "node_modules/@babel/code-frame": { @@ -815,215 +810,6 @@ "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1074,6 +860,652 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.33.0.tgz", + "integrity": "sha512-ML6qRW8/HiBANteqfyFAR1Zu0VrJu+6o4gkPLsssq74hQ7wDMkufBYJXI16PGSERxEYNwKxO5fesCuMssgTv9w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.33.0.tgz", + "integrity": "sha512-WimmcyrGpTOntj7F7CO9RMssncOKYall93nBnzJbI2ZZDhVRuCkvFwTpwz80cZqwYm5udXRXfF40ZXcCxjp9jg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.33.0.tgz", + "integrity": "sha512-PorspsX9O5ISstVaq34OK4esN0LVcuU4DVg+XuSqJsfJ//gn6z6WH2Tt7s0rTQaqEcp76g7+QdWQOmnJDZsEVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.33.0.tgz", + "integrity": "sha512-8278bqQtOcHRPhhzcqwN9KIideut+cftBjF8d2TOsSQrlsJSFx41wCCJ38mFmH9NOmU1M+x9jpeobHnbRP1okw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.33.0.tgz", + "integrity": "sha512-BiqYVwWFHLf5dkfg0aCKsXa9rpi//vH1+xePCpd7Ulz9yp9pJKP4DWgS5g+OW8MaqOtt7iyAszhxtk/j1nDKHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.33.0.tgz", + "integrity": "sha512-oAVmmurXx0OKbNOVv71oK92LsF1LwYWpnhDnX0VaAy/NLsCKf4B7Zo7lxkJh80nfhU20TibcdwYfoHVaqlStPQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.33.0.tgz", + "integrity": "sha512-YB6S8CiRol59oRxnuclJiWoV6l+l8ru/NsuQNYjXZnnPXfSTXKtMLWHCnL/figpCFYA1E7JyjrBbar1qxe2aZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.33.0.tgz", + "integrity": "sha512-hrYy+FpWoB6N24E9oGRimhVkqlls9yeqcRmQakEPUHoAbij6rYxsHHYIp3+FHRiQZFAOUxWKn/CCQoy/Mv3Dgw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.33.0.tgz", + "integrity": "sha512-O1YIzymGRdWj9cG5iVTjkP7zk9/hSaVN8ZEbqMnWZjLC1phXlv54cUvANGGXndgJp2JS4W9XENn7eo5I4jZueg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.33.0.tgz", + "integrity": "sha512-2lrkNe+B0w1tCgQTaozfUNQCYMbqKKCGcnTDATmWCZzO77W2sh+3n04r1lk9Q1CK3bI+C3fPwhFPUR2X2BvlyQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.33.0.tgz", + "integrity": "sha512-8DSG1q0M6097vowHAkEyHnKed75/BWr1IBtgCJfytnWQg+Jn1X4DryhfjqonKZOZiv74oFQl5J8TCbdDuXXdtQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.33.0.tgz", + "integrity": "sha512-eWaxnpPz7+p0QGUnw7GGviVBDOXabr6Cd0w7S/vnWTqQo9z1VroT7XXFnJEZ3dBwxMB9lphyuuYi/GLTCxqxlg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.33.0.tgz", + "integrity": "sha512-+mH8cQTqq+Tu2CdoB2/Wmk9CqotXResi+gPvXpb+AAUt/LiwpicTQqSolMheQKogkDTYHPuUiSN23QYmy7IXNQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.33.0.tgz", + "integrity": "sha512-fjyslAYAPE2+B6Ckrs5LuDQ6lB1re5MumPnzefAXsen3JGwiRilra6XdjUmszTNoExJKbewoxxd6bcLSTpkAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.33.0.tgz", + "integrity": "sha512-ve/jGBlTt35Jl/I0A0SfCQX3wKnadzPDdyOFEwe2ZgHHIT9uhqhAv1PaVXTenSBpauICEWYH8mWy+ittzlVE/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.33.0.tgz", + "integrity": "sha512-lsWRgY9e+uPvwXnuDiJkmJ2Zs3XwwaQkaALJ3/SXU9kjZP0Qh8/tGW8Tk/Z6WL32sDxx+aOK5HuU7qFY9dHJhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.33.0.tgz", + "integrity": "sha512-w8AQHyGDRZutxtQ7IURdBEddwFrtHQiG6+yIFpNJ4HiMyYEqeAWzwBQBfwSAxtSNh6Y9qqbbc1OM2mHN6AB3Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.33.0.tgz", + "integrity": "sha512-j2X4iumKVwDzQtUx3JBDkaydx6eLuncgUZPl2ybZ8llxJMFbZIniws70FzUQePMfMtzLozIm7vo4bjkvQFsOzw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.33.0.tgz", + "integrity": "sha512-lsBQxbepASwOBUh3chcKAjU+jVAQhLElbPYiagIq26cU8vA9Bttj6t20bMvCQCw31m440IRlNhrK7NpnUI8mzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.48.0.tgz", + "integrity": "sha512-1Pz/stJvveO9ZO7ll4ZoEY3f6j2FiUgBLBcCRCiW6ylId9L9UKs+gn3X28m3eTnoiFCkhKwmJJ+VO6vwsu7Qtg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.48.0.tgz", + "integrity": "sha512-Zc42RWGE8huo6Ht0lXKjd0NH2lWNmimQHUmD0JFcvShLOuwN+RSEE/kRakc2/0LIgOUuU/R7PaDMCOdQlPgNUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.48.0.tgz", + "integrity": "sha512-jgZs563/4vaG5jH2RSt2TSh8A2jwsFdmhLXrElMdm3Mmto0HPf85FgInLSNi9HcwzQFvkYV8JofcoUg2GH1HTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.48.0.tgz", + "integrity": "sha512-kvo87BujEUjCJREuWDC4aPh1WoXCRFFWE4C7uF6wuoMw2f6N2hypA/cHHcYn9DdL8R2RrgUZPefC8JExyeIMKA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.48.0.tgz", + "integrity": "sha512-eyzzPaHQKn0RIM+ueDfgfJF2RU//Wp4oaKs2JVoVYcM5HjbCL36+O0S3wO5Xe1NWpcZIG3cEHc/SuOCDRqZDSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.48.0.tgz", + "integrity": "sha512-p3kSloztK7GRO7FyO3u38UCjZxQTl92VaLDsMQAq0eGoiNmeeEF1KPeE4+Fr+LSkQhF8WvJKSuls6TwOlurdPA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.48.0.tgz", + "integrity": "sha512-uWM+wiTqLW/V0ZmY/eyTWs8ykhIkzU+K2tz/8m35YepYEzohiUGRbnkpAFXj2ioXpQL+GUe5vmM3SLH6ozlfFw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.48.0.tgz", + "integrity": "sha512-OhQNPjs/OICaYqxYJjKKMaIY7p3nJ9IirXcFoHKD+CQE1BZFCeUUAknMzUeLclDCfudH9Vb/UgjFm8+ZM5puAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.48.0.tgz", + "integrity": "sha512-adu5txuwGvQ4C4fjYHJD+vnY+OCwCixBzn7J3KF3iWlVHBBImcosSv+Ye+fbMMJui4HGjifNXzonjKm9pXmOiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.48.0.tgz", + "integrity": "sha512-inlQQRUnHCny/7b7wA6NjEoJSSZPNea4qnDhWyeqBYWx8ukf2kzNDSiamfhOw6bfAYPm/PVlkVRYaNXQbkLeTQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.48.0.tgz", + "integrity": "sha512-YiJx6sW6bYebQDZRVWLKm/Drswx/hcjIgbLIhULSn0rRcBKc7d9V6mkqPjKDbhcxJgQD5Zi0yVccJiOdF40AWA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.48.0.tgz", + "integrity": "sha512-zwSqxMgmb2ITamNfDv9Q9EKBc/4ZhCBP9gkg2hhcgR6sEVGPUDl1AKPC89CBKMxkmPUi3685C38EvqtZn5OtHw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.48.0.tgz", + "integrity": "sha512-c/+2oUWAOsQB5JTem0rW8ODlZllF6pAtGSGXoLSvPTonKI1vAwaKhD9Qw1X36jRbcI3Etkpu/9z/RRjMba8vFQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.48.0.tgz", + "integrity": "sha512-PhauDqeFW5DGed6QxCY5lXZYKSlcBdCXJnH03ZNU6QmDZ0BFM/zSy1oPT2MNb1Afx1G6yOOVk8ErjWsQ7c59ng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.48.0.tgz", + "integrity": "sha512-6d7LIFFZGiavbHndhf1cK9kG9qmy2Dmr37sV9Ep7j3H+ciFdKSuOzdLh85mEUYMih+b+esMDlF5DU0WQRZPQjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.48.0.tgz", + "integrity": "sha512-r+0KK9lK6vFp3tXAgDMOW32o12dxvKS3B9La1uYMGdWAMoSeu2RzG34KmzSpXu6MyLDl4aSVyZLFM8KGdEjwaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.48.0.tgz", + "integrity": "sha512-Nkw/MocyT3HSp0OJsKPXrcbxZqSPMTYnLLfsqsoiFKoL1ppVNL65MFa7vuTxJehPlBkjy+95gUgacZtuNMECrg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.48.0.tgz", + "integrity": "sha512-reO1SpefvRmeZSP+WeyWkQd1ArxxDD1MyKgMUKuB8lNuUoxk9QEohYtKnsfsxJuFwMT0JTr7p9wZjouA85GzGQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.48.0.tgz", + "integrity": "sha512-T6zwhfcsrorqAybkOglZdPkTLlEwipbtdO1qjE+flbawvwOMsISoyiuaa7vM7zEyfq1hmDvMq1ndvkYFioranA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.2", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", @@ -1431,6 +1863,13 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@tailwindcss/node": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", @@ -1774,6 +2213,24 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1781,13 +2238,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { "version": "24.10.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.10.tgz", @@ -1820,276 +2270,6 @@ "@types/react": "^19.2.0" } }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", - "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/type-utils": "8.54.0", - "@typescript-eslint/utils": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.54.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", - "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", - "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.54.0", - "@typescript-eslint/types": "^8.54.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", - "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", - "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", - "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/utils": "8.54.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", - "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", - "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.54.0", - "@typescript-eslint/tsconfig-utils": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", - "debug": "^4.4.3", - "minimatch": "^9.0.5", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", - "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", - "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.54.0", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@vitejs/plugin-react": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.3.tgz", @@ -2111,76 +2291,126 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", "dev": true, "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" }, - "engines": { - "node": ">=0.4.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", "dev": true, "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "tinyrainbow": "^3.0.3" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://opencollective.com/vitest" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://opencollective.com/vitest" } }, - "node_modules/argparse": { + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, - "license": "Python-2.0" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12" + } }, "node_modules/baseline-browser-mapping": { "version": "2.9.19", @@ -2192,17 +2422,6 @@ "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -2238,16 +2457,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001767", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", @@ -2269,50 +2478,16 @@ ], "license": "CC-BY-4.0" }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=18" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2333,21 +2508,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2373,13 +2533,6 @@ } } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2411,6 +2564,13 @@ "node": ">=10.13.0" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -2463,241 +2623,26 @@ "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-prettier": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", - "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "funding": { - "url": "https://opencollective.com/eslint-config-prettier" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "@types/estree": "^1.0.0" } }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.26", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", - "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=12.0.0" } }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2716,57 +2661,6 @@ } } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2792,32 +2686,6 @@ "node": ">=6.9.0" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2825,100 +2693,6 @@ "dev": true, "license": "ISC" }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -2936,19 +2710,6 @@ "dev": true, "license": "MIT" }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2962,27 +2723,6 @@ "node": ">=6" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2996,30 +2736,6 @@ "node": ">=6" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/lightningcss": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", @@ -3281,29 +2997,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3324,19 +3017,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3363,13 +3043,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -3377,88 +3050,108 @@ "dev": true, "license": "MIT" }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/oxfmt": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.33.0.tgz", + "integrity": "sha512-ogxBXA9R4BFeo8F1HeMIIxHr5kGnQwKTYZ5k131AEGOq1zLxInNhvYSpyRQ+xIXVMYfCN7yZHKff/lb5lp4auQ==", "dev": true, "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "tinypool": "2.1.0" + }, + "bin": { + "oxfmt": "bin/oxfmt" }, "engines": { - "node": ">=10" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxfmt/binding-android-arm-eabi": "0.33.0", + "@oxfmt/binding-android-arm64": "0.33.0", + "@oxfmt/binding-darwin-arm64": "0.33.0", + "@oxfmt/binding-darwin-x64": "0.33.0", + "@oxfmt/binding-freebsd-x64": "0.33.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.33.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.33.0", + "@oxfmt/binding-linux-arm64-gnu": "0.33.0", + "@oxfmt/binding-linux-arm64-musl": "0.33.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.33.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.33.0", + "@oxfmt/binding-linux-riscv64-musl": "0.33.0", + "@oxfmt/binding-linux-s390x-gnu": "0.33.0", + "@oxfmt/binding-linux-x64-gnu": "0.33.0", + "@oxfmt/binding-linux-x64-musl": "0.33.0", + "@oxfmt/binding-openharmony-arm64": "0.33.0", + "@oxfmt/binding-win32-arm64-msvc": "0.33.0", + "@oxfmt/binding-win32-ia32-msvc": "0.33.0", + "@oxfmt/binding-win32-x64-msvc": "0.33.0" } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/oxlint": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.48.0.tgz", + "integrity": "sha512-m5vyVBgPtPhVCJc3xI//8je9lRc8bYuYB4R/1PH3VPGOjA4vjVhkHtyJukdEjYEjwrw4Qf1eIf+pP9xvfhfMow==", "dev": true, "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" + "bin": { + "oxlint": "bin/oxlint" }, "engines": { - "node": ">=10" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" + "url": "https://github.com/sponsors/Boshen" }, - "engines": { - "node": ">=6" + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.48.0", + "@oxlint/binding-android-arm64": "1.48.0", + "@oxlint/binding-darwin-arm64": "1.48.0", + "@oxlint/binding-darwin-x64": "1.48.0", + "@oxlint/binding-freebsd-x64": "1.48.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.48.0", + "@oxlint/binding-linux-arm-musleabihf": "1.48.0", + "@oxlint/binding-linux-arm64-gnu": "1.48.0", + "@oxlint/binding-linux-arm64-musl": "1.48.0", + "@oxlint/binding-linux-ppc64-gnu": "1.48.0", + "@oxlint/binding-linux-riscv64-gnu": "1.48.0", + "@oxlint/binding-linux-riscv64-musl": "1.48.0", + "@oxlint/binding-linux-s390x-gnu": "1.48.0", + "@oxlint/binding-linux-x64-gnu": "1.48.0", + "@oxlint/binding-linux-x64-musl": "1.48.0", + "@oxlint/binding-openharmony-arm64": "1.48.0", + "@oxlint/binding-win32-arm64-msvc": "1.48.0", + "@oxlint/binding-win32-ia32-msvc": "1.48.0", + "@oxlint/binding-win32-x64-msvc": "1.48.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=0.12.2" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + } } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", @@ -3510,42 +3203,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -3617,16 +3274,6 @@ "react-dom": ">=18" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/rollup": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", @@ -3694,28 +3341,12 @@ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, - "node_modules/shebang-command": { + "node_modules/siginfo": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "ISC" }, "node_modules/sonner": { "version": "2.0.7", @@ -3737,31 +3368,19 @@ "node": ">=0.10.0" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } + "license": "MIT" }, "node_modules/tailwindcss": { "version": "4.1.18", @@ -3784,6 +3403,23 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3801,17 +3437,24 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "node_modules/tinypool": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", + "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", "dev": true, "license": "MIT", "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" + "node": "^20.0.0 || >=22.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" } }, "node_modules/tslib": { @@ -3820,26 +3463,12 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3848,30 +3477,6 @@ "node": ">=14.17" } }, - "node_modules/typescript-eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", - "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.54.0", - "@typescript-eslint/parser": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/utils": "8.54.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -3910,16 +3515,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -3996,30 +3591,99 @@ } } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, "engines": { - "node": ">=0.10.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" } }, "node_modules/yallist": { @@ -4028,43 +3692,6 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } } } } diff --git a/frontend/package.json b/frontend/package.json index e64fb30..438d952 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,35 +6,34 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview" + "lint": "oxlint src/", + "format": "oxfmt --write src/", + "format:check": "oxfmt --check src/", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@tanstack/react-query": "^5.90.20", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "react-router-dom": "^7.13.0", - "sonner": "^2.0.7" + "@dnd-kit/core": "6.3.1", + "@dnd-kit/sortable": "10.0.0", + "@dnd-kit/utilities": "3.2.2", + "@tanstack/react-query": "5.90.20", + "react": "19.2.4", + "react-dom": "19.2.4", + "react-router-dom": "7.13.0", + "sonner": "2.0.7" }, "devDependencies": { - "@eslint/js": "^9.39.1", - "@tailwindcss/vite": "^4.1.18", - "@types/node": "^24.10.1", - "@types/react": "^19.2.5", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", - "eslint": "^9.39.1", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", - "prettier": "^3.8.1", - "tailwindcss": "^4.1.18", - "typescript": "~5.9.3", - "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" + "@tailwindcss/vite": "4.1.18", + "@types/node": "24.10.10", + "@types/react": "19.2.11", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "5.1.3", + "oxfmt": "0.33.0", + "oxlint": "1.48.0", + "tailwindcss": "4.1.18", + "typescript": "5.9.3", + "vite": "7.3.1", + "vitest": "4.0.18" } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ef8b805..bfa6900 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -42,18 +42,12 @@ function App() { } /> } /> } /> - } - /> + } /> } /> } /> } /> } /> - } - /> + } /> diff --git a/frontend/src/api/admin.ts b/frontend/src/api/admin.ts index a3bb289..84dac49 100644 --- a/frontend/src/api/admin.ts +++ b/frontend/src/api/admin.ts @@ -30,11 +30,9 @@ import type { import type { Genlocke } from '../types/game' // Games -export const createGame = (data: CreateGameInput) => - api.post('/games', data) +export const createGame = (data: CreateGameInput) => api.post('/games', data) -export const updateGame = (id: number, data: UpdateGameInput) => - api.put(`/games/${id}`, data) +export const updateGame = (id: number, data: UpdateGameInput) => api.put(`/games/${id}`, data) export const deleteGame = (id: number) => api.del(`/games/${id}`) @@ -42,11 +40,8 @@ export const deleteGame = (id: number) => api.del(`/games/${id}`) export const createRoute = (gameId: number, data: CreateRouteInput) => api.post(`/games/${gameId}/routes`, data) -export const updateRoute = ( - gameId: number, - routeId: number, - data: UpdateRouteInput -) => api.put(`/games/${gameId}/routes/${routeId}`, data) +export const updateRoute = (gameId: number, routeId: number, data: UpdateRouteInput) => + api.put(`/games/${gameId}/routes/${routeId}`, data) export const deleteRoute = (gameId: number, routeId: number) => api.del(`/games/${gameId}/routes/${routeId}`) @@ -55,12 +50,7 @@ export const reorderRoutes = (gameId: number, routes: RouteReorderItem[]) => api.put(`/games/${gameId}/routes/reorder`, { routes }) // Pokemon -export const listPokemon = ( - search?: string, - limit = 50, - offset = 0, - type?: string -) => { +export const listPokemon = (search?: string, limit = 50, offset = 0, type?: string) => { const params = new URLSearchParams() if (search) params.set('search', search) if (type) params.set('type', type) @@ -69,8 +59,7 @@ export const listPokemon = ( return api.get(`/pokemon?${params}`) } -export const createPokemon = (data: CreatePokemonInput) => - api.post('/pokemon', data) +export const createPokemon = (data: CreatePokemonInput) => api.post('/pokemon', data) export const updatePokemon = (id: number, data: UpdatePokemonInput) => api.put(`/pokemon/${id}`, data) @@ -97,12 +86,7 @@ export const bulkImportBosses = (gameId: number, items: unknown[]) => api.post(`/games/${gameId}/bosses/bulk-import`, items) // Evolutions -export const listEvolutions = ( - search?: string, - limit = 50, - offset = 0, - trigger?: string -) => { +export const listEvolutions = (search?: string, limit = 50, offset = 0, trigger?: string) => { const params = new URLSearchParams() if (search) params.set('search', search) if (trigger) params.set('trigger', trigger) @@ -120,8 +104,7 @@ export const updateEvolution = (id: number, data: UpdateEvolutionInput) => export const deleteEvolution = (id: number) => api.del(`/evolutions/${id}`) // Export -export const exportGames = () => - api.get[]>('/export/games') +export const exportGames = () => api.get[]>('/export/games') export const exportGameRoutes = (gameId: number) => api.get<{ filename: string; data: unknown }>(`/export/games/${gameId}/routes`) @@ -129,27 +112,19 @@ export const exportGameRoutes = (gameId: number) => export const exportGameBosses = (gameId: number) => api.get<{ filename: string; data: unknown }>(`/export/games/${gameId}/bosses`) -export const exportPokemon = () => - api.get[]>('/export/pokemon') +export const exportPokemon = () => api.get[]>('/export/pokemon') -export const exportEvolutions = () => - api.get[]>('/export/evolutions') +export const exportEvolutions = () => api.get[]>('/export/evolutions') // Route Encounters -export const addRouteEncounter = ( - routeId: number, - data: CreateRouteEncounterInput -) => api.post(`/routes/${routeId}/pokemon`, data) +export const addRouteEncounter = (routeId: number, data: CreateRouteEncounterInput) => + api.post(`/routes/${routeId}/pokemon`, data) export const updateRouteEncounter = ( routeId: number, encounterId: number, data: UpdateRouteEncounterInput -) => - api.put( - `/routes/${routeId}/pokemon/${encounterId}`, - data - ) +) => api.put(`/routes/${routeId}/pokemon/${encounterId}`, data) export const removeRouteEncounter = (routeId: number, encounterId: number) => api.del(`/routes/${routeId}/pokemon/${encounterId}`) @@ -158,11 +133,8 @@ export const removeRouteEncounter = (routeId: number, encounterId: number) => export const createBossBattle = (gameId: number, data: CreateBossBattleInput) => api.post(`/games/${gameId}/bosses`, data) -export const updateBossBattle = ( - gameId: number, - bossId: number, - data: UpdateBossBattleInput -) => api.put(`/games/${gameId}/bosses/${bossId}`, data) +export const updateBossBattle = (gameId: number, bossId: number, data: UpdateBossBattleInput) => + api.put(`/games/${gameId}/bosses/${bossId}`, data) export const deleteBossBattle = (gameId: number, bossId: number) => api.del(`/games/${gameId}/bosses/${bossId}`) @@ -170,11 +142,8 @@ export const deleteBossBattle = (gameId: number, bossId: number) => export const reorderBosses = (gameId: number, bosses: BossReorderItem[]) => api.put(`/games/${gameId}/bosses/reorder`, { bosses }) -export const setBossTeam = ( - gameId: number, - bossId: number, - team: BossPokemonInput[] -) => api.put(`/games/${gameId}/bosses/${bossId}/pokemon`, team) +export const setBossTeam = (gameId: number, bossId: number, team: BossPokemonInput[]) => + api.put(`/games/${gameId}/bosses/${bossId}/pokemon`, team) // Genlockes export const updateGenlocke = (id: number, data: UpdateGenlockeInput) => diff --git a/frontend/src/api/bosses.ts b/frontend/src/api/bosses.ts index 0960fc3..c212ea9 100644 --- a/frontend/src/api/bosses.ts +++ b/frontend/src/api/bosses.ts @@ -1,14 +1,7 @@ import { api } from './client' -import type { - BossBattle, - BossResult, - CreateBossResultInput, -} from '../types/game' +import type { BossBattle, BossResult, CreateBossResultInput } from '../types/game' -export function getGameBosses( - gameId: number, - all?: boolean -): Promise { +export function getGameBosses(gameId: number, all?: boolean): Promise { const params = all ? '?all=true' : '' return api.get(`/games/${gameId}/bosses${params}`) } @@ -17,16 +10,10 @@ export function getBossResults(runId: number): Promise { return api.get(`/runs/${runId}/boss-results`) } -export function createBossResult( - runId: number, - data: CreateBossResultInput -): Promise { +export function createBossResult(runId: number, data: CreateBossResultInput): Promise { return api.post(`/runs/${runId}/boss-results`, data) } -export function deleteBossResult( - runId: number, - resultId: number -): Promise { +export function deleteBossResult(runId: number, resultId: number): Promise { return api.del(`/runs/${runId}/boss-results/${resultId}`) } diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index ec6424b..9974ad2 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1,4 +1,4 @@ -const API_BASE = import.meta.env.VITE_API_URL ?? '' +const API_BASE = import.meta.env['VITE_API_URL'] ?? '' export class ApiError extends Error { status: number diff --git a/frontend/src/api/encounters.ts b/frontend/src/api/encounters.ts index 3ffffda..5da8a6f 100644 --- a/frontend/src/api/encounters.ts +++ b/frontend/src/api/encounters.ts @@ -14,10 +14,7 @@ export function createEncounter( return api.post(`/runs/${runId}/encounters`, data) } -export function updateEncounter( - id: number, - data: UpdateEncounterInput -): Promise { +export function updateEncounter(id: number, data: UpdateEncounterInput): Promise { return api.patch(`/encounters/${id}`, data) } @@ -25,10 +22,7 @@ export function deleteEncounter(id: number): Promise { return api.del(`/encounters/${id}`) } -export function fetchEvolutions( - pokemonId: number, - region?: string -): Promise { +export function fetchEvolutions(pokemonId: number, region?: string): Promise { const params = region ? `?region=${encodeURIComponent(region)}` : '' return api.get(`/pokemon/${pokemonId}/evolutions${params}`) } diff --git a/frontend/src/api/games.ts b/frontend/src/api/games.ts index f1394ef..a092816 100644 --- a/frontend/src/api/games.ts +++ b/frontend/src/api/games.ts @@ -19,10 +19,7 @@ export function getGameRoutes(gameId: number): Promise { return api.get(`/games/${gameId}/routes?flat=true`) } -export function getRoutePokemon( - routeId: number, - gameId?: number -): Promise { +export function getRoutePokemon(routeId: number, gameId?: number): Promise { const params = gameId != null ? `?game_id=${gameId}` : '' return api.get(`/routes/${routeId}/pokemon${params}`) } diff --git a/frontend/src/api/genlockes.ts b/frontend/src/api/genlockes.ts index be5c42b..0885340 100644 --- a/frontend/src/api/genlockes.ts +++ b/frontend/src/api/genlockes.ts @@ -47,8 +47,5 @@ export function advanceLeg( legOrder: number, data?: AdvanceLegInput ): Promise { - return api.post( - `/genlockes/${genlockeId}/legs/${legOrder}/advance`, - data ?? {} - ) + return api.post(`/genlockes/${genlockeId}/legs/${legOrder}/advance`, data ?? {}) } diff --git a/frontend/src/api/pokemon.ts b/frontend/src/api/pokemon.ts index 17f85fb..04defb9 100644 --- a/frontend/src/api/pokemon.ts +++ b/frontend/src/api/pokemon.ts @@ -16,8 +16,6 @@ export function fetchPokemonEncounterLocations( return api.get(`/pokemon/${pokemonId}/encounter-locations`) } -export function fetchPokemonEvolutionChain( - pokemonId: number -): Promise { +export function fetchPokemonEvolutionChain(pokemonId: number): Promise { return api.get(`/pokemon/${pokemonId}/evolution-chain`) } diff --git a/frontend/src/api/runs.ts b/frontend/src/api/runs.ts index 1dbe336..8c67e74 100644 --- a/frontend/src/api/runs.ts +++ b/frontend/src/api/runs.ts @@ -1,10 +1,5 @@ import { api } from './client' -import type { - NuzlockeRun, - RunDetail, - CreateRunInput, - UpdateRunInput, -} from '../types/game' +import type { NuzlockeRun, RunDetail, CreateRunInput, UpdateRunInput } from '../types/game' export function getRuns(): Promise { return api.get('/runs') @@ -18,10 +13,7 @@ export function createRun(data: CreateRunInput): Promise { return api.post('/runs', data) } -export function updateRun( - id: number, - data: UpdateRunInput -): Promise { +export function updateRun(id: number, data: UpdateRunInput): Promise { return api.patch(`/runs/${id}`, data) } diff --git a/frontend/src/components/BossDefeatModal.tsx b/frontend/src/components/BossDefeatModal.tsx index e53bf84..6765d41 100644 --- a/frontend/src/components/BossDefeatModal.tsx +++ b/frontend/src/components/BossDefeatModal.tsx @@ -10,14 +10,11 @@ interface BossDefeatModalProps { starterName?: string | null } -function matchVariant( - labels: string[], - starterName?: string | null -): string | null { +function matchVariant(labels: string[], starterName?: string | null): string | null { if (!starterName || labels.length === 0) return null const lower = starterName.toLowerCase() const matches = labels.filter((l) => l.toLowerCase().includes(lower)) - return matches.length === 1 ? matches[0] : null + return matches.length === 1 ? (matches[0] ?? null) : null } export function BossDefeatModal({ @@ -46,14 +43,13 @@ export function BossDefeatModal({ ) const showPills = hasVariants && autoMatch === null const [selectedVariant, setSelectedVariant] = useState( - autoMatch ?? (hasVariants ? variantLabels[0] : null) + autoMatch ?? (hasVariants ? (variantLabels[0] ?? null) : null) ) const displayedPokemon = useMemo(() => { if (!hasVariants) return boss.pokemon return boss.pokemon.filter( - (bp) => - bp.conditionLabel === selectedVariant || bp.conditionLabel === null + (bp) => bp.conditionLabel === selectedVariant || bp.conditionLabel === null ) }, [boss.pokemon, hasVariants, selectedVariant]) @@ -72,9 +68,7 @@ export function BossDefeatModal({ Battle: {boss.name} - - {boss.location} - + {boss.location} {/* Boss team preview */} @@ -104,11 +98,7 @@ export function BossDefeatModal({ .map((bp) => ( {bp.pokemon.spriteUrl ? ( - + ) : ( )} @@ -158,9 +148,7 @@ export function BossDefeatModal({ {!hardcoreMode && ( - - Attempts - + Attempts void onClose: () => void @@ -87,12 +87,7 @@ export function EggEncounterModal({ onClick={onClose} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200" > - + ) : ( - {selectedPokemon.name[0].toUpperCase()} + {selectedPokemon.name[0]?.toUpperCase()} )} @@ -183,14 +178,10 @@ export function EggEncounterModal({ className="flex flex-col items-center p-2 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-green-400 dark:hover:border-green-600 text-center transition-colors" > {p.spriteUrl ? ( - + ) : ( - {p.name[0].toUpperCase()} + {p.name[0]?.toUpperCase()} )} @@ -200,13 +191,9 @@ export function EggEncounterModal({ ))} )} - {search.length >= 2 && - !isSearching && - searchResults.length === 0 && ( - - No pokemon found - - )} + {search.length >= 2 && !isSearching && searchResults.length === 0 && ( + No pokemon found + )} > )} diff --git a/frontend/src/components/EncounterMethodBadge.tsx b/frontend/src/components/EncounterMethodBadge.tsx index f436e33..44986d4 100644 --- a/frontend/src/components/EncounterMethodBadge.tsx +++ b/frontend/src/components/EncounterMethodBadge.tsx @@ -1,8 +1,7 @@ export const METHOD_CONFIG: Record = { starter: { label: 'Starter', - color: - 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/40 dark:text-yellow-300', + color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/40 dark:text-yellow-300', }, gift: { label: 'Gift', @@ -10,18 +9,15 @@ export const METHOD_CONFIG: Record = { }, fossil: { label: 'Fossil', - color: - 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300', + color: 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300', }, trade: { label: 'Trade', - color: - 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300', + color: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300', }, walk: { label: 'Grass', - color: - 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300', + color: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300', }, headbutt: { label: 'Headbutt', @@ -33,8 +29,7 @@ export const METHOD_CONFIG: Record = { }, 'rock-smash': { label: 'Rock Smash', - color: - 'bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-300', + color: 'bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-300', }, 'old-rod': { label: 'Old Rod', @@ -46,8 +41,7 @@ export const METHOD_CONFIG: Record = { }, 'super-rod': { label: 'Super Rod', - color: - 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/40 dark:text-indigo-300', + color: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/40 dark:text-indigo-300', }, } @@ -75,8 +69,7 @@ export function getMethodLabel(method: string): string { export function getMethodColor(method: string): string { return ( - METHOD_CONFIG[method]?.color ?? - 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' + METHOD_CONFIG[method]?.color ?? 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' ) } @@ -89,12 +82,9 @@ export function EncounterMethodBadge({ }) { const config = METHOD_CONFIG[method] if (!config) return null - const sizeClass = - size === 'xs' ? 'text-[8px] px-1 py-0' : 'text-[9px] px-1.5 py-0.5' + const sizeClass = size === 'xs' ? 'text-[8px] px-1 py-0' : 'text-[9px] px-1.5 py-0.5' return ( - + {config.label} ) diff --git a/frontend/src/components/EncounterModal.tsx b/frontend/src/components/EncounterModal.tsx index f487837..d860c4d 100644 --- a/frontend/src/components/EncounterModal.tsx +++ b/frontend/src/components/EncounterModal.tsx @@ -1,43 +1,36 @@ import { useState, useEffect, useMemo } from 'react' import { useRoutePokemon } from '../hooks/useGames' import { useNameSuggestions } from '../hooks/useRuns' -import { - EncounterMethodBadge, - getMethodLabel, - METHOD_ORDER, -} from './EncounterMethodBadge' -import type { - Route, - EncounterDetail, - EncounterStatus, - RouteEncounterDetail, -} from '../types' +import { EncounterMethodBadge, getMethodLabel, METHOD_ORDER } from './EncounterMethodBadge' +import type { Route, EncounterDetail, EncounterStatus, RouteEncounterDetail } from '../types' interface EncounterModalProps { route: Route gameId: number runId: number - namingScheme?: string | null - isGenlocke?: boolean - existing?: EncounterDetail - dupedPokemonIds?: Set - retiredPokemonIds?: Set + namingScheme?: string | null | undefined + isGenlocke?: boolean | undefined + existing?: EncounterDetail | undefined + dupedPokemonIds?: Set | undefined + retiredPokemonIds?: Set | undefined onSubmit: (data: { routeId: number pokemonId: number - nickname?: string + nickname?: string | undefined status: EncounterStatus - catchLevel?: number - }) => void - onUpdate?: (data: { - id: number - data: { - nickname?: string - status?: EncounterStatus - faintLevel?: number - deathCause?: string - } + catchLevel?: number | undefined }) => void + onUpdate?: + | ((data: { + id: number + data: { + nickname?: string | undefined + status?: EncounterStatus | undefined + faintLevel?: number | undefined + deathCause?: string | undefined + } + }) => void) + | undefined onClose: () => void isPending: boolean } @@ -91,11 +84,9 @@ function pickRandomPokemon( pokemon: RouteEncounterDetail[], dupedIds?: Set ): RouteEncounterDetail | null { - const eligible = dupedIds - ? pokemon.filter((rp) => !dupedIds.has(rp.pokemonId)) - : pokemon + const eligible = dupedIds ? pokemon.filter((rp) => !dupedIds.has(rp.pokemonId)) : pokemon if (eligible.length === 0) return null - return eligible[Math.floor(Math.random() * eligible.length)] + return eligible[Math.floor(Math.random() * eligible.length)] ?? null } export function EncounterModal({ @@ -112,20 +103,12 @@ export function EncounterModal({ onClose, isPending, }: EncounterModalProps) { - const { data: routePokemon, isLoading: loadingPokemon } = useRoutePokemon( - route.id, - gameId - ) + const { data: routePokemon, isLoading: loadingPokemon } = useRoutePokemon(route.id, gameId) - const [selectedPokemon, setSelectedPokemon] = - useState(null) - const [status, setStatus] = useState( - existing?.status ?? 'caught' - ) + const [selectedPokemon, setSelectedPokemon] = useState(null) + const [status, setStatus] = useState(existing?.status ?? 'caught') const [nickname, setNickname] = useState(existing?.nickname ?? '') - const [catchLevel, setCatchLevel] = useState( - existing?.catchLevel?.toString() ?? '' - ) + const [catchLevel, setCatchLevel] = useState(existing?.catchLevel?.toString() ?? '') const [faintLevel, setFaintLevel] = useState('') const [deathCause, setDeathCause] = useState('') const [search, setSearch] = useState('') @@ -133,8 +116,7 @@ export function EncounterModal({ const isEditing = !!existing const showSuggestions = !!namingScheme && status === 'caught' && !isEditing - const lineagePokemonId = - isGenlocke && selectedPokemon ? selectedPokemon.pokemonId : null + const lineagePokemonId = isGenlocke && selectedPokemon ? selectedPokemon.pokemonId : null const { data: suggestions, refetch: regenerate, @@ -144,9 +126,7 @@ export function EncounterModal({ // Pre-select pokemon when editing useEffect(() => { if (existing && routePokemon) { - const match = routePokemon.find( - (rp) => rp.pokemonId === existing.pokemonId - ) + const match = routePokemon.find((rp) => rp.pokemonId === existing.pokemonId) if (match) setSelectedPokemon(match) } }, [existing, routePokemon]) @@ -198,12 +178,7 @@ export function EncounterModal({ onClick={onClose} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200" > - + - - {route.name} - + {route.name} @@ -233,16 +206,12 @@ export function EncounterModal({ loadingPokemon || !routePokemon || (dupedPokemonIds - ? routePokemon.every((rp) => - dupedPokemonIds.has(rp.pokemonId) - ) + ? routePokemon.every((rp) => dupedPokemonIds.has(rp.pokemonId)) : false) } onClick={() => { if (routePokemon) { - setSelectedPokemon( - pickRandomPokemon(routePokemon, dupedPokemonIds) - ) + setSelectedPokemon(pickRandomPokemon(routePokemon, dupedPokemonIds)) } }} className="px-2.5 py-1 text-xs font-medium rounded-lg border border-purple-300 dark:border-purple-600 text-purple-600 dark:text-purple-400 hover:bg-purple-50 dark:hover:bg-purple-900/20 disabled:opacity-40 disabled:cursor-not-allowed transition-colors" @@ -279,15 +248,12 @@ export function EncounterModal({ )} {pokemon.map((rp) => { - const isDuped = - dupedPokemonIds?.has(rp.pokemonId) ?? false + const isDuped = dupedPokemonIds?.has(rp.pokemonId) ?? false return ( - !isDuped && setSelectedPokemon(rp) - } + onClick={() => !isDuped && setSelectedPokemon(rp)} disabled={isDuped} className={`flex flex-col items-center p-2 rounded-lg border text-center transition-colors ${ isDuped @@ -305,7 +271,7 @@ export function EncounterModal({ /> ) : ( - {rp.pokemon.name[0].toUpperCase()} + {rp.pokemon.name[0]?.toUpperCase()} )} @@ -318,19 +284,13 @@ export function EncounterModal({ : 'already caught'} )} - {!isDuped && - SPECIAL_METHODS.includes( - rp.encounterMethod - ) && ( - - )} + {!isDuped && SPECIAL_METHODS.includes(rp.encounterMethod) && ( + + )} {!isDuped && ( Lv. {rp.minLevel} - {rp.maxLevel !== rp.minLevel && - `–${rp.maxLevel}`} + {rp.maxLevel !== rp.minLevel && `–${rp.maxLevel}`} )} @@ -360,7 +320,7 @@ export function EncounterModal({ /> ) : ( - {existing.pokemon.name[0].toUpperCase()} + {existing.pokemon.name[0]?.toUpperCase()} )} @@ -477,53 +437,45 @@ export function EncounterModal({ )} {/* Faint Level + Death Cause (only when editing a caught pokemon to mark dead) */} - {isEditing && - existing?.status === 'caught' && - existing?.faintLevel === null && ( - <> - - - Faint Level{' '} - - (mark as dead) - - - setFaintLevel(e.target.value)} - placeholder="Leave empty if still alive" - className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" - /> - - - - Cause of Death{' '} - - (optional) - - - setDeathCause(e.target.value)} - placeholder="e.g. Crit from rival's Charizard" - className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" - /> - - > - )} + {isEditing && existing?.status === 'caught' && existing?.faintLevel === null && ( + <> + + + Faint Level (mark as dead) + + setFaintLevel(e.target.value)} + placeholder="Leave empty if still alive" + className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + + + + Cause of Death (optional) + + setDeathCause(e.target.value)} + placeholder="e.g. Crit from rival's Charizard" + className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + + > + )} diff --git a/frontend/src/components/EndRunModal.tsx b/frontend/src/components/EndRunModal.tsx index 4dc79eb..2059e94 100644 --- a/frontend/src/components/EndRunModal.tsx +++ b/frontend/src/components/EndRunModal.tsx @@ -7,12 +7,7 @@ interface EndRunModalProps { genlockeContext?: RunGenlockeContext | null } -export function EndRunModal({ - onConfirm, - onClose, - isPending, - genlockeContext, -}: EndRunModalProps) { +export function EndRunModal({ onConfirm, onClose, isPending, genlockeContext }: EndRunModalProps) { const victoryDescription = genlockeContext ? genlockeContext.isFinalLeg ? 'Complete the final leg of your genlocke!' @@ -31,9 +26,7 @@ export function EndRunModal({ End Run - - How did your run end? - + How did your run end? onConfirm('completed')} diff --git a/frontend/src/components/GameCard.tsx b/frontend/src/components/GameCard.tsx index 22dc7de..5227654 100644 --- a/frontend/src/components/GameCard.tsx +++ b/frontend/src/components/GameCard.tsx @@ -32,27 +32,20 @@ export function GameCard({ game, selected, onSelect }: GameCardProps) { /> ) : ( - + {game.name.replace('Pokemon ', '')} )} - - {game.name} - + {game.name} {game.region.charAt(0).toUpperCase() + game.region.slice(1)} {game.releaseYear && ( - - {game.releaseYear} - + {game.releaseYear} )} @@ -65,11 +58,7 @@ export function GameCard({ game, selected, onSelect }: GameCardProps) { stroke="currentColor" strokeWidth={3} > - + )} diff --git a/frontend/src/components/GameGrid.tsx b/frontend/src/components/GameGrid.tsx index 75f81f5..35056f9 100644 --- a/frontend/src/components/GameGrid.tsx +++ b/frontend/src/components/GameGrid.tsx @@ -18,7 +18,7 @@ interface GameGridProps { games: Game[] selectedId: number | null onSelect: (game: Game) => void - runs?: NuzlockeRun[] + runs?: NuzlockeRun[] | undefined } export function GameGrid({ games, selectedId, onSelect, runs }: GameGridProps) { @@ -27,38 +27,26 @@ export function GameGrid({ games, selectedId, onSelect, runs }: GameGridProps) { const [hideWithActiveRun, setHideWithActiveRun] = useState(false) const [hideCompleted, setHideCompleted] = useState(false) - const generations = useMemo( - () => [...new Set(games.map((g) => g.generation))].sort(), - [games] - ) + const generations = useMemo(() => [...new Set(games.map((g) => g.generation))].sort(), [games]) - const regions = useMemo( - () => [...new Set(games.map((g) => g.region))].sort(), - [games] - ) + const regions = useMemo(() => [...new Set(games.map((g) => g.region))].sort(), [games]) const activeRunGameIds = useMemo(() => { if (!runs) return new Set() - return new Set( - runs.filter((r) => r.status === 'active').map((r) => r.gameId) - ) + return new Set(runs.filter((r) => r.status === 'active').map((r) => r.gameId)) }, [runs]) const completedRunGameIds = useMemo(() => { if (!runs) return new Set() - return new Set( - runs.filter((r) => r.status === 'completed').map((r) => r.gameId) - ) + return new Set(runs.filter((r) => r.status === 'completed').map((r) => r.gameId)) }, [runs]) const filtered = useMemo(() => { let result = games if (filter) result = result.filter((g) => g.generation === filter) if (regionFilter) result = result.filter((g) => g.region === regionFilter) - if (hideWithActiveRun) - result = result.filter((g) => !activeRunGameIds.has(g.id)) - if (hideCompleted) - result = result.filter((g) => !completedRunGameIds.has(g.id)) + if (hideWithActiveRun) result = result.filter((g) => !activeRunGameIds.has(g.id)) + if (hideCompleted) result = result.filter((g) => !completedRunGameIds.has(g.id)) return result }, [ games, @@ -91,9 +79,7 @@ export function GameGrid({ games, selectedId, onSelect, runs }: GameGridProps) { - - Gen: - + Gen: setFilter(null)} @@ -114,9 +100,7 @@ export function GameGrid({ games, selectedId, onSelect, runs }: GameGridProps) { - - Region: - + Region: setRegionFilter(null)} diff --git a/frontend/src/components/GenlockeGraveyard.tsx b/frontend/src/components/GenlockeGraveyard.tsx index 113aedd..68eb273 100644 --- a/frontend/src/components/GenlockeGraveyard.tsx +++ b/frontend/src/components/GenlockeGraveyard.tsx @@ -24,7 +24,7 @@ function GraveyardCard({ entry }: { entry: GraveyardEntry }) { /> ) : ( - {displayPokemon.name[0].toUpperCase()} + {displayPokemon.name[0]?.toUpperCase()} )} @@ -35,9 +35,7 @@ function GraveyardCard({ entry }: { entry: GraveyardEntry }) { {entry.nickname && ( - - {displayPokemon.name} - + {displayPokemon.name} )} @@ -50,9 +48,7 @@ function GraveyardCard({ entry }: { entry: GraveyardEntry }) { Lv. {entry.catchLevel} → {entry.faintLevel} - - {entry.routeName} - + {entry.routeName} Leg {entry.legOrder} — {entry.gameName} @@ -134,8 +130,8 @@ export function GenlockeGraveyard({ genlockeId }: GenlockeGraveyardProps) { {data.deadliestLeg && ( - Deadliest: Leg {data.deadliestLeg.legOrder} —{' '} - {data.deadliestLeg.gameName} ({data.deadliestLeg.deathCount}) + Deadliest: Leg {data.deadliestLeg.legOrder} — {data.deadliestLeg.gameName} ( + {data.deadliestLeg.deathCount}) )} @@ -144,9 +140,7 @@ export function GenlockeGraveyard({ genlockeId }: GenlockeGraveyardProps) { - setFilterLeg(e.target.value ? Number(e.target.value) : null) - } + onChange={(e) => setFilterLeg(e.target.value ? Number(e.target.value) : null)} className="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100" > All Legs diff --git a/frontend/src/components/GenlockeLineage.tsx b/frontend/src/components/GenlockeLineage.tsx index cf600d8..8a5a7e5 100644 --- a/frontend/src/components/GenlockeLineage.tsx +++ b/frontend/src/components/GenlockeLineage.tsx @@ -38,21 +38,13 @@ function LegDot({ leg }: { leg: LineageLegEntry }) { {leg.gameName} {displayPokemon.spriteUrl && ( - + )} {displayPokemon.name} {leg.catchLevel !== null && Caught Lv. {leg.catchLevel}} - {leg.faintLevel !== null && ( - Died Lv. {leg.faintLevel} - )} - {leg.deathCause && ( - {leg.deathCause} - )} + {leg.faintLevel !== null && Died Lv. {leg.faintLevel}} + {leg.deathCause && {leg.deathCause}} [l.legOrder, l])) - const minLeg = lineage.legs[0].legOrder - const maxLeg = lineage.legs[lineage.legs.length - 1].legOrder + const minLeg = lineage.legs[0]!.legOrder + const maxLeg = lineage.legs[lineage.legs.length - 1]!.legOrder return ( { const leg = legMap.get(legOrder) const inRange = legOrder >= minLeg && legOrder <= maxLeg - const showLeftLine = inRange && i > 0 && allLegOrders[i - 1] >= minLeg + const showLeftLine = inRange && i > 0 && (allLegOrders[i - 1] ?? 0) >= minLeg const showRightLine = - inRange && - i < allLegOrders.length - 1 && - allLegOrders[i + 1] <= maxLeg + inRange && i < allLegOrders.length - 1 && (allLegOrders[i + 1] ?? 0) <= maxLeg return ( - + {/* Left half connector */} {showLeftLine && ( @@ -132,14 +118,8 @@ function TimelineGrid({ ) } -function LineageCard({ - lineage, - allLegOrders, -}: { - lineage: LineageEntry - allLegOrders: number[] -}) { - const firstLeg = lineage.legs[0] +function LineageCard({ lineage, allLegOrders }: { lineage: LineageEntry; allLegOrders: number[] }) { + const firstLeg = lineage.legs[0]! const displayPokemon = firstLeg.currentPokemon ?? firstLeg.pokemon return ( @@ -155,7 +135,7 @@ function LineageCard({ /> ) : ( - {displayPokemon.name[0].toUpperCase()} + {displayPokemon.name[0]?.toUpperCase()} )} @@ -194,11 +174,9 @@ export function GenlockeLineage({ genlockeId }: GenlockeLineageProps) { const allLegOrders = useMemo(() => { if (!data) return [] - return [ - ...new Set( - data.lineages.flatMap((l) => l.legs.map((leg) => leg.legOrder)) - ), - ].sort((a, b) => a - b) + return [...new Set(data.lineages.flatMap((l) => l.legs.map((leg) => leg.legOrder)))].sort( + (a, b) => a - b + ) }, [data]) const legGameNames = useMemo(() => { @@ -241,8 +219,8 @@ export function GenlockeLineage({ genlockeId }: GenlockeLineageProps) { {/* Summary bar */} - {data.totalLineages} lineage{data.totalLineages !== 1 ? 's' : ''}{' '} - across {allLegOrders.length} leg{allLegOrders.length !== 1 ? 's' : ''} + {data.totalLineages} lineage{data.totalLineages !== 1 ? 's' : ''} across{' '} + {allLegOrders.length} leg{allLegOrders.length !== 1 ? 's' : ''} @@ -276,7 +254,7 @@ export function GenlockeLineage({ genlockeId }: GenlockeLineageProps) { {data.lineages.map((lineage) => ( diff --git a/frontend/src/components/HofTeamModal.tsx b/frontend/src/components/HofTeamModal.tsx index 7e37cbb..c00eef7 100644 --- a/frontend/src/components/HofTeamModal.tsx +++ b/frontend/src/components/HofTeamModal.tsx @@ -8,12 +8,7 @@ interface HofTeamModalProps { isPending: boolean } -export function HofTeamModal({ - alive, - onSubmit, - onSkip, - isPending, -}: HofTeamModalProps) { +export function HofTeamModal({ alive, onSubmit, onSkip, isPending }: HofTeamModalProps) { const [selected, setSelected] = useState>(() => { // Pre-select all if 6 or fewer if (alive.length <= 6) return new Set(alive.map((e) => e.id)) @@ -74,16 +69,14 @@ export function HofTeamModal({ /> ) : ( - {displayPokemon.name[0].toUpperCase()} + {displayPokemon.name[0]?.toUpperCase()} )} {enc.nickname || displayPokemon.name} {enc.nickname && ( - - {displayPokemon.name} - + {displayPokemon.name} )} ) diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index c4967e3..372b8f7 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -55,12 +55,7 @@ export function Layout() { className="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700" aria-label="Toggle menu" > - + {menuOpen ? ( void + showFaintLevel?: boolean | undefined + onClick?: (() => void) | undefined } -export function PokemonCard({ - encounter, - showFaintLevel, - onClick, -}: PokemonCardProps) { - const { - pokemon, - currentPokemon, - route, - nickname, - catchLevel, - faintLevel, - deathCause, - } = encounter +export function PokemonCard({ encounter, showFaintLevel, onClick }: PokemonCardProps) { + const { pokemon, currentPokemon, route, nickname, catchLevel, faintLevel, deathCause } = encounter const isDead = faintLevel !== null const displayPokemon = currentPokemon ?? pokemon const isEvolved = currentPokemon !== null @@ -33,14 +21,10 @@ export function PokemonCard({ } ${onClick ? 'cursor-pointer hover:ring-2 hover:ring-blue-400 transition-shadow' : ''}`} > {displayPokemon.spriteUrl ? ( - + ) : ( - {displayPokemon.name[0].toUpperCase()} + {displayPokemon.name[0]?.toUpperCase()} )} @@ -53,9 +37,7 @@ export function PokemonCard({ {nickname && ( - - {displayPokemon.name} - + {displayPokemon.name} )} @@ -70,9 +52,7 @@ export function PokemonCard({ : `Lv. ${catchLevel ?? '?'}`} - - {route.name} - + {route.name} {isEvolved && ( diff --git a/frontend/src/components/RuleBadges.tsx b/frontend/src/components/RuleBadges.tsx index c5eab2a..f75bdab 100644 --- a/frontend/src/components/RuleBadges.tsx +++ b/frontend/src/components/RuleBadges.tsx @@ -9,11 +9,7 @@ export function RuleBadges({ rules }: RuleBadgesProps) { const enabledRules = RULE_DEFINITIONS.filter((def) => rules[def.key]) if (enabledRules.length === 0) { - return ( - - No rules enabled - - ) + return No rules enabled } return ( diff --git a/frontend/src/components/RuleToggle.tsx b/frontend/src/components/RuleToggle.tsx index e58da33..1e20f51 100644 --- a/frontend/src/components/RuleToggle.tsx +++ b/frontend/src/components/RuleToggle.tsx @@ -7,21 +7,14 @@ interface RuleToggleProps { onChange: (enabled: boolean) => void } -export function RuleToggle({ - name, - description, - enabled, - onChange, -}: RuleToggleProps) { +export function RuleToggle({ name, description, enabled, onChange }: RuleToggleProps) { const [showTooltip, setShowTooltip] = useState(false) return ( - - {name} - + {name} setShowTooltip(!showTooltip)} aria-label={`Info about ${name}`} > - + {showTooltip && ( - - {description} - + {description} )} void - onReset?: () => void - hiddenRules?: Set + onReset?: (() => void) | undefined + hiddenRules?: Set | undefined } export function RulesConfiguration({ @@ -19,12 +19,8 @@ export function RulesConfiguration({ ? RULE_DEFINITIONS.filter((r) => !hiddenRules.has(r.key)) : RULE_DEFINITIONS const coreRules = visibleRules.filter((r) => r.category === 'core') - const difficultyRules = visibleRules.filter( - (r) => r.category === 'difficulty' - ) - const completionRules = visibleRules.filter( - (r) => r.category === 'completion' - ) + const difficultyRules = visibleRules.filter((r) => r.category === 'difficulty') + const completionRules = visibleRules.filter((r) => r.category === 'completion') const handleRuleChange = (key: keyof NuzlockeRules, value: boolean) => { onChange({ ...rules, [key]: value }) @@ -60,9 +56,7 @@ export function RulesConfiguration({ - - Core Rules - + Core Rules The fundamental rules of a Nuzlocke challenge @@ -105,9 +99,7 @@ export function RulesConfiguration({ {completionRules.length > 0 && ( - - Completion - + Completion When is the run considered complete diff --git a/frontend/src/components/ShinyBox.tsx b/frontend/src/components/ShinyBox.tsx index fa62584..a418430 100644 --- a/frontend/src/components/ShinyBox.tsx +++ b/frontend/src/components/ShinyBox.tsx @@ -3,7 +3,7 @@ import type { EncounterDetail } from '../types' interface ShinyBoxProps { encounters: EncounterDetail[] - onEncounterClick?: (encounter: EncounterDetail) => void + onEncounterClick?: ((encounter: EncounterDetail) => void) | undefined } export function ShinyBox({ encounters, onEncounterClick }: ShinyBoxProps) { @@ -22,9 +22,7 @@ export function ShinyBox({ encounters, onEncounterClick }: ShinyBoxProps) { onEncounterClick(enc) : undefined - } + onClick={onEncounterClick ? () => onEncounterClick(enc) : undefined} /> ))} diff --git a/frontend/src/components/ShinyEncounterModal.tsx b/frontend/src/components/ShinyEncounterModal.tsx index 277c0c1..7582682 100644 --- a/frontend/src/components/ShinyEncounterModal.tsx +++ b/frontend/src/components/ShinyEncounterModal.tsx @@ -1,10 +1,6 @@ import { useState, useMemo } from 'react' import { useRoutePokemon } from '../hooks/useGames' -import { - EncounterMethodBadge, - getMethodLabel, - METHOD_ORDER, -} from './EncounterMethodBadge' +import { EncounterMethodBadge, getMethodLabel, METHOD_ORDER } from './EncounterMethodBadge' import type { Route, RouteEncounterDetail } from '../types' interface ShinyEncounterModalProps { @@ -13,9 +9,9 @@ interface ShinyEncounterModalProps { onSubmit: (data: { routeId: number pokemonId: number - nickname?: string + nickname?: string | undefined status: 'caught' - catchLevel?: number + catchLevel?: number | undefined isShiny: true }) => void onClose: () => void @@ -50,13 +46,9 @@ export function ShinyEncounterModal({ isPending, }: ShinyEncounterModalProps) { const [selectedRouteId, setSelectedRouteId] = useState(null) - const { data: routePokemon, isLoading: loadingPokemon } = useRoutePokemon( - selectedRouteId, - gameId - ) + const { data: routePokemon, isLoading: loadingPokemon } = useRoutePokemon(selectedRouteId, gameId) - const [selectedPokemon, setSelectedPokemon] = - useState(null) + const [selectedPokemon, setSelectedPokemon] = useState(null) const [nickname, setNickname] = useState('') const [catchLevel, setCatchLevel] = useState('') const [search, setSearch] = useState('') @@ -111,12 +103,7 @@ export function ShinyEncounterModal({ onClick={onClose} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200" > - + ) : ( - {rp.pokemon.name[0].toUpperCase()} + {rp.pokemon.name[0]?.toUpperCase()} )} {rp.pokemon.name} {SPECIAL_METHODS.includes(rp.encounterMethod) && ( - + )} Lv. {rp.minLevel} - {rp.maxLevel !== rp.minLevel && - `–${rp.maxLevel}`} + {rp.maxLevel !== rp.minLevel && `–${rp.maxLevel}`} ))} diff --git a/frontend/src/components/StatCard.tsx b/frontend/src/components/StatCard.tsx index b9b68ad..f799751 100644 --- a/frontend/src/components/StatCard.tsx +++ b/frontend/src/components/StatCard.tsx @@ -1,7 +1,7 @@ interface StatCardProps { label: string value: number - total?: number + total?: number | undefined color: string } @@ -22,10 +22,7 @@ export function StatCard({ label, value, total, color }: StatCardProps) { {value} {total !== undefined && ( - - {' '} - / {total} - + / {total} )} {label} diff --git a/frontend/src/components/StatusChangeModal.tsx b/frontend/src/components/StatusChangeModal.tsx index 1df882d..766489d 100644 --- a/frontend/src/components/StatusChangeModal.tsx +++ b/frontend/src/components/StatusChangeModal.tsx @@ -1,9 +1,5 @@ import { useState, useMemo } from 'react' -import type { - EncounterDetail, - UpdateEncounterInput, - CreateEncounterInput, -} from '../types' +import type { EncounterDetail, UpdateEncounterInput, CreateEncounterInput } from '../types' import { useEvolutions, useForms } from '../hooks/useEncounters' import { TypeBadge } from './TypeBadge' import { formatEvolutionMethod } from '../utils/formatEvolution' @@ -25,24 +21,14 @@ export function StatusChangeModal({ region, onCreateEncounter, }: StatusChangeModalProps) { - const { - pokemon, - currentPokemon, - route, - nickname, - catchLevel, - faintLevel, - deathCause, - } = encounter + const { pokemon, currentPokemon, route, nickname, catchLevel, faintLevel, deathCause } = encounter const isDead = faintLevel !== null const displayPokemon = currentPokemon ?? pokemon const [showConfirm, setShowConfirm] = useState(false) const [showEvolve, setShowEvolve] = useState(false) const [showFormChange, setShowFormChange] = useState(false) const [showShedConfirm, setShowShedConfirm] = useState(false) - const [pendingEvolutionId, setPendingEvolutionId] = useState( - null - ) + const [pendingEvolutionId, setPendingEvolutionId] = useState(null) const [shedNickname, setShedNickname] = useState('') const [deathLevel, setDeathLevel] = useState('') const [cause, setCause] = useState('') @@ -115,12 +101,7 @@ export function StatusChangeModal({ onClick={onClose} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200" > - + ) : ( - {displayPokemon.name[0].toUpperCase()} + {displayPokemon.name[0]?.toUpperCase()} )} @@ -179,55 +160,46 @@ export function StatusChangeModal({ {faintLevel !== null && ( - - Level at death: - {' '} + Level at death:{' '} {faintLevel} )} {deathCause && ( - - Cause: - {' '} - {deathCause} + Cause: {deathCause} )} )} {/* Alive pokemon: actions */} - {!isDead && - !showConfirm && - !showEvolve && - !showFormChange && - !showShedConfirm && ( - + {!isDead && !showConfirm && !showEvolve && !showFormChange && !showShedConfirm && ( + + setShowEvolve(true)} + className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors" + > + Evolve + + {forms && forms.length > 0 && ( setShowEvolve(true)} - className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors" + onClick={() => setShowFormChange(true)} + className="flex-1 px-4 py-2 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 transition-colors" > - Evolve + Change Form - {forms && forms.length > 0 && ( - setShowFormChange(true)} - className="flex-1 px-4 py-2 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 transition-colors" - > - Change Form - - )} - setShowConfirm(true)} - className="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 transition-colors" - > - Mark as Dead - - - )} + )} + setShowConfirm(true)} + className="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 transition-colors" + > + Mark as Dead + + + )} {/* Evolution selection */} {!isDead && showEvolve && ( @@ -245,14 +217,10 @@ export function StatusChangeModal({ {evolutionsLoading && ( - - Loading evolutions... - + Loading evolutions... )} {!evolutionsLoading && normalEvolutions.length === 0 && ( - - No evolutions available - + No evolutions available )} {!evolutionsLoading && normalEvolutions.length > 0 && ( @@ -272,7 +240,7 @@ export function StatusChangeModal({ /> ) : ( - {evo.toPokemon.name[0].toUpperCase()} + {evo.toPokemon.name[0]?.toUpperCase()} )} @@ -320,16 +288,12 @@ export function StatusChangeModal({ /> ) : ( - {shedCompanion.toPokemon.name[0].toUpperCase()} + {shedCompanion.toPokemon.name[0]?.toUpperCase()} )} - {displayPokemon.name} shed its shell! Would you also like to - add{' '} - - {shedCompanion.toPokemon.name} - - ? + {displayPokemon.name} shed its shell! Would you also like to add{' '} + {shedCompanion.toPokemon.name}? @@ -338,8 +302,7 @@ export function StatusChangeModal({ htmlFor="shed-nickname" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" > - Nickname{' '} - (optional) + Nickname (optional) applyEvolution(true)} className="flex-1 px-4 py-2 bg-amber-600 text-white rounded-lg font-medium hover:bg-amber-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" > - {isPending - ? 'Saving...' - : `Add ${shedCompanion.toPokemon.name}`} + {isPending ? 'Saving...' : `Add ${shedCompanion.toPokemon.name}`} @@ -400,14 +361,10 @@ export function StatusChangeModal({ className="w-full flex items-center gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-600 hover:bg-purple-50 dark:hover:bg-purple-900/20 hover:border-purple-300 dark:hover:border-purple-600 transition-colors disabled:opacity-50" > {form.spriteUrl ? ( - + ) : ( - {form.name[0].toUpperCase()} + {form.name[0]?.toUpperCase()} )} @@ -441,8 +398,7 @@ export function StatusChangeModal({ htmlFor="death-level" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" > - Level at Death{' '} - (optional) + Level at Death (optional) - Cause of Death{' '} - (optional) + Cause of Death (optional) + isCompleted && onStepClick(step)} @@ -53,11 +50,7 @@ export function StepIndicator({ stroke="currentColor" strokeWidth={3} > - + ) : ( step @@ -68,9 +61,7 @@ export function StepIndicator({ {i < steps.length - 1 && ( )} diff --git a/frontend/src/components/TransferModal.tsx b/frontend/src/components/TransferModal.tsx index ea21e6b..a2d04e1 100644 --- a/frontend/src/components/TransferModal.tsx +++ b/frontend/src/components/TransferModal.tsx @@ -8,15 +8,8 @@ interface TransferModalProps { isPending: boolean } -export function TransferModal({ - hofTeam, - onSubmit, - onSkip, - isPending, -}: TransferModalProps) { - const [selected, setSelected] = useState>( - () => new Set(hofTeam.map((e) => e.id)) - ) +export function TransferModal({ hofTeam, onSubmit, onSkip, isPending }: TransferModalProps) { + const [selected, setSelected] = useState>(() => new Set(hofTeam.map((e) => e.id))) const toggle = (id: number) => { setSelected((prev) => { @@ -39,8 +32,8 @@ export function TransferModal({ Transfer Pokemon to Next Leg - Selected Pokemon will be bred down to their base form and appear as - level 1 encounters in the next leg. + Selected Pokemon will be bred down to their base form and appear as level 1 encounters + in the next leg. @@ -69,20 +62,16 @@ export function TransferModal({ /> ) : ( - {displayPokemon.name[0].toUpperCase()} + {displayPokemon.name[0]?.toUpperCase()} )} {enc.nickname || displayPokemon.name} {enc.nickname && ( - - {displayPokemon.name} - + {displayPokemon.name} )} - - {enc.route.name} - + {enc.route.name} ) })} diff --git a/frontend/src/components/TypeBadge.tsx b/frontend/src/components/TypeBadge.tsx index 52450e8..3250486 100644 --- a/frontend/src/components/TypeBadge.tsx +++ b/frontend/src/components/TypeBadge.tsx @@ -5,7 +5,5 @@ interface TypeBadgeProps { export function TypeBadge({ type, size = 'sm' }: TypeBadgeProps) { const height = size === 'md' ? 'h-5' : 'h-4' - return ( - - ) + return } diff --git a/frontend/src/components/admin/AdminTable.tsx b/frontend/src/components/admin/AdminTable.tsx index bdce169..34d44a8 100644 --- a/frontend/src/components/admin/AdminTable.tsx +++ b/frontend/src/components/admin/AdminTable.tsx @@ -79,10 +79,7 @@ export function AdminTable({ {Array.from({ length: 5 }).map((_, i) => ( {columns.map((col) => ( - + ))} @@ -114,9 +111,7 @@ export function AdminTable({ return ( handleSort(col.header) : undefined - } + onClick={sortable ? () => handleSort(col.header) : undefined} className={`px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider ${col.className ?? ''} ${sortable ? 'cursor-pointer select-none hover:text-gray-700 dark:hover:text-gray-200' : ''}`} > @@ -138,9 +133,7 @@ export function AdminTable({ key={keyFn(row)} onClick={onRowClick ? () => onRowClick(row) : undefined} className={ - onRowClick - ? 'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800' - : '' + onRowClick ? 'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800' : '' } > {columns.map((col) => ( diff --git a/frontend/src/components/admin/BossBattleFormModal.tsx b/frontend/src/components/admin/BossBattleFormModal.tsx index 20a11a2..91729f9 100644 --- a/frontend/src/components/admin/BossBattleFormModal.tsx +++ b/frontend/src/components/admin/BossBattleFormModal.tsx @@ -1,10 +1,7 @@ import { type FormEvent, useState } from 'react' import { FormModal } from './FormModal' import type { BossBattle, Game, Route } from '../../types/game' -import type { - CreateBossBattleInput, - UpdateBossBattleInput, -} from '../../types/admin' +import type { CreateBossBattleInput, UpdateBossBattleInput } from '../../types/admin' interface BossBattleFormModalProps { boss?: BossBattle @@ -70,9 +67,7 @@ export function BossBattleFormModal({ const [badgeImageUrl, setBadgeImageUrl] = useState(boss?.badgeImageUrl ?? '') const [levelCap, setLevelCap] = useState(String(boss?.levelCap ?? '')) const [order, setOrder] = useState(String(boss?.order ?? nextOrder)) - const [afterRouteId, setAfterRouteId] = useState( - String(boss?.afterRouteId ?? '') - ) + const [afterRouteId, setAfterRouteId] = useState(String(boss?.afterRouteId ?? '')) const [location, setLocation] = useState(boss?.location ?? '') const [section, setSection] = useState(boss?.section ?? '') const [spriteUrl, setSpriteUrl] = useState(boss?.spriteUrl ?? '') @@ -212,9 +207,7 @@ export function BossBattleFormModal({ {games && games.length > 1 && ( - - Game (version exclusive) - + Game (version exclusive) setGameId(e.target.value)} @@ -232,9 +225,7 @@ export function BossBattleFormModal({ - - Position After Route - + Position After Route setAfterRouteId(e.target.value)} @@ -261,9 +252,7 @@ export function BossBattleFormModal({ /> - - Badge Image URL - + Badge Image URL - (a[0] ?? '').localeCompare(b[0] ?? '') - ) + const remaining = [...map.entries()].sort((a, b) => (a[0] ?? '').localeCompare(b[0] ?? '')) for (const [label, pokemon] of remaining) { variants.push({ label, pokemon }) } return variants } -export function BossTeamEditor({ - boss, - onSave, - onClose, - isSaving, -}: BossTeamEditorProps) { - const [variants, setVariants] = useState(() => - groupByVariant(boss) - ) +export function BossTeamEditor({ boss, onSave, onClose, isSaving }: BossTeamEditorProps) { + const [variants, setVariants] = useState(() => groupByVariant(boss)) const [activeTab, setActiveTab] = useState(0) const [newVariantName, setNewVariantName] = useState('') const [showAddVariant, setShowAddVariant] = useState(false) const activeVariant = variants[activeTab] ?? variants[0] - const updateVariant = ( - tabIndex: number, - updater: (v: Variant) => Variant - ) => { + const updateVariant = (tabIndex: number, updater: (v: Variant) => Variant) => { setVariants((prev) => prev.map((v, i) => (i === tabIndex ? updater(v) : v))) } @@ -108,16 +96,10 @@ export function BossTeamEditor({ })) } - const updateSlot = ( - index: number, - field: string, - value: number | string | null - ) => { + const updateSlot = (index: number, field: string, value: number | string | null) => { updateVariant(activeTab, (v) => ({ ...v, - pokemon: v.pokemon.map((item, i) => - i === index ? { ...item, [field]: value } : item - ), + pokemon: v.pokemon.map((item, i) => (i === index ? { ...item, [field]: value } : item)), })) } @@ -138,8 +120,9 @@ export function BossTeamEditor({ } const removeVariant = (tabIndex: number) => { - if (variants[tabIndex].label === null) return - if (!window.confirm(`Remove variant "${variants[tabIndex].label}"?`)) return + const variant = variants[tabIndex] + if (!variant || variant.label === null) return + if (!window.confirm(`Remove variant "${variant.label}"?`)) return setVariants((prev) => prev.filter((_, i) => i !== tabIndex)) setActiveTab((prev) => Math.min(prev, variants.length - 2)) } @@ -148,15 +131,14 @@ export function BossTeamEditor({ e.preventDefault() const allPokemon: BossPokemonInput[] = [] for (const variant of variants) { - const conditionLabel = - variants.length === 1 && variant.label === null ? null : variant.label - const validPokemon = variant.pokemon.filter( - (t) => t.pokemonId != null && t.level - ) + const conditionLabel = variants.length === 1 && variant.label === null ? null : variant.label + const validPokemon = variant.pokemon.filter((t) => t.pokemonId != null && t.level) for (let i = 0; i < validPokemon.length; i++) { + const p = validPokemon[i] + if (!p?.pokemonId) continue allPokemon.push({ - pokemonId: validPokemon[i].pokemonId!, - level: Number(validPokemon[i].level), + pokemonId: p.pokemonId, + level: Number(p.level), order: i + 1, conditionLabel, }) @@ -247,11 +229,8 @@ export function BossTeamEditor({ - {activeVariant.pokemon.map((slot, index) => ( - + {activeVariant?.pokemon.map((slot, index) => ( + - - Level - + Level ))} - {activeVariant.pokemon.length < 6 && ( + {activeVariant && activeVariant.pokemon.length < 6 && ( - - JSON Data - + JSON Data - {createdLabel}: {result.created}, {updatedLabel}:{' '} - {result.updated} + {createdLabel}: {result.created}, {updatedLabel}: {result.updated} {result.errors.length > 0 && ( diff --git a/frontend/src/components/admin/DeleteConfirmModal.tsx b/frontend/src/components/admin/DeleteConfirmModal.tsx index a3d3f24..0c61ebe 100644 --- a/frontend/src/components/admin/DeleteConfirmModal.tsx +++ b/frontend/src/components/admin/DeleteConfirmModal.tsx @@ -20,17 +20,9 @@ export function DeleteConfirmModal({ - - {title} - - - {message} - - {error && ( - - {error} - - )} + {title} + {message} + {error && {error}} ( evolution?.fromPokemonId ?? null ) - const [toPokemonId, setToPokemonId] = useState( - evolution?.toPokemonId ?? null - ) + const [toPokemonId, setToPokemonId] = useState(evolution?.toPokemonId ?? null) const [trigger, setTrigger] = useState(evolution?.trigger ?? 'level-up') const [minLevel, setMinLevel] = useState(String(evolution?.minLevel ?? '')) const [item, setItem] = useState(evolution?.item ?? '') diff --git a/frontend/src/components/admin/FormModal.tsx b/frontend/src/components/admin/FormModal.tsx index 0af37e1..ee17e6c 100644 --- a/frontend/src/components/admin/FormModal.tsx +++ b/frontend/src/components/admin/FormModal.tsx @@ -5,11 +5,11 @@ interface FormModalProps { onClose: () => void onSubmit: (e: FormEvent) => void children: ReactNode - submitLabel?: string - isSubmitting?: boolean - onDelete?: () => void - isDeleting?: boolean - headerExtra?: ReactNode + submitLabel?: string | undefined + isSubmitting?: boolean | undefined + onDelete?: (() => void) | undefined + isDeleting?: boolean | undefined + headerExtra?: ReactNode | undefined } export function FormModal({ @@ -55,11 +55,7 @@ export function FormModal({ onBlur={() => setConfirmingDelete(false)} className="px-4 py-2 text-sm font-medium rounded-md text-red-600 dark:text-red-400 border border-red-300 dark:border-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 disabled:opacity-50" > - {isDeleting - ? 'Deleting...' - : confirmingDelete - ? 'Confirm?' - : 'Delete'} + {isDeleting ? 'Deleting...' : confirmingDelete ? 'Confirm?' : 'Delete'} )} diff --git a/frontend/src/components/admin/GameFormModal.tsx b/frontend/src/components/admin/GameFormModal.tsx index 1f0ab8f..5f36781 100644 --- a/frontend/src/components/admin/GameFormModal.tsx +++ b/frontend/src/components/admin/GameFormModal.tsx @@ -34,9 +34,7 @@ export function GameFormModal({ const [generation, setGeneration] = useState(String(game?.generation ?? '')) const [region, setRegion] = useState(game?.region ?? '') const [boxArtUrl, setBoxArtUrl] = useState(game?.boxArtUrl ?? '') - const [releaseYear, setReleaseYear] = useState( - game?.releaseYear ? String(game.releaseYear) : '' - ) + const [releaseYear, setReleaseYear] = useState(game?.releaseYear ? String(game.releaseYear) : '') const [autoSlug, setAutoSlug] = useState(!game) useEffect(() => { @@ -65,10 +63,7 @@ export function GameFormModal({ isDeleting={isDeleting} headerExtra={ detailUrl ? ( - + View Routes & Bosses ) : undefined diff --git a/frontend/src/components/admin/PokemonFormModal.tsx b/frontend/src/components/admin/PokemonFormModal.tsx index 7135fac..4494158 100644 --- a/frontend/src/components/admin/PokemonFormModal.tsx +++ b/frontend/src/components/admin/PokemonFormModal.tsx @@ -9,10 +9,7 @@ import type { EvolutionAdmin, UpdateEvolutionInput, } from '../../types' -import { - usePokemonEncounterLocations, - usePokemonEvolutionChain, -} from '../../hooks/usePokemon' +import { usePokemonEncounterLocations, usePokemonEvolutionChain } from '../../hooks/usePokemon' import { useUpdateEvolution, useDeleteEvolution } from '../../hooks/useAdmin' import { formatEvolutionMethod } from '../../utils/formatEvolution' @@ -36,23 +33,19 @@ export function PokemonFormModal({ isDeleting, }: PokemonFormModalProps) { const [pokeapiId, setPokeapiId] = useState(String(pokemon?.pokeapiId ?? '')) - const [nationalDex, setNationalDex] = useState( - String(pokemon?.nationalDex ?? '') - ) + const [nationalDex, setNationalDex] = useState(String(pokemon?.nationalDex ?? '')) const [name, setName] = useState(pokemon?.name ?? '') const [types, setTypes] = useState(pokemon?.types.join(', ') ?? '') const [spriteUrl, setSpriteUrl] = useState(pokemon?.spriteUrl ?? '') const [activeTab, setActiveTab] = useState('details') - const [editingEvolution, setEditingEvolution] = - useState(null) + const [editingEvolution, setEditingEvolution] = useState(null) const [confirmingDelete, setConfirmingDelete] = useState(false) const isEdit = !!pokemon const pokemonId = pokemon?.id ?? null const { data: encounterLocations, isLoading: encountersLoading } = usePokemonEncounterLocations(pokemonId) - const { data: evolutionChain, isLoading: evolutionsLoading } = - usePokemonEvolutionChain(pokemonId) + const { data: evolutionChain, isLoading: evolutionsLoading } = usePokemonEvolutionChain(pokemonId) const queryClient = useQueryClient() const updateEvolution = useUpdateEvolution() @@ -103,9 +96,7 @@ export function PokemonFormModal({ {/* Header */} - - {pokemon ? 'Edit Pokemon' : 'Add Pokemon'} - + {pokemon ? 'Edit Pokemon' : 'Add Pokemon'} {isEdit && ( {tabs.map((tab) => ( @@ -124,15 +115,10 @@ export function PokemonFormModal({ {/* Details tab (form) */} {activeTab === 'details' && ( - + - - PokeAPI ID - + PokeAPI ID - - National Dex # - + National Dex # - - Types (comma-separated) - + Types (comma-separated) - - Sprite URL - + Sprite URL setConfirmingDelete(false)} className="px-4 py-2 text-sm font-medium rounded-md text-red-600 dark:text-red-400 border border-red-300 dark:border-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 disabled:opacity-50" > - {isDeleting - ? 'Deleting...' - : confirmingDelete - ? 'Confirm?' - : 'Delete'} + {isDeleting ? 'Deleting...' : confirmingDelete ? 'Confirm?' : 'Delete'} )} @@ -237,35 +213,28 @@ export function PokemonFormModal({ {evolutionsLoading && ( - - Loading... - + Loading... + )} + {!evolutionsLoading && (!evolutionChain || evolutionChain.length === 0) && ( + No evolutions + )} + {!evolutionsLoading && evolutionChain && evolutionChain.length > 0 && ( + + {evolutionChain.map((evo) => ( + setEditingEvolution(evo)} + className="w-full text-left text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded px-2 py-1.5 -mx-2 transition-colors" + > + {evo.fromPokemon.name} → {evo.toPokemon.name}{' '} + + ({formatEvolutionMethod(evo)}) + + + ))} + )} - {!evolutionsLoading && - (!evolutionChain || evolutionChain.length === 0) && ( - - No evolutions - - )} - {!evolutionsLoading && - evolutionChain && - evolutionChain.length > 0 && ( - - {evolutionChain.map((evo) => ( - setEditingEvolution(evo)} - className="w-full text-left text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded px-2 py-1.5 -mx-2 transition-colors" - > - {evo.fromPokemon.name} → {evo.toPokemon.name}{' '} - - ({formatEvolutionMethod(evo)}) - - - ))} - - )} {encountersLoading && ( - - Loading... - + Loading... )} - {!encountersLoading && - (!encounterLocations || encounterLocations.length === 0) && ( - - No encounters - - )} - {!encountersLoading && - encounterLocations && - encounterLocations.length > 0 && ( - - {encounterLocations.map((game) => ( - - - {game.gameName} - - - {game.encounters.map((enc, i) => ( - - - {enc.routeName} - - - — {enc.encounterMethod}, Lv. {enc.minLevel}– - {enc.maxLevel} - - - ))} - + {!encountersLoading && (!encounterLocations || encounterLocations.length === 0) && ( + No encounters + )} + {!encountersLoading && encounterLocations && encounterLocations.length > 0 && ( + + {encounterLocations.map((game) => ( + + + {game.gameName} - ))} - - )} + + {game.encounters.map((enc, i) => ( + + + {enc.routeName} + + + — {enc.encounterMethod}, Lv. {enc.minLevel}–{enc.maxLevel} + + + ))} + + + ))} + + )} void } @@ -46,9 +46,7 @@ export function PokemonSelector({ placeholder="Search pokemon..." className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600" /> - {selectedId && ( - - )} + {selectedId && } {open && pokemon.length > 0 && ( {pokemon.map((p) => ( @@ -63,9 +61,7 @@ export function PokemonSelector({ p.id === selectedId ? 'bg-blue-50 dark:bg-blue-900/30' : '' }`} > - {p.spriteUrl && ( - - )} + {p.spriteUrl && } #{p.nationalDex} {p.name} diff --git a/frontend/src/components/admin/RouteEncounterFormModal.tsx b/frontend/src/components/admin/RouteEncounterFormModal.tsx index 8c01c3c..0b7feb1 100644 --- a/frontend/src/components/admin/RouteEncounterFormModal.tsx +++ b/frontend/src/components/admin/RouteEncounterFormModal.tsx @@ -1,11 +1,7 @@ import { type FormEvent, useState } from 'react' import { FormModal } from './FormModal' import { PokemonSelector } from './PokemonSelector' -import { - METHOD_ORDER, - METHOD_CONFIG, - getMethodLabel, -} from '../EncounterMethodBadge' +import { METHOD_ORDER, METHOD_CONFIG, getMethodLabel } from '../EncounterMethodBadge' import type { RouteEncounterDetail, CreateRouteEncounterInput, @@ -14,9 +10,7 @@ import type { interface RouteEncounterFormModalProps { encounter?: RouteEncounterDetail - onSubmit: ( - data: CreateRouteEncounterInput | UpdateRouteEncounterInput - ) => void + onSubmit: (data: CreateRouteEncounterInput | UpdateRouteEncounterInput) => void onClose: () => void isSubmitting?: boolean onDelete?: () => void @@ -38,15 +32,10 @@ export function RouteEncounterFormModal({ const [selectedMethod, setSelectedMethod] = useState( isKnownMethod ? initialMethod : initialMethod ? 'other' : '' ) - const [customMethod, setCustomMethod] = useState( - isKnownMethod ? '' : initialMethod - ) - const encounterMethod = - selectedMethod === 'other' ? customMethod : selectedMethod + const [customMethod, setCustomMethod] = useState(isKnownMethod ? '' : initialMethod) + const encounterMethod = selectedMethod === 'other' ? customMethod : selectedMethod - const [encounterRate, setEncounterRate] = useState( - String(encounter?.encounterRate ?? '') - ) + const [encounterRate, setEncounterRate] = useState(String(encounter?.encounterRate ?? '')) const [minLevel, setMinLevel] = useState(String(encounter?.minLevel ?? '')) const [maxLevel, setMaxLevel] = useState(String(encounter?.maxLevel ?? '')) @@ -87,9 +76,7 @@ export function RouteEncounterFormModal({ /> )} - - Encounter Method - + Encounter Method - - Encounter Rate (%) - + Encounter Rate (%) + View Encounters ) : undefined @@ -90,8 +87,7 @@ export function RouteFormModal({ className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600" /> - Routes in the same zone share an encounter when the Pinwheel Clause is - active + Routes in the same zone share an encounter when the Pinwheel Clause is active diff --git a/frontend/src/hooks/useAdmin.ts b/frontend/src/hooks/useAdmin.ts index 0811516..74689ee 100644 --- a/frontend/src/hooks/useAdmin.ts +++ b/frontend/src/hooks/useAdmin.ts @@ -23,12 +23,7 @@ import type { // --- Queries --- -export function usePokemonList( - search?: string, - limit = 50, - offset = 0, - type?: string -) { +export function usePokemonList(search?: string, limit = 50, offset = 0, type?: string) { return useQuery({ queryKey: ['pokemon', { search, limit, offset, type }], queryFn: () => adminApi.listPokemon(search, limit, offset, type), @@ -92,13 +87,8 @@ export function useCreateRoute(gameId: number) { export function useUpdateRoute(gameId: number) { const qc = useQueryClient() return useMutation({ - mutationFn: ({ - routeId, - data, - }: { - routeId: number - data: UpdateRouteInput - }) => adminApi.updateRoute(gameId, routeId, data), + mutationFn: ({ routeId, data }: { routeId: number; data: UpdateRouteInput }) => + adminApi.updateRoute(gameId, routeId, data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['games', gameId] }) qc.invalidateQueries({ queryKey: ['games', gameId, 'routes'] }) @@ -124,8 +114,7 @@ export function useDeleteRoute(gameId: number) { export function useReorderRoutes(gameId: number) { const qc = useQueryClient() return useMutation({ - mutationFn: (routes: RouteReorderItem[]) => - adminApi.reorderRoutes(gameId, routes), + mutationFn: (routes: RouteReorderItem[]) => adminApi.reorderRoutes(gameId, routes), onSuccess: () => { qc.invalidateQueries({ queryKey: ['games', gameId] }) qc.invalidateQueries({ queryKey: ['games', gameId, 'routes'] }) @@ -188,9 +177,7 @@ export function useBulkImportPokemon() { ) => adminApi.bulkImportPokemon(items), onSuccess: (result) => { qc.invalidateQueries({ queryKey: ['pokemon'] }) - toast.success( - `Import complete: ${result.created} created, ${result.updated} updated` - ) + toast.success(`Import complete: ${result.created} created, ${result.updated} updated`) }, onError: (err) => toast.error(`Import failed: ${err.message}`), }) @@ -202,9 +189,7 @@ export function useBulkImportEvolutions() { mutationFn: (items: unknown[]) => adminApi.bulkImportEvolutions(items), onSuccess: (result) => { qc.invalidateQueries({ queryKey: ['evolutions'] }) - toast.success( - `Import complete: ${result.created} created, ${result.updated} updated` - ) + toast.success(`Import complete: ${result.created} created, ${result.updated} updated`) }, onError: (err) => toast.error(`Import failed: ${err.message}`), }) @@ -217,9 +202,7 @@ export function useBulkImportRoutes(gameId: number) { onSuccess: (result) => { qc.invalidateQueries({ queryKey: ['games', gameId] }) qc.invalidateQueries({ queryKey: ['games', gameId, 'routes'] }) - toast.success( - `Import complete: ${result.created} routes, ${result.updated} encounters` - ) + toast.success(`Import complete: ${result.created} routes, ${result.updated} encounters`) }, onError: (err) => toast.error(`Import failed: ${err.message}`), }) @@ -239,12 +222,7 @@ export function useBulkImportBosses(gameId: number) { // --- Evolution Queries & Mutations --- -export function useEvolutionList( - search?: string, - limit = 50, - offset = 0, - trigger?: string -) { +export function useEvolutionList(search?: string, limit = 50, offset = 0, trigger?: string) { return useQuery({ queryKey: ['evolutions', { search, limit, offset, trigger }], queryFn: () => adminApi.listEvolutions(search, limit, offset, trigger), @@ -293,8 +271,7 @@ export function useDeleteEvolution() { export function useAddRouteEncounter(routeId: number) { const qc = useQueryClient() return useMutation({ - mutationFn: (data: CreateRouteEncounterInput) => - adminApi.addRouteEncounter(routeId, data), + mutationFn: (data: CreateRouteEncounterInput) => adminApi.addRouteEncounter(routeId, data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['routes', routeId, 'pokemon'] }) toast.success('Encounter added') @@ -306,13 +283,8 @@ export function useAddRouteEncounter(routeId: number) { export function useUpdateRouteEncounter(routeId: number) { const qc = useQueryClient() return useMutation({ - mutationFn: ({ - encounterId, - data, - }: { - encounterId: number - data: UpdateRouteEncounterInput - }) => adminApi.updateRouteEncounter(routeId, encounterId, data), + mutationFn: ({ encounterId, data }: { encounterId: number; data: UpdateRouteEncounterInput }) => + adminApi.updateRouteEncounter(routeId, encounterId, data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['routes', routeId, 'pokemon'] }) toast.success('Encounter updated') @@ -324,8 +296,7 @@ export function useUpdateRouteEncounter(routeId: number) { export function useRemoveRouteEncounter(routeId: number) { const qc = useQueryClient() return useMutation({ - mutationFn: (encounterId: number) => - adminApi.removeRouteEncounter(routeId, encounterId), + mutationFn: (encounterId: number) => adminApi.removeRouteEncounter(routeId, encounterId), onSuccess: () => { qc.invalidateQueries({ queryKey: ['routes', routeId, 'pokemon'] }) toast.success('Encounter removed') @@ -339,41 +310,32 @@ export function useRemoveRouteEncounter(routeId: number) { export function useCreateBossBattle(gameId: number) { const qc = useQueryClient() return useMutation({ - mutationFn: (data: CreateBossBattleInput) => - adminApi.createBossBattle(gameId, data), + mutationFn: (data: CreateBossBattleInput) => adminApi.createBossBattle(gameId, data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['games', gameId, 'bosses'] }) toast.success('Boss battle created') }, - onError: (err) => - toast.error(`Failed to create boss battle: ${err.message}`), + onError: (err) => toast.error(`Failed to create boss battle: ${err.message}`), }) } export function useUpdateBossBattle(gameId: number) { const qc = useQueryClient() return useMutation({ - mutationFn: ({ - bossId, - data, - }: { - bossId: number - data: UpdateBossBattleInput - }) => adminApi.updateBossBattle(gameId, bossId, data), + mutationFn: ({ bossId, data }: { bossId: number; data: UpdateBossBattleInput }) => + adminApi.updateBossBattle(gameId, bossId, data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['games', gameId, 'bosses'] }) toast.success('Boss battle updated') }, - onError: (err) => - toast.error(`Failed to update boss battle: ${err.message}`), + onError: (err) => toast.error(`Failed to update boss battle: ${err.message}`), }) } export function useReorderBosses(gameId: number) { const qc = useQueryClient() return useMutation({ - mutationFn: (bosses: BossReorderItem[]) => - adminApi.reorderBosses(gameId, bosses), + mutationFn: (bosses: BossReorderItem[]) => adminApi.reorderBosses(gameId, bosses), onSuccess: () => { qc.invalidateQueries({ queryKey: ['games', gameId, 'bosses'] }) toast.success('Bosses reordered') @@ -390,16 +352,14 @@ export function useDeleteBossBattle(gameId: number) { qc.invalidateQueries({ queryKey: ['games', gameId, 'bosses'] }) toast.success('Boss battle deleted') }, - onError: (err) => - toast.error(`Failed to delete boss battle: ${err.message}`), + onError: (err) => toast.error(`Failed to delete boss battle: ${err.message}`), }) } export function useSetBossTeam(gameId: number, bossId: number) { const qc = useQueryClient() return useMutation({ - mutationFn: (team: BossPokemonInput[]) => - adminApi.setBossTeam(gameId, bossId, team), + mutationFn: (team: BossPokemonInput[]) => adminApi.setBossTeam(gameId, bossId, team), onSuccess: () => { qc.invalidateQueries({ queryKey: ['games', gameId, 'bosses'] }) toast.success('Boss team updated') @@ -438,8 +398,7 @@ export function useDeleteGenlocke() { export function useAddGenlockeLeg(genlockeId: number) { const qc = useQueryClient() return useMutation({ - mutationFn: (data: AddGenlockeLegInput) => - adminApi.addGenlockeLeg(genlockeId, data), + mutationFn: (data: AddGenlockeLegInput) => adminApi.addGenlockeLeg(genlockeId, data), onSuccess: () => { qc.invalidateQueries({ queryKey: ['genlockes'] }) qc.invalidateQueries({ queryKey: ['genlockes', genlockeId] }) @@ -452,8 +411,7 @@ export function useAddGenlockeLeg(genlockeId: number) { export function useDeleteGenlockeLeg(genlockeId: number) { const qc = useQueryClient() return useMutation({ - mutationFn: (legId: number) => - adminApi.deleteGenlockeLeg(genlockeId, legId), + mutationFn: (legId: number) => adminApi.deleteGenlockeLeg(genlockeId, legId), onSuccess: () => { qc.invalidateQueries({ queryKey: ['genlockes'] }) qc.invalidateQueries({ queryKey: ['genlockes', genlockeId] }) diff --git a/frontend/src/hooks/useBosses.ts b/frontend/src/hooks/useBosses.ts index 44e6e6d..fe814e8 100644 --- a/frontend/src/hooks/useBosses.ts +++ b/frontend/src/hooks/useBosses.ts @@ -1,11 +1,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { - getGameBosses, - getBossResults, - createBossResult, - deleteBossResult, -} from '../api/bosses' +import { getGameBosses, getBossResults, createBossResult, deleteBossResult } from '../api/bosses' import type { CreateBossResultInput } from '../types/game' export function useGameBosses(gameId: number | null, all?: boolean) { diff --git a/frontend/src/hooks/useGenlockes.ts b/frontend/src/hooks/useGenlockes.ts index a312300..b3b786a 100644 --- a/frontend/src/hooks/useGenlockes.ts +++ b/frontend/src/hooks/useGenlockes.ts @@ -57,11 +57,7 @@ export function useCreateGenlocke() { }) } -export function useLegSurvivors( - genlockeId: number, - legOrder: number, - enabled: boolean -) { +export function useLegSurvivors(genlockeId: number, legOrder: number, enabled: boolean) { return useQuery({ queryKey: ['genlockes', genlockeId, 'legs', legOrder, 'survivors'], queryFn: () => getLegSurvivors(genlockeId, legOrder), @@ -81,11 +77,7 @@ export function useAdvanceLeg() { legOrder: number transferEncounterIds?: number[] }) => - advanceLeg( - genlockeId, - legOrder, - transferEncounterIds ? { transferEncounterIds } : undefined - ), + advanceLeg(genlockeId, legOrder, transferEncounterIds ? { transferEncounterIds } : undefined), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['runs'] }) queryClient.invalidateQueries({ queryKey: ['genlockes'] }) diff --git a/frontend/src/hooks/useRuns.ts b/frontend/src/hooks/useRuns.ts index 9b7fa5a..462fbfc 100644 --- a/frontend/src/hooks/useRuns.ts +++ b/frontend/src/hooks/useRuns.ts @@ -68,10 +68,7 @@ export function useNamingCategories() { }) } -export function useNameSuggestions( - runId: number | null, - pokemonId?: number | null -) { +export function useNameSuggestions(runId: number | null, pokemonId?: number | null) { return useQuery({ queryKey: ['name-suggestions', runId, pokemonId ?? null], queryFn: () => getNameSuggestions(runId!, 10, pokemonId ?? undefined), diff --git a/frontend/src/pages/GenlockeDetail.tsx b/frontend/src/pages/GenlockeDetail.tsx index a5764bf..6fda81a 100644 --- a/frontend/src/pages/GenlockeDetail.tsx +++ b/frontend/src/pages/GenlockeDetail.tsx @@ -1,12 +1,7 @@ import { Link, useParams } from 'react-router-dom' import { useGenlocke } from '../hooks/useGenlockes' import { usePokemonFamilies } from '../hooks/usePokemon' -import { - GenlockeGraveyard, - GenlockeLineage, - StatCard, - RuleBadges, -} from '../components' +import { GenlockeGraveyard, GenlockeLineage, StatCard, RuleBadges } from '../components' import type { GenlockeLegDetail, RetiredPokemon, RunStatus } from '../types' import { useMemo, useState } from 'react' @@ -23,8 +18,7 @@ const statusRing: Record = { } const statusStyles: Record = { - active: - 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300', + active: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300', completed: 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300', failed: 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300', } @@ -48,19 +42,14 @@ function LegIndicator({ leg }: { leg: GenlockeLegDetail }) { {leg.game.name} {status && ( - - {status} - + {status} )} ) if (hasRun) { return ( - + {content} ) @@ -86,7 +75,7 @@ function PokemonSprite({ pokemon }: { pokemon: RetiredPokemon }) { className="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-sm font-bold" title={pokemon.name} > - {pokemon.name[0].toUpperCase()} + {pokemon.name[0]?.toUpperCase()} ) } @@ -116,9 +105,7 @@ export function GenlockeDetail() { } return genlocke.legs - .filter( - (leg) => leg.retiredPokemonIds && leg.retiredPokemonIds.length > 0 - ) + .filter((leg) => leg.retiredPokemonIds && leg.retiredPokemonIds.length > 0) .map((leg) => { // Find base Pokemon (lowest ID) for each family in this leg's retired list const seen = new Set() @@ -170,16 +157,11 @@ export function GenlockeDetail() { {/* Header */} - + ← Back to Genlockes - - {genlocke.name} - + {genlocke.name} @@ -190,9 +172,7 @@ export function GenlockeDetail() { {/* Progress Timeline */} - - Progress - + Progress {genlocke.legs.map((leg, i) => ( @@ -201,9 +181,7 @@ export function GenlockeDetail() { {i < genlocke.legs.length - 1 && ( )} @@ -219,16 +197,8 @@ export function GenlockeDetail() { Cumulative Stats - - + + {retiredByLeg.map((leg) => ( - + Leg {leg.legOrder} — {leg.gameName} diff --git a/frontend/src/pages/GenlockeList.tsx b/frontend/src/pages/GenlockeList.tsx index e9894cd..58ab5db 100644 --- a/frontend/src/pages/GenlockeList.tsx +++ b/frontend/src/pages/GenlockeList.tsx @@ -3,8 +3,7 @@ import { useGenlockes } from '../hooks/useGenlockes' import type { RunStatus } from '../types' const statusStyles: Record = { - active: - 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300', + active: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300', completed: 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300', failed: 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300', } @@ -15,9 +14,7 @@ export function GenlockeList() { return ( - - Your Genlockes - + Your Genlockes = { - active: - 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300', + active: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300', completed: 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300', failed: 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300', } @@ -75,10 +74,7 @@ export function Home() { Recent Runs - + View all @@ -91,9 +87,7 @@ export function Home() { > - - {run.name} - + {run.name} {new Date(run.startedAt).toLocaleDateString(undefined, { year: 'numeric', diff --git a/frontend/src/pages/NewGenlocke.tsx b/frontend/src/pages/NewGenlocke.tsx index 4752340..5446ca4 100644 --- a/frontend/src/pages/NewGenlocke.tsx +++ b/frontend/src/pages/NewGenlocke.tsx @@ -18,10 +18,7 @@ interface LegEntry { type PresetType = 'true' | 'normal' | 'custom' | null -function buildLegsFromPreset( - regions: Region[], - preset: 'true' | 'normal' -): LegEntry[] { +function buildLegsFromPreset(regions: Region[], preset: 'true' | 'normal'): LegEntry[] { const legs: LegEntry[] = [] for (const region of regions) { const targetSlug = @@ -45,8 +42,7 @@ export function NewGenlocke() { const [name, setName] = useState('') const [legs, setLegs] = useState([]) const [preset, setPreset] = useState(null) - const [nuzlockeRules, setNuzlockeRules] = - useState(DEFAULT_RULES) + const [nuzlockeRules, setNuzlockeRules] = useState(DEFAULT_RULES) const [genlockeRules, setGenlockeRules] = useState({ retireHoF: false, }) @@ -64,9 +60,7 @@ export function NewGenlocke() { } const handleGameChange = (index: number, game: Game) => { - setLegs((prev) => - prev.map((leg, i) => (i === index ? { ...leg, game } : leg)) - ) + setLegs((prev) => prev.map((leg, i) => (i === index ? { ...leg, game } : leg))) } const handleRemoveLeg = (index: number) => { @@ -75,8 +69,7 @@ export function NewGenlocke() { const handleAddLeg = (region: Region) => { const defaultSlug = region.genlockeDefaults.normalGenlocke - const game = - region.games.find((g) => g.slug === defaultSlug) ?? region.games[0] + const game = region.games.find((g) => g.slug === defaultSlug) ?? region.games[0] if (game) { setLegs((prev) => [...prev, { region: region.name, game }]) } @@ -87,7 +80,7 @@ export function NewGenlocke() { if (target < 0 || target >= legs.length) return setLegs((prev) => { const next = [...prev] - ;[next[index], next[target]] = [next[target], next[index]] + ;[next[index], next[target]] = [next[target]!, next[index]!] return next }) } @@ -115,23 +108,16 @@ export function NewGenlocke() { ) } - const enabledRuleCount = RULE_DEFINITIONS.filter( - (r) => nuzlockeRules[r.key] - ).length + const enabledRuleCount = RULE_DEFINITIONS.filter((r) => nuzlockeRules[r.key]).length const totalRuleCount = RULE_DEFINITIONS.length // Regions not yet used in legs (for "add leg" picker) - const availableRegions = - regions?.filter((r) => !legs.some((l) => l.region === r.name)) ?? [] + const availableRegions = regions?.filter((r) => !legs.some((l) => l.region === r.name)) ?? [] return ( - - New Genlocke - - - Set up your generational challenge. - + New Genlocke + Set up your generational challenge. @@ -250,17 +236,11 @@ export function NewGenlocke() { )} {/* Also allow adding extra regions for presets */} - {preset && - preset !== 'custom' && - availableRegions.length > 0 && - legs.length > 0 && ( - - - - )} + {preset && preset !== 'custom' && availableRegions.length > 0 && legs.length > 0 && ( + + + + )} - + {/* Genlocke-specific rules */} @@ -319,8 +296,7 @@ export function NewGenlocke() { Keep Hall of Fame - Pokemon that beat the Elite Four can continue to the - next leg + Pokemon that beat the Elite Four can continue to the next leg @@ -337,8 +313,8 @@ export function NewGenlocke() { Retire Hall of Fame - Pokemon that beat the Elite Four are retired and cannot - be used in the next leg + Pokemon that beat the Elite Four are retired and cannot be used in the next + leg @@ -354,8 +330,8 @@ export function NewGenlocke() { Naming Scheme - Get nickname suggestions from a themed word list when catching - Pokemon. Applied to all legs. + Get nickname suggestions from a themed word list when catching Pokemon. Applied to + all legs. @@ -402,12 +378,8 @@ export function NewGenlocke() { - - Name - - - {name} - + Name + {name} @@ -426,8 +398,7 @@ export function NewGenlocke() { {leg.game.name} - {leg.region.charAt(0).toUpperCase() + - leg.region.slice(1)} + {leg.region.charAt(0).toUpperCase() + leg.region.slice(1)} @@ -436,34 +407,25 @@ export function NewGenlocke() { - - Rules - + Rules - - Nuzlocke Rules - + Nuzlocke Rules {enabledRuleCount} of {totalRuleCount} enabled - - Hall of Fame - + Hall of Fame {genlockeRules.retireHoF ? 'Retire' : 'Keep'} - - Naming Scheme - + Naming Scheme {namingScheme - ? namingScheme.charAt(0).toUpperCase() + - namingScheme.slice(1) + ? namingScheme.charAt(0).toUpperCase() + namingScheme.slice(1) : 'None'} @@ -548,9 +510,7 @@ function LegRow({ ))} ) : ( - - {leg.game.name} - + {leg.game.name} )} @@ -568,11 +528,7 @@ function LegRow({ stroke="currentColor" strokeWidth={2} > - + - + - + @@ -644,11 +592,7 @@ function AddLegDropdown({ stroke="currentColor" strokeWidth={2} > - + Add Region diff --git a/frontend/src/pages/NewRun.tsx b/frontend/src/pages/NewRun.tsx index 140b493..cbb5490 100644 --- a/frontend/src/pages/NewRun.tsx +++ b/frontend/src/pages/NewRun.tsx @@ -51,20 +51,14 @@ export function NewRun() { ) } - const visibleRuleKeys = RULE_DEFINITIONS.filter( - (r) => !hiddenRules?.has(r.key) - ).map((r) => r.key) + const visibleRuleKeys = RULE_DEFINITIONS.filter((r) => !hiddenRules?.has(r.key)).map((r) => r.key) const enabledRuleCount = visibleRuleKeys.filter((k) => rules[k]).length const totalRuleCount = visibleRuleKeys.length return ( - - New Nuzlocke Run - - - Set up your run in a few steps. - + New Nuzlocke Run + Set up your run in a few steps. @@ -84,8 +78,7 @@ export function NewRun() { {selectedGame.name} - {selectedGame.region.charAt(0).toUpperCase() + - selectedGame.region.slice(1)} + {selectedGame.region.charAt(0).toUpperCase() + selectedGame.region.slice(1)} @@ -138,11 +131,7 @@ export function NewRun() { {step === 2 && ( - + - Get nickname suggestions from a themed word list when catching - Pokemon. + Get nickname suggestions from a themed word list when catching Pokemon. )} - - Summary - + Summary Game @@ -230,8 +216,7 @@ export function NewRun() { Region {selectedGame && - selectedGame.region.charAt(0).toUpperCase() + - selectedGame.region.slice(1)} + selectedGame.region.charAt(0).toUpperCase() + selectedGame.region.slice(1)} @@ -241,13 +226,10 @@ export function NewRun() { - - Naming Scheme - + Naming Scheme {namingScheme - ? namingScheme.charAt(0).toUpperCase() + - namingScheme.slice(1) + ? namingScheme.charAt(0).toUpperCase() + namingScheme.slice(1) : 'None'} diff --git a/frontend/src/pages/RunDashboard.tsx b/frontend/src/pages/RunDashboard.tsx index 59fed98..019b60c 100644 --- a/frontend/src/pages/RunDashboard.tsx +++ b/frontend/src/pages/RunDashboard.tsx @@ -3,21 +3,12 @@ import { useParams, Link } from 'react-router-dom' import { useRun, useUpdateRun, useNamingCategories } from '../hooks/useRuns' import { useGameRoutes } from '../hooks/useGames' import { useCreateEncounter, useUpdateEncounter } from '../hooks/useEncounters' -import { - StatCard, - PokemonCard, - RuleBadges, - StatusChangeModal, - EndRunModal, -} from '../components' +import { StatCard, PokemonCard, RuleBadges, StatusChangeModal, EndRunModal } from '../components' import type { RunStatus, EncounterDetail } from '../types' type TeamSortKey = 'route' | 'level' | 'species' | 'dex' -function sortEncounters( - encounters: EncounterDetail[], - key: TeamSortKey -): EncounterDetail[] { +function sortEncounters(encounters: EncounterDetail[], key: TeamSortKey): EncounterDetail[] { return [...encounters].sort((a, b) => { switch (key) { case 'route': @@ -31,8 +22,7 @@ function sortEncounters( } case 'dex': return ( - (a.currentPokemon ?? a.pokemon).nationalDex - - (b.currentPokemon ?? b.pokemon).nationalDex + (a.currentPokemon ?? a.pokemon).nationalDex - (b.currentPokemon ?? b.pokemon).nationalDex ) default: return 0 @@ -41,8 +31,7 @@ function sortEncounters( } const statusStyles: Record = { - active: - 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300', + active: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300', completed: 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300', failed: 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300', } @@ -64,8 +53,7 @@ export function RunDashboard() { const updateEncounter = useUpdateEncounter(runIdNum) const updateRun = useUpdateRun(runIdNum) const { data: namingCategories } = useNamingCategories() - const [selectedEncounter, setSelectedEncounter] = - useState(null) + const [selectedEncounter, setSelectedEncounter] = useState(null) const [showEndRun, setShowEndRun] = useState(false) const [teamSort, setTeamSort] = useState('route') @@ -73,9 +61,7 @@ export function RunDashboard() { const alive = useMemo( () => sortEncounters( - encounters.filter( - (e) => e.status === 'caught' && e.faintLevel === null - ), + encounters.filter((e) => e.status === 'caught' && e.faintLevel === null), teamSort ), [encounters, teamSort] @@ -83,9 +69,7 @@ export function RunDashboard() { const dead = useMemo( () => sortEncounters( - encounters.filter( - (e) => e.status === 'caught' && e.faintLevel !== null - ), + encounters.filter((e) => e.status === 'caught' && e.faintLevel !== null), teamSort ), [encounters, teamSort] @@ -105,10 +89,7 @@ export function RunDashboard() { Failed to load run. It may not exist. - + Back to runs @@ -131,14 +112,10 @@ export function RunDashboard() { - - {run.name} - + {run.name} {run.game.name} ·{' '} - {run.game.region.charAt(0).toUpperCase() + - run.game.region.slice(1)}{' '} - · Started{' '} + {run.game.region.charAt(0).toUpperCase() + run.game.region.slice(1)} · Started{' '} {new Date(run.startedAt).toLocaleDateString(undefined, { year: 'numeric', month: 'short', @@ -204,26 +181,15 @@ export function RunDashboard() { {/* Stats */} - + - + {/* Rules */} - - Active Rules - + Active Rules @@ -236,9 +202,7 @@ export function RunDashboard() { {isActive ? ( - updateRun.mutate({ namingScheme: e.target.value || null }) - } + onChange={(e) => updateRun.mutate({ namingScheme: e.target.value || null })} className="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100" > None @@ -251,8 +215,7 @@ export function RunDashboard() { ) : ( {run.namingScheme - ? run.namingScheme.charAt(0).toUpperCase() + - run.namingScheme.slice(1) + ? run.namingScheme.charAt(0).toUpperCase() + run.namingScheme.slice(1) : 'None'} )} @@ -280,8 +243,7 @@ export function RunDashboard() { {alive.length === 0 ? ( - No pokemon caught yet — head to encounters to start building your - team! + No pokemon caught yet — head to encounters to start building your team! ) : ( @@ -299,9 +261,7 @@ export function RunDashboard() { {/* Graveyard */} {dead.length > 0 && ( - - Graveyard - + Graveyard {dead.map((enc) => ( { - updateRun.mutate( - { status }, - { onSuccess: () => setShowEndRun(false) } - ) + updateRun.mutate({ status }, { onSuccess: () => setShowEndRun(false) }) }} onClose={() => setShowEndRun(false)} isPending={updateRun.isPending} diff --git a/frontend/src/pages/RunEncounters.tsx b/frontend/src/pages/RunEncounters.tsx index 6a592cf..30f7eaf 100644 --- a/frontend/src/pages/RunEncounters.tsx +++ b/frontend/src/pages/RunEncounters.tsx @@ -3,17 +3,9 @@ import { useParams, Link, useNavigate } from 'react-router-dom' import { useRun, useUpdateRun } from '../hooks/useRuns' import { useAdvanceLeg } from '../hooks/useGenlockes' import { useGameRoutes } from '../hooks/useGames' -import { - useCreateEncounter, - useUpdateEncounter, - useBulkRandomize, -} from '../hooks/useEncounters' +import { useCreateEncounter, useUpdateEncounter, useBulkRandomize } from '../hooks/useEncounters' import { usePokemonFamilies } from '../hooks/usePokemon' -import { - useGameBosses, - useBossResults, - useCreateBossResult, -} from '../hooks/useBosses' +import { useGameBosses, useBossResults, useCreateBossResult } from '../hooks/useBosses' import { EggEncounterModal, EncounterModal, @@ -43,10 +35,7 @@ import type { type TeamSortKey = 'route' | 'level' | 'species' | 'dex' -function sortEncounters( - encounters: EncounterDetail[], - key: TeamSortKey -): EncounterDetail[] { +function sortEncounters(encounters: EncounterDetail[], key: TeamSortKey): EncounterDetail[] { return [...encounters].sort((a, b) => { switch (key) { case 'route': @@ -60,8 +49,7 @@ function sortEncounters( } case 'dex': return ( - (a.currentPokemon ?? a.pokemon).nationalDex - - (b.currentPokemon ?? b.pokemon).nationalDex + (a.currentPokemon ?? a.pokemon).nationalDex - (b.currentPokemon ?? b.pokemon).nationalDex ) default: return 0 @@ -70,8 +58,7 @@ function sortEncounters( } const statusStyles: Record = { - active: - 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300', + active: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300', completed: 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300', failed: 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300', } @@ -91,10 +78,7 @@ function getRouteStatus(encounter?: EncounterDetail): RouteStatus { return encounter.status } -const statusIndicator: Record< - RouteStatus, - { dot: string; label: string; bg: string } -> = { +const statusIndicator: Record = { caught: { dot: 'bg-green-500', label: 'Caught', @@ -186,14 +170,11 @@ function countDistinctZones(group: RouteWithChildren): number { return zones.size } -function matchVariant( - labels: string[], - starterName?: string | null -): string | null { +function matchVariant(labels: string[], starterName?: string | null): string | null { if (!starterName || labels.length === 0) return null const lower = starterName.toLowerCase() const matches = labels.filter((l) => l.toLowerCase().includes(lower)) - return matches.length === 1 ? matches[0] : null + return matches.length === 1 ? (matches[0] ?? null) : null } function BossTeamPreview({ @@ -218,14 +199,13 @@ function BossTeamPreview({ ) const showPills = hasVariants && autoMatch === null const [selectedVariant, setSelectedVariant] = useState( - autoMatch ?? (hasVariants ? variantLabels[0] : null) + autoMatch ?? (hasVariants ? (variantLabels[0] ?? null) : null) ) const displayed = useMemo(() => { if (!hasVariants) return pokemon return pokemon.filter( - (bp) => - bp.conditionLabel === selectedVariant || bp.conditionLabel === null + (bp) => bp.conditionLabel === selectedVariant || bp.conditionLabel === null ) }, [pokemon, hasVariants, selectedVariant]) @@ -255,17 +235,11 @@ function BossTeamPreview({ .map((bp) => ( {bp.pokemon.spriteUrl ? ( - + ) : ( )} - - Lvl {bp.level} - + Lvl {bp.level} ))} @@ -294,9 +268,7 @@ function RouteGroup({ }: RouteGroupProps) { const groupEncounter = getGroupEncounter(group, encounterByRoute) const usePinwheel = pinwheelClause && groupHasZones(group) - const zoneEncounters = usePinwheel - ? getZoneEncounters(group, encounterByRoute) - : null + const zoneEncounters = usePinwheel ? getZoneEncounters(group, encounterByRoute) : null // For pinwheel groups, determine status from all zone statuses let groupStatus: RouteStatus @@ -354,28 +326,19 @@ function RouteGroup({ {groupEncounter.nickname ?? groupEncounter.pokemon.name} {groupEncounter.status === 'caught' && groupEncounter.faintLevel !== null && - (groupEncounter.deathCause - ? ` — ${groupEncounter.deathCause}` - : ' (dead)')} + (groupEncounter.deathCause ? ` — ${groupEncounter.deathCause}` : ' (dead)')} )} - - {si.label} - + {si.label} - + @@ -409,13 +372,9 @@ function RouteGroup({ : 'hover:bg-gray-100 dark:hover:bg-gray-700/50' } ${childSi.bg}`} > - + - - {child.name} - + {child.name} {!childEncounter && child.encounterMethods.length > 0 && ( {child.encounterMethods.map((m) => ( @@ -425,14 +384,10 @@ function RouteGroup({ )} {childEncounter && ( - - {childSi.label} - + {childSi.label} )} {isDisabled && ( - - (locked) - + (locked) )} ) @@ -450,9 +405,7 @@ export function RunEncounters() { const { data: run, isLoading, error } = useRun(runIdNum) const advanceLeg = useAdvanceLeg() const [showTransferModal, setShowTransferModal] = useState(false) - const { data: routes, isLoading: loadingRoutes } = useGameRoutes( - run?.gameId ?? null - ) + const { data: routes, isLoading: loadingRoutes } = useGameRoutes(run?.gameId ?? null) const createEncounter = useCreateEncounter(runIdNum) const updateEncounter = useUpdateEncounter(runIdNum) const bulkRandomize = useBulkRandomize(runIdNum) @@ -464,10 +417,8 @@ export function RunEncounters() { const [selectedRoute, setSelectedRoute] = useState(null) const [selectedBoss, setSelectedBoss] = useState(null) - const [editingEncounter, setEditingEncounter] = - useState(null) - const [selectedTeamEncounter, setSelectedTeamEncounter] = - useState(null) + const [editingEncounter, setEditingEncounter] = useState(null) + const [selectedTeamEncounter, setSelectedTeamEncounter] = useState(null) const [showEndRun, setShowEndRun] = useState(false) const [showHofModal, setShowHofModal] = useState(false) const [showShinyModal, setShowShinyModal] = useState(false) @@ -511,32 +462,31 @@ export function RunEncounters() { [run?.transferEncounterIds] ) - const { normalEncounters, shinyEncounters, transferEncounters } = - useMemo(() => { - if (!run) - return { - normalEncounters: [], - shinyEncounters: [], - transferEncounters: [], - } - const normal: EncounterDetail[] = [] - const shiny: EncounterDetail[] = [] - const transfer: EncounterDetail[] = [] - for (const enc of run.encounters) { - if (transferIdSet.has(enc.id)) { - transfer.push(enc) - } else if (enc.isShiny) { - shiny.push(enc) - } else { - normal.push(enc) - } - } + const { normalEncounters, shinyEncounters, transferEncounters } = useMemo(() => { + if (!run) return { - normalEncounters: normal, - shinyEncounters: shiny, - transferEncounters: transfer, + normalEncounters: [], + shinyEncounters: [], + transferEncounters: [], } - }, [run, transferIdSet]) + const normal: EncounterDetail[] = [] + const shiny: EncounterDetail[] = [] + const transfer: EncounterDetail[] = [] + for (const enc of run.encounters) { + if (transferIdSet.has(enc.id)) { + transfer.push(enc) + } else if (enc.isShiny) { + shiny.push(enc) + } else { + normal.push(enc) + } + } + return { + normalEncounters: normal, + shinyEncounters: shiny, + transferEncounters: transfer, + } + }, [run, transferIdSet]) // Map routeId → encounter for quick lookup (normal encounters only) const encounterByRoute = useMemo(() => { @@ -638,9 +588,7 @@ export function RunEncounters() { const currentLevelCap = useMemo(() => { if (!nextBoss) { // All defeated — no cap (or use last boss's level) - return sortedBosses.length > 0 - ? sortedBosses[sortedBosses.length - 1].levelCap - : null + return sortedBosses.length > 0 ? sortedBosses[sortedBosses.length - 1]!.levelCap : null } return nextBoss.levelCap }, [nextBoss, sortedBosses]) @@ -650,8 +598,8 @@ export function RunEncounters() { const sectionDividerAfterBoss = useMemo(() => { const map = new Map() for (let i = 0; i < sortedBosses.length - 1; i++) { - const current = sortedBosses[i] - const next = sortedBosses[i + 1] + const current = sortedBosses[i]! + const next = sortedBosses[i + 1]! if (next.section != null && current.section !== next.section) { map.set(current.id, next.section) } @@ -677,8 +625,7 @@ export function RunEncounters() { useEffect(() => { if (organizedRoutes.length === 0 || expandedGroups.size > 0) return const firstUnvisited = organizedRoutes.find( - (r) => - r.children.length > 0 && getGroupEncounter(r, encounterByRoute) === null + (r) => r.children.length > 0 && getGroupEncounter(r, encounterByRoute) === null ) if (firstUnvisited) { updateExpandedGroups(() => new Set([firstUnvisited.id])) @@ -699,9 +646,7 @@ export function RunEncounters() { const dead = useMemo( () => sortEncounters( - normalEncounters.filter( - (e) => e.status === 'caught' && e.faintLevel !== null - ), + normalEncounters.filter((e) => e.status === 'caught' && e.faintLevel !== null), teamSort ), [normalEncounters, teamSort] @@ -728,10 +673,7 @@ export function RunEncounters() { Failed to load run. - + Back to runs @@ -803,10 +745,10 @@ export function RunEncounters() { const handleUpdate = (data: { id: number data: { - nickname?: string - status?: EncounterStatus - faintLevel?: number - deathCause?: string + nickname?: string | undefined + status?: EncounterStatus | undefined + faintLevel?: number | undefined + deathCause?: string | undefined } }) => { updateEncounter.mutate(data, { @@ -852,14 +794,10 @@ export function RunEncounters() { - - {run.name} - + {run.name} {run.game.name} ·{' '} - {run.game.region.charAt(0).toUpperCase() + - run.game.region.slice(1)}{' '} - · Started{' '} + {run.game.region.charAt(0).toUpperCase() + run.game.region.slice(1)} · Started{' '} {new Date(run.startedAt).toLocaleDateString(undefined, { year: 'numeric', month: 'short', @@ -959,40 +897,36 @@ export function RunEncounters() { - {run.status === 'completed' && - run.genlocke && - !run.genlocke.isFinalLeg && ( - { - if (hofTeam && hofTeam.length > 0) { - setShowTransferModal(true) - } else { - advanceLeg.mutate( - { - genlockeId: run.genlocke!.genlockeId, - legOrder: run.genlocke!.legOrder, + {run.status === 'completed' && run.genlocke && !run.genlocke.isFinalLeg && ( + { + if (hofTeam && hofTeam.length > 0) { + setShowTransferModal(true) + } else { + advanceLeg.mutate( + { + genlockeId: run.genlocke!.genlockeId, + legOrder: run.genlocke!.legOrder, + }, + { + onSuccess: (genlocke) => { + const nextLeg = genlocke.legs.find( + (l) => l.legOrder === run.genlocke!.legOrder + 1 + ) + if (nextLeg?.runId) { + navigate(`/runs/${nextLeg.runId}`) + } }, - { - onSuccess: (genlocke) => { - const nextLeg = genlocke.legs.find( - (l) => l.legOrder === run.genlocke!.legOrder + 1 - ) - if (nextLeg?.runId) { - navigate(`/runs/${nextLeg.runId}`) - } - }, - } - ) - } - }} - disabled={advanceLeg.isPending} - className="px-4 py-2 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 transition-colors" - > - {advanceLeg.isPending - ? 'Advancing...' - : 'Advance to Next Leg'} - - )} + } + ) + } + }} + disabled={advanceLeg.isPending} + className="px-4 py-2 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 transition-colors" + > + {advanceLeg.isPending ? 'Advancing...' : 'Advance to Next Leg'} + + )} {/* HoF Team Display */} {run.status === 'completed' && ( @@ -1016,14 +950,10 @@ export function RunEncounters() { return ( {dp.spriteUrl ? ( - + ) : ( - {dp.name[0].toUpperCase()} + {dp.name[0]?.toUpperCase()} )} @@ -1045,19 +975,10 @@ export function RunEncounters() { {/* Stats */} - + - + {/* Level Cap Bar */} @@ -1123,9 +1044,7 @@ export function RunEncounters() { {/* Rules */} - - Active Rules - + Active Rules @@ -1180,11 +1099,7 @@ export function RunEncounters() { setSelectedTeamEncounter(enc) - : undefined - } + onClick={isActive ? () => setSelectedTeamEncounter(enc) : undefined} /> ))} @@ -1200,11 +1115,7 @@ export function RunEncounters() { key={enc.id} encounter={enc} showFaintLevel - onClick={ - isActive - ? () => setSelectedTeamEncounter(enc) - : undefined - } + onClick={isActive ? () => setSelectedTeamEncounter(enc) : undefined} /> ))} @@ -1220,9 +1131,7 @@ export function RunEncounters() { setSelectedTeamEncounter(enc) : undefined - } + onEncounterClick={isActive ? (enc) => setSelectedTeamEncounter(enc) : undefined} /> )} @@ -1238,9 +1147,7 @@ export function RunEncounters() { setSelectedTeamEncounter(enc) : undefined - } + onClick={isActive ? () => setSelectedTeamEncounter(enc) : undefined} /> ))} @@ -1251,9 +1158,7 @@ export function RunEncounters() { - - Encounters - + Encounters {isActive && completedCount < totalLocations && ( { const remaining = totalLocations - completedCount if ( - window.confirm( - `Randomize encounters for all ${remaining} remaining locations?` - ) + window.confirm(`Randomize encounters for all ${remaining} remaining locations?`) ) { bulkRandomize.mutate() } @@ -1325,9 +1228,7 @@ export function RunEncounters() { {filteredRoutes.map((route) => { // Collect all route IDs to check for boss cards after const routeIds: number[] = - route.children.length > 0 - ? [route.id, ...route.children.map((c) => c.id)] - : [route.id] + route.children.length > 0 ? [route.id, ...route.children.map((c) => c.id)] : [route.id] // Find boss battles positioned after this route (or any of its children) const bossesHere: BossBattle[] = [] @@ -1361,9 +1262,7 @@ export function RunEncounters() { onClick={() => handleRouteClick(route)} className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-left transition-colors hover:bg-gray-100 dark:hover:bg-gray-700/50 ${si.bg}`} > - + {route.name} @@ -1381,20 +1280,14 @@ export function RunEncounters() { {encounter.nickname ?? encounter.pokemon.name} {encounter.status === 'caught' && encounter.faintLevel !== null && - (encounter.deathCause - ? ` — ${encounter.deathCause}` - : ' (dead)')} + (encounter.deathCause ? ` — ${encounter.deathCause}` : ' (dead)')} ) : ( route.encounterMethods.length > 0 && ( {route.encounterMethods.map((m) => ( - + ))} ) @@ -1449,7 +1342,7 @@ export function RunEncounters() { return ( - + {boss.spriteUrl && ( - + )} @@ -1488,13 +1373,10 @@ export function RunEncounters() { {bossTypeLabel[boss.bossType] ?? boss.bossType} - {boss.specialtyType && ( - - )} + {boss.specialtyType && } - {boss.location} · Level Cap:{' '} - {boss.levelCap} + {boss.location} · Level Cap: {boss.levelCap} @@ -1515,10 +1397,7 @@ export function RunEncounters() { {/* Boss pokemon team */} {isBossExpanded && boss.pokemon.length > 0 && ( - + )} {sectionAfter && ( diff --git a/frontend/src/pages/RunList.tsx b/frontend/src/pages/RunList.tsx index cc2cae0..7718619 100644 --- a/frontend/src/pages/RunList.tsx +++ b/frontend/src/pages/RunList.tsx @@ -3,8 +3,7 @@ import { useRuns } from '../hooks/useRuns' import type { RunStatus } from '../types' const statusStyles: Record = { - active: - 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300', + active: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300', completed: 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300', failed: 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300', } @@ -15,9 +14,7 @@ export function RunList() { return ( - - Your Runs - + Your Runs - - {title} - + {title} {pokemon.length === 0 ? ( No data ) : ( <> {visible.map((p, i) => ( - - - {i + 1}. - + + {i + 1}. {p.spriteUrl ? ( - + ) : ( )} - - {p.name} - + {p.name} {p.count} @@ -130,14 +110,10 @@ function HorizontalBar({ /> {label} @@ -150,18 +126,10 @@ function HorizontalBar({ ) } -function Section({ - title, - children, -}: { - title: string - children: React.ReactNode -}) { +function Section({ title, children }: { title: string; children: React.ReactNode }) { return ( - - {title} - + {title} {children} ) @@ -178,19 +146,13 @@ function StatsContent({ stats }: { stats: StatsResponse }) { - + Win Rate:{' '} - - {pct(stats.winRate)} - + {pct(stats.winRate)} Avg Duration:{' '} @@ -211,8 +173,7 @@ function StatsContent({ stats }: { stats: StatsResponse }) { label={g.gameName} value={g.count} max={gameMax} - colorHex={g.gameColor ?? undefined} - color={g.gameColor ? undefined : 'bg-blue-500'} + {...(g.gameColor ? { colorHex: g.gameColor } : { color: 'bg-blue-500' })} /> ))} @@ -244,9 +205,7 @@ function StatsContent({ stats }: { stats: StatsResponse }) { Catch Rate:{' '} - - {pct(stats.catchRate)} - + {pct(stats.catchRate)} Avg per Run:{' '} @@ -261,44 +220,31 @@ function StatsContent({ stats }: { stats: StatsResponse }) { - + {/* Team & Deaths */} - + {pct(stats.mortalityRate)} - - Mortality Rate - + Mortality Rate {fmt(stats.avgCatchLevel)} - - Avg Catch Lv. - + Avg Catch Lv. {fmt(stats.avgFaintLevel)} - - Avg Faint Lv. - + Avg Faint Lv. @@ -310,12 +256,8 @@ function StatsContent({ stats }: { stats: StatsResponse }) { {stats.topDeathCauses.map((d, i) => ( - - {i + 1}. - - - {d.cause} - + {i + 1}. + {d.cause} {d.count} @@ -351,9 +293,7 @@ export function Stats() { return ( - - Stats - + Stats {isLoading && ( @@ -370,9 +310,7 @@ export function Stats() { {stats && stats.totalRuns === 0 && ( No data yet - - Start a Nuzlocke run to see your stats here. - + Start a Nuzlocke run to see your stats here. )} diff --git a/frontend/src/pages/admin/AdminEvolutions.tsx b/frontend/src/pages/admin/AdminEvolutions.tsx index 65fbb3b..0e048b3 100644 --- a/frontend/src/pages/admin/AdminEvolutions.tsx +++ b/frontend/src/pages/admin/AdminEvolutions.tsx @@ -11,11 +11,7 @@ import { } from '../../hooks/useAdmin' import { exportEvolutions } from '../../api/admin' import { downloadJson } from '../../utils/download' -import type { - EvolutionAdmin, - CreateEvolutionInput, - UpdateEvolutionInput, -} from '../../types' +import type { EvolutionAdmin, CreateEvolutionInput, UpdateEvolutionInput } from '../../types' const PAGE_SIZE = 50 @@ -67,9 +63,7 @@ export function AdminEvolutions() { header: 'To', accessor: (e) => ( - {e.toPokemon.spriteUrl && ( - - )} + {e.toPokemon.spriteUrl && } {e.toPokemon.name} ), @@ -163,8 +157,7 @@ export function AdminEvolutions() { {totalPages > 1 && ( - Showing {offset + 1}-{Math.min(offset + PAGE_SIZE, total)} of{' '} - {total} + Showing {offset + 1}-{Math.min(offset + PAGE_SIZE, total)} of {total} void }) { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id: group.id }) + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id: group.id, + }) const style = { transform: CSS.Transform.toString(transform), @@ -127,9 +119,7 @@ function SortableRouteGroup({ - - {group.order} - + {group.order} {group.name} {group.pinwheelZone != null ? group.pinwheelZone : '\u2014'} @@ -155,9 +145,7 @@ function SortableRouteGroup({ {child.order} - - {'\u2514'} - + {'\u2514'} {child.name} @@ -191,14 +179,9 @@ function SortableBossRow({ onPositionChange: (bossId: number, afterRouteId: number | null) => void onClick: (b: BossBattle) => void }) { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id: boss.id }) + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id: boss.id, + }) const style = { transform: CSS.Transform.toString(transform), @@ -247,15 +230,9 @@ function SortableBossRow({ {boss.bossType.replace('_', ' ')} - {boss.specialtyType ? ( - - ) : ( - '\u2014' - )} - - - {boss.section ?? '\u2014'} + {boss.specialtyType ? : '\u2014'} + {boss.section ?? '\u2014'} {boss.location} {boss.levelCap} - - {boss.pokemon.length} - + {boss.pokemon.length} ) } @@ -315,16 +290,12 @@ export function AdminGameDetail() { useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ) - if (isLoading) - return Loading... - if (!game) - return Game not found + if (isLoading) return Loading... + if (!game) return Game not found const routes = game.routes ?? [] const routeGroups = organizeRoutes(routes) - const versionGroupGames = (allGames ?? []).filter( - (g) => g.versionGroupId === game.versionGroupId - ) + const versionGroupGames = (allGames ?? []).filter((g) => g.versionGroupId === game.versionGroupId) const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event @@ -336,7 +307,7 @@ export function AdminGameDetail() { const reordered = [...routeGroups] const [moved] = reordered.splice(oldIndex, 1) - reordered.splice(newIndex, 0, moved) + reordered.splice(newIndex, 0, moved!) // Flatten groups back to individual routes with sequential order numbers let order = 1 @@ -361,7 +332,7 @@ export function AdminGameDetail() { const reordered = [...bosses] const [moved] = reordered.splice(oldIndex, 1) - reordered.splice(newIndex, 0, moved) + reordered.splice(newIndex, 0, moved!) const newOrders = reordered.map((b, i) => ({ id: b.id, @@ -383,8 +354,8 @@ export function AdminGameDetail() { {game.name} - {game.region.charAt(0).toUpperCase() + game.region.slice(1)} · - Gen {game.generation} + {game.region.charAt(0).toUpperCase() + game.region.slice(1)} · Gen{' '} + {game.generation} {game.releaseYear ? ` \u00b7 ${game.releaseYear}` : ''} @@ -500,11 +471,7 @@ export function AdminGameDetail() { {showCreate && ( 0 - ? Math.max(...routes.map((r) => r.order)) + 1 - : 1 - } + nextOrder={routes.length > 0 ? Math.max(...routes.map((r) => r.order)) + 1 : 1} onSubmit={(data) => createRoute.mutate(data as CreateRouteInput, { onSuccess: () => setShowCreate(false), @@ -655,9 +622,7 @@ export function AdminGameDetail() { b.order)) + 1 : 1 - } + nextOrder={bosses ? Math.max(0, ...bosses.map((b) => b.order)) + 1 : 1} onSubmit={(data) => createBoss.mutate(data as CreateBossBattleInput, { onSuccess: () => setShowCreateBoss(false), diff --git a/frontend/src/pages/admin/AdminGames.tsx b/frontend/src/pages/admin/AdminGames.tsx index 8e47db7..e199f38 100644 --- a/frontend/src/pages/admin/AdminGames.tsx +++ b/frontend/src/pages/admin/AdminGames.tsx @@ -2,11 +2,7 @@ import { useState, useMemo } from 'react' import { AdminTable, type Column } from '../../components/admin/AdminTable' import { GameFormModal } from '../../components/admin/GameFormModal' import { useGames } from '../../hooks/useGames' -import { - useCreateGame, - useUpdateGame, - useDeleteGame, -} from '../../hooks/useAdmin' +import { useCreateGame, useUpdateGame, useDeleteGame } from '../../hooks/useAdmin' import { exportGames } from '../../api/admin' import { downloadJson } from '../../utils/download' import type { Game, CreateGameInput, UpdateGameInput } from '../../types' @@ -22,10 +18,7 @@ export function AdminGames() { const [regionFilter, setRegionFilter] = useState('') const [genFilter, setGenFilter] = useState('') - const regions = useMemo( - () => [...new Set(games.map((g) => g.region))].sort(), - [games] - ) + const regions = useMemo(() => [...new Set(games.map((g) => g.region))].sort(), [games]) const generations = useMemo( () => [...new Set(games.map((g) => g.generation))].sort((a, b) => a - b), [games] @@ -34,8 +27,7 @@ export function AdminGames() { const filteredGames = useMemo(() => { let result = games if (regionFilter) result = result.filter((g) => g.region === regionFilter) - if (genFilter) - result = result.filter((g) => g.generation === Number(genFilter)) + if (genFilter) result = result.filter((g) => g.generation === Number(genFilter)) return result }, [games, regionFilter, genFilter]) diff --git a/frontend/src/pages/admin/AdminGenlockeDetail.tsx b/frontend/src/pages/admin/AdminGenlockeDetail.tsx index ed0925b..5153df1 100644 --- a/frontend/src/pages/admin/AdminGenlockeDetail.tsx +++ b/frontend/src/pages/admin/AdminGenlockeDetail.tsx @@ -28,23 +28,18 @@ export function AdminGenlockeDetail() { const [addingLeg, setAddingLeg] = useState(false) const [selectedGameId, setSelectedGameId] = useState('') - if (isLoading) - return Loading... - if (!genlocke) - return ( - Genlocke not found - ) + if (isLoading) return Loading... + if (!genlocke) return Genlocke not found const editName = name ?? genlocke.name const editStatus = status ?? genlocke.status - const hasChanges = - editName !== genlocke.name || editStatus !== genlocke.status + const hasChanges = editName !== genlocke.name || editStatus !== genlocke.status const handleSave = () => { const data: Record = {} - if (editName !== genlocke.name) data.name = editName - if (editStatus !== genlocke.status) data.status = editStatus + if (editName !== genlocke.name) data['name'] = editName + if (editStatus !== genlocke.status) data['status'] = editStatus if (Object.keys(data).length === 0) return updateGenlocke.mutate( { id, data }, @@ -77,9 +72,7 @@ export function AdminGenlockeDetail() { Genlockes {' / '} - - {genlocke.name} - + {genlocke.name} {/* Header */} @@ -131,22 +124,16 @@ export function AdminGenlockeDetail() { {/* Rules (read-only) */} - - Rules - + Rules - - Genlocke rules: - + Genlocke rules: {JSON.stringify(genlocke.genlockeRules, null, 2)} - - Nuzlocke rules: - + Nuzlocke rules: {JSON.stringify(genlocke.nuzlockeRules, null, 2)} @@ -157,9 +144,7 @@ export function AdminGenlockeDetail() { {/* Legs */} - - Legs ({genlocke.legs.length}) - + Legs ({genlocke.legs.length}) setAddingLeg(!addingLeg)} className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700" @@ -172,9 +157,7 @@ export function AdminGenlockeDetail() { - setSelectedGameId(e.target.value ? Number(e.target.value) : '') - } + onChange={(e) => setSelectedGameId(e.target.value ? Number(e.target.value) : '')} className="flex-1 px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600" > Select a game... @@ -239,12 +222,8 @@ export function AdminGenlockeDetail() { {genlocke.legs.map((leg) => ( - - {leg.legOrder} - - - {leg.game.name} - + {leg.legOrder} + {leg.game.name} {leg.runId ? ( — )} - - {leg.encounterCount} - - - {leg.deathCount} - + {leg.encounterCount} + {leg.deathCount} deleteLeg.mutate(leg.id)} @@ -305,9 +280,7 @@ export function AdminGenlockeDetail() { {/* Stats */} - - Stats - + Stats Legs @@ -317,20 +290,14 @@ export function AdminGenlockeDetail() { Encounters - - {genlocke.stats.totalEncounters} - + {genlocke.stats.totalEncounters} Deaths - - {genlocke.stats.totalDeaths} - + {genlocke.stats.totalDeaths} - - Survival Rate - + Survival Rate {genlocke.stats.totalEncounters > 0 ? `${Math.round(((genlocke.stats.totalEncounters - genlocke.stats.totalDeaths) / genlocke.stats.totalEncounters) * 100)}%` diff --git a/frontend/src/pages/admin/AdminPokemon.tsx b/frontend/src/pages/admin/AdminPokemon.tsx index 52e38c8..7ef53c0 100644 --- a/frontend/src/pages/admin/AdminPokemon.tsx +++ b/frontend/src/pages/admin/AdminPokemon.tsx @@ -11,11 +11,7 @@ import { } from '../../hooks/useAdmin' import { exportPokemon } from '../../api/admin' import { downloadJson } from '../../utils/download' -import type { - Pokemon, - CreatePokemonInput, - UpdatePokemonInput, -} from '../../types' +import type { Pokemon, CreatePokemonInput, UpdatePokemonInput } from '../../types' const PAGE_SIZE = 50 @@ -164,8 +160,7 @@ export function AdminPokemon() { {totalPages > 1 && ( - Showing {offset + 1}-{Math.min(offset + PAGE_SIZE, total)} of{' '} - {total} + Showing {offset + 1}-{Math.min(offset + PAGE_SIZE, total)} of {total} - bulkImport.mutateAsync( - items as Parameters[0] - ) + bulkImport.mutateAsync(items as Parameters[0]) } onClose={() => setShowBulkImport(false)} /> diff --git a/frontend/src/pages/admin/AdminRouteDetail.tsx b/frontend/src/pages/admin/AdminRouteDetail.tsx index c750e6f..1184cfc 100644 --- a/frontend/src/pages/admin/AdminRouteDetail.tsx +++ b/frontend/src/pages/admin/AdminRouteDetail.tsx @@ -46,8 +46,7 @@ export function AdminRouteDetail() { ) const currentIndex = sortedRoutes.findIndex((r) => r.id === rId) const route = currentIndex >= 0 ? sortedRoutes[currentIndex] : undefined - const prevRoute = - currentIndex > 0 ? sortedRoutes[currentIndex - 1] : undefined + const prevRoute = currentIndex > 0 ? sortedRoutes[currentIndex - 1] : undefined const nextRoute = currentIndex >= 0 && currentIndex < sortedRoutes.length - 1 ? sortedRoutes[currentIndex + 1] @@ -55,9 +54,7 @@ export function AdminRouteDetail() { const childRoutes = useMemo( () => - (game?.routes ?? []) - .filter((r) => r.parentRouteId === rId) - .sort((a, b) => a.order - b.order), + (game?.routes ?? []).filter((r) => r.parentRouteId === rId).sort((a, b) => a.order - b.order), [game?.routes, rId] ) @@ -72,11 +69,7 @@ export function AdminRouteDetail() { accessor: (e) => ( {e.pokemon.spriteUrl ? ( - + ) : null} #{e.pokemon.nationalDex} {e.pokemon.name} @@ -89,9 +82,7 @@ export function AdminRouteDetail() { { header: 'Levels', accessor: (e) => - e.minLevel === e.maxLevel - ? `Lv ${e.minLevel}` - : `Lv ${e.minLevel}-${e.maxLevel}`, + e.minLevel === e.maxLevel ? `Lv ${e.minLevel}` : `Lv ${e.minLevel}-${e.maxLevel}`, }, ] @@ -109,9 +100,7 @@ export function AdminRouteDetail() { - navigate(`/admin/games/${gId}/routes/${e.target.value}`) - } + onChange={(e) => navigate(`/admin/games/${gId}/routes/${e.target.value}`)} > {sortedRoutes.map((r) => ( @@ -175,12 +164,9 @@ export function AdminRouteDetail() { {showCreate && ( - addEncounter.mutate( - { ...data, gameId: gId } as CreateRouteEncounterInput, - { - onSuccess: () => setShowCreate(false), - } - ) + addEncounter.mutate({ ...data, gameId: gId } as CreateRouteEncounterInput, { + onSuccess: () => setShowCreate(false), + }) } onClose={() => setShowCreate(false)} isSubmitting={addEncounter.isPending} @@ -213,9 +199,7 @@ export function AdminRouteDetail() { {/* Sub-areas */} - - Sub-areas ({childRoutes.length}) - + Sub-areas ({childRoutes.length}) setShowCreateChild(true)} className="px-3 py-1.5 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700" @@ -224,16 +208,11 @@ export function AdminRouteDetail() { {childRoutes.length === 0 ? ( - - No sub-areas for this route. - + No sub-areas for this route. ) : ( {childRoutes.map((child) => ( - + - createRoute.mutate( - { ...data, parentRouteId: rId } as CreateRouteInput, - { onSuccess: () => setShowCreateChild(false) } - ) + createRoute.mutate({ ...data, parentRouteId: rId } as CreateRouteInput, { + onSuccess: () => setShowCreateChild(false), + }) } onClose={() => setShowCreateChild(false)} isSubmitting={createRoute.isPending} diff --git a/frontend/src/pages/admin/AdminRuns.tsx b/frontend/src/pages/admin/AdminRuns.tsx index bd9bd6c..8ee1229 100644 --- a/frontend/src/pages/admin/AdminRuns.tsx +++ b/frontend/src/pages/admin/AdminRuns.tsx @@ -14,16 +14,12 @@ export function AdminRuns() { const [statusFilter, setStatusFilter] = useState('') const [gameFilter, setGameFilter] = useState('') - const gameMap = useMemo( - () => new Map(games.map((g) => [g.id, g.name])), - [games] - ) + const gameMap = useMemo(() => new Map(games.map((g) => [g.id, g.name])), [games]) const filteredRuns = useMemo(() => { let result = runs if (statusFilter) result = result.filter((r) => r.status === statusFilter) - if (gameFilter) - result = result.filter((r) => r.gameId === Number(gameFilter)) + if (gameFilter) result = result.filter((r) => r.gameId === Number(gameFilter)) return result }, [runs, statusFilter, gameFilter]) @@ -31,10 +27,7 @@ export function AdminRuns() { () => [ ...new Map( - runs.map((r) => [ - r.gameId, - gameMap.get(r.gameId) ?? `Game #${r.gameId}`, - ]) + runs.map((r) => [r.gameId, gameMap.get(r.gameId) ?? `Game #${r.gameId}`]) ).entries(), ].sort((a, b) => a[1].localeCompare(b[1])), [runs, gameMap] diff --git a/frontend/src/types/game.ts b/frontend/src/types/game.ts index c829f2a..8929781 100644 --- a/frontend/src/types/game.ts +++ b/frontend/src/types/game.ts @@ -1,9 +1,4 @@ -export type GameCategory = - | 'original' - | 'remake' - | 'enhanced' - | 'sequel' - | 'spinoff' +export type GameCategory = 'original' | 'remake' | 'enhanced' | 'sequel' | 'spinoff' export interface Game { id: number @@ -152,19 +147,19 @@ export interface UpdateRunInput { export interface CreateEncounterInput { routeId: number pokemonId: number - nickname?: string + nickname?: string | undefined status: EncounterStatus - catchLevel?: number - isShiny?: boolean - origin?: string + catchLevel?: number | undefined + isShiny?: boolean | undefined + origin?: string | undefined } export interface UpdateEncounterInput { - nickname?: string - status?: EncounterStatus - faintLevel?: number - deathCause?: string - currentPokemonId?: number + nickname?: string | undefined + status?: EncounterStatus | undefined + faintLevel?: number | undefined + deathCause?: string | undefined + currentPokemonId?: number | undefined } // Boss battles diff --git a/frontend/src/types/rules.ts b/frontend/src/types/rules.ts index 4b669db..846e39d 100644 --- a/frontend/src/types/rules.ts +++ b/frontend/src/types/rules.ts @@ -60,8 +60,7 @@ export const RULE_DEFINITIONS: RuleDefinition[] = [ { key: 'nicknameRequired', name: 'Nickname Required', - description: - 'All caught Pokémon must be given a nickname to form a stronger bond.', + description: 'All caught Pokémon must be given a nickname to form a stronger bond.', category: 'core', }, { @@ -90,8 +89,7 @@ export const RULE_DEFINITIONS: RuleDefinition[] = [ { key: 'hardcoreMode', name: 'Hardcore Mode', - description: - 'No items may be used during battle. Held items are still allowed.', + description: 'No items may be used during battle. Held items are still allowed.', category: 'difficulty', }, { diff --git a/frontend/src/utils/formatEvolution.ts b/frontend/src/utils/formatEvolution.ts index d78f36e..1d9530d 100644 --- a/frontend/src/utils/formatEvolution.ts +++ b/frontend/src/utils/formatEvolution.ts @@ -11,15 +11,11 @@ export function formatEvolutionMethod(evo: { } else if (evo.trigger === 'level-up') { parts.push('Level up') } else if (evo.trigger === 'use-item' && evo.item) { - parts.push( - evo.item.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) - ) + parts.push(evo.item.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())) } else if (evo.trigger === 'trade') { parts.push('Trade') } else { - parts.push( - evo.trigger.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) - ) + parts.push(evo.trigger.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())) } if (evo.heldItem) { parts.push( diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index a9b5a59..b3d38bf 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -18,6 +18,10 @@ /* Linting */ "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, "noUnusedLocals": true, "noUnusedParameters": true, "erasableSyntaxOnly": true, diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 1100cfc..212b05d 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,3 +1,4 @@ +/// import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' @@ -5,6 +6,9 @@ import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], + test: { + environment: 'jsdom', + }, server: { proxy: { '/api': {
- {boss.location} -
{boss.location}
- No pokemon found -
No pokemon found
- {route.name} -
{route.name}
- How did your run end? -
How did your run end?
- {description} -
{description}
The fundamental rules of a Nuzlocke challenge
When is the run considered complete
- Loading evolutions... -
Loading evolutions...
- No evolutions available -
No evolutions available
- {displayPokemon.name} shed its shell! Would you also like to - add{' '} - - {shedCompanion.toPokemon.name} - - ? + {displayPokemon.name} shed its shell! Would you also like to add{' '} + {shedCompanion.toPokemon.name}?
- Selected Pokemon will be bred down to their base form and appear as - level 1 encounters in the next leg. + Selected Pokemon will be bred down to their base form and appear as level 1 encounters + in the next leg.
- {createdLabel}: {result.created}, {updatedLabel}:{' '} - {result.updated} + {createdLabel}: {result.created}, {updatedLabel}: {result.updated}
- {message} -
- {error} -
{message}
{error}
- Loading... -
Loading...
No evolutions
- No evolutions -
- No encounters -
No encounters
- Routes in the same zone share an encounter when the Pinwheel Clause is - active + Routes in the same zone share an encounter when the Pinwheel Clause is active
{new Date(run.startedAt).toLocaleDateString(undefined, { year: 'numeric', diff --git a/frontend/src/pages/NewGenlocke.tsx b/frontend/src/pages/NewGenlocke.tsx index 4752340..5446ca4 100644 --- a/frontend/src/pages/NewGenlocke.tsx +++ b/frontend/src/pages/NewGenlocke.tsx @@ -18,10 +18,7 @@ interface LegEntry { type PresetType = 'true' | 'normal' | 'custom' | null -function buildLegsFromPreset( - regions: Region[], - preset: 'true' | 'normal' -): LegEntry[] { +function buildLegsFromPreset(regions: Region[], preset: 'true' | 'normal'): LegEntry[] { const legs: LegEntry[] = [] for (const region of regions) { const targetSlug = @@ -45,8 +42,7 @@ export function NewGenlocke() { const [name, setName] = useState('') const [legs, setLegs] = useState([]) const [preset, setPreset] = useState(null) - const [nuzlockeRules, setNuzlockeRules] = - useState(DEFAULT_RULES) + const [nuzlockeRules, setNuzlockeRules] = useState(DEFAULT_RULES) const [genlockeRules, setGenlockeRules] = useState({ retireHoF: false, }) @@ -64,9 +60,7 @@ export function NewGenlocke() { } const handleGameChange = (index: number, game: Game) => { - setLegs((prev) => - prev.map((leg, i) => (i === index ? { ...leg, game } : leg)) - ) + setLegs((prev) => prev.map((leg, i) => (i === index ? { ...leg, game } : leg))) } const handleRemoveLeg = (index: number) => { @@ -75,8 +69,7 @@ export function NewGenlocke() { const handleAddLeg = (region: Region) => { const defaultSlug = region.genlockeDefaults.normalGenlocke - const game = - region.games.find((g) => g.slug === defaultSlug) ?? region.games[0] + const game = region.games.find((g) => g.slug === defaultSlug) ?? region.games[0] if (game) { setLegs((prev) => [...prev, { region: region.name, game }]) } @@ -87,7 +80,7 @@ export function NewGenlocke() { if (target < 0 || target >= legs.length) return setLegs((prev) => { const next = [...prev] - ;[next[index], next[target]] = [next[target], next[index]] + ;[next[index], next[target]] = [next[target]!, next[index]!] return next }) } @@ -115,23 +108,16 @@ export function NewGenlocke() { ) } - const enabledRuleCount = RULE_DEFINITIONS.filter( - (r) => nuzlockeRules[r.key] - ).length + const enabledRuleCount = RULE_DEFINITIONS.filter((r) => nuzlockeRules[r.key]).length const totalRuleCount = RULE_DEFINITIONS.length // Regions not yet used in legs (for "add leg" picker) - const availableRegions = - regions?.filter((r) => !legs.some((l) => l.region === r.name)) ?? [] + const availableRegions = regions?.filter((r) => !legs.some((l) => l.region === r.name)) ?? [] return ( - - New Genlocke - - - Set up your generational challenge. - + New Genlocke + Set up your generational challenge. @@ -250,17 +236,11 @@ export function NewGenlocke() { )} {/* Also allow adding extra regions for presets */} - {preset && - preset !== 'custom' && - availableRegions.length > 0 && - legs.length > 0 && ( - - - - )} + {preset && preset !== 'custom' && availableRegions.length > 0 && legs.length > 0 && ( + + + + )} - + {/* Genlocke-specific rules */} @@ -319,8 +296,7 @@ export function NewGenlocke() { Keep Hall of Fame - Pokemon that beat the Elite Four can continue to the - next leg + Pokemon that beat the Elite Four can continue to the next leg @@ -337,8 +313,8 @@ export function NewGenlocke() { Retire Hall of Fame - Pokemon that beat the Elite Four are retired and cannot - be used in the next leg + Pokemon that beat the Elite Four are retired and cannot be used in the next + leg
- Set up your generational challenge. -
Set up your generational challenge.
- Get nickname suggestions from a themed word list when catching - Pokemon. Applied to all legs. + Get nickname suggestions from a themed word list when catching Pokemon. Applied to + all legs.
- {name} -
{name}
- Set up your run in a few steps. -
Set up your run in a few steps.
- {selectedGame.region.charAt(0).toUpperCase() + - selectedGame.region.slice(1)} + {selectedGame.region.charAt(0).toUpperCase() + selectedGame.region.slice(1)}
- Get nickname suggestions from a themed word list when catching - Pokemon. + Get nickname suggestions from a themed word list when catching Pokemon.
{run.game.name} ·{' '} - {run.game.region.charAt(0).toUpperCase() + - run.game.region.slice(1)}{' '} - · Started{' '} + {run.game.region.charAt(0).toUpperCase() + run.game.region.slice(1)} · Started{' '} {new Date(run.startedAt).toLocaleDateString(undefined, { year: 'numeric', month: 'short', @@ -204,26 +181,15 @@ export function RunDashboard() { {/* Stats */}
- No pokemon caught yet — head to encounters to start building your - team! + No pokemon caught yet — head to encounters to start building your team!
{run.game.name} ·{' '} - {run.game.region.charAt(0).toUpperCase() + - run.game.region.slice(1)}{' '} - · Started{' '} + {run.game.region.charAt(0).toUpperCase() + run.game.region.slice(1)} · Started{' '} {new Date(run.startedAt).toLocaleDateString(undefined, { year: 'numeric', month: 'short', @@ -959,40 +897,36 @@ export function RunEncounters() {
- {boss.location} · Level Cap:{' '} - {boss.levelCap} + {boss.location} · Level Cap: {boss.levelCap}
No data
No data yet
- Start a Nuzlocke run to see your stats here. -
Start a Nuzlocke run to see your stats here.
- {game.region.charAt(0).toUpperCase() + game.region.slice(1)} · - Gen {game.generation} + {game.region.charAt(0).toUpperCase() + game.region.slice(1)} · Gen{' '} + {game.generation} {game.releaseYear ? ` \u00b7 ${game.releaseYear}` : ''}
{JSON.stringify(genlocke.genlockeRules, null, 2)}
{JSON.stringify(genlocke.nuzlockeRules, null, 2)}
- {genlocke.stats.totalEncounters} -
{genlocke.stats.totalEncounters}
- {genlocke.stats.totalDeaths} -
{genlocke.stats.totalDeaths}
{genlocke.stats.totalEncounters > 0 ? `${Math.round(((genlocke.stats.totalEncounters - genlocke.stats.totalDeaths) / genlocke.stats.totalEncounters) * 100)}%` diff --git a/frontend/src/pages/admin/AdminPokemon.tsx b/frontend/src/pages/admin/AdminPokemon.tsx index 52e38c8..7ef53c0 100644 --- a/frontend/src/pages/admin/AdminPokemon.tsx +++ b/frontend/src/pages/admin/AdminPokemon.tsx @@ -11,11 +11,7 @@ import { } from '../../hooks/useAdmin' import { exportPokemon } from '../../api/admin' import { downloadJson } from '../../utils/download' -import type { - Pokemon, - CreatePokemonInput, - UpdatePokemonInput, -} from '../../types' +import type { Pokemon, CreatePokemonInput, UpdatePokemonInput } from '../../types' const PAGE_SIZE = 50 @@ -164,8 +160,7 @@ export function AdminPokemon() { {totalPages > 1 && (
- No sub-areas for this route. -
No sub-areas for this route.