commit 04cc53e66d1630063fe185d62e0feb6c6243c452 Author: Drobysh Date: Sun Sep 20 15:08:32 2026 +0300 initial commit (frontend) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b6411a --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.env +.env.* +deploy.zip diff --git a/README.md b/README.md new file mode 100644 index 0000000..c300135 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) + +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) + +``` diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..ef614d2 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,22 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + }, +]) diff --git a/index.html b/index.html new file mode 100644 index 0000000..c2dc76a --- /dev/null +++ b/index.html @@ -0,0 +1,27 @@ + + + + + + + Filwords PL - Nauka polskiego przez grę + + + + +
+ + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8492ee3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2829 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router-dom": "^7.18.1" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "eslint": "^10.6.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.3", + "globals": "^17.7.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.62.0", + "vite": "^8.1.1" + } + }, + "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-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/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/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "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.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "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/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "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/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.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.65.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.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.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/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.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/@vitejs/plugin-react": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz", + "integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "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/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/baseline-browser-mapping": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz", + "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "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.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "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/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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.396", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", + "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==", + "dev": true, + "license": "ISC" + }, + "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": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@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", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.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", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz", + "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "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.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz", + "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", + "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": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/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/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/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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": "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/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.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "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.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "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/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.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "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.16", + "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.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-router": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", + "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.18.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", + "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "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/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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "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": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.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/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "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": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "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/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" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1fa1c98 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router-dom": "^7.18.1" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "eslint": "^10.6.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.3", + "globals": "^17.7.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.62.0", + "vite": "^8.1.1" + } +} diff --git a/public/carousel/01-partners.svg b/public/carousel/01-partners.svg new file mode 100644 index 0000000..7772f41 --- /dev/null +++ b/public/carousel/01-partners.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/carousel/01-partners@2x.png b/public/carousel/01-partners@2x.png new file mode 100644 index 0000000..b47db2d Binary files /dev/null and b/public/carousel/01-partners@2x.png differ diff --git a/public/carousel/02-contacts.svg b/public/carousel/02-contacts.svg new file mode 100644 index 0000000..2360751 --- /dev/null +++ b/public/carousel/02-contacts.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/carousel/02-contacts@2x.png b/public/carousel/02-contacts@2x.png new file mode 100644 index 0000000..185b2eb Binary files /dev/null and b/public/carousel/02-contacts@2x.png differ diff --git a/public/carousel/03-apps.svg b/public/carousel/03-apps.svg new file mode 100644 index 0000000..c017de2 --- /dev/null +++ b/public/carousel/03-apps.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/carousel/03-apps@2x.png b/public/carousel/03-apps@2x.png new file mode 100644 index 0000000..29bf256 Binary files /dev/null and b/public/carousel/03-apps@2x.png differ diff --git a/public/carousel/04-news.svg b/public/carousel/04-news.svg new file mode 100644 index 0000000..a3735e9 --- /dev/null +++ b/public/carousel/04-news.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/carousel/04-news@2x.png b/public/carousel/04-news@2x.png new file mode 100644 index 0000000..599eea5 Binary files /dev/null and b/public/carousel/04-news@2x.png differ diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..f90339d --- /dev/null +++ b/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..55e2bed --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,159 @@ +import { useEffect, useState } from 'react'; +import { BrowserRouter, Routes, Route, useNavigate, useLocation } from 'react-router-dom'; +import { LanguageProvider, useLanguage } from './i18n/LanguageContext'; +import LanguageSelectPage from './pages/LanguageSelectPage'; +import MenuPage from './pages/MenuPage'; +import WordSearchPage from './pages/WordSearchPage'; +import LeaderboardPage from './pages/LeaderboardPage'; +import SentenceBuilderPage from './pages/SentenceBuilderPage'; +import FlashcardsPage from './pages/FlashcardsPage'; +import WordQuizPage from './pages/WordQuizPage'; +import BubbleWordsPage from './pages/BubbleWordsPage'; +import PronunciationPage from './pages/PronunciationPage'; +import LessonsPage from './pages/LessonsPage'; +import TopicLessonsPage from './pages/TopicLessonsPage'; +import LessonViewPage from './pages/LessonViewPage'; +import TestViewPage from './pages/TestViewPage'; +import PracticeViewPage from './pages/PracticeViewPage'; +import TeacherChatPage from './pages/TeacherChatPage'; +import TypingPage from './pages/TypingPage'; +import AdminDashboardModal from './components/AdminDashboardModal'; +import { registerUser, trackDailyLogin, getIsAdmin } from './api'; +import { getUserId } from './telegram'; + +function AppContent() { + const navigate = useNavigate(); + const location = useLocation(); + const { setLang } = useLanguage(); + const [showDashboard, setShowDashboard] = useState(false); + const [isAdmin, setIsAdmin] = useState(() => { + // Начальное значение из localStorage, чтобы не мигало при загрузке + return localStorage.getItem('filwords_is_admin') === '1'; + }); + + useEffect(() => { + const tg = window.Telegram?.WebApp; + if (tg) { + tg.ready(); + tg.expand(); + + if (typeof tg.disableVerticalSwipes === 'function') { + tg.disableVerticalSwipes(); + } + } + + const userId = getUserId(); + if (userId) { + registerUser() + .then((data) => { + trackDailyLogin().catch(() => {}); + + // 🔧 ИСПРАВЛЕНО: получаем is_admin с бэка, а не из хардкода + if (typeof data.is_admin === 'boolean') { + setIsAdmin(data.is_admin); + localStorage.setItem('filwords_is_admin', data.is_admin ? '1' : '0'); + } else { + // Fallback: если бэк по какой-то причине не вернул флаг — спрашиваем отдельно + getIsAdmin() + .then((adminData) => { + setIsAdmin(adminData.is_admin); + localStorage.setItem('filwords_is_admin', adminData.is_admin ? '1' : '0'); + }) + .catch(() => {}); + } + + const langFromStorage = localStorage.getItem('filwords_lang'); + + if (!langFromStorage) { + // Первый визит — показываем выбор языка + navigate('/language'); + } else if (data.app_language && data.app_language !== langFromStorage) { + // Синхронизируем язык с сервером + setLang(data.app_language); + } + }) + .catch(() => {}); + } + }, []); + + // 🔧 Слушаем событие "open-dashboard" из MenuPage — открывает модалку дашборда + useEffect(() => { + const handleOpenDashboard = () => { + if (isAdmin) { + setShowDashboard(true); + } + }; + + window.addEventListener('open-dashboard', handleOpenDashboard); + return () => { + window.removeEventListener('open-dashboard', handleOpenDashboard); + }; + }, [isAdmin]); + + useEffect(() => { + window.scrollTo(0, 0); + document.documentElement.scrollTop = 0; + document.body.scrollTop = 0; + + const raf = requestAnimationFrame(() => { + window.scrollTo(0, 0); + document.documentElement.scrollTop = 0; + document.body.scrollTop = 0; + }); + + const timer = setTimeout(() => { + window.scrollTo(0, 0); + document.documentElement.scrollTop = 0; + document.body.scrollTop = 0; + }, 50); + + return () => { + cancelAnimationFrame(raf); + clearTimeout(timer); + }; + }, [location.pathname]); + + return ( + <> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + {/* Дашборд админа — открывается через событие "open-dashboard" из MenuPage */} + {showDashboard && isAdmin && ( + setShowDashboard(false)} + /> + )} + + ); +} + +function App() { + return ( + + + + + + ); +} + +export default App; \ No newline at end of file diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..c27280a --- /dev/null +++ b/src/api.ts @@ -0,0 +1,973 @@ +import { getInitData } from './telegram'; + +const API_BASE = 'https://api.filwordspl.com'; + +const fetchAPI = async (endpoint: string, options: RequestInit = {}) => { + const initData = getInitData(); + + const url = `${API_BASE}${endpoint}${endpoint.includes('?') ? '&' : '?'}initData=${encodeURIComponent(initData)}`; + + const res = await fetch(url, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }); + + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`API Error: ${res.status} - ${errorText}`); + } + + const data = await res.json(); + return data; +}; + +// ============ Пользователь ============ +export const registerUser = (): Promise<{ status: string; app_language: string; current_level: string; user_id: number; is_admin: boolean }> => + fetchAPI('/users/register', { method: 'POST' }); + +export const getIsAdmin = (): Promise<{ is_admin: boolean }> => + fetchAPI('/users/me/is-admin'); + +export const setLanguage = (language: string) => + fetchAPI('/users/set-language', { + method: 'POST', + body: JSON.stringify({ language }), + }); + +export const setUserLevel = (level: string) => + fetchAPI('/users/set-level', { + method: 'POST', + body: JSON.stringify({ level }), + }); + +// ============ Админ-панель ============ +export const getAdminDashboard = (): Promise<{ + total_users: number; + new_today: number; + online_now: number; + active_24h: number; + level_counts: Record; +}> => + fetchAPI('/admin/dashboard'); + +export const getSentryErrors = (): Promise<{ + errors: Array<{ + id: string; + short_id: string; + title: string; + culprit: string; + level: string; + count: number; + user_count: number; + first_seen: string; + last_seen: string; + permalink: string; + priority: string; + }>; + count?: number; + error?: string; +}> => + fetchAPI('/admin/sentry-errors'); + +// ============ Прогресс ============ +export const fetchProgress = (): Promise<{ last_completed_level: number; total_score: number; current_level?: string }> => + fetchAPI('/game/progress'); + +// ============ Игра ============ +export const getLevel = (levelNumber: number) => + fetchAPI(`/game/level/${levelNumber}`); + +export const checkWord = (levelId: number, cells: number[][]) => + fetchAPI('/game/check-word', { + method: 'POST', + body: JSON.stringify({ level_id: levelId, cells }), + }); + +export const completeLevel = (levelId: number, hintsUsed: number, completionTime?: number, perfectStreak?: number) => + fetchAPI('/game/complete-level', { + method: 'POST', + body: JSON.stringify({ + level_id: levelId, + hints_used: hintsUsed, + completion_time: completionTime || undefined, + perfect_streak: perfectStreak || undefined, + }), + }); + +// ============ Подсказки ============ +export const getFirstLetterHint = (levelId: number) => + fetchAPI(`/hints/first-letter/${levelId}`); + +export const getWordList = (levelId: number) => + fetchAPI(`/hints/word-list/${levelId}`); + +// ============ Рейтинг ============ +export const getLeaderboard = (limit: number = 50) => + fetchAPI(`/leaderboard?limit=${limit}`); + +export const getAdminStats = () => + fetchAPI('/leaderboard/admin/stats'); + +// ============ Предложения ============ +export const getNextSentence = () => + fetchAPI('/sentences/next'); + +export const checkSentence = (sentenceId: number, words: string[], completionTime?: number) => + fetchAPI('/sentences/check', { + method: 'POST', + body: JSON.stringify({ + sentence_id: sentenceId, + words: words, + completion_time: completionTime || undefined, + }), + }); + +export const getSentenceHint = (sentenceId: number) => + fetchAPI(`/sentences/${sentenceId}/hint`); + +// ============ Word Quiz ============ +export const getNextQuiz = () => + fetchAPI('/wordquiz/next'); + +export const checkQuizAnswer = (correctPolish: string, answer: string, mistakes: number, completionTime?: number) => + fetchAPI('/wordquiz/check', { + method: 'POST', + body: JSON.stringify({ + correct_polish: correctPolish, + answer: answer, + mistakes: mistakes, + completion_time: completionTime || undefined, + }), + }); + +export const getQuizProgress = () => + fetchAPI('/wordquiz/progress'); + +// ============ Bubble Words ============ +export const getBubbleLevel = () => + fetchAPI('/bubblewords/next'); + +export const completeBubbleLevel = (hintsUsed: number, wordsCount: number, completionTime?: number) => + fetchAPI('/bubblewords/complete', { + method: 'POST', + body: JSON.stringify({ + hints_used: hintsUsed, + words_count: wordsCount, + completion_time: completionTime || undefined, + }), + }); + +export const getBubbleProgress = () => + fetchAPI('/bubblewords/progress'); + +// ============ Произношение ============ +export const getNextPronunciationWord = () => + fetchAPI('/pronunciation/next'); + +// ============ TTS (Edge TTS) ============ +export const getTTS = (text: string, voice: string = 'female'): Promise => { + const initData = getInitData(); + + return fetch(`${API_BASE}/tts?text=${encodeURIComponent(text)}&voice=${voice}&initData=${encodeURIComponent(initData)}`, { + method: 'GET', + }).then(async (res) => { + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`API Error: ${res.status} - ${errorText}`); + } + return res.blob(); + }); +}; + +// ============ AI Assistant ============ +export const sendMessageToAssistant = (message: string): Promise<{ response: string; limit_exceeded?: boolean; daily_limit?: number; used?: number; remaining?: number }> => + fetchAPI('/assistant/chat', { + method: 'POST', + body: JSON.stringify({ message }), + }); + +export const getAssistantLimit = (): Promise<{ is_admin: boolean; daily_limit: number | null; used: number; remaining: number | null }> => + fetchAPI('/assistant/limit'); + +export const getAssistantHistory = (): Promise<{ history: Array<{ message: string; is_user: boolean; created_at: string | null }> }> => + fetchAPI('/assistant/history'); + +export const clearAssistantHistory = (): Promise<{ status: string }> => + fetchAPI('/assistant/history/clear', { method: 'POST' }); + +export const saveAnalyticsToHistory = (message: string): Promise<{ status: string }> => + fetchAPI('/assistant/save-analytics', { + method: 'POST', + body: JSON.stringify({ message }), + }); + +export const getPersonalStats = (): Promise => + fetchAPI('/assistant/stats/personal'); + +export const getAdminStatsFromAssistant = (): Promise => + fetchAPI('/assistant/stats/admin'); + +// ============ Уроки (Learning) ============ +export const getLearningLevels = () => + fetchAPI('/learning/levels'); + +export const getLearningTopics = (level: string) => + fetchAPI(`/learning/topics?level=${level}`); + +export const getLearningTopic = (topicId: string) => + fetchAPI(`/learning/topics/${topicId}`); + +export const getLearningLesson = (lessonId: string) => + fetchAPI(`/learning/lessons/${lessonId}`); + +export const getTopicTest = (topicId: string) => + fetchAPI(`/learning/test/${topicId}`); + +export const startLearningSession = (levelCode: string, topicId: string, sessionType: string = 'lesson') => + fetchAPI('/learning/session/start', { + method: 'POST', + body: JSON.stringify({ level_code: levelCode, topic_id: topicId, session_type: sessionType }), + }); + +export const saveLearningAnswer = (sessionId: number, questionId: string, answer: string, timeTakenMs: number = 0, isFinalAnswer: boolean = true, hintUsed: boolean = false) => + fetchAPI('/learning/answer', { + method: 'POST', + body: JSON.stringify({ + session_id: sessionId, + question_id: questionId, + answer: answer, + time_taken_ms: timeTakenMs, + is_final_answer: isFinalAnswer, + hint_used: hintUsed, + }), + }); + +export const completeLearningSession = (sessionId: number) => + fetchAPI('/learning/session/complete', { + method: 'POST', + body: JSON.stringify({ session_id: sessionId }), + }); + +export const getLearningProgress = () => + fetchAPI('/learning/progress'); + +export const getLearningAnalytics = (): Promise<{ analytics_enabled: boolean; response?: string; message?: string; weak_skills?: any[]; has_practice_available?: boolean; cta_label?: string }> => + fetchAPI('/learning/analytics', { method: 'POST' }); + +export const getAIContext = () => + fetchAPI('/learning/ai/context'); + +// ============ Практика слабых навыков ============ +export const startWeakSkillsPractice = (): Promise<{ has_weak_skills: boolean; message?: string; session_id?: number; explanations?: any[]; questions?: any[] }> => + fetchAPI('/learning/practice/start', { method: 'POST' }); + +// ============ Печать (Typing) ============ +export const getNextTypingSentence = (): Promise<{ completed: boolean; message?: string; sentence_id?: number; translation?: string; level?: string }> => + fetchAPI('/typing/next'); + +export const checkTypingSentence = (sentenceId: number, text: string): Promise<{ correct: boolean; correct_answer: string }> => + fetchAPI('/typing/check', { + method: 'POST', + body: JSON.stringify({ sentence_id: sentenceId, text }), + }); + +export const getTypingHint = (sentenceId: number): Promise<{ correct_answer: string }> => + fetchAPI(`/typing/hint/${sentenceId}`); + +export const getTypingProgress = (): Promise<{ total: number; completed: number; remaining: number }> => + fetchAPI('/typing/progress'); + +// ============ Достижения ============ +export const getAllAchievements = () => + fetchAPI('/achievements'); + +export const getUserAchievements = () => + fetchAPI('/achievements/user'); + +export const trackLogin = () => + fetchAPI('/achievements/track-login', { method: 'POST' }); + +export const trackTeacherMenuOpened = () => + fetchAPI('/achievements/track-teacher-menu', { method: 'POST' }); + +export const trackProgressOpened = () => + fetchAPI('/achievements/track-progress', { method: 'POST' }); + +export const trackCheatsheetsOpened = () => + fetchAPI('/achievements/track-cheatsheets', { method: 'POST' }); + +export const trackPronunciationOpened = () => + fetchAPI('/achievements/track-pronunciation', { method: 'POST' }); + +export const trackAIAssistantOpened = () => + fetchAPI('/achievements/track-ai-assistant', { method: 'POST' }); + +export const trackAlphabetOpened = () => + fetchAPI('/achievements/track-alphabet', { method: 'POST' }); + +export const trackRulesOpened = () => + fetchAPI('/achievements/track-rules', { method: 'POST' }); + +// ============ Дневной вход ============ +export const trackDailyLogin = async () => { + try { + const data = await trackLogin(); + return data; + } catch (e) { + console.error('Error tracking login:', e); + return null; + } +}; + +// ============ Teacher (Чат-учитель) ============ +export const teacherStart = (): Promise<{ + message_type: string; + session_id?: number; + lesson_id?: string; + topic_id?: string; + topic_name?: string; + text: string; + question?: any; + question_index?: number; + total_questions?: number; + has_explain_command?: boolean; + has_translate_command?: boolean; +}> => + fetchAPI('/teacher/start', { method: 'POST' }); + +export const teacherResume = (): Promise<{ + has_active_session: boolean; + session_id?: number; + session_type?: string; + topic_id?: string; + question?: any; + question_index?: number; + total_questions?: number; + session_completed?: boolean; +}> => + fetchAPI('/teacher/resume', { method: 'POST' }); + +export const teacherAnswer = (sessionId: number, questionId: string, answer: string, timeTakenMs?: number): Promise<{ + message_type: string; + reaction: string; + explanation?: string; + is_correct: boolean; + correct_answer?: string; + wrong_because?: string; + question?: any; + question_index?: number; + total_questions?: number; + session_completed?: boolean; + session_stats?: { + total: number; + correct_count: number; + }; + session_type?: string; + has_explain_command?: boolean; + has_translate_command?: boolean; +}> => + fetchAPI('/teacher/answer', { + method: 'POST', + body: JSON.stringify({ + session_id: sessionId, + question_id: questionId, + answer: answer, + time_taken_ms: timeTakenMs || 0, + }), + }); + +export const teacherExplain = (lessonId?: string, questionId?: string, topic?: string): Promise<{ response: string; limit_exceeded?: boolean; message?: string }> => + fetchAPI('/teacher/explain', { + method: 'POST', + body: JSON.stringify({ + lesson_id: lessonId || '', + question_id: questionId || '', + topic: topic || '', + }), + }); + +export const teacherTranslate = (text: string): Promise<{ response: string; limit_exceeded?: boolean; message?: string }> => + fetchAPI('/teacher/translate', { + method: 'POST', + body: JSON.stringify({ text }), + }); + +export const teacherExamples = (topic: string, lessonId?: string, topicId?: string): Promise<{ response: string; limit_exceeded?: boolean; message?: string }> => + fetchAPI('/teacher/examples', { + method: 'POST', + body: JSON.stringify({ + topic: topic || '', + lesson_id: lessonId || '', + topic_id: topicId || '', + }), + }); + +// ============ Teacher Free Message (свободный ввод) ============ +export const teacherFreeMessage = (message: string, lessonId?: string, topicId?: string, topicName?: string): Promise<{ + response: string; + limit_exceeded?: boolean; + message?: string; +}> => + fetchAPI('/teacher/free-message', { + method: 'POST', + body: JSON.stringify({ + message, + lesson_id: lessonId || '', + topic_id: topicId || '', + topic_name: topicName || '', + }), + }); + +// ============ Teacher Diagnostic Test (узнать уровень) ============ +export const teacherDiagnosticTest = (): Promise<{ + session_id: number; + question?: any; + question_index?: number; + total_questions?: number; +}> => + fetchAPI('/teacher/diagnostic-test', { method: 'POST' }); + +// ============ Teacher Translate To Polish (переведи на польский) ============ +export const teacherTranslateToPolish = (text: string): Promise<{ + response: string; + limit_exceeded?: boolean; + message?: string; +}> => + fetchAPI('/teacher/translate-to-polish', { + method: 'POST', + body: JSON.stringify({ text }), + }); + +// ============ Teacher History ============ +export const saveTeacherHistory = (messages: any[]): Promise<{ status: string }> => + fetchAPI('/teacher/history/save', { + method: 'POST', + body: JSON.stringify({ messages }), + }); + +export const loadTeacherHistory = (): Promise<{ messages: any[] }> => + fetchAPI('/teacher/history/load'); + +export const clearTeacherHistory = (): Promise<{ status: string }> => + fetchAPI('/teacher/history/clear', { method: 'POST' }); + +// ============ Прогресс по уровням для меню учителя ============ +export const getTeacherProgress = (): Promise<{ + levels: Array<{ + code: string; + name_pl: string; + name_ru: string; + name_uk: string; + name_en: string; + topics_total: number; + topics_completed: number; + }>; +}> => + fetchAPI('/teacher/progress'); + +// ============ Фраза дня ============ +export const getPhraseOfDay = (): Promise<{ + phrase_pl: string; + date: string | null; + translation?: string; + grammar_breakdown: string; +}> => + fetchAPI('/teacher/phrase-of-day'); + +// ============ ПОДДЕРЖКА ============ +export const sendSupportMessage = (message: string): Promise<{ status: string; ticket_id: number }> => + fetchAPI('/support/send', { + method: 'POST', + body: JSON.stringify({ message }), + }); + +export const getUserTickets = (): Promise<{ + tickets: Array<{ + id: number; + status: string; + created_at: string; + updated_at: string; + }>; +}> => + fetchAPI('/support/user-tickets'); + +export const getUserMessages = (ticketId: number): Promise<{ + messages: Array<{ + id: number; + sender_id: number; + message: string; + is_read: boolean; + created_at: string; + }>; +}> => + fetchAPI(`/support/user-messages/${ticketId}`); + +export const deleteTicket = (ticketId: number): Promise<{ status: string }> => + fetchAPI(`/support/delete-ticket/${ticketId}`, { method: 'POST' }); + +export const getAdminTickets = (): Promise<{ + tickets: Array<{ + id: number; + user_id: number; + status: string; + created_at: string; + updated_at: string; + username: string | null; + app_language: string; + unread_count: number; + last_message: string | null; + }>; +}> => + fetchAPI('/support/admin-tickets'); + +export const getAdminMessages = (ticketId: number): Promise<{ + messages: Array<{ + id: number; + sender_id: number; + message: string; + is_read: boolean; + created_at: string; + }>; +}> => + fetchAPI(`/support/admin-messages/${ticketId}`); + +export const adminReply = (ticketId: number, message: string): Promise<{ status: string }> => + fetchAPI('/support/admin-reply', { + method: 'POST', + body: JSON.stringify({ ticket_id: ticketId, message }), + }); + +export const getUnreadCount = (): Promise<{ unread_count: number }> => + fetchAPI('/support/unread-count'); + +export const getUserUnreadCount = (): Promise<{ unread_count: number }> => + fetchAPI('/support/user-unread-count'); + +export const translateMessage = (text: string, targetLang: string): Promise<{ translation: string }> => + fetchAPI('/support/translate', { + method: 'POST', + body: JSON.stringify({ text, target_lang: targetLang }), + }); + +// ============ 🔧 FEED (карусель) ============ +export type FeedSection = 'news' | 'partners' | 'contacts' | 'apps'; + +export interface FeedPostResponse { + id: number; + section: FeedSection; + title: string; + body: string; + images: string[]; + linkUrl: string | null; + telegramUrl: string | null; + createdAt: string | null; + updatedAt: string | null; + expiresAt: string | null; + isPinned: boolean; +} + +export interface FeedPostPayload { + section?: FeedSection; + source_language?: 'pl' | 'ru' | 'uk' | 'en'; + title?: string; + body?: string; + link_url?: string | null; + telegram_url?: string | null; + is_pinned?: boolean; + is_published?: boolean; + expires_at?: string | null; + translate_again?: boolean; +} + +export interface FeedListResponse { + posts: FeedPostResponse[]; + section: FeedSection; + lang: string; +} + +/** + * Получить ленту постов для секции. + * lang — опционально, если не передан — бэк возьмёт язык из профиля юзера. + */ +export const getFeedSection = (section: FeedSection, lang?: string): Promise => { + const langParam = lang ? `?lang=${encodeURIComponent(lang)}` : ''; + return fetchAPI(`/feed/${section}${langParam}`); +}; + +/** + * Получить один пост по ID. + */ +export const getFeedPost = (postId: number, lang?: string): Promise => { + const langParam = lang ? `?lang=${encodeURIComponent(lang)}` : ''; + return fetchAPI(`/feed/post/${postId}${langParam}`); +}; + +/** + * Создать пост (только админ). + * Бэк автоматически переведёт title и body на 4 языка через MiniMax-M3. + */ +export const createFeedPost = (payload: FeedPostPayload): Promise<{ status: string; post_id: number }> => + fetchAPI('/admin/feed/post', { + method: 'POST', + body: JSON.stringify(payload), + }); + +/** + * Обновить пост (только админ). + * Если translate_again = true — перевод будет пересоздан. + */ +export const updateFeedPost = (postId: number, payload: FeedPostPayload): Promise<{ status: string; post_id: number }> => + fetchAPI(`/admin/feed/post/${postId}`, { + method: 'PUT', + body: JSON.stringify(payload), + }); + +/** + * Удалить пост (только админ). + * + * 🔧 ФИКС (18.09.2026): POST вместо DELETE. + * + * Причина: Telegram WebView (iOS / macOS) не пропускает DELETE — + * браузер обязан отправить CORS preflight (OPTIONS), а Cloudflare + * кеширует его на 20 дней и отдаёт устаревший ответ без DELETE в + * allow-methods. В итоге WebView кидает "Load failed". + * + * POST без Content-Type и без кастомных заголовков — simple request, + * preflight не отправляется, Cloudflare не вмешивается. + */ +export const deleteFeedPost = async (postId: number): Promise<{ status: string }> => { + const initData = getInitData(); + const url = `${API_BASE}/admin/feed/post/${postId}/delete?initData=${encodeURIComponent(initData)}`; + + const res = await fetch(url, { method: 'POST' }); + + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`API Error: ${res.status} - ${errorText}`); + } + + return res.json(); +}; + +/** + * Перевести title+body без сохранения (для превью в админ-UI). + */ +export const translateFeedPreview = (payload: { + title: string; + body: string; + source_language: 'pl' | 'ru' | 'uk' | 'en'; +}): Promise<{ + title_pl: string; title_ru: string; title_uk: string; title_en: string; + body_pl: string; body_ru: string; body_uk: string; body_en: string; +}> => + fetchAPI('/admin/feed/translate', { + method: 'POST', + body: JSON.stringify(payload), + }); + +/** + * Загрузить картинку к посту (только админ). + * ⚠️ НЕ через fetchAPI — потому что там жёстко Content-Type: application/json, + * а для файлов нужен multipart/form-data (boundary ставит браузер). + * + * 🔧 ФИКС (18.09.2026, вечер): принимает готовый Blob, а не File. + * + * Причина: в Telegram WebView (iOS/macOS) File-объект «протухает» через + * несколько сотен мс после выбора из галереи. Если читать file.arrayBuffer() + * здесь, второй раз — fetch падает с TypeError: Failed to fetch ДО отправки + * запроса. В uvicorn это не видно. + * + * Решение: вызывающий код (FeedPostComposer) читает байты ОДИН РАЗ в + * handleSlotFile, создаёт Blob и хранит его в state. Мы просто кладём + * Blob в FormData и отправляем — WebView его не «съест», потому что это + * уже in-memory объект. + * + * @param postId ID поста, к которому привязать картинку + * @param blob Готовый Blob с байтами (создан из arrayBuffer() в UI) + * @param originalName Оригинальное имя файла (для расширения) + * @param mimeType MIME-тип (image/jpeg, image/png, ...) + */ +export const uploadFeedImage = async ( + postId: number, + blob: Blob, + originalName: string, + mimeType: string, +): Promise<{ + status: string; + file_path: string; + order_number: number; +}> => { + const initData = getInitData(); + const url = `${API_BASE}/admin/feed/upload-image?initData=${encodeURIComponent(initData)}`; + + // Определяем имя с правильным расширением + const extFromType = (mimeType || 'image/jpeg').split('/')[1] || 'jpg'; + const cleanExt = extFromType === 'jpeg' ? 'jpg' : extFromType; + const cleanName = (originalName || 'image').replace(/\.[^.]+$/, '') + '.' + cleanExt; + + const formData = new FormData(); + formData.append('post_id', String(postId)); + formData.append('file', blob, cleanName); + + const res = await fetch(url, { + method: 'POST', + body: formData, + // ❌ НЕ ставим Content-Type — браузер сам поставит multipart/form-data + boundary + }); + + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`API Error: ${res.status} - ${errorText}`); + } + + return res.json(); +}; + +// ============ ADMIN CONTENT (контент-менеджер) ============ + +export interface ContentSummaryLevel { + code: string; + name_ru: string; + topics_total: number; + lessons_total: number; + words_total: number; + texts_total: number; + last_edited: string | null; +} + +export interface ContentSummary { + levels: ContentSummaryLevel[]; +} + +export interface ContentWordLevel { + code: string; + name_ru: string; + words_count: number; + last_edited: string | null; +} + +export interface ContentWord { + id: number; + word_pl: string; + translation_ru: string; + translation_uk: string; + translation_en: string; + edited_at: string | null; +} + +export interface ContentWordsList { + level: string; + count: number; + words: ContentWord[]; +} + +export interface ContentTextLevel { + code: string; + name_ru: string; + texts_count: number; + last_edited: string | null; +} + +export interface ContentText { + id: number; + sentence_number: number; + sentence_pl: string; + translation_ru: string; + translation_uk: string; + translation_en: string; + difficulty: number; + edited_at: string | null; +} + +export interface ContentTextsList { + level: string; + count: number; + texts: ContentText[]; +} + +// Сводка по всем уровням +export const getContentSummary = (): Promise => + fetchAPI('/admin/content/summary'); + +// Слова +export const getWordLevels = (): Promise<{ levels: ContentWordLevel[] }> => + fetchAPI('/admin/content/words/levels'); + +export const getLevelWords = (level: string, q?: string): Promise => { + const params = new URLSearchParams(); + if (q) params.set('q', q); + const qs = params.toString(); + return fetchAPI(`/admin/content/words/level/${level}${qs ? '?' + qs : ''}`); +}; + +export const getWord = (wordId: number): Promise => + fetchAPI(`/admin/content/words/word/${wordId}`); + +export const updateWord = (wordId: number, payload: { + word_pl: string; + translation_ru: string; + translation_uk: string; + translation_en: string; +}): Promise<{ status: string; word_id: number }> => + fetchAPI(`/admin/content/words/word/${wordId}`, { + method: 'PUT', + body: JSON.stringify(payload), + }); + +export const createWord = (level: string, payload: { + word_pl: string; + translation_ru: string; + translation_uk: string; + translation_en: string; +}): Promise<{ status: string; word_id: number }> => + fetchAPI(`/admin/content/words/level/${level}`, { + method: 'POST', + body: JSON.stringify(payload), + }); + +// Тексты +export const getTextLevels = (): Promise<{ levels: ContentTextLevel[] }> => + fetchAPI('/admin/content/texts/levels'); + +export const getLevelTexts = (level: string, q?: string): Promise => { + const params = new URLSearchParams(); + if (q) params.set('q', q); + const qs = params.toString(); + return fetchAPI(`/admin/content/texts/level/${level}${qs ? '?' + qs : ''}`); +}; + +export const getText = (textId: number): Promise => + fetchAPI(`/admin/content/texts/text/${textId}`); + +export const updateText = (textId: number, payload: { + sentence_pl: string; + translation_ru: string; + translation_uk: string; + translation_en: string; +}): Promise<{ status: string; text_id: number; difficulty: number }> => + fetchAPI(`/admin/content/texts/text/${textId}`, { + method: 'PUT', + body: JSON.stringify(payload), + }); + +export const createText = (level: string, payload: { + sentence_pl: string; + translation_ru: string; + translation_uk: string; + translation_en: string; +}): Promise<{ status: string; text_id: number; sentence_number: number }> => + fetchAPI(`/admin/content/texts/level/${level}`, { + method: 'POST', + body: JSON.stringify(payload), + }); + + // ============ ADMIN CONTENT: LESSONS ============ + +export interface ContentLessonLevel { + code: string; + name_ru: string; + topics_count: number; + lessons_count: number; + last_edited: string | null; +} + +export interface ContentLessonTopic { + topic_id: string; + order_number: number; + name_pl: string; + name_ru: string; + name_uk: string; + name_en: string; + lessons_count: number; + test_questions_count: number; +} + +export interface ContentLessonItem { + lesson_id: string; + order_number: number; + skill_tag: string; + title_pl: string; + title_ru: string; + title_uk: string; + title_en: string; +} + +export interface ContentTopicDetail { + topic: { + topic_id: string; + level_code: string; + order_number: number; + name_pl: string; + name_ru: string; + name_uk: string; + name_en: string; + description_pl: string | null; + description_ru: string | null; + description_uk: string | null; + description_en: string | null; + }; + lessons: ContentLessonItem[]; + has_test: boolean; + test_questions_count: number; +} + +// Уровни уроков +export const getLessonLevels = (): Promise<{ levels: ContentLessonLevel[] }> => + fetchAPI('/admin/content/lessons/levels'); + +// Темы уровня +export const getLessonTopics = (level: string): Promise<{ level: string; topics: ContentLessonTopic[] }> => + fetchAPI(`/admin/content/lessons/level/${level}/topics`); + +// Тема целиком (с уроками) +export const getLessonTopic = (topicId: string): Promise => + fetchAPI(`/admin/content/lessons/topic/${topicId}`); + +// Урок целиком (для WYSIWYG) +export const getLessonFull = (lessonId: string): Promise<{ + lesson: any; + examples: any[]; + questions: any[]; +}> => + fetchAPI(`/admin/content/lessons/lesson/${lessonId}`); + +// Сохранить урок +export const updateLessonFull = ( + lessonId: string, + payload: { + title_pl: string; title_ru: string; title_uk: string; title_en: string; + objective_pl: string; objective_ru: string; objective_uk: string; objective_en: string; + explanation_pl: string; explanation_ru: string; explanation_uk: string; explanation_en: string; + examples: Array<{ text_pl: string; text_ru: string; text_uk: string; text_en: string }>; + questions: Array<{ + question_id: string; + prompt_pl: string; prompt_ru: string; prompt_uk: string; prompt_en: string; + hint_type: string | null; + hint_value: string | null; + options: Array<{ + text_pl: string; text_ru: string; text_uk: string; text_en: string; + is_correct: boolean; + wrong_because: string | null; + }>; + }>; + } +): Promise<{ status: string; lesson_id: string; examples_count: number; questions_count: number }> => + fetchAPI(`/admin/content/lessons/lesson/${lessonId}`, { + method: 'PUT', + body: JSON.stringify(payload), + }); + +// Справочник wrong_because +export interface WrongBecauseItem { + wrong_because: string; + explanation_pl: string; + explanation_ru: string; + explanation_uk: string; + explanation_en: string; +} + +export const getWrongBecause = (): Promise<{ items: WrongBecauseItem[] }> => + fetchAPI('/admin/content/wrong-because'); \ No newline at end of file diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/src/assets/hero.png differ diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/src/components/AdminDashboardModal.tsx b/src/components/AdminDashboardModal.tsx new file mode 100644 index 0000000..f0ac18f --- /dev/null +++ b/src/components/AdminDashboardModal.tsx @@ -0,0 +1,621 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useLanguage } from '../i18n/LanguageContext'; +import { radius } from '../styles/theme'; +import { getAdminDashboard, getSentryErrors } from '../api'; +import ContentEditorMenu from './ContentEditorMenu'; +import WordsEditor from './WordsEditor'; +import TextsEditor from './TextsEditor'; +import LessonsEditor from './LessonsEditor'; +import LessonEditorModal from './LessonEditorModal'; + +interface AdminDashboardModalProps { + isOpen: boolean; + onClose: () => void; +} + +interface DashboardData { + total_users: number; + new_today: number; + online_now: number; + active_24h: number; + level_counts: Record; +} + +interface SentryError { + id: string; + short_id: string; + title: string; + culprit: string; + level: string; + count: number; + user_count: number; + first_seen: string; + last_seen: string; + permalink: string; + priority: string; +} + +const AdminDashboardModal: React.FC = ({ isOpen, onClose }) => { + const { language } = useLanguage(); + const [dashboard, setDashboard] = useState(null); + const [errors, setErrors] = useState([]); + const [errorsLoading, setErrorsLoading] = useState(false); + const [errorsError, setErrorsError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [lastUpdate, setLastUpdate] = useState(new Date()); + + // 🔧 НОВОЕ (19.09.2026): контент-менеджер + const [showContentEditor, setShowContentEditor] = useState(false); + const [showWordsEditor, setShowWordsEditor] = useState(false); + const [showTextsEditor, setShowTextsEditor] = useState(false); + const [showLessonsEditor, setShowLessonsEditor] = useState(false); + + // 🔧 НОВОЕ (19.09.2026): модалка урока (WYSIWYG) + const [editingLessonId, setEditingLessonId] = useState(null); + const [editingTopicPath, setEditingTopicPath] = useState<{ + levelCode: string; + topicOrder: number; + topicName: string; + } | null>(null); + + const brandColor = '#2ECC71'; + const dashColor = '#E8B93F'; + const errorColor = '#E74C3C'; + const textColor = '#1A1A2E'; + const subColor = '#6B7280'; + const cardBg = 'rgba(255,255,255,0.55)'; + + const glass = (extra: React.CSSProperties = {}): React.CSSProperties => ({ + background: cardBg, + backdropFilter: 'blur(24px) saturate(180%)', + WebkitBackdropFilter: 'blur(24px) saturate(180%)', + border: '1px solid rgba(255,255,255,0.7)', + boxShadow: '0 8px 32px rgba(31,38,73,0.10), inset 0 1px 0 rgba(255,255,255,0.85)', + ...extra, + }); + + const getLabel = (pl: string, ru: string, uk: string, en: string): string => { + const labels: Record = { pl, ru, uk, en }; + return labels[language] || labels.en; + }; + + const loadData = useCallback(async () => { + setIsLoading(true); + setErrorsLoading(true); + setErrorsError(null); + + getAdminDashboard() + .then((data: DashboardData) => { + setDashboard(data); + setLastUpdate(new Date()); + }) + .catch((e) => { + console.error('Dashboard error:', e); + }) + .finally(() => setIsLoading(false)); + + getSentryErrors() + .then((data: any) => { + setErrors(data.errors || []); + if (data.error) { + setErrorsError(data.error); + } + }) + .catch((e) => { + console.error('Sentry errors error:', e); + setErrorsError('Failed to load errors'); + }) + .finally(() => setErrorsLoading(false)); + }, []); + + useEffect(() => { + if (isOpen) { + loadData(); + } + }, [isOpen, loadData]); + + const formatDate = (isoStr: string): string => { + try { + const date = new Date(isoStr); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMin = Math.floor(diffMs / 60000); + const diffHr = Math.floor(diffMin / 60); + const diffDay = Math.floor(diffHr / 24); + + if (diffMin < 1) return getLabel('teraz', 'только что', 'щойно', 'just now'); + if (diffMin < 60) return `${diffMin} ${getLabel('min temu', 'мин назад', 'хв тому', 'min ago')}`; + if (diffHr < 24) return `${diffHr} ${getLabel('godz. temu', 'ч назад', 'год тому', 'h ago')}`; + if (diffDay < 30) return `${diffDay} ${getLabel('dni temu', 'дн назад', 'дн тому', 'd ago')}`; + + return date.toLocaleDateString(); + } catch { + return isoStr; + } + }; + + const getLevelColor = (level: string): string => { + const colors: Record = { + A1: '#2ECC71', + A2: '#27AE60', + B1: '#F39C12', + B2: '#E67E22', + C1: '#E74C3C', + C2: '#C0392B', + }; + return colors[level] || brandColor; + }; + + const maxLevelCount = dashboard + ? Math.max(...Object.values(dashboard.level_counts), 1) + : 1; + + if (!isOpen) return null; + + return ( + <> +
{ if (e.target === e.currentTarget) onClose(); }} + > + + +
e.stopPropagation()} + > + {/* Заголовок */} +
+
+ 📊 +

+ {getLabel('Panel admina', 'Админ-панель', 'Адмін-панель', 'Admin Panel')} +

+
+
+ + +
+
+ + {isLoading && !dashboard ? ( +
+ {getLabel('Ładowanie...', 'Загрузка...', 'Завантаження...', 'Loading...')} +
+ ) : ( + <> + {/* 🔧 НОВОЕ (19.09.2026): кнопка Контент-менеджер — сверху, зелёная */} +
+ +
+ + {/* ============ СЕКЦИЯ 1: СТАТИСТИКА ============ */} +
+
+ 👥 {getLabel('Statystyki', 'Статистика', 'Статистика', 'Statistics')} +
+ +
+
+
+ {getLabel('Wszyscy', 'Всего', 'Всього', 'Total')} +
+
+ {dashboard?.total_users ?? 0} +
+
+ +
+
+ 🆕 {getLabel('Nowi dziś', 'Новых сегодня', 'Нових сьогодні', 'New today')} +
+
+ {dashboard?.new_today ?? 0} +
+
+ +
+
+ 🟢 {getLabel('Online', 'Онлайн', 'Онлайн', 'Online')} +
+
+
+
+ {dashboard?.online_now ?? 0} +
+
+
+ +
+
+ 📈 {getLabel('Aktywni 24h', 'Активных 24ч', 'Активних 24г', 'Active 24h')} +
+
+ {dashboard?.active_24h ?? 0} +
+
+
+
+ + {/* ============ СЕКЦИЯ 2: ПО УРОВНЯМ ============ */} +
+
+ 🎯 {getLabel('Poziomy', 'Уровни', 'Рівні', 'Levels')} +
+ +
+ {dashboard && Object.entries(dashboard.level_counts).map(([level, count]) => { + const percent = count > 0 ? (count / maxLevelCount) * 100 : 0; + return ( +
+
+ {level} + 0 ? textColor : subColor }}>{count} +
+
+
+
+
+ ); + })} +
+
+ + {/* ============ СЕКЦИЯ 3: ОШИБКИ SENTRY ============ */} +
+
+
0 ? errorColor : dashColor, + letterSpacing: '0.5px', + textTransform: 'uppercase', + }}> + ⚠️ {getLabel('Błędy', 'Ошибки', 'Помилки', 'Errors')} ({errors.length}) +
+ + {getLabel('Otwórz Sentry', 'Открыть Sentry', 'Відкрити Sentry', 'Open Sentry')} ↗ + +
+ + {errorsLoading ? ( +
+ {getLabel('Ładowanie...', 'Загрузка...', 'Завантаження...', 'Loading...')} +
+ ) : errorsError ? ( +
+ {errorsError} +
+ ) : errors.length === 0 ? ( +
+ ✅ {getLabel('Brak błędów', 'Ошибок нет', 'Помилок немає', 'No errors')} +
+ ) : ( + + )} +
+ + {/* Футер */} +
+ {getLabel('Aktualizacja', 'Обновлено', 'Оновлено', 'Updated')}: {lastUpdate.toLocaleTimeString()} +
+ + )} +
+
+ + {/* 🔧 НОВОЕ (19.09.2026): модалка контент-менеджера */} + setShowContentEditor(false)} + onNavigate={(section) => { + setShowContentEditor(false); + if (section === 'words') { + setShowWordsEditor(true); + } else if (section === 'texts') { + setShowTextsEditor(true); + } else if (section === 'lessons') { + setShowLessonsEditor(true); + } + }} + /> + + setShowWordsEditor(false)} + /> + + setShowTextsEditor(false)} + /> + + {/* 🔧 НОВОЕ (19.09.2026): Уроки — иерархия */} + { + setShowLessonsEditor(false); + setEditingLessonId(null); + setEditingTopicPath(null); + }} + onOpenLesson={(lessonId, topicPath) => { + setEditingLessonId(lessonId); + setEditingTopicPath(topicPath); + }} + /> + + {/* 🔧 НОВОЕ (19.09.2026): WYSIWYG-модалка урока */} + { + setEditingLessonId(null); + setEditingTopicPath(null); + }} + onSaved={() => { + setEditingLessonId(null); + setEditingTopicPath(null); + }} + /> + + ); +}; + +export default AdminDashboardModal; \ No newline at end of file diff --git a/src/components/AnimatedCat.tsx b/src/components/AnimatedCat.tsx new file mode 100644 index 0000000..e1b56d2 --- /dev/null +++ b/src/components/AnimatedCat.tsx @@ -0,0 +1,134 @@ +import { useState, useRef, useCallback } from 'react'; + +interface AnimatedCatProps { + size?: number; + onClick?: () => void; + place?: 1 | 2 | 3; +} + +interface Particle { + id: number; + x: number; + y: number; + size: number; + rotation: number; + delay: number; + duration: number; + driftX: number; + driftY: number; +} + +const AnimatedCat: React.FC = ({ size = 48, onClick, place = 1 }) => { + const [isPressed, setIsPressed] = useState(false); + const [particles, setParticles] = useState([]); + const idRef = useRef(0); + + const handleClick = useCallback(() => { + setIsPressed(true); + setTimeout(() => setIsPressed(false), 200); + + const newParticles: Particle[] = []; + + // Золотые звёздочки разных размеров, разлетаются во все стороны + for (let i = 0; i < 20; i++) { + const angle = (Math.PI * 2 * i) / 20 + (Math.random() - 0.5) * 0.5; // Равномерно по кругу + случайность + const distance = 30 + Math.random() * 50; // Расстояние разлёта + + newParticles.push({ + id: idRef.current++, + x: Math.cos(angle) * distance, + y: Math.sin(angle) * distance, + size: 6 + Math.random() * 14, // Разные размеры: 6-20px + rotation: Math.random() * 360, + delay: Math.random() * 0.2, + duration: 0.8 + Math.random() * 0.6, // Разная скорость + driftX: (Math.random() - 0.5) * 30, + driftY: -10 - Math.random() * 30, // Улетают вверх + }); + } + + setParticles(prev => [...prev, ...newParticles]); + setTimeout(() => { + setParticles(prev => prev.filter(p => !newParticles.find(np => np.id === p.id))); + }, 2000); + + if (onClick) onClick(); + }, [onClick]); + + const medal = place === 1 ? '🥇' : place === 2 ? '🥈' : '🥉'; + const glowColor = place === 1 ? '255,215,0' : place === 2 ? '192,192,192' : '205,127,50'; + + return ( +
+ + + {particles.map(p => ( + + ⭐ + + ))} + + + {medal} + +
+ ); +}; + +export default AnimatedCat; \ No newline at end of file diff --git a/src/components/BackToMenuButton.tsx b/src/components/BackToMenuButton.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/ContentEditorMenu.tsx b/src/components/ContentEditorMenu.tsx new file mode 100644 index 0000000..45b55e2 --- /dev/null +++ b/src/components/ContentEditorMenu.tsx @@ -0,0 +1,258 @@ +import React, { useEffect, useState } from 'react'; +import { radius } from '../styles/theme'; +import { getContentSummary, type ContentSummary } from '../api'; + +interface ContentEditorMenuProps { + isOpen: boolean; + onClose: () => void; + onNavigate: (section: 'lessons' | 'words' | 'texts') => void; +} + +const ContentEditorMenu: React.FC = ({ isOpen, onClose, onNavigate }) => { + const [summary, setSummary] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!isOpen) return; + setLoading(true); + setError(null); + getContentSummary() + .then((data) => setSummary(data)) + .catch((e) => { + console.error('[ContentEditorMenu] load failed:', e); + setError('Не удалось загрузить сводку'); + }) + .finally(() => setLoading(false)); + }, [isOpen]); + + if (!isOpen) return null; + + const textColor = '#1A1A2E'; + const subColor = '#6B7280'; + const cardBg = 'rgba(255,255,255,0.55)'; + + const glass = (extra: React.CSSProperties = {}): React.CSSProperties => ({ + background: cardBg, + backdropFilter: 'blur(24px) saturate(180%)', + WebkitBackdropFilter: 'blur(24px) saturate(180%)', + border: '1px solid rgba(255,255,255,0.7)', + boxShadow: '0 8px 32px rgba(31,38,73,0.10), inset 0 1px 0 rgba(255,255,255,0.85)', + ...extra, + }); + + return ( +
{ if (e.target === e.currentTarget) onClose(); }} + > +
e.stopPropagation()} + style={{ + ...glass({ + borderRadius: radius.lg, + padding: '20px', + width: '100%', + maxWidth: '480px', + maxHeight: '90vh', + display: 'flex', + flexDirection: 'column', + }), + }} + > + {/* Шапка */} +
+

+ ✏️ Контент-менеджер +

+ +
+ + {/* Контент */} +
+ {loading && ( +
+ Загрузка... +
+ )} + + {error && ( +
+ ⚠️ {error} +
+ )} + + {!loading && !error && summary && ( + <> + {/* Три карточки разделов */} + + + + + + + {/* Сводка по уровням */} +
+
+ 📊 Сводка по уровням +
+
+ {summary.levels.map((lvl) => { + const isReady = lvl.topics_total > 0 && lvl.words_total > 0 && lvl.texts_total > 0; + const icon = isReady ? '✅' : (lvl.topics_total > 0 ? '🟡' : '⚪'); + return ( +
+ {icon} + + {lvl.code} + + + {lvl.topics_total} тем · {lvl.lessons_total} уроков · {lvl.words_total} слов · {lvl.texts_total} текстов + +
+ ); + })} +
+
+ + )} +
+
+
+ ); +}; + +export default ContentEditorMenu; \ No newline at end of file diff --git a/src/components/FeedModal.tsx b/src/components/FeedModal.tsx new file mode 100644 index 0000000..8ffdb22 --- /dev/null +++ b/src/components/FeedModal.tsx @@ -0,0 +1,955 @@ +import { useState, useEffect, useRef, useCallback } from 'react'; +import { useLanguage } from '../i18n/LanguageContext'; +import { radius } from '../styles/theme'; +import { getFeedSection, deleteFeedPost, type FeedPostResponse } from '../api'; +import FeedPostComposer from './FeedPostComposer'; +import ImageLightbox from './ImageLightbox'; + +export type FeedSection = 'news' | 'partners' | 'contacts' | 'apps'; + +interface FeedModalProps { + isOpen: boolean; + onClose: () => void; + section: FeedSection; + /** Флаг админа — для админ-строки ввода */ + isAdmin?: boolean; +} + +type FeedPost = FeedPostResponse; + +const SECTION_CONFIG: Record = { + news: { icon: '📢', titleKey: 'carouselNews', accent: '#E8B93F' }, + partners: { icon: '💼', titleKey: 'carouselPartners', accent: '#2ECC71' }, + contacts: { icon: '📇', titleKey: 'carouselContacts', accent: '#8B5CF6' }, + apps: { icon: '📲', titleKey: 'carouselApps', accent: '#4FA9E8' }, +}; + +// 🔧 URL поддержки (единый аккаунт для заявок) +const SUPPORT_URL = 'https://t.me/fwpl_support'; + +// 🔧 API base для склейки относительных URL картинок (fallback для старых постов) +const API_BASE = 'https://api.filwordspl.com'; + +// 🔧 Внутренний отступ карточки поста (текстовый блок под картинкой) +const POST_PADDING = '16px'; + +const FeedModal: React.FC = ({ isOpen, onClose, section, isAdmin = false }) => { + const { t, language } = useLanguage(); + const [posts, setPosts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const scrollRef = useRef(null); + const isMountedRef = useRef(true); + + // 🔧 Шаг 6: состояния админ-режима + const [showComposer, setShowComposer] = useState(false); + const [editingPost, setEditingPost] = useState(null); + const [deletingPost, setDeletingPost] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); + const [deleteError, setDeleteError] = useState(null); + + // 🔧 Лайтбокс: какие картинки открыть + с какого индекса + const [lightboxOpen, setLightboxOpen] = useState(false); + const [lightboxImages, setLightboxImages] = useState([]); + const [lightboxIndex, setLightboxIndex] = useState(0); + + const config = SECTION_CONFIG[section]; + const textColor = '#1A1A2E'; + const subColor = '#6B7280'; + const cardBg = 'rgba(255,255,255,0.55)'; + + const glass = (extra: React.CSSProperties = {}): React.CSSProperties => ({ + background: cardBg, + backdropFilter: 'blur(24px) saturate(180%)', + WebkitBackdropFilter: 'blur(24px) saturate(180%)', + border: '1px solid rgba(255,255,255,0.7)', + boxShadow: '0 8px 32px rgba(31,38,73,0.10), inset 0 1px 0 rgba(255,255,255,0.85)', + ...extra, + }); + + // 🔧 Утилита: превращает относительный путь в полный URL + const fullImageUrl = (path: string): string => + path.startsWith('http') ? path : `${API_BASE}${path}`; + + const loadFeed = useCallback(() => { + setLoading(true); + setError(null); + + getFeedSection(section, language) + .then((data) => { + if (!isMountedRef.current) return; + setPosts(data.posts || []); + }) + .catch((e) => { + if (!isMountedRef.current) return; + console.error('Failed to load feed:', e); + setError('Failed to load feed'); + setPosts([]); + }) + .finally(() => { + if (!isMountedRef.current) return; + setLoading(false); + }); + }, [section, language]); + + useEffect(() => { + if (!isOpen) return; + + isMountedRef.current = true; + loadFeed(); + + return () => { + isMountedRef.current = false; + }; + }, [isOpen, loadFeed]); + + // Скролл вверх при открытии + useEffect(() => { + if (isOpen && scrollRef.current) { + scrollRef.current.scrollTop = 0; + } + }, [isOpen, section]); + + // Escape для закрытия (только если не открыт composer/lightbox/delete) + useEffect(() => { + if (!isOpen) return; + const handleKey = (e: KeyboardEvent) => { + if (e.key === 'Escape' && !showComposer && !deletingPost && !lightboxOpen) onClose(); + }; + window.addEventListener('keydown', handleKey); + return () => window.removeEventListener('keydown', handleKey); + }, [isOpen, onClose, showComposer, deletingPost, lightboxOpen]); + + // ============ Обработчики админа ============ + const handleCreate = () => { + setEditingPost(null); + setShowComposer(true); + }; + + const handleEdit = (post: FeedPost) => { + setEditingPost(post); + setShowComposer(true); + }; + + const handleDeleteConfirm = async () => { + if (!deletingPost) return; + + setIsDeleting(true); + setDeleteError(null); + + try { + await deleteFeedPost(deletingPost.id); + setDeletingPost(null); + loadFeed(); + } catch (e: any) { + const realMessage = e?.message || String(e); + setDeleteError(realMessage); + console.error('[handleDeleteConfirm] FAILED', realMessage); + } finally { + setIsDeleting(false); + } + }; + + const handleComposerSuccess = () => { + loadFeed(); + }; + + // ============ Открытие лайтбокса ============ + const openLightbox = (images: string[], startIndex: number = 0) => { + setLightboxImages(images.map(fullImageUrl)); + setLightboxIndex(startIndex); + setLightboxOpen(true); + }; + + // ============ Автолинк ============ + const renderTextWithLinks = (text: string) => { + const linkRegex = /(https?:\/\/[^\s]+)/g; + const phoneRegex = /(\+[\d\s\-()]{7,})/g; + + type Match = { start: number; end: number; text: string; href: string; type: 'link' | 'phone' }; + const matches: Match[] = []; + + let m: RegExpExecArray | null; + + const linkRegexGlobal = new RegExp(linkRegex.source, 'g'); + while ((m = linkRegexGlobal.exec(text)) !== null) { + matches.push({ + start: m.index, + end: m.index + m[0].length, + text: m[0], + href: m[0], + type: 'link', + }); + } + + const phoneRegexGlobal = new RegExp(phoneRegex.source, 'g'); + while ((m = phoneRegexGlobal.exec(text)) !== null) { + const overlaps = matches.some( + (existing) => m!.index < existing.end && m!.index + m![0].length > existing.start + ); + if (!overlaps) { + matches.push({ + start: m.index, + end: m.index + m[0].length, + text: m[0], + href: `tel:${m[0].replace(/[\s\-()]/g, '')}`, + type: 'phone', + }); + } + } + + matches.sort((a, b) => a.start - b.start); + + const result: React.ReactNode[] = []; + let lastIndex = 0; + + matches.forEach((match, i) => { + if (match.start > lastIndex) { + result.push(text.slice(lastIndex, match.start)); + } + result.push( + e.stopPropagation()} + > + {match.text} + + ); + lastIndex = match.end; + }); + + if (lastIndex < text.length) { + result.push(text.slice(lastIndex)); + } + + return result; + }; + + const formatDate = (iso: string | null): string => { + if (!iso) return ''; + try { + const date = new Date(iso); + const locales: Record = { + pl: 'pl-PL', + ru: 'ru-RU', + uk: 'uk-UA', + en: 'en-US', + }; + return date.toLocaleDateString(locales[language] || 'en-US', { + day: 'numeric', + month: 'short', + year: 'numeric', + }); + } catch { + return iso; + } + }; + + const formatExpires = (iso: string | null | undefined): string | null => { + if (!iso) return null; + try { + const date = new Date(iso); + const locales: Record = { + pl: 'pl-PL', + ru: 'ru-RU', + uk: 'uk-UA', + en: 'en-US', + }; + return date.toLocaleDateString(locales[language] || 'en-US', { + day: 'numeric', + month: 'short', + }); + } catch { + return null; + } + }; + + if (!isOpen) return null; + + const showPartnersBanner = section === 'partners'; + + return ( + <> +
{ if (e.target === e.currentTarget) onClose(); }} + > + + +
e.stopPropagation()} + style={{ + ...glass({ + borderRadius: radius.lg, + padding: '20px', + width: '100%', + maxWidth: '440px', + height: '85vh', + maxHeight: '720px', + display: 'flex', + flexDirection: 'column', + boxShadow: '0 24px 60px rgba(31,38,73,0.25), inset 0 1px 0 rgba(255,255,255,0.85)', + }), + animation: 'feedModalSlideUp 0.3s ease', + }} + > + {/* ============ Шапка ============ */} +
+
+ + +
+ +
+

+ {config.icon} {t(config.titleKey)} +

+
+
+ + {/* ============ Баннер «По вопросам рекламы» (только Партнёры) ============ */} + {showPartnersBanner && ( +
window.open(SUPPORT_URL, '_blank')} + style={{ + ...glass({ + padding: '14px 16px', + borderRadius: radius.md, + marginBottom: '12px', + }), + border: `1px solid ${config.accent}40`, + background: `${config.accent}10`, + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + gap: '12px', + flexShrink: 0, + transition: 'all 0.2s ease', + }} + onTouchStart={(e) => { e.currentTarget.style.transform = 'scale(0.98)'; }} + onTouchEnd={(e) => { e.currentTarget.style.transform = 'scale(1)'; }} + > + 📩 +
+
+ {t('feedPartnersBannerTitle') || 'По вопросам рекламы'} +
+
+ {t('feedPartnersBannerSubtitle') || 'Написать нам →'} +
+
+
+ )} + + {/* ============ Лента постов ============ */} +
+ {loading ? ( +
+ {t('loading')} +
+ ) : error ? ( +
+
⚠️
+
+ {t('feedError') || 'Не удалось загрузить. Попробуйте ещё раз.'} +
+ +
+ ) : posts.length === 0 ? ( +
+
+ {config.icon} +
+ {t('feedEmpty') || 'Пока нет постов'} +
+ ) : ( + posts.map((post, index) => { + const expiresLabel = formatExpires(post.expiresAt); + const hasImages = post.images.length > 0; + + return ( +
+ {/* ============ КАРТИНКА (всегда одна на превью, edge-to-edge) ============ */} + {/* 🔧 ФИКС (18.09.2026): на посте видна ТОЛЬКО первая картинка. + По клику открывается лайтбокс со ВСЕМИ картинками поста — + там можно свайпать влево/вправо (ImageLightbox). */} + {hasImages && ( +
{ + e.stopPropagation(); + openLightbox(post.images, 0); + }} + style={{ + position: 'relative', + width: '100%', + aspectRatio: '16 / 9', + background: '#f0f0f5', + cursor: 'pointer', + borderTopLeftRadius: radius.md, + borderTopRightRadius: radius.md, + overflow: 'hidden', + transition: 'transform 0.15s ease', + }} + onTouchStart={(e) => { e.currentTarget.style.transform = 'scale(0.99)'; }} + onTouchEnd={(e) => { e.currentTarget.style.transform = 'scale(1)'; }} + > + {/* Всегда рендерим ТОЛЬКО первую картинку как превью */} + { + (e.currentTarget as HTMLImageElement).style.display = 'none'; + }} + style={{ + width: '100%', + height: '100%', + objectFit: 'cover', + display: 'block', + userSelect: 'none', + WebkitUserSelect: 'none', + }} + /> + + {/* 🔧 Бейдж «📷 N» — если картинок больше 1. + Показывает юзеру, что внутри ещё есть фото. */} + {post.images.length > 1 && ( +
+ 📷 {post.images.length} +
+ )} +
+ )} + + {/* ============ ТЕКСТОВЫЙ БЛОК (с padding) ============ */} +
+ + {/* Бейдж «Закреплено» */} + {post.isPinned && ( +
+ 📌 +
+ )} + + {/* 🔧 Шаг 6: админ-иконки ✏️ / 🗑️ */} + {isAdmin && ( +
+ + +
+ )} + + {/* Заголовок */} +
+ {post.title} +
+ + {/* Текст поста */} +
+ {renderTextWithLinks(post.body)} +
+ + {/* Ссылка */} + {post.linkUrl && ( + e.stopPropagation()} + style={{ + display: 'inline-flex', + alignItems: 'center', + gap: '6px', + padding: '8px 14px', + borderRadius: radius.sm, + background: `${config.accent}15`, + border: `1px solid ${config.accent}40`, + color: config.accent, + fontSize: '13px', + fontWeight: 700, + textDecoration: 'none', + marginBottom: '8px', + transition: 'all 0.2s ease', + }} + onTouchStart={(e) => { e.currentTarget.style.transform = 'scale(0.96)'; }} + onTouchEnd={(e) => { e.currentTarget.style.transform = 'scale(1)'; }} + > + 🔗 {t('feedOpenLink') || 'Открыть ссылку'} + + )} + + {/* Telegram-ссылка */} + {post.telegramUrl && ( + e.stopPropagation()} + style={{ + display: 'inline-flex', + alignItems: 'center', + gap: '6px', + padding: '8px 14px', + borderRadius: radius.sm, + background: `#0088cc15`, + border: `1px solid #0088cc40`, + color: '#0088cc', + fontSize: '13px', + fontWeight: 700, + textDecoration: 'none', + marginBottom: '8px', + marginLeft: post.linkUrl ? '8px' : '0', + transition: 'all 0.2s ease', + }} + onTouchStart={(e) => { e.currentTarget.style.transform = 'scale(0.96)'; }} + onTouchEnd={(e) => { e.currentTarget.style.transform = 'scale(1)'; }} + > + ✈️ Telegram + + )} + + {/* Футер: дата + таймер */} +
+ 🕐 {formatDate(post.createdAt)} + {expiresLabel && ( + + ⏰ до {expiresLabel} + + )} +
+
+
+ ); + }) + )} +
+ + {/* ============ Админ-кнопка «Написать пост» ============ */} + {isAdmin && ( +
+ +
+ )} +
+
+ + {/* ============ Composer (создание / редактирование) ============ */} + { + setShowComposer(false); + setEditingPost(null); + }} + onSuccess={handleComposerSuccess} + section={section} + editingPost={editingPost} + /> + + {/* ============ 🔧 Лайтбокс для просмотра картинок ============ */} + setLightboxOpen(false)} + /> + + {/* ============ Модалка подтверждения удаления ============ */} + {deletingPost && ( +
{ if (e.target === e.currentTarget && !isDeleting) setDeletingPost(null); }} + > +
e.stopPropagation()} + style={{ + ...glass({ + borderRadius: radius.lg, + padding: '24px', + width: '100%', + maxWidth: '360px', + textAlign: 'center', + }), + }} + > +
🗑️
+

