commit 01538b7e6940122b2e393f104dc99ab1dcc963ce Author: rassadin11 Date: Wed Jun 10 16:56:59 2026 +0300 deploy diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8ab46b3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +node_modules +dist +.git +.gitignore +.claude +.planning +.env.* +*.log +npm-debug.log* +.DS_Store +.vscode +.idea +.dockerignore +docker-compose.yml +README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cedcc95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +.claude +.planning +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e90dbcf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +# syntax=docker/dockerfile:1.7 + +FROM public.ecr.aws/docker/library/node:20-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +FROM public.ecr.aws/docker/library/node:20-alpine AS build +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npm run build + +FROM public.ecr.aws/nginx/nginx:1.27-alpine AS runtime +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..98f1d30 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# ЭКСА — Admin Frontend + +Standalone admin panel for ЭКСА, extracted from the main `elcsa-gitea2/frontend` app +into its own microservice. React 19 + Vite, Feature-Sliced Design. + +## Commands + +```bash +npm install # installs deps (lockfile committed) +npm run dev # dev server on port 3001 +npm run build # tsc -b && vite build +npm run preview # preview production build +npm run lint # ESLint +``` + +## Notes + +- **Auth is fully independent** of the user app. The admin API lives at + `https://app.admin.elcsa.ru` (hardcoded in `src/features/admin/api/adminApi.ts`), + with its own in-memory access token + localStorage-backed refresh token. No CSRF. +- **Routes** are preserved from the original app: the panel is served under the + obscured path `/sys-c7f29a4e-d81b-4630-ops-console`. See `src/shared/config/routes.ts`. + On a dedicated admin host this could be simplified to `/` — left unchanged to + preserve existing behavior. +- Path aliases (`@app/*`, `@pages/*`, `@widgets/*`, `@features/*`, `@shared/*`) match + the FSD layers, same as the parent project. +- Only the shared UI primitives the admin actually uses were copied + (`Button`, `FormField`, `Notification`, `PrimaryButton`). diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5e088f0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + admin-frontend: + build: + context: . + dockerfile: Dockerfile + image: elcsa-admin-frontend:latest + container_name: elcsa-admin-frontend + ports: + - "3001:80" + restart: unless-stopped diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..092408a --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,28 @@ +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' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/index.html b/index.html new file mode 100644 index 0000000..6cde3a6 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + + ЭКСА — Панель администрирования + + +
+ + + diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..aab1c08 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,21 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + gzip on; + gzip_comp_level 6; + gzip_min_length 1024; + gzip_types text/plain text/css application/javascript application/json image/svg+xml; + + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..20070df --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3382 @@ +{ + "name": "elcsa-admin", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "elcsa-admin", + "version": "0.0.1", + "dependencies": { + "@tanstack/react-query": "^5.100.9", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.1.5" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.17.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.16", + "globals": "^15.14.0", + "typescript": "~5.6.2", + "typescript-eslint": "^8.18.2", + "vite": "^6.0.5", + "vite-tsconfig-paths": "^5.1.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "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.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "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.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "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.5", + "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.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "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.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.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", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz", + "integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz", + "integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "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/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", + "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/type-utils": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.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.61.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.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.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", + "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", + "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.0", + "@typescript-eslint/types": "^8.61.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.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", + "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.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.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", + "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", + "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.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", + "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", + "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", + "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.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", + "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.0", + "@typescript-eslint/tsconfig-utils": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.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.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz", + "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", + "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "eslint-visitor-keys": "^5.0.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/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "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" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.35", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz", + "integrity": "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "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.30001797", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", + "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "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==", + "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_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", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "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", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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/electron-to-chromium": { + "version": "1.5.371", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz", + "integrity": "sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "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.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@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.14.0", + "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.5", + "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-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "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.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "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": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, + "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/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/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "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", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "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", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "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/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", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "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", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "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.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "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" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "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" + }, + "engines": { + "node": ">=6" + } + }, + "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==", + "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" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "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/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.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", + "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz", + "integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==", + "license": "MIT", + "dependencies": { + "react-router": "7.17.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "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.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "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" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz", + "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.61.0", + "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "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": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ffb24f2 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "elcsa-admin", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint ." + }, + "dependencies": { + "@tanstack/react-query": "^5.100.9", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.1.5" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.17.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.16", + "globals": "^15.14.0", + "typescript": "~5.6.2", + "typescript-eslint": "^8.18.2", + "vite": "^6.0.5", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/src/app/App.tsx b/src/app/App.tsx new file mode 100644 index 0000000..f05471a --- /dev/null +++ b/src/app/App.tsx @@ -0,0 +1,9 @@ +import { RouterProvider, QueryProvider } from './providers' + +export function App() { + return ( + + + + ) +} diff --git a/src/app/providers/QueryProvider.tsx b/src/app/providers/QueryProvider.tsx new file mode 100644 index 0000000..90b31a7 --- /dev/null +++ b/src/app/providers/QueryProvider.tsx @@ -0,0 +1,10 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import type { ReactNode } from 'react' + +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, +}) + +export function QueryProvider({ children }: { children: ReactNode }) { + return {children} +} diff --git a/src/app/providers/RouterProvider.tsx b/src/app/providers/RouterProvider.tsx new file mode 100644 index 0000000..804c8ac --- /dev/null +++ b/src/app/providers/RouterProvider.tsx @@ -0,0 +1,18 @@ +import { BrowserRouter, Route, Routes } from 'react-router-dom' +import { AdminPage } from '@pages/admin' +import { AdminOrganizationPage } from '@pages/admin-organization' +import { ROUTES } from '@shared/config/routes' +import { ScrollToTop } from './ScrollToTop' + +export function RouterProvider() { + return ( + + + + {/* Admin panel — own auth gate, independent of the user auth system */} + } /> + } /> + + + ) +} diff --git a/src/app/providers/ScrollToTop.tsx b/src/app/providers/ScrollToTop.tsx new file mode 100644 index 0000000..424e7fa --- /dev/null +++ b/src/app/providers/ScrollToTop.tsx @@ -0,0 +1,12 @@ +import { useEffect } from 'react' +import { useLocation } from 'react-router-dom' + +export function ScrollToTop() { + const { pathname } = useLocation() + + useEffect(() => { + window.scrollTo(0, 0) + }, [pathname]) + + return null +} diff --git a/src/app/providers/index.ts b/src/app/providers/index.ts new file mode 100644 index 0000000..3f7685f --- /dev/null +++ b/src/app/providers/index.ts @@ -0,0 +1,2 @@ +export { RouterProvider } from './RouterProvider' +export { QueryProvider } from './QueryProvider' diff --git a/src/app/styles/fonts.css b/src/app/styles/fonts.css new file mode 100644 index 0000000..2f3288f --- /dev/null +++ b/src/app/styles/fonts.css @@ -0,0 +1 @@ +@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap'); diff --git a/src/app/styles/global.css b/src/app/styles/global.css new file mode 100644 index 0000000..ed39c61 --- /dev/null +++ b/src/app/styles/global.css @@ -0,0 +1,32 @@ +body { + font-family: var(--font-sans); + background: var(--bg-deep); + color: var(--text-primary); + overflow-x: hidden; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + scrollbar-color: var(--grad-center) var(--bg-mid); + scrollbar-width: thin; +} + +#root { + min-height: 100vh; +} + +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: var(--bg-mid); +} + +::-webkit-scrollbar-thumb { + background: var(--grad-center); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--highlight); +} diff --git a/src/app/styles/reset.css b/src/app/styles/reset.css new file mode 100644 index 0000000..1f4aae6 --- /dev/null +++ b/src/app/styles/reset.css @@ -0,0 +1,34 @@ +*, +*::before, +*::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +button { + font: inherit; + color: inherit; + background: none; + border: none; + cursor: pointer; +} + +a { + color: inherit; + text-decoration: none; +} + +input { + font: inherit; +} + +img, +svg { + display: block; + max-width: 100%; +} diff --git a/src/app/styles/variables.css b/src/app/styles/variables.css new file mode 100644 index 0000000..70f1f06 --- /dev/null +++ b/src/app/styles/variables.css @@ -0,0 +1,20 @@ +:root { + --bg-deep: #0a0b2e; + --bg-mid: #1b1547; + --grad-edge: #3d2a8e; + --grad-center: #5b3db8; + + --text-primary: #ffffff; + --text-secondary: #b5b0cc; + + --interactive: #4a6dff; + --highlight: #00d4ff; + --success: #26a17b; + --error: #ff4466; + + --glass-border: rgba(255, 255, 255, 0.08); + --glass-bg: rgba(255, 255, 255, 0.04); + + --font-sans: 'Manrope', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', monospace; +} diff --git a/src/features/admin/api/adminApi.ts b/src/features/admin/api/adminApi.ts new file mode 100644 index 0000000..2c89c78 --- /dev/null +++ b/src/features/admin/api/adminApi.ts @@ -0,0 +1,219 @@ +import type { + AdminLoginRequest, + AdminLoginResponse, + AdminMeResponse, + AdminRefreshResponse, + CreateOrganizationRequest, + CreateWalletsResponse, + WalletResponse, + DocumentResponse, + DocumentTypeSlug, + Organization, + OrganizationListResponse, + PurchaseRequestListResponse, + UpdateOrganizationRequest, +} from '../model/types' + +const ADMIN_API_URL = 'https://app.admin.elcsa.ru' + +// In-memory admin access token — deliberately separate from the user `tokenStore` +// so the two independent auth systems never collide. No CSRF on the admin API. +let adminToken: string | null = null + +export const adminTokenStore = { + get: () => adminToken, + set: (token: string) => { adminToken = token }, + clear: () => { adminToken = null }, +} + +// The refresh token is body-based (sent in the `/v1/auth/refresh` request body), +// so it must persist across reloads to restore a session. localStorage-backed. +const ADMIN_REFRESH_KEY = 'admin_refresh_token' + +export const adminRefreshStore = { + get: (): string | null => { + try { return localStorage.getItem(ADMIN_REFRESH_KEY) } catch { return null } + }, + set: (token: string) => { + try { localStorage.setItem(ADMIN_REFRESH_KEY, token) } catch { /* ignore */ } + }, + clear: () => { + try { localStorage.removeItem(ADMIN_REFRESH_KEY) } catch { /* ignore */ } + }, +} + +async function doAdminRequest( + path: string, + options: RequestInit, + allowRetry: boolean, +): Promise { + const bearer = adminTokenStore.get() + // For multipart uploads we must NOT set Content-Type — the browser adds the + // boundary itself. Detect FormData bodies and skip the JSON header. + const isFormData = options.body instanceof FormData + + const res = await fetch(`${ADMIN_API_URL}${path}`, { + ...options, + credentials: 'include', + headers: { + ...(isFormData ? {} : { 'Content-Type': 'application/json' }), + ...(bearer ? { Authorization: `Bearer ${bearer}` } : {}), + ...options.headers, + }, + }) + + if (res.status === 401 && allowRetry) { + try { + await refreshAdminToken() + return doAdminRequest(path, options, false) + } catch { + adminTokenStore.clear() + throw new Error('Unauthorized') + } + } + + const data = await res.json().catch(() => null) + if (!res.ok) throw data + return data as T +} + +// Body-based refresh: the API expects the refresh token in the request body and +// returns a fresh access + refresh token pair (the refresh token rotates). +export async function refreshAdminToken(): Promise { + const refreshToken = adminRefreshStore.get() + if (!refreshToken) throw new Error('Unauthorized') + + const res = await fetch(`${ADMIN_API_URL}/v1/auth/refresh`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refresh_token: refreshToken }), + }) + if (!res.ok) { + adminRefreshStore.clear() + adminTokenStore.clear() + throw new Error('Unauthorized') + } + const data = (await res.json()) as AdminRefreshResponse + if (data.access_token) adminTokenStore.set(data.access_token) + if (data.refresh_token) adminRefreshStore.set(data.refresh_token) + return data.access_token +} + +export async function adminLogin(payload: AdminLoginRequest): Promise { + const data = await doAdminRequest( + '/v1/auth/login', + { method: 'POST', body: JSON.stringify(payload) }, + false, + ) + if (data.access_token) adminTokenStore.set(data.access_token) + if (data.refresh_token) adminRefreshStore.set(data.refresh_token) + return data +} + +export function getAdminMe(): Promise { + return doAdminRequest('/v1/auth/me', {}, true) +} + +export async function adminLogout(): Promise { + try { + await doAdminRequest('/v1/auth/logout', { method: 'POST' }, false) + } finally { + adminTokenStore.clear() + adminRefreshStore.clear() + } +} + +export function getOrganizations(limit = 50, offset = 0): Promise { + return doAdminRequest( + `/v1/organizations?limit=${limit}&offset=${offset}`, + {}, + true, + ) +} + +export function createOrganization(payload: CreateOrganizationRequest): Promise { + return doAdminRequest( + '/v1/organizations', + { method: 'POST', body: JSON.stringify(payload) }, + true, + ) +} + +export function getOrganization(id: string): Promise { + return doAdminRequest(`/v1/organizations/${id}`, {}, true) +} + +export function createOrganizationWallets(id: string): Promise { + return doAdminRequest( + `/v1/organizations/${id}/wallets/create`, + { method: 'POST', body: JSON.stringify({ id }) }, + true, + ) +} + +export function getOrganizationWallets(id: string): Promise { + return doAdminRequest( + `/v1/organizations/${id}/wallets`, + {}, + true, + ) +} + +export function getDocuments(orgId: string): Promise { + return doAdminRequest( + `/v1/organizations/${orgId}/documents`, + {}, + true, + ) +} + +// Upload/replace a single typed document. The type is part of the path and +// the body carries only the file — PUT acts as an upsert for that slot. +export function uploadDocument( + orgId: string, + type: DocumentTypeSlug, + file: File, +): Promise { + const body = new FormData() + body.append('file', file) + + return doAdminRequest( + `/v1/organizations/${orgId}/documents/${type}`, + { method: 'PUT', body }, + true, + ) +} + +export async function getPurchaseRequests(params: { + organizationId?: string + status?: string + limit?: number + offset?: number +}): Promise { + const query = new URLSearchParams() + if (params.organizationId) query.set('organization_id', params.organizationId) + if (params.status) query.set('status', params.status) + query.set('limit', String(params.limit ?? 50)) + query.set('offset', String(params.offset ?? 0)) + + const data = await doAdminRequest( + `/v1/purchase-requests?${query.toString()}`, + {}, + true, + ) + // TEMP: inspect real backend shape — especially which `status` values appear. + console.log('[purchase-requests] list response:', data) + return data +} + +export function updateOrganization( + id: string, + payload: UpdateOrganizationRequest, +): Promise { + return doAdminRequest( + `/v1/organizations/${id}`, + { method: 'PATCH', body: JSON.stringify(payload) }, + true, + ) +} diff --git a/src/features/admin/hooks/useAdminAuth.ts b/src/features/admin/hooks/useAdminAuth.ts new file mode 100644 index 0000000..254ad6d --- /dev/null +++ b/src/features/admin/hooks/useAdminAuth.ts @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query' +import { getAdminMe } from '../api/adminApi' + +export const ADMIN_AUTH_QUERY_KEY = ['admin-auth'] + +export function useAdminAuth(): { isAuthenticated: boolean; isLoading: boolean } { + // `getAdminMe` is the real "is the session valid" check. It runs through + // `doAdminRequest` with retry, so a 401 transparently triggers a body-based + // refresh (using the persisted refresh token) and replays the request. + const { data, isLoading, isError } = useQuery({ + queryKey: ADMIN_AUTH_QUERY_KEY, + queryFn: getAdminMe, + retry: false, + staleTime: Infinity, + gcTime: Infinity, + refetchOnWindowFocus: false, + }) + return { isAuthenticated: !!data && !isError, isLoading } +} diff --git a/src/features/admin/hooks/useAdminLogin.ts b/src/features/admin/hooks/useAdminLogin.ts new file mode 100644 index 0000000..405bc8d --- /dev/null +++ b/src/features/admin/hooks/useAdminLogin.ts @@ -0,0 +1,22 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { adminLogin } from '../api/adminApi' +import { ADMIN_AUTH_QUERY_KEY } from './useAdminAuth' + +export function useAdminLogin() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: adminLogin, + onSuccess: (data) => { + // Tokens are already stored by adminLogin. Seed the gate's cache with the + // AdminMeResponse-shaped profile (login response carries all these fields) + // so we flip to "authenticated" without an extra /auth/me round-trip. + queryClient.setQueryData(ADMIN_AUTH_QUERY_KEY, { + id: data.id, + login: data.login, + first_name: data.first_name, + last_name: data.last_name, + role: data.role, + }) + }, + }) +} diff --git a/src/features/admin/hooks/useAdminLogout.ts b/src/features/admin/hooks/useAdminLogout.ts new file mode 100644 index 0000000..40372b7 --- /dev/null +++ b/src/features/admin/hooks/useAdminLogout.ts @@ -0,0 +1,14 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { adminLogout } from '../api/adminApi' +import { ADMIN_AUTH_QUERY_KEY } from './useAdminAuth' + +export function useAdminLogout() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: adminLogout, + onSuccess: () => { + // Flip the gate back to "not authenticated" without triggering a /refresh refetch. + queryClient.setQueryData(ADMIN_AUTH_QUERY_KEY, null) + }, + }) +} diff --git a/src/features/admin/hooks/useCreateOrganization.ts b/src/features/admin/hooks/useCreateOrganization.ts new file mode 100644 index 0000000..b35dedb --- /dev/null +++ b/src/features/admin/hooks/useCreateOrganization.ts @@ -0,0 +1,13 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { createOrganization } from '../api/adminApi' +import { ORGANIZATIONS_QUERY_KEY } from './useOrganizations' + +export function useCreateOrganization() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: createOrganization, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ORGANIZATIONS_QUERY_KEY }) + }, + }) +} diff --git a/src/features/admin/hooks/useCreateOrganizationWallets.ts b/src/features/admin/hooks/useCreateOrganizationWallets.ts new file mode 100644 index 0000000..8b339f7 --- /dev/null +++ b/src/features/admin/hooks/useCreateOrganizationWallets.ts @@ -0,0 +1,18 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { createOrganizationWallets } from '../api/adminApi' +import { ORGANIZATIONS_QUERY_KEY } from './useOrganizations' +import { ORGANIZATION_QUERY_KEY } from './useOrganization' +import { WALLETS_QUERY_KEY } from './useOrganizationWallets' + +export function useCreateOrganizationWallets() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (organizationId: string) => createOrganizationWallets(organizationId), + onSuccess: (_result, organizationId) => { + // `has_wallets` flips to true server-side — refresh list and detail view. + queryClient.invalidateQueries({ queryKey: ORGANIZATIONS_QUERY_KEY }) + queryClient.invalidateQueries({ queryKey: ORGANIZATION_QUERY_KEY(organizationId) }) + queryClient.invalidateQueries({ queryKey: WALLETS_QUERY_KEY(organizationId) }) + }, + }) +} diff --git a/src/features/admin/hooks/useDocuments.ts b/src/features/admin/hooks/useDocuments.ts new file mode 100644 index 0000000..6e76234 --- /dev/null +++ b/src/features/admin/hooks/useDocuments.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query' +import { getDocuments } from '../api/adminApi' + +export const DOCUMENTS_QUERY_KEY = (orgId: string) => ['admin-documents', orgId] + +export function useDocuments(orgId: string | undefined) { + return useQuery({ + queryKey: DOCUMENTS_QUERY_KEY(orgId ?? ''), + queryFn: () => getDocuments(orgId as string), + enabled: !!orgId, + }) +} diff --git a/src/features/admin/hooks/useOrganization.ts b/src/features/admin/hooks/useOrganization.ts new file mode 100644 index 0000000..9bed771 --- /dev/null +++ b/src/features/admin/hooks/useOrganization.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query' +import { getOrganization } from '../api/adminApi' + +export const ORGANIZATION_QUERY_KEY = (id: string) => ['admin-organization', id] + +export function useOrganization(id: string | undefined) { + return useQuery({ + queryKey: ORGANIZATION_QUERY_KEY(id ?? ''), + queryFn: () => getOrganization(id as string), + enabled: !!id, + }) +} diff --git a/src/features/admin/hooks/useOrganizationWallets.ts b/src/features/admin/hooks/useOrganizationWallets.ts new file mode 100644 index 0000000..f5753a9 --- /dev/null +++ b/src/features/admin/hooks/useOrganizationWallets.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query' +import { getOrganizationWallets } from '../api/adminApi' + +export const WALLETS_QUERY_KEY = (orgId: string) => ['admin-wallets', orgId] + +export function useOrganizationWallets(orgId: string | undefined) { + return useQuery({ + queryKey: WALLETS_QUERY_KEY(orgId ?? ''), + queryFn: () => getOrganizationWallets(orgId as string), + enabled: !!orgId, + }) +} diff --git a/src/features/admin/hooks/useOrganizations.ts b/src/features/admin/hooks/useOrganizations.ts new file mode 100644 index 0000000..f8584c6 --- /dev/null +++ b/src/features/admin/hooks/useOrganizations.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query' +import { getOrganizations } from '../api/adminApi' + +export const ORGANIZATIONS_QUERY_KEY = ['admin-organizations'] + +export function useOrganizations() { + return useQuery({ + queryKey: ORGANIZATIONS_QUERY_KEY, + queryFn: () => getOrganizations(), + }) +} diff --git a/src/features/admin/hooks/usePurchaseRequests.ts b/src/features/admin/hooks/usePurchaseRequests.ts new file mode 100644 index 0000000..a76da36 --- /dev/null +++ b/src/features/admin/hooks/usePurchaseRequests.ts @@ -0,0 +1,15 @@ +import { useQuery } from '@tanstack/react-query' +import { getPurchaseRequests } from '../api/adminApi' + +export const PURCHASE_REQUESTS_QUERY_KEY = (orgId: string) => [ + 'admin-purchase-requests', + orgId, +] + +export function usePurchaseRequests(orgId: string | undefined) { + return useQuery({ + queryKey: PURCHASE_REQUESTS_QUERY_KEY(orgId ?? ''), + queryFn: () => getPurchaseRequests({ organizationId: orgId }), + enabled: !!orgId, + }) +} diff --git a/src/features/admin/hooks/useUpdateOrganization.ts b/src/features/admin/hooks/useUpdateOrganization.ts new file mode 100644 index 0000000..2181547 --- /dev/null +++ b/src/features/admin/hooks/useUpdateOrganization.ts @@ -0,0 +1,16 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { updateOrganization } from '../api/adminApi' +import type { UpdateOrganizationRequest } from '../model/types' +import { ORGANIZATIONS_QUERY_KEY } from './useOrganizations' +import { ORGANIZATION_QUERY_KEY } from './useOrganization' + +export function useUpdateOrganization(id: string) { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (payload: UpdateOrganizationRequest) => updateOrganization(id, payload), + onSuccess: (data) => { + queryClient.setQueryData(ORGANIZATION_QUERY_KEY(id), data) + queryClient.invalidateQueries({ queryKey: ORGANIZATIONS_QUERY_KEY }) + }, + }) +} diff --git a/src/features/admin/hooks/useUploadDocument.ts b/src/features/admin/hooks/useUploadDocument.ts new file mode 100644 index 0000000..1be5437 --- /dev/null +++ b/src/features/admin/hooks/useUploadDocument.ts @@ -0,0 +1,20 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { uploadDocument } from '../api/adminApi' +import type { DocumentTypeSlug } from '../model/types' +import { DOCUMENTS_QUERY_KEY } from './useDocuments' + +interface UploadArgs { + type: DocumentTypeSlug + file: File +} + +export function useUploadDocument(orgId: string) { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: ({ type, file }: UploadArgs) => + uploadDocument(orgId, type, file), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: DOCUMENTS_QUERY_KEY(orgId) }) + }, + }) +} diff --git a/src/features/admin/index.ts b/src/features/admin/index.ts new file mode 100644 index 0000000..603c344 --- /dev/null +++ b/src/features/admin/index.ts @@ -0,0 +1,43 @@ +export { + adminLogin, + adminLogout, + getAdminMe, + getOrganizations, + getOrganization, + createOrganization, + createOrganizationWallets, + getOrganizationWallets, + updateOrganization, + getDocuments, + uploadDocument, + getPurchaseRequests, + refreshAdminToken, + adminTokenStore, +} from './api/adminApi' +export type { + AdminLoginRequest, + AdminLoginResponse, + AdminMeResponse, + Organization, + OrganizationListResponse, + CreateOrganizationRequest, + UpdateOrganizationRequest, + WalletResponse, + DocumentResponse, + DocumentTypeSlug, + PurchaseRequestResponse, + PurchaseRequestListResponse, + BankDetails, +} from './model/types' +export { useAdminAuth, ADMIN_AUTH_QUERY_KEY } from './hooks/useAdminAuth' +export { useAdminLogin } from './hooks/useAdminLogin' +export { useAdminLogout } from './hooks/useAdminLogout' +export { useOrganizations, ORGANIZATIONS_QUERY_KEY } from './hooks/useOrganizations' +export { useOrganization, ORGANIZATION_QUERY_KEY } from './hooks/useOrganization' +export { useCreateOrganization } from './hooks/useCreateOrganization' +export { useCreateOrganizationWallets } from './hooks/useCreateOrganizationWallets' +export { useUpdateOrganization } from './hooks/useUpdateOrganization' +export { useOrganizationWallets, WALLETS_QUERY_KEY } from './hooks/useOrganizationWallets' +export { useDocuments, DOCUMENTS_QUERY_KEY } from './hooks/useDocuments' +export { useUploadDocument } from './hooks/useUploadDocument' +export { usePurchaseRequests, PURCHASE_REQUESTS_QUERY_KEY } from './hooks/usePurchaseRequests' diff --git a/src/features/admin/model/types.ts b/src/features/admin/model/types.ts new file mode 100644 index 0000000..0c8be2c --- /dev/null +++ b/src/features/admin/model/types.ts @@ -0,0 +1,151 @@ +export interface AdminLoginRequest { + login: string + password: string +} + +export interface AdminLoginResponse { + access_token: string + refresh_token: string + token_type: string + id: string + login: string + first_name: string | null + last_name: string | null + role: string +} + +export interface AdminRefreshResponse { + access_token: string + refresh_token: string + token_type?: string +} + +export interface AdminMeResponse { + id: string + login: string + first_name: string | null + last_name: string | null + role: string +} + +export type BankDetails = Record + +export interface Organization { + id: string + user_id: string + name: string + short_name: string | null + inn: string + ogrn: string | null + kpp: string | null + legal_address: string | null + actual_address: string | null + bank_details: BankDetails | null + contact_person: string | null + contact_phone: string | null + status: string + kyc_verified: boolean + kyc_verified_at: string | null + has_wallets: boolean + created_by: string | null + created_at: string | null + updated_at: string | null +} + +export interface OrganizationListResponse { + items: Organization[] + total: number +} + +export interface WalletResponse { + id: string + chain: string + address: string + derivation_path: string + created_at: string | null +} + +export interface CreateWalletsRequest { + id: string +} + +export interface CreateWalletsResponse { + wallets: WalletResponse[] + mnemonic: string +} + +// Fixed document type slugs — match the path segments of the per-type +// PUT/GET endpoints (`/documents/{slug}`). The API no longer accepts a +// free-text document type. +export type DocumentTypeSlug = + | 'charter' + | 'inn-certificate' + | 'ogrn-certificate' + | 'bank-details' + | 'kyc-representative' + | 'power-of-attorney' + | 'other' + +export interface DocumentResponse { + organization_id: string + document_type: string + s3_key: string | null + file_name: string | null + content_type: string | null + file_size_bytes: number | null + download_url: string | null +} + +// Monetary fields are strings to preserve decimal precision — do not coerce to number. +export interface PurchaseRequestResponse { + id: string + organization_id: string + status: string + usdt_amount: string + rub_amount: string | null + exchange_rate: string | null + service_fee_percent: string | null + comment: string | null + admin_comment: string | null + target_wallet_chain: string | null + target_wallet_address: string | null + tx_hash: string | null + assigned_to: string | null + created_at: string | null + updated_at: string | null + completed_at: string | null +} + +export interface PurchaseRequestListResponse { + items: PurchaseRequestResponse[] + total: number +} + +export interface UpdateOrganizationRequest { + name?: string | null + short_name?: string | null + ogrn?: string | null + kpp?: string | null + legal_address?: string | null + actual_address?: string | null + bank_details?: BankDetails | null + contact_person?: string | null + contact_phone?: string | null + status?: string | null +} + +export interface CreateOrganizationRequest { + email: string + password: string + name: string + inn: string + short_name?: string | null + ogrn?: string | null + kpp?: string | null + legal_address?: string | null + actual_address?: string | null + bank_details?: BankDetails | null + contact_person?: string | null + contact_phone?: string | null + status?: string +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..7cd73d2 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,15 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' + +import './app/styles/fonts.css' +import './app/styles/reset.css' +import './app/styles/variables.css' +import './app/styles/global.css' + +import { App } from './app/App' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/pages/admin-organization/index.ts b/src/pages/admin-organization/index.ts new file mode 100644 index 0000000..5a455a3 --- /dev/null +++ b/src/pages/admin-organization/index.ts @@ -0,0 +1 @@ +export { AdminOrganizationPage } from './ui/AdminOrganizationPage' diff --git a/src/pages/admin-organization/model/useOrganizationForm.ts b/src/pages/admin-organization/model/useOrganizationForm.ts new file mode 100644 index 0000000..a484aa1 --- /dev/null +++ b/src/pages/admin-organization/model/useOrganizationForm.ts @@ -0,0 +1,90 @@ +import { useEffect, useState } from 'react' +import { useUpdateOrganization } from '@features/admin' +import type { Organization, UpdateOrganizationRequest } from '@features/admin' + +interface FormState { + name: string + short_name: string + ogrn: string + kpp: string + legal_address: string + actual_address: string + contact_person: string + contact_phone: string + status: string +} + +function toForm(org: Organization): FormState { + return { + name: org.name ?? '', + short_name: org.short_name ?? '', + ogrn: org.ogrn ?? '', + kpp: org.kpp ?? '', + legal_address: org.legal_address ?? '', + actual_address: org.actual_address ?? '', + contact_person: org.contact_person ?? '', + contact_phone: org.contact_phone ?? '', + status: org.status ?? '', + } +} + +function extractErrorMessage(error: unknown): string { + const e = error as { detail?: unknown } + if (typeof e?.detail === 'string') return e.detail + if (Array.isArray(e?.detail) && (e.detail[0] as { msg?: string })?.msg) { + return (e.detail[0] as { msg: string }).msg + } + return 'Не удалось сохранить изменения' +} + +export function useOrganizationForm( + org: Organization | undefined, + id: string, + onSaved?: () => void, +) { + const [form, setForm] = useState(() => + org ? toForm(org) : { + name: '', short_name: '', ogrn: '', kpp: '', legal_address: '', + actual_address: '', contact_person: '', contact_phone: '', status: '', + }, + ) + const mutation = useUpdateOrganization(id) + + // Sync local form state once the organization loads / changes. + useEffect(() => { + if (org) setForm(toForm(org)) + }, [org]) + + const setField = (key: keyof FormState) => (value: string) => + setForm((prev) => ({ ...prev, [key]: value })) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + + const trimmedOrNull = (v: string) => (v.trim() ? v.trim() : null) + + const payload: UpdateOrganizationRequest = { + name: form.name.trim(), + short_name: trimmedOrNull(form.short_name), + ogrn: trimmedOrNull(form.ogrn), + kpp: trimmedOrNull(form.kpp), + legal_address: trimmedOrNull(form.legal_address), + actual_address: trimmedOrNull(form.actual_address), + contact_person: trimmedOrNull(form.contact_person), + contact_phone: trimmedOrNull(form.contact_phone), + status: trimmedOrNull(form.status), + } + + mutation.mutate(payload, { onSuccess: () => onSaved?.() }) + } + + const error = mutation.isError ? extractErrorMessage(mutation.error) : null + + return { + form, + setField, + handleSubmit, + isSaving: mutation.isPending, + error, + } +} diff --git a/src/pages/admin-organization/ui/AdminOrganizationPage.module.css b/src/pages/admin-organization/ui/AdminOrganizationPage.module.css new file mode 100644 index 0000000..af102a9 --- /dev/null +++ b/src/pages/admin-organization/ui/AdminOrganizationPage.module.css @@ -0,0 +1,154 @@ +.page { + min-height: 100vh; + background: var(--bg-deep); + padding: 40px 48px; +} + +.header { + max-width: 900px; + margin: 0 auto 28px; +} + +.back { + background: none; + border: none; + color: var(--text-secondary, rgba(255, 255, 255, 0.6)); + font-size: 14px; + cursor: pointer; + padding: 0; + margin-bottom: 14px; + transition: color 0.2s; +} + +.back:hover { + color: var(--text-primary, #fff); +} + +.title { + font-size: clamp(24px, 3.5vw, 34px); + font-weight: 700; + color: var(--text-primary, #fff); + margin: 0; +} + +.form { + max-width: 900px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 24px; +} + +.tabs { + max-width: 900px; + margin: 0 auto 24px; + display: flex; + gap: 8px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.tab { + background: none; + border: none; + border-bottom: 2px solid transparent; + color: var(--text-secondary, rgba(255, 255, 255, 0.6)); + font-size: 15px; + font-weight: 600; + padding: 12px 16px; + cursor: pointer; + transition: color 0.2s, border-color 0.2s; +} + +.tab:hover { + color: var(--text-primary, #fff); +} + +.tabActive { + color: var(--text-primary, #fff); + border-bottom-color: var(--interactive, #4a6dff); +} + +.tabPanel { + max-width: 900px; + margin: 0 auto; +} + +.section { + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 20px; + padding: 24px; +} + +.sectionTitle { + font-size: 14px; + letter-spacing: 1.5px; + text-transform: uppercase; + color: var(--text-secondary, rgba(255, 255, 255, 0.5)); + font-weight: 600; + margin: 0 0 18px; +} + +.grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 18px; +} + +.bankLabel { + display: block; + font-size: 12px; + color: var(--text-secondary, rgba(255, 255, 255, 0.5)); + font-weight: 600; + margin-bottom: 8px; +} + +.textarea { + width: 100%; + background: var(--glass-bg, rgba(255, 255, 255, 0.06)); + border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1)); + border-radius: 10px; + color: var(--text-primary, #fff); + font-family: var(--font-mono, monospace); + font-size: 13px; + padding: 12px 14px; + resize: vertical; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.textarea:focus { + border-color: var(--interactive, #4a6dff); + box-shadow: 0 0 0 3px rgba(74, 109, 255, 0.15); +} + +.state { + max-width: 900px; + margin: 0 auto; + padding: 40px 16px; + text-align: center; + color: var(--text-secondary, rgba(255, 255, 255, 0.6)); +} + +.error { + color: #ff5a5a; + font-size: 13px; + margin: 0; + text-align: center; +} + +.actions { + max-width: 320px; + margin: 0 auto; + width: 100%; +} + +@media (max-width: 768px) { + .page { + padding: 28px 20px; + } + + .grid { + grid-template-columns: 1fr; + } +} diff --git a/src/pages/admin-organization/ui/AdminOrganizationPage.tsx b/src/pages/admin-organization/ui/AdminOrganizationPage.tsx new file mode 100644 index 0000000..b5a2760 --- /dev/null +++ b/src/pages/admin-organization/ui/AdminOrganizationPage.tsx @@ -0,0 +1,150 @@ +import { useState } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import { useAdminAuth, useOrganization } from '@features/admin' +import { ROUTES } from '@shared/config/routes' +import { FormField, Notification, PrimaryButton } from '@shared/ui' +import { AdminLoginForm } from '@widgets/admin-login-form' +import { OrganizationDocuments } from '@widgets/organization-documents' +import { OrganizationPurchaseRequests } from '@widgets/organization-purchase-requests' +import { OrganizationWallets } from '@widgets/organization-wallets' +import { useOrganizationForm } from '../model/useOrganizationForm' +import styles from './AdminOrganizationPage.module.css' + +type Tab = 'info' | 'wallets' | 'documents' | 'requests' + +const TABS: { id: Tab; label: string }[] = [ + { id: 'info', label: 'Общая информация' }, + { id: 'wallets', label: 'Кошельки' }, + { id: 'documents', label: 'Документы' }, + { id: 'requests', label: 'Заявки' }, +] + +function formatDateTime(value: string | null): string { + if (!value) return '—' + const d = new Date(value) + if (Number.isNaN(d.getTime())) return '—' + return d.toLocaleString('ru-RU') +} + +export function AdminOrganizationPage() { + const { isAuthenticated, isLoading: isAuthLoading } = useAdminAuth() + const { organizationId } = useParams<{ organizationId: string }>() + const navigate = useNavigate() + const { data: org, isLoading, isError } = useOrganization(organizationId) + const [notice, setNotice] = useState(false) + const [activeTab, setActiveTab] = useState('info') + const { form, setField, handleSubmit, isSaving, error } = useOrganizationForm( + org, + organizationId ?? '', + () => setNotice(true), + ) + + if (isAuthLoading) return null + if (!isAuthenticated) return + + return ( +
+
+ +

{org ? org.name : 'Юридическое лицо'}

+
+ + {isLoading &&
Загрузка...
} + {isError &&
Не удалось загрузить организацию
} + + {org && ( +
+ {TABS.map((tab) => ( + + ))} +
+ )} + + {org && activeTab === 'info' && ( +
+
+

Реквизиты

+
+ + + + + + +
+
+ +
+

Адреса

+
+ + +
+
+ +
+

Контакты

+
+ + +
+
+ +
+

Системная информация

+
+ + + + + + + +
+
+ + {error &&

{error}

} + +
+ +
+
+ )} + + {org && activeTab === 'wallets' && ( +
+ +
+ )} + + {org && activeTab === 'documents' && ( +
+ +
+ )} + + {org && activeTab === 'requests' && ( +
+ +
+ )} + + {notice && ( + setNotice(false)} + /> + )} +
+ ) +} diff --git a/src/pages/admin/index.ts b/src/pages/admin/index.ts new file mode 100644 index 0000000..7f56083 --- /dev/null +++ b/src/pages/admin/index.ts @@ -0,0 +1 @@ +export { AdminPage } from './ui/AdminPage' diff --git a/src/pages/admin/ui/AdminPage.module.css b/src/pages/admin/ui/AdminPage.module.css new file mode 100644 index 0000000..bcf01c8 --- /dev/null +++ b/src/pages/admin/ui/AdminPage.module.css @@ -0,0 +1,84 @@ +.page { + min-height: 100vh; + background: var(--bg-deep); + padding: 40px 48px; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + max-width: 1200px; + margin: 0 auto 36px; +} + +.greeting { + font-size: clamp(28px, 4vw, 40px); + font-weight: 700; + color: var(--text-primary, #fff); + margin: 0; +} + +.logout { + background: rgba(255, 255, 255, 0.06); + border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1)); + color: var(--text-secondary, rgba(255, 255, 255, 0.7)); + border-radius: 10px; + height: 40px; + padding: 0 18px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s, color 0.2s, border-color 0.2s; +} + +.logout:hover { + background: rgba(255, 90, 90, 0.12); + border-color: rgba(255, 90, 90, 0.3); + color: #ff5a5a; +} + +.content { + max-width: 1200px; + margin: 0 auto; +} + +.toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.sectionTitle { + font-size: 20px; + font-weight: 700; + color: var(--text-primary, #fff); + margin: 0; +} + +.addBtn { + background: linear-gradient(135deg, var(--grad-edge, #4a6dff), var(--grad-center, #6f4aff)); + border: none; + color: #fff; + border-radius: 12px; + height: 44px; + padding: 0 20px; + font-size: 14px; + font-weight: 700; + cursor: pointer; + transition: filter 0.2s, box-shadow 0.2s; +} + +.addBtn:hover { + filter: brightness(1.12); + box-shadow: 0 6px 20px rgba(74, 109, 255, 0.35); +} + +@media (max-width: 768px) { + .page { + padding: 28px 20px; + } +} diff --git a/src/pages/admin/ui/AdminPage.tsx b/src/pages/admin/ui/AdminPage.tsx new file mode 100644 index 0000000..74ca8dc --- /dev/null +++ b/src/pages/admin/ui/AdminPage.tsx @@ -0,0 +1,77 @@ +import { useState } from 'react' +import { useAdminAuth, useAdminLogout, useCreateOrganizationWallets } from '@features/admin' +import type { Organization } from '@features/admin' +import { Notification } from '@shared/ui' +import { AdminLoginForm } from '@widgets/admin-login-form' +import { LegalEntitiesTable } from '@widgets/legal-entities-table' +import { AddLegalEntityModal } from '@widgets/add-legal-entity-modal' +import styles from './AdminPage.module.css' + +type NotificationState = { message: string; status: 'success' | 'error' | 'warning' } + +export function AdminPage() { + const { isAuthenticated, isLoading } = useAdminAuth() + const logout = useAdminLogout() + const createWallets = useCreateOrganizationWallets() + const [modalOpen, setModalOpen] = useState(false) + const [notification, setNotification] = useState(null) + + // After a legal entity is created we immediately provision its wallets. + // The page stays mounted (unlike the modal), so these mutate callbacks fire reliably. + function handleCreated(organization: Organization) { + setNotification({ status: 'success', message: 'Юридическое лицо добавлено' }) + createWallets.mutate(organization.id, { + onSuccess: (result) => { + setNotification({ + status: 'success', + message: `Кошельки созданы (${result.wallets.length})`, + }) + }, + onError: () => { + setNotification({ + status: 'warning', + message: 'Юридическое лицо создано, но кошельки создать не удалось', + }) + }, + }) + } + + if (isLoading) return null + if (!isAuthenticated) return + + return ( +
+
+

Привет, Марк!

+ +
+ +
+
+

Юридические лица

+ +
+ + +
+ + setModalOpen(false)} + onCreated={handleCreated} + /> + + {notification && ( + setNotification(null)} + /> + )} +
+ ) +} diff --git a/src/shared/config/routes.ts b/src/shared/config/routes.ts new file mode 100644 index 0000000..358fca1 --- /dev/null +++ b/src/shared/config/routes.ts @@ -0,0 +1,7 @@ +export const ROUTES = { + ADMIN: '/sys-c7f29a4e-d81b-4630-ops-console', + ADMIN_ORGANIZATION: '/sys-c7f29a4e-d81b-4630-ops-console/organizations/:organizationId', +} as const + +export const adminOrganizationPath = (id: string) => + `/sys-c7f29a4e-d81b-4630-ops-console/organizations/${id}` diff --git a/src/shared/ui/Button/Button.module.css b/src/shared/ui/Button/Button.module.css new file mode 100644 index 0000000..aac835a --- /dev/null +++ b/src/shared/ui/Button/Button.module.css @@ -0,0 +1,56 @@ +.btn { + height: 44px; + padding: 0 20px; + border-radius: 10px; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.04em; + cursor: pointer; + font-family: var(--font-sans); + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: background 0.2s, opacity 0.2s, border-color 0.2s; +} + +.primary { + background: linear-gradient(135deg, var(--grad-edge), var(--grad-center)); + border: none; + color: var(--text-primary); +} + +.primary:hover { + opacity: 0.85; +} + +.danger { + background: rgba(255, 77, 77, 0.1); + border: 1px solid rgba(255, 77, 77, 0.3); + color: #ff4d4d; +} + +.danger:hover { + background: rgba(255, 77, 77, 0.2); +} + +.ghost { + background: rgba(74, 109, 255, 0.15); + border: 1px solid rgba(74, 109, 255, 0.3); + color: var(--interactive); +} + +.ghost:hover { + background: rgba(74, 109, 255, 0.25); +} + +.outline { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.15); + color: var(--text-primary); +} + +.outline:hover { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.25); +} diff --git a/src/shared/ui/Button/Button.tsx b/src/shared/ui/Button/Button.tsx new file mode 100644 index 0000000..211c46f --- /dev/null +++ b/src/shared/ui/Button/Button.tsx @@ -0,0 +1,17 @@ +import styles from './Button.module.css' + +interface Props { + variant: 'primary' | 'danger' | 'ghost' | 'outline' + children: React.ReactNode + onClick?: () => void + type?: 'button' | 'submit' + disabled?: boolean +} + +export function Button({ variant, children, onClick, type = 'button', disabled }: Props) { + return ( + + ) +} diff --git a/src/shared/ui/Button/index.ts b/src/shared/ui/Button/index.ts new file mode 100644 index 0000000..4d0a670 --- /dev/null +++ b/src/shared/ui/Button/index.ts @@ -0,0 +1 @@ +export { Button } from './Button' diff --git a/src/shared/ui/FormField/FormField.module.css b/src/shared/ui/FormField/FormField.module.css new file mode 100644 index 0000000..9e9393d --- /dev/null +++ b/src/shared/ui/FormField/FormField.module.css @@ -0,0 +1,97 @@ +.field { + display: flex; + flex-direction: column; + gap: 6px; +} + +.label { + font-size: 12px; + color: var(--text-secondary); + font-weight: 600; + letter-spacing: 0.08em; +} + +.wrap { + position: relative; +} + +.input { + width: 100%; + height: 48px; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 10px; + color: var(--text-primary); + font-size: 14px; + font-family: var(--font-sans); + padding: 0 16px; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.input:focus { + border-color: var(--interactive); + box-shadow: 0 0 0 3px rgba(74, 109, 255, 0.15); +} + +.readonly { + background: rgba(255, 255, 255, 0.03); + border-style: dashed; + color: var(--text-secondary); + cursor: pointer; +} + +.copied { + border-color: var(--success); + border-style: solid; + box-shadow: 0 0 0 3px rgba(38, 161, 123, 0.15); +} + +.iconCopied { + position: absolute; + right: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--success); + font-size: 16px; +} + +.iconCheck { + position: absolute; + right: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--success); + font-size: 16px; +} + +.iconLock { + position: absolute; + right: 14px; + top: 50%; + transform: translateY(-50%); + font-size: 14px; +} + +.withToggle { + padding-right: 48px; +} + +.togglePw { + position: absolute; + right: 14px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; + color: var(--text-secondary); + display: flex; + align-items: center; + transition: color 0.2s; + padding: 0; +} + +.togglePw:hover { + color: var(--text-primary); +} \ No newline at end of file diff --git a/src/shared/ui/FormField/FormField.tsx b/src/shared/ui/FormField/FormField.tsx new file mode 100644 index 0000000..af1a341 --- /dev/null +++ b/src/shared/ui/FormField/FormField.tsx @@ -0,0 +1,70 @@ +import { useState } from 'react' +import styles from './FormField.module.css' + +interface Props { + label: string + value: string + placeholder?: string + type?: 'text' | 'email' | 'tel' | 'password' + onChange?: (value: string) => void + onBlur?: () => void + readOnly?: boolean + required?: boolean + icon?: 'check' | 'lock' +} + +export function FormField({ label, value, placeholder, type = 'text', onChange, onBlur, readOnly, required, icon }: Props) { + const [copied, setCopied] = useState(false) + const [isVisible, setIsVisible] = useState(false) + + const isPassword = type === 'password' + const inputType = isPassword ? (isVisible ? 'text' : 'password') : type + + const handleClick = () => { + if (!readOnly) return + navigator.clipboard.writeText(value).then(() => { + setCopied(true) + setTimeout(() => setCopied(false), 1500) + }) + } + + return ( +
+ +
+ onChange(e.target.value) } + : { defaultValue: value } + )} + placeholder={placeholder} + readOnly={readOnly} + required={required} + onBlur={onBlur} + /> + {isPassword && ( + + )} + {icon === 'check' && } + {icon === 'lock' && 🔒} +
+
+ ) +} diff --git a/src/shared/ui/FormField/index.ts b/src/shared/ui/FormField/index.ts new file mode 100644 index 0000000..31e7cf5 --- /dev/null +++ b/src/shared/ui/FormField/index.ts @@ -0,0 +1 @@ +export { FormField } from './FormField' diff --git a/src/shared/ui/Notification/Notification.module.css b/src/shared/ui/Notification/Notification.module.css new file mode 100644 index 0000000..c1685f4 --- /dev/null +++ b/src/shared/ui/Notification/Notification.module.css @@ -0,0 +1,108 @@ +.notification { + position: fixed; + bottom: 24px; + right: 24px; + z-index: 1000; + display: flex; + align-items: center; + gap: 12px; + padding: 16px 18px; + min-width: 280px; + max-width: 360px; + border-radius: 12px; + background: var(--bg-mid); + border: 1px solid var(--glass-border); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + animation: slideIn 0.3s cubic-bezier(0.22, 1, 0.36, 1) forwards; +} + +.notificationWrapper { + display: flex; + gap: 12px; +} + +.notification.closing { + animation: slideOut 0.25s cubic-bezier(0.55, 0, 1, 0.45) forwards; +} + +@keyframes slideIn { + from { + transform: translateX(calc(100% + 24px)); + opacity: 0; + } + + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slideOut { + from { + transform: translateX(0); + opacity: 1; + } + + to { + transform: translateX(calc(100% + 24px)); + opacity: 0; + } +} + +.icon { + flex-shrink: 0; + width: 22px; + height: 22px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; + margin-top: 1px; +} + +.success .icon { + background: var(--success); + color: #fff; +} + +.error .icon { + background: var(--error); + color: #fff; +} + +.info .icon { + background: var(--interactive); + color: #fff; +} + +.warning .icon { + background: #f59e0b; + color: #fff; +} + +.message { + flex: 1; + font-size: 14px; + color: var(--text-secondary); + line-height: 1.5; + margin: 0; +} + +.close { + flex-shrink: 0; + background: none; + border: none; + color: var(--text-secondary); + font-size: 14px; + cursor: pointer; + padding: 0; + line-height: 1; + margin-top: 2px; + transition: color 0.15s; +} + +.close:hover { + color: var(--text-primary); +} \ No newline at end of file diff --git a/src/shared/ui/Notification/Notification.tsx b/src/shared/ui/Notification/Notification.tsx new file mode 100644 index 0000000..1d5b154 --- /dev/null +++ b/src/shared/ui/Notification/Notification.tsx @@ -0,0 +1,42 @@ +import { useState } from 'react' +import styles from './Notification.module.css' + +type Status = 'success' | 'error' | 'info' | 'warning' + +interface Props { + message: string + status: Status + onClose: () => void +} + +const ICONS: Record = { + success: '✓', + error: '✕', + info: 'i', + warning: '!', +} + +export function Notification({ message, status, onClose }: Props) { + const [closing, setClosing] = useState(false) + + function handleClose() { + setClosing(true) + } + + function handleAnimationEnd() { + if (closing) onClose() + } + + return ( +
+
+ {ICONS[status]} +

{message}

+
+ +
+ ) +} diff --git a/src/shared/ui/Notification/index.ts b/src/shared/ui/Notification/index.ts new file mode 100644 index 0000000..2e2470d --- /dev/null +++ b/src/shared/ui/Notification/index.ts @@ -0,0 +1 @@ +export { Notification } from './Notification' diff --git a/src/shared/ui/PrimaryButton/PrimaryButton.module.css b/src/shared/ui/PrimaryButton/PrimaryButton.module.css new file mode 100644 index 0000000..d3dd951 --- /dev/null +++ b/src/shared/ui/PrimaryButton/PrimaryButton.module.css @@ -0,0 +1,19 @@ +.btn { + width: 100%; + height: 56px; + background: linear-gradient(135deg, var(--grad-edge), var(--grad-center)); + border: none; + border-radius: 14px; + color: var(--text-primary); + font-size: 17px; + font-weight: 700; + cursor: pointer; + font-family: var(--font-sans); + letter-spacing: 0.3px; + transition: filter 0.25s, box-shadow 0.25s; +} + +.btn:hover { + filter: brightness(1.15); + box-shadow: 0 0 24px rgba(91, 61, 184, 0.5); +} diff --git a/src/shared/ui/PrimaryButton/PrimaryButton.tsx b/src/shared/ui/PrimaryButton/PrimaryButton.tsx new file mode 100644 index 0000000..10240d2 --- /dev/null +++ b/src/shared/ui/PrimaryButton/PrimaryButton.tsx @@ -0,0 +1,16 @@ +import styles from './PrimaryButton.module.css' + +interface Props { + label?: string + onClick?: () => void + type?: 'button' | 'submit' + disabled?: boolean +} + +export function PrimaryButton({ label = 'Подтвердить своп', onClick, type = 'submit', disabled }: Props) { + return ( + + ) +} diff --git a/src/shared/ui/PrimaryButton/index.ts b/src/shared/ui/PrimaryButton/index.ts new file mode 100644 index 0000000..12a56a0 --- /dev/null +++ b/src/shared/ui/PrimaryButton/index.ts @@ -0,0 +1 @@ +export { PrimaryButton } from './PrimaryButton' diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts new file mode 100644 index 0000000..63fcd73 --- /dev/null +++ b/src/shared/ui/index.ts @@ -0,0 +1,4 @@ +export { Button } from './Button' +export { FormField } from './FormField' +export { Notification } from './Notification' +export { PrimaryButton } from './PrimaryButton' diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/widgets/add-legal-entity-modal/index.ts b/src/widgets/add-legal-entity-modal/index.ts new file mode 100644 index 0000000..0f639e1 --- /dev/null +++ b/src/widgets/add-legal-entity-modal/index.ts @@ -0,0 +1 @@ +export { AddLegalEntityModal } from './ui/AddLegalEntityModal' diff --git a/src/widgets/add-legal-entity-modal/model/useAddLegalEntityForm.ts b/src/widgets/add-legal-entity-modal/model/useAddLegalEntityForm.ts new file mode 100644 index 0000000..495a6b2 --- /dev/null +++ b/src/widgets/add-legal-entity-modal/model/useAddLegalEntityForm.ts @@ -0,0 +1,86 @@ +import { useState } from 'react' +import { useCreateOrganization } from '@features/admin' +import type { CreateOrganizationRequest, Organization } from '@features/admin' + +const INITIAL = { + email: '', + password: '', + name: '', + inn: '', + short_name: '', + ogrn: '', + kpp: '', + legal_address: '', + actual_address: '', + contact_person: '', + contact_phone: '', + status: 'active', + bank_name: '', + bik: '', + account: '', + corr_account: '', +} + +type FormState = typeof INITIAL + +function extractErrorMessage(error: unknown): string { + const e = error as { detail?: unknown } + if (typeof e?.detail === 'string') return e.detail + if (Array.isArray(e?.detail) && (e.detail[0] as { msg?: string })?.msg) { + return (e.detail[0] as { msg: string }).msg + } + return 'Не удалось добавить юридическое лицо' +} + +export function useAddLegalEntityForm(onSuccess: (organization: Organization) => void) { + const [form, setForm] = useState(INITIAL) + const mutation = useCreateOrganization() + + const setField = (key: keyof FormState) => (value: string) => + setForm((prev) => ({ ...prev, [key]: value })) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + + const trimmedOrNull = (v: string) => (v.trim() ? v.trim() : null) + + const bankEntries: Record = {} + if (form.bank_name.trim()) bankEntries.bank_name = form.bank_name.trim() + if (form.bik.trim()) bankEntries.bik = form.bik.trim() + if (form.account.trim()) bankEntries.account = form.account.trim() + if (form.corr_account.trim()) bankEntries.corr_account = form.corr_account.trim() + + const payload: CreateOrganizationRequest = { + email: form.email.trim(), + password: form.password, + name: form.name.trim(), + inn: form.inn.trim(), + short_name: trimmedOrNull(form.short_name), + ogrn: trimmedOrNull(form.ogrn), + kpp: trimmedOrNull(form.kpp), + legal_address: trimmedOrNull(form.legal_address), + actual_address: trimmedOrNull(form.actual_address), + contact_person: trimmedOrNull(form.contact_person), + contact_phone: trimmedOrNull(form.contact_phone), + bank_details: Object.keys(bankEntries).length ? bankEntries : null, + status: form.status.trim() || 'active', + } + + mutation.mutate(payload, { + onSuccess: (organization) => { + setForm(INITIAL) + onSuccess(organization) + }, + }) + } + + const error = mutation.isError ? extractErrorMessage(mutation.error) : null + + return { + form, + setField, + handleSubmit, + isLoading: mutation.isPending, + error, + } +} diff --git a/src/widgets/add-legal-entity-modal/ui/AddLegalEntityModal.module.css b/src/widgets/add-legal-entity-modal/ui/AddLegalEntityModal.module.css new file mode 100644 index 0000000..62d29e1 --- /dev/null +++ b/src/widgets/add-legal-entity-modal/ui/AddLegalEntityModal.module.css @@ -0,0 +1,101 @@ +@keyframes dialogIn { + from { opacity: 0; transform: scale(0.96); } + to { opacity: 1; transform: scale(1); } +} + +.overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 16px; +} + +.dialog { + background: var(--bg-mid, #151520); + border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1)); + border-radius: 20px; + width: 100%; + max-width: 720px; + max-height: 90vh; + display: flex; + flex-direction: column; + animation: dialogIn 0.18s ease; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px 0; + flex-shrink: 0; +} + +.title { + font-size: 17px; + font-weight: 700; + color: var(--text-primary, #fff); +} + +.closeBtn { + background: none; + border: none; + color: var(--text-secondary, rgba(255, 255, 255, 0.4)); + font-size: 16px; + cursor: pointer; + padding: 4px; + line-height: 1; + transition: color 0.15s; +} + +.closeBtn:hover { + color: var(--text-primary, #fff); +} + +.body { + padding: 16px 24px 24px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 8px; +} + +.groupLabel { + font-size: 12px; + letter-spacing: 1.5px; + text-transform: uppercase; + color: var(--text-secondary, rgba(255, 255, 255, 0.5)); + font-weight: 600; + margin: 16px 0 4px; +} + +.groupLabel:first-child { + margin-top: 4px; +} + +.grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.error { + color: #ff5a5a; + font-size: 13px; + margin: 12px 0 0; + text-align: center; +} + +.actions { + margin-top: 20px; +} + +@media (max-width: 560px) { + .grid { + grid-template-columns: 1fr; + } +} diff --git a/src/widgets/add-legal-entity-modal/ui/AddLegalEntityModal.tsx b/src/widgets/add-legal-entity-modal/ui/AddLegalEntityModal.tsx new file mode 100644 index 0000000..edc0e13 --- /dev/null +++ b/src/widgets/add-legal-entity-modal/ui/AddLegalEntityModal.tsx @@ -0,0 +1,80 @@ +import { useEffect } from 'react' +import type { Organization } from '@features/admin' +import { FormField, PrimaryButton } from '@shared/ui' +import { useAddLegalEntityForm } from '../model/useAddLegalEntityForm' +import styles from './AddLegalEntityModal.module.css' + +interface Props { + open: boolean + onClose: () => void + onCreated: (organization: Organization) => void +} + +export function AddLegalEntityModal({ open, onClose, onCreated }: Props) { + const { form, setField, handleSubmit, isLoading, error } = useAddLegalEntityForm((organization) => { + onCreated(organization) + onClose() + }) + + useEffect(() => { + if (!open) return + function onKey(e: KeyboardEvent) { + if (e.key === 'Escape') onClose() + } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [open, onClose]) + + if (!open) return null + + function handleOverlay(e: React.MouseEvent) { + if (e.target === e.currentTarget) onClose() + } + + return ( +
+
+
+ Добавить юридическое лицо + +
+ +
+

Обязательные поля

+
+ + + + +
+ +

Дополнительные поля

+
+ + + + + + + + +
+ +

Банковские реквизиты

+
+ + + + +
+ + {error &&

{error}

} + +
+ +
+
+
+
+ ) +} diff --git a/src/widgets/admin-login-form/index.ts b/src/widgets/admin-login-form/index.ts new file mode 100644 index 0000000..b417604 --- /dev/null +++ b/src/widgets/admin-login-form/index.ts @@ -0,0 +1 @@ +export { AdminLoginForm } from './ui/AdminLoginForm' diff --git a/src/widgets/admin-login-form/model/useAdminLoginForm.ts b/src/widgets/admin-login-form/model/useAdminLoginForm.ts new file mode 100644 index 0000000..6f430ab --- /dev/null +++ b/src/widgets/admin-login-form/model/useAdminLoginForm.ts @@ -0,0 +1,35 @@ +import { useState } from 'react' +import { useAdminLogin } from '@features/admin' + +function extractErrorMessage(error: unknown): string { + const e = error as { detail?: unknown } + if (typeof e?.detail === 'string') return e.detail + if (Array.isArray(e?.detail) && (e.detail[0] as { msg?: string })?.msg) { + return (e.detail[0] as { msg: string }).msg + } + return 'Неверный логин или пароль' +} + +export function useAdminLoginForm() { + const [login, setLogin] = useState('') + const [password, setPassword] = useState('') + const loginMutation = useAdminLogin() + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (!login || !password) return + loginMutation.mutate({ login, password }) + } + + const error = loginMutation.isError ? extractErrorMessage(loginMutation.error) : null + + return { + login, + setLogin, + password, + setPassword, + isLoading: loginMutation.isPending, + error, + handleSubmit, + } +} diff --git a/src/widgets/admin-login-form/ui/AdminLoginForm.module.css b/src/widgets/admin-login-form/ui/AdminLoginForm.module.css new file mode 100644 index 0000000..4d9c196 --- /dev/null +++ b/src/widgets/admin-login-form/ui/AdminLoginForm.module.css @@ -0,0 +1,49 @@ +.wrap { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + background: var(--bg-deep); +} + +.card { + background: var(--glass-bg, rgba(255, 255, 255, 0.06)); + border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1)); + border-radius: 24px; + padding: 36px 32px; + width: 100%; + max-width: 420px; +} + +.title { + text-align: center; + font-size: 24px; + font-weight: 700; + color: var(--text-primary, #fff); + margin: 0 0 4px; +} + +.subtitle { + text-align: center; + font-size: 14px; + color: var(--text-secondary, rgba(255, 255, 255, 0.5)); + margin: 0 0 28px; +} + +.fields { + display: flex; + flex-direction: column; + gap: 18px; +} + +.error { + color: #ff5a5a; + font-size: 13px; + margin: 14px 0 0; + text-align: center; +} + +.submit { + margin-top: 26px; +} diff --git a/src/widgets/admin-login-form/ui/AdminLoginForm.tsx b/src/widgets/admin-login-form/ui/AdminLoginForm.tsx new file mode 100644 index 0000000..948342e --- /dev/null +++ b/src/widgets/admin-login-form/ui/AdminLoginForm.tsx @@ -0,0 +1,40 @@ +import { FormField, PrimaryButton } from '@shared/ui' +import { useAdminLoginForm } from '../model/useAdminLoginForm' +import styles from './AdminLoginForm.module.css' + +export function AdminLoginForm() { + const { login, setLogin, password, setPassword, isLoading, error, handleSubmit } = useAdminLoginForm() + + return ( +
+
+

Панель администратора

+

Войдите, чтобы продолжить

+ +
+ + +
+ + {error &&

{error}

} + +
+ +
+
+
+ ) +} diff --git a/src/widgets/legal-entities-table/index.ts b/src/widgets/legal-entities-table/index.ts new file mode 100644 index 0000000..828de42 --- /dev/null +++ b/src/widgets/legal-entities-table/index.ts @@ -0,0 +1 @@ +export { LegalEntitiesTable } from './ui/LegalEntitiesTable' diff --git a/src/widgets/legal-entities-table/ui/LegalEntitiesTable.module.css b/src/widgets/legal-entities-table/ui/LegalEntitiesTable.module.css new file mode 100644 index 0000000..6c7284c --- /dev/null +++ b/src/widgets/legal-entities-table/ui/LegalEntitiesTable.module.css @@ -0,0 +1,93 @@ +.tableWrap { + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 24px; + padding: 24px; + overflow-x: auto; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.table th { + text-align: left; + font-size: 12px; + letter-spacing: 1.5px; + text-transform: uppercase; + color: var(--text-secondary); + font-weight: 500; + padding: 0 16px 18px; + white-space: nowrap; +} + +.table td { + padding: 16px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + vertical-align: middle; + transition: background 0.2s ease; + font-size: 14px; + color: var(--text-primary); +} + +.table tr:hover td { + background: rgba(255, 255, 255, 0.04); +} + +.row { + cursor: pointer; +} + +.name { + font-weight: 600; + display: block; +} + +.subname { + display: block; + font-size: 12px; + color: var(--text-secondary); + margin-top: 2px; +} + +.mono { + font-family: var(--font-mono, monospace); + font-size: 13px; +} + +.status { + display: inline-block; + padding: 4px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 600; + background: rgba(74, 109, 255, 0.12); + color: #7c95ff; + white-space: nowrap; +} + +.kyc { + display: inline-block; + padding: 4px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 600; +} + +.kycOk { + background: rgba(0, 196, 140, 0.14); + color: #00c48c; +} + +.kycNo { + background: rgba(255, 90, 90, 0.14); + color: #ff5a5a; +} + +.state { + padding: 40px 16px; + text-align: center; + color: var(--text-secondary); + font-size: 14px; +} diff --git a/src/widgets/legal-entities-table/ui/LegalEntitiesTable.tsx b/src/widgets/legal-entities-table/ui/LegalEntitiesTable.tsx new file mode 100644 index 0000000..f88e16f --- /dev/null +++ b/src/widgets/legal-entities-table/ui/LegalEntitiesTable.tsx @@ -0,0 +1,88 @@ +import { useNavigate } from 'react-router-dom' +import { useOrganizations } from '@features/admin' +import { adminOrganizationPath } from '@shared/config/routes' +import styles from './LegalEntitiesTable.module.css' + +const STATUS_LABELS: Record = { + active: 'Активно', + blocked: 'Заблокировано', + inactive: 'Неактивно', +} + +function formatDate(value: string | null): string { + if (!value) return '—' + const d = new Date(value) + if (Number.isNaN(d.getTime())) return '—' + return d.toLocaleDateString('ru-RU') +} + +export function LegalEntitiesTable() { + const { data, isLoading, isError } = useOrganizations() + const navigate = useNavigate() + + if (isLoading) { + return
Загрузка...
+ } + + if (isError) { + return ( +
+
Не удалось загрузить список юридических лиц
+
+ ) + } + + if (!data || data.items.length === 0) { + return ( +
+
Юридические лица ещё не добавлены
+
+ ) + } + + return ( +
+ + + + + + + + + + + + + + + {data.items.map((org) => ( + navigate(adminOrganizationPath(org.id))} + > + + + + + + + + + + ))} + +
НазваниеИННКППКонтактное лицоТелефонСтатусKYCСоздано
+ {org.name} + {org.short_name && {org.short_name}} + {org.inn}{org.kpp ?? '—'}{org.contact_person ?? '—'}{org.contact_phone ?? '—'} + {STATUS_LABELS[org.status] ?? org.status} + + + {org.kyc_verified ? 'Да' : 'Нет'} + + {formatDate(org.created_at)}
+
+ ) +} diff --git a/src/widgets/organization-documents/index.ts b/src/widgets/organization-documents/index.ts new file mode 100644 index 0000000..fd0ebcc --- /dev/null +++ b/src/widgets/organization-documents/index.ts @@ -0,0 +1 @@ +export { OrganizationDocuments } from './ui/OrganizationDocuments' diff --git a/src/widgets/organization-documents/ui/OrganizationDocuments.module.css b/src/widgets/organization-documents/ui/OrganizationDocuments.module.css new file mode 100644 index 0000000..430ad97 --- /dev/null +++ b/src/widgets/organization-documents/ui/OrganizationDocuments.module.css @@ -0,0 +1,124 @@ +.section { + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 20px; + padding: 24px; +} + +.sectionTitle { + font-size: 14px; + letter-spacing: 1.5px; + text-transform: uppercase; + color: var(--text-secondary, rgba(255, 255, 255, 0.5)); + font-weight: 600; + margin: 0 0 18px; +} + +.slots { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; +} + +.slot { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; + padding: 16px 0; + border-top: 1px solid rgba(255, 255, 255, 0.06); +} + +.slot:first-child { + border-top: none; +} + +.slotInfo { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 200px; +} + +.slotLabel { + font-size: 14px; + font-weight: 600; + color: var(--text-primary, #fff); +} + +.slotFile { + font-size: 13px; + color: var(--text-secondary, rgba(255, 255, 255, 0.7)); +} + +.slotActions { + display: flex; + align-items: center; + gap: 10px; +} + +.hiddenInput { + display: none; +} + +.uploadBtn { + background: var(--interactive, #4a6dff); + border: none; + border-radius: 10px; + color: #fff; + font-size: 14px; + font-weight: 600; + padding: 11px 20px; + cursor: pointer; + transition: opacity 0.2s; + white-space: nowrap; +} + +.uploadBtn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.mono { + font-family: var(--font-mono, monospace); + font-size: 13px; +} + +.downloadBtn { + display: inline-block; + background: rgba(74, 109, 255, 0.12); + border: 1px solid rgba(74, 109, 255, 0.3); + border-radius: 8px; + color: #7c95ff; + font-size: 13px; + font-weight: 600; + padding: 7px 14px; + cursor: pointer; + text-decoration: none; + transition: background 0.2s; + white-space: nowrap; +} + +.downloadBtn:hover { + background: rgba(74, 109, 255, 0.22); +} + +.muted { + color: var(--text-secondary, rgba(255, 255, 255, 0.4)); +} + +.state { + padding: 32px 16px; + text-align: center; + color: var(--text-secondary, rgba(255, 255, 255, 0.6)); + font-size: 14px; +} + +.error { + color: #ff5a5a; + font-size: 13px; + margin: 0 0 12px; +} diff --git a/src/widgets/organization-documents/ui/OrganizationDocuments.tsx b/src/widgets/organization-documents/ui/OrganizationDocuments.tsx new file mode 100644 index 0000000..bee7800 --- /dev/null +++ b/src/widgets/organization-documents/ui/OrganizationDocuments.tsx @@ -0,0 +1,148 @@ +import { useRef, useState } from 'react' +import { useDocuments, useUploadDocument } from '@features/admin' +import type { DocumentResponse, DocumentTypeSlug } from '@features/admin' +import styles from './OrganizationDocuments.module.css' + +interface Props { + orgId: string +} + +const DOCUMENT_SLOTS: { type: DocumentTypeSlug; label: string }[] = [ + { type: 'charter', label: 'Устав' }, + { type: 'inn-certificate', label: 'Свидетельство ИНН' }, + { type: 'ogrn-certificate', label: 'Свидетельство ОГРН' }, + { type: 'bank-details', label: 'Банковские реквизиты' }, + { type: 'kyc-representative', label: 'Документ представителя (KYC)' }, + { type: 'power-of-attorney', label: 'Доверенность' }, + { type: 'other', label: 'Прочее' }, +] + +function formatSize(bytes: number | null): string { + if (bytes == null) return '—' + if (bytes < 1024) return `${bytes} Б` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} КБ` + return `${(bytes / (1024 * 1024)).toFixed(1)} МБ` +} + +function extractErrorMessage(error: unknown): string { + const e = error as { detail?: unknown; message?: unknown } + if (typeof e?.detail === 'string') return e.detail + if (Array.isArray(e?.detail) && (e.detail[0] as { msg?: string })?.msg) { + return (e.detail[0] as { msg: string }).msg + } + if (typeof e?.message === 'string') return e.message + return 'Не удалось выполнить операцию' +} + +export function OrganizationDocuments({ orgId }: Props) { + const { data: documents, isLoading, isError } = useDocuments(orgId) + const upload = useUploadDocument(orgId) + + // Which slot's file picker is currently being uploaded — drives the per-row + // "Загрузка..." state and disables only the active row. + const [activeType, setActiveType] = useState(null) + + // Map list items onto fixed slots. `document_type` may come back in + // underscore form (e.g. `inn_certificate`) while our slugs use hyphens. + const byType = new Map() + for (const doc of documents ?? []) { + byType.set(doc.document_type.replace(/_/g, '-'), doc) + } + + function handleSelect(type: DocumentTypeSlug, input: HTMLInputElement) { + const file = input.files?.[0] + // Reset so picking the same file again still fires onChange. + input.value = '' + if (!file) return + setActiveType(type) + upload.mutate( + { type, file }, + { onSettled: () => setActiveType(null) }, + ) + } + + const uploadError = upload.isError ? extractErrorMessage(upload.error) : null + + return ( +
+

Документы

+ + {uploadError &&

{uploadError}

} + + {isLoading &&
Загрузка...
} + {isError &&
Не удалось загрузить документы
} + + {!isLoading && !isError && ( +
    + {DOCUMENT_SLOTS.map(({ type, label }) => { + const doc = byType.get(type) + const isUploading = upload.isPending && activeType === type + return ( +
  • +
    + {label} + {doc ? ( + + {doc.file_name ?? '—'} + · {formatSize(doc.file_size_bytes)} + + ) : ( + Не загружен + )} +
    + +
    + {doc?.download_url && ( + + Скачать + + )} + handleSelect(type, input)} + /> +
    +
  • + ) + })} +
+ )} +
+ ) +} + +interface UploadButtonProps { + label: string + busy: boolean + disabled: boolean + onSelect: (input: HTMLInputElement) => void +} + +function UploadButton({ label, busy, disabled, onSelect }: UploadButtonProps) { + const inputRef = useRef(null) + return ( + <> + + onSelect(e.currentTarget)} + /> + + ) +} diff --git a/src/widgets/organization-purchase-requests/index.ts b/src/widgets/organization-purchase-requests/index.ts new file mode 100644 index 0000000..29b1895 --- /dev/null +++ b/src/widgets/organization-purchase-requests/index.ts @@ -0,0 +1 @@ +export { OrganizationPurchaseRequests } from './ui/OrganizationPurchaseRequests' diff --git a/src/widgets/organization-purchase-requests/ui/OrganizationPurchaseRequests.module.css b/src/widgets/organization-purchase-requests/ui/OrganizationPurchaseRequests.module.css new file mode 100644 index 0000000..67d19cd --- /dev/null +++ b/src/widgets/organization-purchase-requests/ui/OrganizationPurchaseRequests.module.css @@ -0,0 +1,50 @@ +.tableWrap { + overflow-x: auto; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.table th { + text-align: left; + font-size: 12px; + letter-spacing: 1.5px; + text-transform: uppercase; + color: var(--text-secondary); + font-weight: 500; + padding: 0 16px 14px; + white-space: nowrap; +} + +.table td { + padding: 14px 16px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + vertical-align: middle; + font-size: 14px; + color: var(--text-primary); +} + +.mono { + font-family: var(--font-mono, monospace); + font-size: 13px; +} + +.status { + display: inline-block; + padding: 4px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 600; + background: rgba(74, 109, 255, 0.12); + color: #7c95ff; + white-space: nowrap; +} + +.state { + padding: 32px 16px; + text-align: center; + color: var(--text-secondary, rgba(255, 255, 255, 0.6)); + font-size: 14px; +} diff --git a/src/widgets/organization-purchase-requests/ui/OrganizationPurchaseRequests.tsx b/src/widgets/organization-purchase-requests/ui/OrganizationPurchaseRequests.tsx new file mode 100644 index 0000000..257a663 --- /dev/null +++ b/src/widgets/organization-purchase-requests/ui/OrganizationPurchaseRequests.tsx @@ -0,0 +1,63 @@ +import { usePurchaseRequests } from '@features/admin' +import styles from './OrganizationPurchaseRequests.module.css' + +interface Props { + orgId: string +} + +function formatAmount(value: string | null, suffix: string): string { + if (!value) return '—' + return `${value} ${suffix}` +} + +function formatDate(value: string | null): string { + if (!value) return '—' + const d = new Date(value) + if (Number.isNaN(d.getTime())) return '—' + return d.toLocaleString('ru-RU') +} + +export function OrganizationPurchaseRequests({ orgId }: Props) { + const { data, isLoading, isError } = usePurchaseRequests(orgId) + + if (isLoading) { + return
Загрузка...
+ } + + if (isError) { + return
Не удалось загрузить заявки
+ } + + if (!data || data.items.length === 0) { + return
Заявок пока нет
+ } + + return ( +
+ + + + + + + + + + + + {data.items.map((req) => ( + + + + + + + + ))} + +
USDTСумма ₽КурсСтатусСоздана
{formatAmount(req.usdt_amount, 'USDT')}{formatAmount(req.rub_amount, '₽')}{req.exchange_rate ?? '—'} + {req.status} + {formatDate(req.created_at)}
+
+ ) +} diff --git a/src/widgets/organization-wallets/index.ts b/src/widgets/organization-wallets/index.ts new file mode 100644 index 0000000..b431a2a --- /dev/null +++ b/src/widgets/organization-wallets/index.ts @@ -0,0 +1 @@ +export { OrganizationWallets } from './ui/OrganizationWallets' diff --git a/src/widgets/organization-wallets/ui/OrganizationWallets.module.css b/src/widgets/organization-wallets/ui/OrganizationWallets.module.css new file mode 100644 index 0000000..e7c63b5 --- /dev/null +++ b/src/widgets/organization-wallets/ui/OrganizationWallets.module.css @@ -0,0 +1,52 @@ +.section { + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 20px; + padding: 24px; +} + +.sectionTitle { + font-size: 14px; + letter-spacing: 1.5px; + text-transform: uppercase; + color: var(--text-secondary, rgba(255, 255, 255, 0.5)); + font-weight: 600; + margin: 0 0 18px; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.table th { + text-align: left; + font-size: 12px; + letter-spacing: 1.5px; + text-transform: uppercase; + color: var(--text-secondary); + font-weight: 500; + padding: 0 16px 14px; + white-space: nowrap; +} + +.table td { + padding: 14px 16px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + vertical-align: middle; + font-size: 14px; + color: var(--text-primary); +} + +.mono { + font-family: var(--font-mono, monospace); + font-size: 13px; + word-break: break-all; +} + +.state { + padding: 32px 16px; + text-align: center; + color: var(--text-secondary, rgba(255, 255, 255, 0.6)); + font-size: 14px; +} diff --git a/src/widgets/organization-wallets/ui/OrganizationWallets.tsx b/src/widgets/organization-wallets/ui/OrganizationWallets.tsx new file mode 100644 index 0000000..0c3ded3 --- /dev/null +++ b/src/widgets/organization-wallets/ui/OrganizationWallets.tsx @@ -0,0 +1,53 @@ +import { useOrganizationWallets } from '@features/admin' +import styles from './OrganizationWallets.module.css' + +interface Props { + orgId: string +} + +function formatDate(value: string | null): string { + if (!value) return '—' + const d = new Date(value) + if (Number.isNaN(d.getTime())) return '—' + return d.toLocaleString('ru-RU') +} + +export function OrganizationWallets({ orgId }: Props) { + const { data: wallets, isLoading, isError } = useOrganizationWallets(orgId) + + return ( +
+

Кошельки

+ + {isLoading &&
Загрузка...
} + {isError &&
Не удалось загрузить кошельки
} + + {wallets && wallets.length === 0 && ( +
Кошельки ещё не созданы
+ )} + + {wallets && wallets.length > 0 && ( + + + + + + + + + + + {wallets.map((wallet) => ( + + + + + + + ))} + +
СетьАдресDerivation pathСоздано
{wallet.chain}{wallet.address}{wallet.derivation_path}{formatDate(wallet.created_at)}
+ )} +
+ ) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3370a46 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@app/*": ["src/app/*"], + "@pages/*": ["src/pages/*"], + "@widgets/*": ["src/widgets/*"], + "@features/*": ["src/features/*"], + "@entities/*": ["src/entities/*"], + "@shared/*": ["src/shared/*"] + } + }, + "include": ["src"] +} diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo new file mode 100644 index 0000000..435bb18 --- /dev/null +++ b/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/app.tsx","./src/app/providers/queryprovider.tsx","./src/app/providers/routerprovider.tsx","./src/app/providers/scrolltotop.tsx","./src/app/providers/index.ts","./src/features/admin/index.ts","./src/features/admin/api/adminapi.ts","./src/features/admin/hooks/useadminauth.ts","./src/features/admin/hooks/useadminlogin.ts","./src/features/admin/hooks/useadminlogout.ts","./src/features/admin/hooks/usecreateorganization.ts","./src/features/admin/hooks/usecreateorganizationwallets.ts","./src/features/admin/hooks/usedocuments.ts","./src/features/admin/hooks/useorganization.ts","./src/features/admin/hooks/useorganizationwallets.ts","./src/features/admin/hooks/useorganizations.ts","./src/features/admin/hooks/usepurchaserequests.ts","./src/features/admin/hooks/useupdateorganization.ts","./src/features/admin/hooks/useuploaddocument.ts","./src/features/admin/model/types.ts","./src/pages/admin/index.ts","./src/pages/admin/ui/adminpage.tsx","./src/pages/admin-organization/index.ts","./src/pages/admin-organization/model/useorganizationform.ts","./src/pages/admin-organization/ui/adminorganizationpage.tsx","./src/shared/config/routes.ts","./src/shared/ui/index.ts","./src/shared/ui/button/button.tsx","./src/shared/ui/button/index.ts","./src/shared/ui/formfield/formfield.tsx","./src/shared/ui/formfield/index.ts","./src/shared/ui/notification/notification.tsx","./src/shared/ui/notification/index.ts","./src/shared/ui/primarybutton/primarybutton.tsx","./src/shared/ui/primarybutton/index.ts","./src/widgets/add-legal-entity-modal/index.ts","./src/widgets/add-legal-entity-modal/model/useaddlegalentityform.ts","./src/widgets/add-legal-entity-modal/ui/addlegalentitymodal.tsx","./src/widgets/admin-login-form/index.ts","./src/widgets/admin-login-form/model/useadminloginform.ts","./src/widgets/admin-login-form/ui/adminloginform.tsx","./src/widgets/legal-entities-table/index.ts","./src/widgets/legal-entities-table/ui/legalentitiestable.tsx","./src/widgets/organization-documents/index.ts","./src/widgets/organization-documents/ui/organizationdocuments.tsx","./src/widgets/organization-purchase-requests/index.ts","./src/widgets/organization-purchase-requests/ui/organizationpurchaserequests.tsx","./src/widgets/organization-wallets/index.ts","./src/widgets/organization-wallets/ui/organizationwallets.tsx"],"version":"5.6.3"} \ No newline at end of file diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..0f32683 --- /dev/null +++ b/vercel.json @@ -0,0 +1,3 @@ +{ + "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..185d75f --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tsconfigPaths from 'vite-tsconfig-paths' + +export default defineConfig({ + plugins: [react(), tsconfigPaths()], + server: { + port: 3001, + }, +})