+ {t('feedAdminDeleteConfirm') || 'Удалить этот пост?'} +

+
+ {deletingPost.title} +
+ + {deleteError && ( +
+ ⚠️ {deleteError} +
+ )} + +
+ + +
+
+
+ )} + + ); +}; + +export default FeedModal; \ No newline at end of file diff --git a/src/components/FeedPostComposer.tsx b/src/components/FeedPostComposer.tsx new file mode 100644 index 0000000..9e2c28b --- /dev/null +++ b/src/components/FeedPostComposer.tsx @@ -0,0 +1,1005 @@ +import { useState, useRef, useEffect } from 'react'; +import { useLanguage } from '../i18n/LanguageContext'; +import { radius } from '../styles/theme'; +import { + createFeedPost, + updateFeedPost, + uploadFeedImage, + translateFeedPreview, + type FeedSection, + type FeedPostResponse, +} from '../api'; + +interface FeedPostComposerProps { + isOpen: boolean; + onClose: () => void; + onSuccess: () => void; + section: FeedSection; + /** Если передан — режим редактирования. Если null — создание. */ + editingPost: FeedPostResponse | null; +} + +type SourceLang = 'pl' | 'ru' | 'uk' | 'en'; + +interface TranslationPreview { + title_pl: string; title_ru: string; title_uk: string; title_en: string; + body_pl: string; body_ru: string; body_uk: string; body_en: string; +} + +/** + * 🔧 ФИКС (18.09.2026, вечер): новая архитектура загрузки картинок. + * + * Было: один — Telegram WebView (особенно iOS) + * часто отдавал меньше файлов, чем выбрал юзер. Реально выбираешь 3, + * до бэка долетает 1. + * + * Стало: ТРИ отдельных слота, каждый со своим БЕЗ multiple. + * За раз выбирается ровно 1 картинка — WebView не может ничего + * «потерять». Так же работает на Kufar, Instagram Stories, Telegram. + * + * Слот хранит Blob (in-memory объект), а не File. Это гарантирует, что + * к моменту публикации байты доступны — WebView их не «съест». + */ +interface ImageSlot { + blob: Blob; + name: string; + type: string; + previewUrl: string; +} + +/** + * 🔧 СЖАТИЕ (19.09.2026): уменьшает картинку до maxDimension по длинной стороне + * и жмёт в JPEG с итеративным давлением до maxBytes. + * + * Зачем: юзер грузит фото как есть — 3 × 4 МБ = 12 МБ с телефона. После сжатия + * 3 × ~500 КБ = 1.5 МБ. Фид грузится быстрее, трафик меньше, nginx не напрягается. + * + * Логика: + * 1. Открываем картинку через createImageBitmap с fallback на Image() — + * для старых WebView и HEIC. + * 2. Ресайзим до maxDimension (1600px по длинной стороне). + * 3. Жмём в JPEG: quality 0.85 → 0.75 → 0.65 → 0.55, пока не уложимся + * в maxBytes (2 МБ). + * 4. Если даже на 0.55 не влезли — режем ещё сильнее по размеру. + * + * Если что-то пошло не так (HEIC в старом WebView, canvas недоступен) — + * кидает ошибку. Вызывающий код ловит и использует оригинал. + */ +async function compressImage( + blob: Blob, + maxDimension = 1600, + maxBytes = 2 * 1024 * 1024, +): Promise<{ blob: Blob; width: number; height: number }> { + // 1. Открываем картинку — createImageBitmap с fallback на Image() + let bitmap: ImageBitmap | HTMLImageElement; + try { + bitmap = await createImageBitmap(blob); + } catch { + bitmap = await new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = () => reject(new Error('image load failed')); + img.src = URL.createObjectURL(blob); + }); + } + + const w0 = 'width' in bitmap ? bitmap.width : (bitmap as HTMLImageElement).naturalWidth; + const h0 = 'height' in bitmap ? bitmap.height : (bitmap as HTMLImageElement).naturalHeight; + + if (!w0 || !h0) throw new Error('invalid image dimensions'); + + // 2. Целевые размеры + const scale = Math.min(1, maxDimension / Math.max(w0, h0)); + const w = Math.round(w0 * scale); + const h = Math.round(h0 * scale); + + // 3. Рисуем в canvas + const canvas = document.createElement('canvas'); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d'); + if (!ctx) throw new Error('canvas 2d context unavailable'); + ctx.drawImage(bitmap as any, 0, 0, w, h); + + // 4. Итеративное сжатие: 0.85 → 0.75 → 0.65 → 0.55 + const qualities = [0.85, 0.75, 0.65, 0.55]; + let resultBlob: Blob | null = null; + for (const q of qualities) { + const b = await new Promise((resolve) => + canvas.toBlob((blob) => resolve(blob), 'image/jpeg', q), + ); + if (!b) continue; + resultBlob = b; + if (b.size <= maxBytes) break; + } + + if (!resultBlob) throw new Error('canvas.toBlob failed'); + + // 5. Если даже на 0.55 не влезли — режем ещё сильнее по размеру + if (resultBlob.size > maxBytes) { + const ratio = Math.sqrt(maxBytes / resultBlob.size) * 0.95; + const w2 = Math.round(w * ratio); + const h2 = Math.round(h * ratio); + const canvas2 = document.createElement('canvas'); + canvas2.width = w2; + canvas2.height = h2; + const ctx2 = canvas2.getContext('2d'); + if (!ctx2) throw new Error('canvas 2d context unavailable'); + ctx2.drawImage(bitmap as any, 0, 0, w2, h2); + const b2 = await new Promise((resolve) => + canvas2.toBlob((b) => resolve(b), 'image/jpeg', 0.7), + ); + if (b2) resultBlob = b2; + } + + // Освобождаем ImageBitmap + if ('close' in bitmap && typeof (bitmap as ImageBitmap).close === 'function') { + (bitmap as ImageBitmap).close(); + } + + return { blob: resultBlob, width: w, height: h }; +} + +const FeedPostComposer: React.FC = ({ + isOpen, + onClose, + onSuccess, + section, + editingPost, +}) => { + const { t, language } = useLanguage(); + const isEditing = editingPost !== null; + + // ============ Состояние формы ============ + const [title, setTitle] = useState(''); + const [body, setBody] = useState(''); + const [linkUrl, setLinkUrl] = useState(''); + const [telegramUrl, setTelegramUrl] = useState(''); + const [isPinned, setIsPinned] = useState(false); + const [expiresAt, setExpiresAt] = useState(''); + const [sourceLang, setSourceLang] = useState('ru'); + const [translateAgain, setTranslateAgain] = useState(false); + + // ============ Картинки (новая схема: 3 слота) ============ + // Ровно 3 слота, всегда. Пустой слот = null. + const [slots, setSlots] = useState<(ImageSlot | null)[]>([null, null, null]); + // Существующие картинки (режим редактирования) — занимают слоты в начале + const [existingImages, setExistingImages] = useState([]); + + // Три отдельных input[type=file] — по одному на слот. Без multiple. + const slotInputRef0 = useRef(null); + const slotInputRef1 = useRef(null); + const slotInputRef2 = useRef(null); + const slotInputRefs = [slotInputRef0, slotInputRef1, slotInputRef2]; + + // ============ Статусы ============ + const [isTranslating, setIsTranslating] = useState(false); + const [isPublishing, setIsPublishing] = useState(false); + const [translationPreview, setTranslationPreview] = useState(null); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + // Синхронизируем состояние с editingPost при открытии + useEffect(() => { + if (isOpen && editingPost) { + setTitle(editingPost.title); + setBody(editingPost.body); + setLinkUrl(editingPost.linkUrl || ''); + setTelegramUrl(editingPost.telegramUrl || ''); + setIsPinned(editingPost.isPinned); + setExpiresAt(editingPost.expiresAt ? editingPost.expiresAt.slice(0, 16) : ''); + setExistingImages(editingPost.images || []); + setSourceLang((language as SourceLang) || 'ru'); + setTranslateAgain(false); + setSlots([null, null, null]); + } else if (isOpen) { + setTitle(''); + setBody(''); + setLinkUrl(''); + setTelegramUrl(''); + setIsPinned(false); + setExpiresAt(''); + setSourceLang('ru'); + setTranslateAgain(false); + setSlots([null, null, null]); + setExistingImages([]); + } + setTranslationPreview(null); + setError(null); + setSuccess(false); + }, [isOpen, editingPost, language]); + + // 🔧 ФИКС: revoke ObjectURL при размонтировании + useEffect(() => { + return () => { + setSlots(prev => { + prev.forEach(slot => { + if (slot?.previewUrl) { + try { URL.revokeObjectURL(slot.previewUrl); } catch { /* ignore */ } + } + }); + return prev; + }); + }; + }, []); + + // ============ Работа с одним слотом ============ + const handleSlotFile = async (slotIndex: number, e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) { + return; + } + + // Сразу очищаем input, чтобы можно было выбрать тот же файл повторно + e.target.value = ''; + + // Валидация размера (страховка от совсем тяжёлых — например HEIC 15 МБ, + // который fallback не сжал) + if (file.size > 5 * 1024 * 1024) { + setError(t('feedAdminImageTooBig') || 'Фото слишком большое (макс. 5 МБ)'); + return; + } + + // Валидация типа + const okTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif', 'image/heic', 'image/heif']; + if (!okTypes.includes(file.type)) { + setError(t('feedAdminImageBadType') || `Тип не разрешён: ${file.type}`); + return; + } + + // 🔧 КЛЮЧЕВОЙ ФИКС: читаем байты СРАЗУ и создаём Blob. + // Никаких повторных file.arrayBuffer() позже — только blob. + try { + const buffer = await file.arrayBuffer(); + let blob = new Blob([buffer], { type: file.type || 'image/jpeg' }); + + // 🔧 СЖАТИЕ: если больше 500 КБ — жмём до 1600px / 2 МБ + if (blob.size > 500 * 1024) { + try { + const { blob: compressed } = await compressImage(blob, 1600, 2 * 1024 * 1024); + blob = compressed; + } catch (e) { + console.error('[handleSlotFile] compress FAILED, using original:', e); + // оставляем оригинал — лучше отправить как есть, чем упасть + } + } + + const previewUrl = URL.createObjectURL(blob); + + setSlots(prev => { + const old = prev[slotIndex]; + if (old?.previewUrl) { + try { URL.revokeObjectURL(old.previewUrl); } catch { /* ignore */ } + } + const next = [...prev]; + next[slotIndex] = { + blob, + name: file.name, + // После сжатия всегда JPEG. Если сжатие упало — оставляем оригинальный тип. + type: blob.type || 'image/jpeg', + previewUrl, + }; + return next; + }); + + setError(null); + } catch (err) { + console.error('[handleSlotFile] read FAIL:', err); + setError('Не удалось прочитать файл: ' + err); + } + }; + + const clearSlot = (slotIndex: number) => { + setSlots(prev => { + const old = prev[slotIndex]; + if (old?.previewUrl) { + try { URL.revokeObjectURL(old.previewUrl); } catch { /* ignore */ } + } + const next = [...prev]; + next[slotIndex] = null; + return next; + }); + }; + + const handleRemoveExistingImage = (index: number) => { + setExistingImages((prev) => prev.filter((_, i) => i !== index)); + }; + + // ============ Перевод (превью) ============ + const handleTranslate = async () => { + if (!title.trim() || !body.trim()) { + setError(t('feedAdminTextRequired') || 'Введите текст поста'); + return; + } + + setIsTranslating(true); + setError(null); + + try { + const data = await translateFeedPreview({ + title: title.trim(), + body: body.trim(), + source_language: sourceLang, + }); + setTranslationPreview(data); + } catch (e) { + console.error('[Translate] FAILED:', e); + setError(t('feedAdminErrorTranslate') || 'Не удалось перевести'); + } finally { + setIsTranslating(false); + } + }; + + // ============ Публикация / обновление ============ + const handlePublish = async () => { + const imagesToUpload = slots.filter((s): s is ImageSlot => s !== null); + + if (!title.trim() || !body.trim()) { + setError(t('feedAdminTextRequired') || 'Введите текст поста'); + return; + } + + setIsPublishing(true); + setError(null); + + try { + let postId: number; + + if (isEditing && editingPost) { + const result = await updateFeedPost(editingPost.id, { + title: title.trim(), + body: body.trim(), + link_url: linkUrl.trim() || null, + telegram_url: telegramUrl.trim() || null, + is_pinned: isPinned, + expires_at: expiresAt ? new Date(expiresAt).toISOString() : null, + translate_again: translateAgain, + }); + postId = result.post_id; + } else { + const result = await createFeedPost({ + section, + source_language: sourceLang, + title: title.trim(), + body: body.trim(), + link_url: linkUrl.trim() || null, + telegram_url: telegramUrl.trim() || null, + is_pinned: isPinned, + is_published: true, + expires_at: expiresAt ? new Date(expiresAt).toISOString() : null, + }); + postId = result.post_id; + } + + if (!postId || typeof postId !== 'number') { + throw new Error(`Backend returned invalid postId: ${postId}`); + } + + // 🔧 Отдельно обрабатываем каждый слот, чтобы не терять + const uploadResults: any[] = []; + const failedUploads: string[] = []; + + for (let i = 0; i < imagesToUpload.length; i++) { + const slot = imagesToUpload[i]; + try { + const res = await uploadFeedImage(postId, slot.blob, slot.name, slot.type); + uploadResults.push(res); + } catch (e: any) { + console.error(`[Publish] upload ${i + 1} FAILED:`, e?.message || e); + failedUploads.push(slot.name); + } + } + + // 🔧 ФИКС: если хоть одна загрузка упала — не считаем публикацию успешной + if (failedUploads.length > 0) { + setError( + `Не удалось загрузить: ${failedUploads.join(', ')}. ` + + `Загружено ${uploadResults.length} из ${imagesToUpload.length} фото.` + ); + return; + } + + setSuccess(true); + setTimeout(() => { + onSuccess(); + onClose(); + }, 600); + } catch (e: any) { + console.error('[Publish] FAILED:', e); + setError(t('feedAdminErrorPublish') || 'Не удалось опубликовать'); + } finally { + setIsPublishing(false); + } + }; + + if (!isOpen) return null; + + const textColor = '#1A1A2E'; + const subColor = '#6B7280'; + const cardBg = 'rgba(255,255,255,0.55)'; + const brandColor = '#8B5CF6'; + + const glass = (extra: React.CSSProperties = {}): React.CSSProperties => ({ + background: cardBg, + backdropFilter: 'blur(24px) saturate(180%)', + WebkitBackdropFilter: 'blur(24px) saturate(180%)', + border: '1px solid rgba(255,255,255,0.7)', + boxShadow: '0 8px 32px rgba(31,38,73,0.10), inset 0 1px 0 rgba(255,255,255,0.85)', + ...extra, + }); + + // Сколько слотов доступно под НОВЫЕ картинки (учитывая existing) + const newSlotsAvailable = Math.max(0, 3 - existingImages.length); + const totalImages = existingImages.length + slots.filter(Boolean).length; + + return ( +
{ if (e.target === e.currentTarget && !isPublishing) onClose(); }} + > +
e.stopPropagation()} + style={{ + ...glass({ + borderRadius: radius.lg, + padding: '20px', + width: '100%', + maxWidth: '480px', + maxHeight: '90vh', + display: 'flex', + flexDirection: 'column', + }), + }} + > + {/* ============ Шапка ============ */} +
+

+ {isEditing ? `✏️ ${t('feedAdminEdit')}` : `➕ ${t('feedAdminSend')}`} +

+ +
+ + {/* ============ Скролл-контент ============ */} +
+ + {/* Язык источника */} +
+ +
+ {(['pl', 'ru', 'uk', 'en'] as SourceLang[]).map((lang) => ( + + ))} +
+
+ + {/* Заголовок */} +
+ + setTitle(e.target.value)} + placeholder="Заголовок поста" + disabled={isPublishing} + style={{ + width: '100%', + padding: '12px 14px', + borderRadius: radius.md, + border: '1px solid rgba(0,0,0,0.08)', + background: 'rgba(255,255,255,0.8)', + color: textColor, + fontSize: '15px', + fontWeight: 600, + outline: 'none', + boxSizing: 'border-box', + }} + onFocus={(e) => { + e.currentTarget.style.borderColor = brandColor; + e.currentTarget.style.boxShadow = `0 0 0 3px ${brandColor}15`; + }} + onBlur={(e) => { + e.currentTarget.style.borderColor = 'rgba(0,0,0,0.08)'; + e.currentTarget.style.boxShadow = 'none'; + }} + /> +
+ + {/* Текст */} +
+ +