diff --git a/.gitignore b/.gitignore index d3b91dc..c9836a3 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,5 @@ miniapp/ # AI & Agents context .ai/ +CLAUDE.md +**/CLAUDE.md diff --git a/package-lock.json b/package-lock.json index 73d9f3a..1d3cf25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@kastov/cryptohapp": "^1.1.2", - "@lottiefiles/dotlottie-react": "^0.8.0", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -26,41 +25,42 @@ "@radix-ui/react-visually-hidden": "^1.2.4", "@tanstack/react-query": "^5.8.0", "@telegram-apps/sdk-react": "^3.3.9", + "autoprefixer": "^10.4.24", "axios": "^1.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "dompurify": "^3.3.1", "framer-motion": "^12.29.2", - "i18next": "^23.7.0", - "i18next-browser-languagedetector": "^7.2.0", + "i18next": "^25.8.4", + "i18next-browser-languagedetector": "^8.2.0", "jsencrypt": "^3.5.4", "ogl": "^1.0.11", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-i18next": "^13.5.0", - "react-router-dom": "^6.30.3", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-i18next": "^16.5.4", + "react-router": "^7.13.0", "tailwind-merge": "^3.4.0", - "zustand": "^4.4.0" + "zustand": "^5.0.11" }, "devDependencies": { "@eslint/js": "^9.39.2", "@types/dompurify": "^3.0.5", - "@types/react": "^18.2.37", - "@types/react-dom": "^18.2.15", + "@types/node": "^25.2.1", + "@types/react": "^19.2.13", + "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.2", - "autoprefixer": "^10.4.16", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.26", + "eslint-plugin-react-refresh": "^0.5.0", "globals": "^17.1.0", "husky": "^9.1.7", "lint-staged": "^16.2.7", "postcss": "^8.4.31", "prettier": "^3.8.1", "prettier-plugin-tailwindcss": "^0.7.2", - "tailwindcss": "^3.3.5", + "tailwindcss": "^3.4.19", "typescript": "^5.2.2", "typescript-eslint": "^8.54.0", "vite": "^7.3.1" @@ -80,13 +80,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -95,9 +95,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "engines": { @@ -105,22 +105,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -136,25 +136,15 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/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/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -164,13 +154,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -180,16 +170,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets/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/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -201,29 +181,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -233,9 +213,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { @@ -273,27 +253,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -335,42 +315,42 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -378,9 +358,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { @@ -1198,25 +1178,6 @@ } } }, - "node_modules/@lottiefiles/dotlottie-react": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.8.12.tgz", - "integrity": "sha512-6S9q5mH2l8JDnKAruAq4NP7bzHVzJNUt8UsXONfveSyJPUgT5hGw5b5vCjW19Ra3hgN/d69qUtRLHfmyRAgblQ==", - "license": "MIT", - "dependencies": { - "@lottiefiles/dotlottie-web": "0.33.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@lottiefiles/dotlottie-web": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.33.0.tgz", - "integrity": "sha512-/KerIs0UkmwuDKYWlNGWYSSZkZaLNXXHqg6qCnP7P2JuzgI4bHRmlIk3+LoJjl/jWqd3WyI4t5oRV6ch0sQwvg==", - "license": "MIT" - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2282,26 +2243,17 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, - "node_modules/@remix-run/router": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", - "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", - "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", "cpu": [ "arm" ], @@ -2313,9 +2265,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", - "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", "cpu": [ "arm64" ], @@ -2327,9 +2279,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", - "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", "cpu": [ "arm64" ], @@ -2341,9 +2293,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", - "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", "cpu": [ "x64" ], @@ -2355,9 +2307,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", - "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", "cpu": [ "arm64" ], @@ -2369,9 +2321,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", - "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", "cpu": [ "x64" ], @@ -2383,9 +2335,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", - "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", "cpu": [ "arm" ], @@ -2397,9 +2349,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", - "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", "cpu": [ "arm" ], @@ -2411,9 +2363,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", - "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", "cpu": [ "arm64" ], @@ -2425,9 +2377,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", - "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", "cpu": [ "arm64" ], @@ -2439,9 +2391,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", - "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", "cpu": [ "loong64" ], @@ -2453,9 +2419,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", - "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", "cpu": [ "ppc64" ], @@ -2467,9 +2447,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", - "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", "cpu": [ "riscv64" ], @@ -2481,9 +2461,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", - "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", "cpu": [ "riscv64" ], @@ -2495,9 +2475,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", - "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", "cpu": [ "s390x" ], @@ -2509,9 +2489,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", - "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", "cpu": [ "x64" ], @@ -2523,9 +2503,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", - "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", "cpu": [ "x64" ], @@ -2536,10 +2516,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", - "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", "cpu": [ "arm64" ], @@ -2551,9 +2545,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", - "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", "cpu": [ "arm64" ], @@ -2565,9 +2559,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", - "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", "cpu": [ "ia32" ], @@ -2579,9 +2573,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", - "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", "cpu": [ "x64" ], @@ -2593,9 +2587,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", - "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", "cpu": [ "x64" ], @@ -2607,9 +2601,9 @@ ] }, "node_modules/@tanstack/query-core": { - "version": "5.90.16", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.16.tgz", - "integrity": "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww==", + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", "license": "MIT", "funding": { "type": "github", @@ -2617,12 +2611,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.90.16", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.16.tgz", - "integrity": "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ==", + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.20.tgz", + "integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.90.16" + "@tanstack/query-core": "5.90.20" }, "funding": { "type": "github", @@ -2655,6 +2649,23 @@ "integrity": "sha512-bqNgF/J8Po7ZtsELm3E1a6aPr7awwxO3sIqD8J6u12urOlGoW5+1KxKKbkPa58mgXuQdsltd8apI+OVy0IYiUA==", "license": "MIT" }, + "node_modules/@telegram-apps/sdk": { + "version": "3.11.8", + "resolved": "https://registry.npmjs.org/@telegram-apps/sdk/-/sdk-3.11.8.tgz", + "integrity": "sha512-vlpkYzMJCV9Cadsn9Q+/hbHNR39j5o96N9z4CjZ6AFHkkxOeOqVRrq9zRBw0gmffROkF2bU0WQxhRj8KZ/bPhw==", + "license": "MIT", + "dependencies": { + "@telegram-apps/bridge": "^2.11.0", + "@telegram-apps/navigation": "^1.0.14", + "@telegram-apps/signals": "^1.1.2", + "@telegram-apps/toolkit": "^2.1.3", + "@telegram-apps/transformers": "^2.2.6", + "@telegram-apps/types": "^2.0.3", + "better-promises": "^0.4.1", + "error-kid": "^0.0.7", + "valibot": "1.0.0" + } + }, "node_modules/@telegram-apps/sdk-react": { "version": "3.3.9", "resolved": "https://registry.npmjs.org/@telegram-apps/sdk-react/-/sdk-react-3.3.9.tgz", @@ -2673,23 +2684,6 @@ } } }, - "node_modules/@telegram-apps/sdk-react/node_modules/@telegram-apps/sdk": { - "version": "3.11.8", - "resolved": "https://registry.npmjs.org/@telegram-apps/sdk/-/sdk-3.11.8.tgz", - "integrity": "sha512-vlpkYzMJCV9Cadsn9Q+/hbHNR39j5o96N9z4CjZ6AFHkkxOeOqVRrq9zRBw0gmffROkF2bU0WQxhRj8KZ/bPhw==", - "license": "MIT", - "dependencies": { - "@telegram-apps/bridge": "^2.11.0", - "@telegram-apps/navigation": "^1.0.14", - "@telegram-apps/signals": "^1.1.2", - "@telegram-apps/toolkit": "^2.1.3", - "@telegram-apps/transformers": "^2.2.6", - "@telegram-apps/types": "^2.0.3", - "better-promises": "^0.4.1", - "error-kid": "^0.0.7", - "valibot": "1.0.0" - } - }, "node_modules/@telegram-apps/signals": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@telegram-apps/signals/-/signals-1.1.2.tgz", @@ -2804,34 +2798,37 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "devOptional": true, - "license": "MIT" + "node_modules/@types/node": { + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.1.tgz", + "integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } }, "node_modules/@types/react": { - "version": "18.3.27", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", - "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "version": "19.2.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", + "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", "devOptional": true, "license": "MIT", "peer": true, "dependencies": { - "@types/prop-types": "*", "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "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==", "devOptional": true, "license": "MIT", "peer": true, "peerDependencies": { - "@types/react": "^18.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@types/trusted-types": { @@ -3056,6 +3053,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@typescript-eslint/utils": { "version": "8.54.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", @@ -3099,16 +3109,16 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", - "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.3.tgz", + "integrity": "sha512-NVUnA6gQCl8jfoYqKqQU5Clv0aPw14KkZYCsX6T9Lfu9slI0LOU10OTwFHS/WmptsMMpshNd/1tuWsHQ2Uk+cg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.5", + "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.53", + "@rolldown/pluginutils": "1.0.0-rc.2", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, @@ -3161,9 +3171,9 @@ } }, "node_modules/ansi-escapes": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", - "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "dev": true, "license": "MIT", "dependencies": { @@ -3259,10 +3269,9 @@ "license": "MIT" }, "node_modules/autoprefixer": { - "version": "10.4.23", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", - "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", - "dev": true, + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", "funding": [ { "type": "opencollective", @@ -3280,7 +3289,7 @@ "license": "MIT", "dependencies": { "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", + "caniuse-lite": "^1.0.30001766", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -3296,9 +3305,9 @@ } }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -3314,10 +3323,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", - "dev": true, + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -3373,7 +3381,6 @@ "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, "funding": [ { "type": "opencollective", @@ -3438,10 +3445,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001762", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", - "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", - "dev": true, + "version": "1.0.30001768", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001768.tgz", + "integrity": "sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==", "funding": [ { "type": "opencollective", @@ -3623,13 +3629,13 @@ } }, "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=20" } }, "node_modules/concat-map": { @@ -3646,6 +3652,19 @@ "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", @@ -3759,10 +3778,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", - "dev": true, + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -3882,7 +3900,6 @@ "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" @@ -3999,13 +4016,13 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.26", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", - "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.0.tgz", + "integrity": "sha512-ZYvmh7VfVgqR/7wR71I3Zl6hK/C5CcxdWYKZSpHawS5JCNgE4efhQWg/+/WPpgGAp9Ngp/rRZYyaIwmPQBq/lA==", "dev": true, "license": "MIT", "peerDependencies": { - "eslint": ">=8.40" + "eslint": ">=9" } }, "node_modules/eslint-scope": { @@ -4274,7 +4291,6 @@ "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, "license": "MIT", "engines": { "node": "*" @@ -4285,12 +4301,12 @@ } }, "node_modules/framer-motion": { - "version": "12.29.2", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.29.2.tgz", - "integrity": "sha512-lSNRzBJk4wuIy0emYQ/nfZ7eWhqud2umPKw2QAQki6uKhZPKm2hRQHeQoHTG9MIvfobb+A/LbEWPJU794ZUKrg==", + "version": "12.33.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.33.0.tgz", + "integrity": "sha512-ca8d+rRPcDP5iIF+MoT3WNc0KHJMjIyFAbtVLvM9eA7joGSpeqDfiNH/kCs1t4CHi04njYvWyj0jS4QlEK/rJQ==", "license": "MIT", "dependencies": { - "motion-dom": "^12.29.2", + "motion-dom": "^12.33.0", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, @@ -4418,9 +4434,9 @@ } }, "node_modules/globals": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.1.0.tgz", - "integrity": "sha512-8HoIcWI5fCvG5NADj4bDav+er9B9JMj2vyL2pI8D0eismKyUvPLTSs+Ln3wqhwcp306i73iyVnEKx3F6T47TGw==", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", + "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", "dev": true, "license": "MIT", "engines": { @@ -4534,9 +4550,9 @@ } }, "node_modules/i18next": { - "version": "23.16.8", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz", - "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==", + "version": "25.8.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.4.tgz", + "integrity": "sha512-a9A0MnUjKvzjEN/26ZY1okpra9kA8MEwzYEz1BNm+IyxUKPRH6ihf0p7vj8YvULwZHKHl3zkJ6KOt4hewxBecQ==", "funding": [ { "type": "individual", @@ -4554,13 +4570,21 @@ "license": "MIT", "peer": true, "dependencies": { - "@babel/runtime": "^7.23.2" + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/i18next-browser-languagedetector": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.2.tgz", - "integrity": "sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", + "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.2" @@ -4703,6 +4727,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -4841,16 +4866,6 @@ "url": "https://opencollective.com/lint-staged" } }, - "node_modules/lint-staged/node_modules/commander": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", - "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, "node_modules/listr2": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", @@ -4912,18 +4927,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5021,9 +5024,9 @@ "license": "MIT" }, "node_modules/motion-dom": { - "version": "12.29.2", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.29.2.tgz", - "integrity": "sha512-/k+NuycVV8pykxyiTCoFzIVLA95Nb1BFIVvfSu9L50/6K6qNeAYtkxXILy/LRutt7AzaYDc2myj0wkCVVYAPPA==", + "version": "12.33.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.33.0.tgz", + "integrity": "sha512-XRPebVypsl0UM+7v0Hr8o9UAj0S2djsQWRdHBd5iVouVpMrQqAI0C/rDAT3QaYnXnHuC5hMcwDHCboNeyYjPoQ==", "license": "MIT", "dependencies": { "motion-utils": "^12.29.2" @@ -5071,7 +5074,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -5097,7 +5099,6 @@ "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, "license": "MIT" }, "node_modules/normalize-path": { @@ -5246,7 +5247,6 @@ "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": { @@ -5299,7 +5299,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -5456,7 +5455,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, "license": "MIT" }, "node_modules/prelude-ls": { @@ -5603,44 +5601,42 @@ "license": "MIT" }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", "peer": true, "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.2.4" } }, "node_modules/react-i18next": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-13.5.0.tgz", - "integrity": "sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA==", + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.4.tgz", + "integrity": "sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.22.5", - "html-parse-stringify": "^3.0.1" + "@babel/runtime": "^7.28.4", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" }, "peerDependencies": { - "i18next": ">= 23.2.3", - "react": ">= 16.8.0" + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" }, "peerDependenciesMeta": { "react-dom": { @@ -5648,6 +5644,9 @@ }, "react-native": { "optional": true + }, + "typescript": { + "optional": true } } }, @@ -5709,35 +5708,25 @@ } }, "node_modules/react-router": { - "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", - "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.2" + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" }, "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", - "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.2", - "react-router": "6.30.3" + "react": ">=18", + "react-dom": ">=18" }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } } }, "node_modules/react-style-singleton": { @@ -5852,9 +5841,9 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", - "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "dev": true, "license": "MIT", "dependencies": { @@ -5868,28 +5857,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.54.0", - "@rollup/rollup-android-arm64": "4.54.0", - "@rollup/rollup-darwin-arm64": "4.54.0", - "@rollup/rollup-darwin-x64": "4.54.0", - "@rollup/rollup-freebsd-arm64": "4.54.0", - "@rollup/rollup-freebsd-x64": "4.54.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", - "@rollup/rollup-linux-arm-musleabihf": "4.54.0", - "@rollup/rollup-linux-arm64-gnu": "4.54.0", - "@rollup/rollup-linux-arm64-musl": "4.54.0", - "@rollup/rollup-linux-loong64-gnu": "4.54.0", - "@rollup/rollup-linux-ppc64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-musl": "4.54.0", - "@rollup/rollup-linux-s390x-gnu": "4.54.0", - "@rollup/rollup-linux-x64-gnu": "4.54.0", - "@rollup/rollup-linux-x64-musl": "4.54.0", - "@rollup/rollup-openharmony-arm64": "4.54.0", - "@rollup/rollup-win32-arm64-msvc": "4.54.0", - "@rollup/rollup-win32-ia32-msvc": "4.54.0", - "@rollup/rollup-win32-x64-gnu": "4.54.0", - "@rollup/rollup-win32-x64-msvc": "4.54.0", + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" } }, @@ -5918,27 +5910,27 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } + "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": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "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" - }, - "engines": { - "node": ">=10" } }, + "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", @@ -6009,7 +6001,6 @@ "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" @@ -6026,9 +6017,9 @@ } }, "node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", + "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", "dev": true, "license": "MIT", "dependencies": { @@ -6094,6 +6085,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6331,11 +6332,17 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "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", @@ -6698,20 +6705,18 @@ } }, "node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, "engines": { - "node": ">=12.7.0" + "node": ">=12.20.0" }, "peerDependencies": { - "@types/react": ">=16.8", + "@types/react": ">=18.0.0", "immer": ">=9.0.6", - "react": ">=16.8" + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" }, "peerDependenciesMeta": { "@types/react": { @@ -6722,6 +6727,9 @@ }, "react": { "optional": true + }, + "use-sync-external-store": { + "optional": true } } } diff --git a/package.json b/package.json index f7bd6cb..488d7c2 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@kastov/cryptohapp": "^1.1.2", - "@lottiefiles/dotlottie-react": "^0.8.0", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -34,41 +33,42 @@ "@radix-ui/react-visually-hidden": "^1.2.4", "@tanstack/react-query": "^5.8.0", "@telegram-apps/sdk-react": "^3.3.9", + "autoprefixer": "^10.4.24", "axios": "^1.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "dompurify": "^3.3.1", "framer-motion": "^12.29.2", - "i18next": "^23.7.0", - "i18next-browser-languagedetector": "^7.2.0", + "i18next": "^25.8.4", + "i18next-browser-languagedetector": "^8.2.0", "jsencrypt": "^3.5.4", "ogl": "^1.0.11", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-i18next": "^13.5.0", - "react-router-dom": "^6.30.3", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-i18next": "^16.5.4", + "react-router": "^7.13.0", "tailwind-merge": "^3.4.0", - "zustand": "^4.4.0" + "zustand": "^5.0.11" }, "devDependencies": { "@eslint/js": "^9.39.2", "@types/dompurify": "^3.0.5", - "@types/react": "^18.2.37", - "@types/react-dom": "^18.2.15", + "@types/node": "^25.2.1", + "@types/react": "^19.2.13", + "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.2", - "autoprefixer": "^10.4.16", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.26", + "eslint-plugin-react-refresh": "^0.5.0", "globals": "^17.1.0", "husky": "^9.1.7", "lint-staged": "^16.2.7", "postcss": "^8.4.31", "prettier": "^3.8.1", "prettier-plugin-tailwindcss": "^0.7.2", - "tailwindcss": "^3.3.5", + "tailwindcss": "^3.4.19", "typescript": "^5.2.2", "typescript-eslint": "^8.54.0", "vite": "^7.3.1" diff --git a/src/App.tsx b/src/App.tsx index 9935cf0..8d1ba28 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import { lazy, Suspense } from 'react'; -import { Routes, Route, Navigate, useLocation } from 'react-router-dom'; +import { Routes, Route, Navigate, useLocation } from 'react-router'; import { useAuthStore } from './store/auth'; import { useBlockingStore } from './store/blocking'; import Layout from './components/layout/Layout'; diff --git a/src/AppWithNavigator.tsx b/src/AppWithNavigator.tsx index 33ae2a3..2945f40 100644 --- a/src/AppWithNavigator.tsx +++ b/src/AppWithNavigator.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useCallback } from 'react'; -import { BrowserRouter, useLocation, useNavigate } from 'react-router-dom'; +import { BrowserRouter, useLocation, useNavigate } from 'react-router'; import { showBackButton, hideBackButton, @@ -7,6 +7,7 @@ import { offBackButtonClick, } from '@telegram-apps/sdk-react'; import App from './App'; +import { ErrorBoundary } from './components/ErrorBoundary'; import { PlatformProvider } from './platform/PlatformProvider'; import { ThemeColorsProvider } from './providers/ThemeColorsProvider'; import { WebSocketProvider } from './providers/WebSocketProvider'; @@ -66,17 +67,19 @@ export function AppWithNavigator() { return ( {isTelegram && } - - - - - - - - - - - + + + + + + + + + + + + + ); } diff --git a/src/api/client.ts b/src/api/client.ts index 1240afd..cc2b423 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -7,6 +7,7 @@ import { safeRedirectToLogin, } from '../utils/token'; import { useBlockingStore } from '../store/blocking'; +import { API } from '../config/constants'; const API_BASE_URL = import.meta.env.VITE_API_URL || '/api'; @@ -57,6 +58,7 @@ const getTelegramInitData = (): string | null => { export const apiClient = axios.create({ baseURL: API_BASE_URL, + timeout: API.TIMEOUT_MS, headers: { 'Content-Type': 'application/json', }, diff --git a/src/api/contests.ts b/src/api/contests.ts index 75f7054..5e3b2f1 100644 --- a/src/api/contests.ts +++ b/src/api/contests.ts @@ -13,6 +13,7 @@ export interface ContestInfo { export interface ContestGameData { round_id: number; game_type: string; + // Dynamic game data from backend - shape depends on game_type // eslint-disable-next-line @typescript-eslint/no-explicit-any game_data: Record; instructions: string; diff --git a/src/api/index.ts b/src/api/index.ts index 404dcce..ffe0600 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -10,3 +10,22 @@ export { promoApi } from './promo'; export { notificationsApi } from './notifications'; export { infoApi } from './info'; export { adminSettingsApi } from './adminSettings'; +export { adminApi, statsApi } from './admin'; +export { adminAppsApi } from './adminApps'; +export { adminBroadcastsApi } from './adminBroadcasts'; +export { adminEmailTemplatesApi } from './adminEmailTemplates'; +export { adminPaymentMethodsApi } from './adminPaymentMethods'; +export { adminPaymentsApi } from './adminPayments'; +export { adminRemnawaveApi } from './adminRemnawave'; +export { adminUsersApi } from './adminUsers'; +export { banSystemApi } from './banSystem'; +export { brandingApi } from './branding'; +export { campaignsApi } from './campaigns'; +export { currencyApi } from './currency'; +export { promocodesApi } from './promocodes'; +export { promoOffersApi } from './promoOffers'; +export { serversApi } from './servers'; +export { tariffsApi } from './tariffs'; +export { themeColorsApi } from './themeColors'; +export { ticketNotificationsApi } from './ticketNotifications'; +export { wheelApi, adminWheelApi } from './wheel'; diff --git a/src/api/miniapp.ts b/src/api/miniapp.ts deleted file mode 100644 index 6346b33..0000000 --- a/src/api/miniapp.ts +++ /dev/null @@ -1,44 +0,0 @@ -import axios, { AxiosError } from 'axios'; - -export interface MiniappCreatePaymentPayload { - initData: string; - method: string; - amountKopeks?: number | null; - option?: string | null; -} - -export interface MiniappCreatePaymentResponse { - payment_url: string; - amount_kopeks?: number; - extra?: Record; -} - -export const miniappApi = { - // Create payment inside Telegram Mini App (same flow as miniapp/index.html) - createPayment: async ( - payload: MiniappCreatePaymentPayload, - ): Promise => { - try { - const response = await axios.post( - '/miniapp/payments/create', - { - initData: payload.initData || '', - method: payload.method, - amountKopeks: payload.amountKopeks ?? null, - option: payload.option ?? null, - }, - { - headers: { 'Content-Type': 'application/json' }, - }, - ); - return response.data; - } catch (error) { - const axiosError = error as AxiosError<{ detail?: string; message?: string }>; - const message = - axiosError.response?.data?.detail || - axiosError.response?.data?.message || - 'Failed to create payment'; - throw new Error(message); - } - }, -}; diff --git a/src/api/promo.ts b/src/api/promo.ts index 459f141..1610917 100644 --- a/src/api/promo.ts +++ b/src/api/promo.ts @@ -9,6 +9,7 @@ export interface PromoOffer { is_active: boolean; is_claimed: boolean; claimed_at: string | null; + // Dynamic extra data from backend // eslint-disable-next-line @typescript-eslint/no-explicit-any extra_data: Record | null; } diff --git a/src/api/promoOffers.ts b/src/api/promoOffers.ts index fdc656a..533cfdd 100644 --- a/src/api/promoOffers.ts +++ b/src/api/promoOffers.ts @@ -31,6 +31,7 @@ export interface PromoOffer { claimed_at: string | null; is_active: boolean; effect_type: string; + // Dynamic extra data from backend - shape depends on effect_type // eslint-disable-next-line @typescript-eslint/no-explicit-any extra_data: Record; created_at: string; diff --git a/src/components/ChangeEmailModal.tsx b/src/components/ChangeEmailModal.tsx index 5d8f081..fe49917 100644 --- a/src/components/ChangeEmailModal.tsx +++ b/src/components/ChangeEmailModal.tsx @@ -3,9 +3,10 @@ import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { authApi } from '../api/auth'; +import { isValidEmail } from '../utils/validation'; +import { UI } from '../config/constants'; import { useAuthStore } from '../store/auth'; -import { useTelegramWebApp } from '../hooks/useTelegramWebApp'; -import { useBackButton } from '@/platform'; +import { useTelegramSDK } from '../hooks/useTelegramSDK'; // Icons const CloseIcon = () => ( @@ -50,13 +51,13 @@ function useIsMobile() { return isMobile; } -const RESEND_COOLDOWN = 60; // seconds +const RESEND_COOLDOWN = UI.RESEND_COOLDOWN_SEC; export default function ChangeEmailModal({ onClose, currentEmail }: ChangeEmailModalProps) { const { t } = useTranslation(); const queryClient = useQueryClient(); const { setUser } = useAuthStore(); - const { isTelegramWebApp, safeAreaInset, contentSafeAreaInset } = useTelegramWebApp(); + const { isTelegramWebApp, safeAreaInset, contentSafeAreaInset } = useTelegramSDK(); const isMobileScreen = useIsMobile(); const emailInputRef = useRef(null); @@ -88,9 +89,6 @@ export default function ChangeEmailModal({ onClose, currentEmail }: ChangeEmailM return () => document.removeEventListener('keydown', handleKeyDown); }, [handleClose]); - // Telegram back button - using platform hook - useBackButton(handleClose); - // Scroll lock useEffect(() => { const scrollY = window.scrollY; @@ -181,18 +179,13 @@ export default function ChangeEmailModal({ onClose, currentEmail }: ChangeEmailM }, }); - const validateEmail = (email: string): boolean => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(email.trim()); - }; - const handleSendCode = () => { setError(null); if (!newEmail.trim()) { setError(t('profile.emailRequired')); return; } - if (!validateEmail(newEmail)) { + if (!isValidEmail(newEmail.trim())) { setError(t('profile.invalidEmail')); return; } diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..aa4e47a --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -0,0 +1,93 @@ +import { Component, type ErrorInfo, type ReactNode } from 'react'; + +interface ErrorBoundaryProps { + children: ReactNode; + level?: 'app' | 'page' | 'widget'; + onReset?: () => void; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +export class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('[ErrorBoundary]', error, errorInfo); + } + + handleReset = () => { + this.setState({ hasError: false, error: null }); + this.props.onReset?.(); + }; + + render() { + if (!this.state.hasError) { + return this.props.children; + } + + const { level = 'page' } = this.props; + + if (level === 'app') { + return ( +
+
+
⚠️
+

Something went wrong

+

+ An unexpected error occurred. Please try reloading the page. +

+ +
+
+ ); + } + + if (level === 'widget') { + return ( +
+

Failed to load this section

+ +
+ ); + } + + // level === 'page' (default) + return ( +
+
+
⚠️
+

Something went wrong

+

+ {this.state.error?.message || 'An unexpected error occurred'} +

+ +
+
+ ); + } +} diff --git a/src/components/InsufficientBalancePrompt.tsx b/src/components/InsufficientBalancePrompt.tsx index d7ceeda..75ebae1 100644 --- a/src/components/InsufficientBalancePrompt.tsx +++ b/src/components/InsufficientBalancePrompt.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { useNavigate, useLocation } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router'; import { useTranslation } from 'react-i18next'; import { useCurrency } from '../hooks/useCurrency'; diff --git a/src/components/PromoDiscountBadge.tsx b/src/components/PromoDiscountBadge.tsx deleted file mode 100644 index 6bc227d..0000000 --- a/src/components/PromoDiscountBadge.tsx +++ /dev/null @@ -1,385 +0,0 @@ -import { useState, useRef, useEffect, useCallback } from 'react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { useNavigate } from 'react-router-dom'; -import { createPortal } from 'react-dom'; -import { useTranslation } from 'react-i18next'; -import { promoApi } from '../api/promo'; - -const SparklesIcon = () => ( - - - -); - -const ClockIcon = () => ( - - - -); - -const XCircleIcon = () => ( - - - -); - -const WarningIcon = () => ( - - - -); - -const formatTimeLeft = (expiresAt: string, t: (key: string) => string): string => { - const now = new Date(); - // Ensure UTC parsing - if no timezone specified, assume UTC - let expires: Date; - if (expiresAt.includes('Z') || expiresAt.includes('+') || expiresAt.includes('-', 10)) { - expires = new Date(expiresAt); - } else { - // No timezone - treat as UTC - expires = new Date(expiresAt + 'Z'); - } - const diffMs = expires.getTime() - now.getTime(); - - if (diffMs <= 0) return ''; - - const hours = Math.floor(diffMs / (1000 * 60 * 60)); - const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); - - if (hours > 24) { - const days = Math.floor(hours / 24); - return `${days}${t('promo.time.days')}`; - } - if (hours > 0) { - return `${hours}${t('promo.time.hours')} ${minutes}${t('promo.time.minutes')}`; - } - return `${minutes}${t('promo.time.minutes')}`; -}; - -// ------- Confirmation Modal ------- - -interface DeactivateConfirmModalProps { - isOpen: boolean; - discountPercent: number; - isDeactivating: boolean; - onConfirm: () => void; - onCancel: () => void; -} - -function DeactivateConfirmModal({ - isOpen, - discountPercent, - isDeactivating, - onConfirm, - onCancel, -}: DeactivateConfirmModalProps) { - const { t } = useTranslation(); - - // Escape key to close - useEffect(() => { - if (!isOpen) return; - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape' && !isDeactivating) { - e.preventDefault(); - onCancel(); - } - }; - - document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); - }, [isOpen, isDeactivating, onCancel]); - - // Scroll lock - useEffect(() => { - if (!isOpen) return; - - document.body.style.overflow = 'hidden'; - return () => { - document.body.style.overflow = ''; - }; - }, [isOpen]); - - if (!isOpen) return null; - - const modalContent = ( -
- {/* Backdrop */} -
- - {/* Modal */} -
e.stopPropagation()} - > - {/* Warning header */} -
-
- -
-

- {t('promo.deactivate.confirmTitle')} -

-

- {t('promo.deactivate.confirmDescription', { percent: discountPercent })} -

-
- - {/* Warning text */} -
-
-

{t('promo.deactivate.warning')}

-
-
- - {/* Action buttons */} -
- - -
-
-
- ); - - if (typeof document !== 'undefined') { - return createPortal(modalContent, document.body); - } - return modalContent; -} - -// ------- Main Component ------- - -export default function PromoDiscountBadge() { - const { t } = useTranslation(); - const navigate = useNavigate(); - const queryClient = useQueryClient(); - const [isOpen, setIsOpen] = useState(false); - const [showConfirmModal, setShowConfirmModal] = useState(false); - const [deactivateError, setDeactivateError] = useState(null); - const dropdownRef = useRef(null); - - const { data: activeDiscount } = useQuery({ - queryKey: ['active-discount'], - queryFn: promoApi.getActiveDiscount, - staleTime: 30000, - refetchInterval: 60000, // Refresh every minute - }); - - const deactivateMutation = useMutation({ - mutationFn: async () => { - const source = activeDiscount?.source; - if (source && source.startsWith('promocode:')) { - await promoApi.deactivateDiscount(); - } else { - await promoApi.clearActiveDiscount(); - } - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['active-discount'] }); - queryClient.invalidateQueries({ queryKey: ['promo-offers'] }); - queryClient.invalidateQueries({ queryKey: ['subscription'] }); - queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); - setShowConfirmModal(false); - setIsOpen(false); - setDeactivateError(null); - }, - onError: (error: unknown) => { - const axiosErr = error as { response?: { data?: { detail?: string } } }; - setDeactivateError(axiosErr.response?.data?.detail || t('promo.deactivate.error')); - }, - }); - - const handleCloseConfirmModal = useCallback(() => { - if (!deactivateMutation.isPending) { - setShowConfirmModal(false); - setDeactivateError(null); - } - }, [deactivateMutation.isPending]); - - const handleConfirmDeactivate = useCallback(() => { - setDeactivateError(null); - deactivateMutation.mutate(); - }, [deactivateMutation]); - - // Close dropdown on click outside - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setIsOpen(false); - } - } - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); - - // Don't render if no active discount - if (!activeDiscount || !activeDiscount.is_active || !activeDiscount.discount_percent) { - return null; - } - - const timeLeft = activeDiscount.expires_at ? formatTimeLeft(activeDiscount.expires_at, t) : null; - - const handleClick = () => { - setIsOpen(!isOpen); - }; - - const handleGoToSubscription = () => { - setIsOpen(false); - navigate('/subscription'); - }; - - const handleDeactivateClick = () => { - setDeactivateError(null); - setShowConfirmModal(true); - }; - - return ( - <> -
- {/* Badge button */} - - - {/* Dropdown - mobile: fixed centered, desktop: absolute right */} - {isOpen && ( - <> - {/* Mobile backdrop */} -
setIsOpen(false)} - /> - -
- {/* Header */} -
-
-
- -
-
-
{t('promo.discountActive')}
-
- -{activeDiscount.discount_percent}% -
-
-
-
- - {/* Content */} -
-

{t('promo.discountDescription')}

- - {/* Time remaining */} - {timeLeft && ( -
- - - {t('promo.expiresIn')}:{' '} - {timeLeft} - -
- )} - - {/* Deactivation error */} - {deactivateError && ( -
- {deactivateError} -
- )} - - {/* CTA Button */} - - - {/* Deactivate Button */} - -
-
- - )} -
- - {/* Deactivation Confirmation Modal */} - - - ); -} diff --git a/src/components/PromoOffersSection.tsx b/src/components/PromoOffersSection.tsx index f14ee55..b0a3ac4 100644 --- a/src/components/PromoOffersSection.tsx +++ b/src/components/PromoOffersSection.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router'; import { promoApi, PromoOffer } from '../api/promo'; import { ClockIcon, CheckIcon } from './icons'; import { usePlatform } from '@/platform/hooks/usePlatform'; diff --git a/src/components/SuccessNotificationModal.tsx b/src/components/SuccessNotificationModal.tsx index dcb4c45..a97b49a 100644 --- a/src/components/SuccessNotificationModal.tsx +++ b/src/components/SuccessNotificationModal.tsx @@ -6,11 +6,11 @@ import { useEffect, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router'; import { useSuccessNotification } from '../store/successNotification'; import { useCurrency } from '../hooks/useCurrency'; -import { useTelegramWebApp } from '../hooks/useTelegramWebApp'; -import { useBackButton, useHaptic } from '@/platform'; +import { useTelegramSDK } from '../hooks/useTelegramSDK'; +import { useHaptic } from '@/platform'; // Icons const CheckCircleIcon = () => ( @@ -80,7 +80,7 @@ export default function SuccessNotificationModal() { const navigate = useNavigate(); const { isOpen, data, hide } = useSuccessNotification(); const { formatAmount, currencySymbol } = useCurrency(); - const { safeAreaInset, contentSafeAreaInset, isTelegramWebApp } = useTelegramWebApp(); + const { safeAreaInset, contentSafeAreaInset, isTelegramWebApp } = useTelegramSDK(); const haptic = useHaptic(); const safeBottom = isTelegramWebApp @@ -106,9 +106,6 @@ export default function SuccessNotificationModal() { return () => document.removeEventListener('keydown', handleKeyDown); }, [isOpen, handleClose]); - // Telegram back button - using platform hook - useBackButton(isOpen ? handleClose : null); - // Haptic feedback on open useEffect(() => { if (isOpen) { diff --git a/src/components/ThemeBentoPicker.tsx b/src/components/ThemeBentoPicker.tsx deleted file mode 100644 index dadc957..0000000 --- a/src/components/ThemeBentoPicker.tsx +++ /dev/null @@ -1,579 +0,0 @@ -import { useState, useEffect, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { COLOR_PRESETS, ColorPreset } from '../data/colorPresets'; -import { hexToHsl, hslToHex, isValidHex, HSLColor } from '../utils/colorConversion'; -import { ThemeColors, DEFAULT_THEME_COLORS } from '../types/theme'; -import { applyThemeColors } from '../hooks/useThemeColors'; - -interface ThemeBentoPickerProps { - currentColors: ThemeColors; - onColorsChange: (colors: ThemeColors) => void; - onSave: () => void; - isSaving: boolean; -} - -const CheckIcon = () => ( - - - -); - -const ChevronDownIcon = () => ( - - - -); - -const MoonIcon = () => ( - - - -); - -const SunIcon = () => ( - - - -); - -const StatusIcon = () => ( - - - -); - -const PaletteIcon = () => ( - - - -); - -function PresetCard({ - preset, - isSelected, - onClick, -}: { - preset: ColorPreset; - isSelected: boolean; - onClick: () => void; -}) { - const { i18n } = useTranslation(); - const isRu = i18n.language === 'ru'; - - return ( - - ); -} - -function HSLSlider({ - label, - value, - onChange, - max, - gradient, - suffix = '', -}: { - label: string; - value: number; - onChange: (value: number) => void; - max: number; - gradient: string; - suffix?: string; -}) { - return ( -
-
- - - {value} - {suffix} - -
- onChange(parseInt(e.target.value))} - className="h-2.5 w-full cursor-pointer appearance-none rounded-full" - style={{ background: gradient }} - /> -
- ); -} - -function CompactColorInput({ - label, - value, - onChange, -}: { - label: string; - value: string; - onChange: (color: string) => void; -}) { - const [localValue, setLocalValue] = useState(value); - const [isEditing, setIsEditing] = useState(false); - - useEffect(() => { - setLocalValue(value); - }, [value]); - - const handleChange = (newValue: string) => { - let formatted = newValue.toUpperCase(); - if (!formatted.startsWith('#')) { - formatted = '#' + formatted; - } - setLocalValue(formatted); - if (isValidHex(formatted)) { - onChange(formatted); - } - }; - - const handleBlur = () => { - setIsEditing(false); - if (!isValidHex(localValue)) { - setLocalValue(value); - } - }; - - return ( -
- - )} -
-
- ); -} - -function CollapsibleSection({ - title, - icon, - isOpen, - onToggle, - children, - badge, -}: { - title: string; - icon: React.ReactNode; - isOpen: boolean; - onToggle: () => void; - children: React.ReactNode; - badge?: string; -}) { - return ( -
- - -
-
-
{children}
-
-
-
- ); -} - -export function ThemeBentoPicker({ - currentColors, - onColorsChange, - onSave, - isSaving, -}: ThemeBentoPickerProps) { - const { t } = useTranslation(); - - const [hsl, setHsl] = useState(() => hexToHsl(currentColors.accent)); - const [hexInput, setHexInput] = useState(currentColors.accent); - const [hasChanges, setHasChanges] = useState(false); - - const [isPresetsOpen, setIsPresetsOpen] = useState(false); - const [isAccentOpen, setIsAccentOpen] = useState(false); - const [isDarkOpen, setIsDarkOpen] = useState(false); - const [isLightOpen, setIsLightOpen] = useState(false); - const [isStatusOpen, setIsStatusOpen] = useState(false); - - const selectedPresetId = useMemo(() => { - const match = COLOR_PRESETS.find( - (p) => - p.colors.accent.toLowerCase() === currentColors.accent.toLowerCase() && - p.colors.darkBackground.toLowerCase() === currentColors.darkBackground.toLowerCase() && - p.colors.lightBackground.toLowerCase() === currentColors.lightBackground.toLowerCase(), - ); - return match?.id ?? null; - }, [currentColors.accent, currentColors.darkBackground, currentColors.lightBackground]); - - useEffect(() => { - setHsl(hexToHsl(currentColors.accent)); - setHexInput(currentColors.accent); - }, [currentColors.accent]); - - const updateColor = useCallback( - (key: keyof ThemeColors, value: string) => { - const newColors = { ...currentColors, [key]: value }; - onColorsChange(newColors); - applyThemeColors(newColors); - setHasChanges(true); - }, - [currentColors, onColorsChange], - ); - - const updateAccentFromHsl = useCallback( - (newHsl: HSLColor) => { - setHsl(newHsl); - const newHex = hslToHex(newHsl.h, newHsl.s, newHsl.l); - setHexInput(newHex); - updateColor('accent', newHex); - }, - [updateColor], - ); - - const handleHexInputChange = (value: string) => { - setHexInput(value); - if (isValidHex(value)) { - const newHsl = hexToHsl(value); - setHsl(newHsl); - updateColor('accent', value); - } - }; - - const handlePresetSelect = (preset: ColorPreset) => { - onColorsChange(preset.colors); - applyThemeColors(preset.colors); - setHasChanges(true); - }; - - const hueGradient = useMemo(() => { - return `linear-gradient(to right, - hsl(0, ${hsl.s}%, ${hsl.l}%), - hsl(60, ${hsl.s}%, ${hsl.l}%), - hsl(120, ${hsl.s}%, ${hsl.l}%), - hsl(180, ${hsl.s}%, ${hsl.l}%), - hsl(240, ${hsl.s}%, ${hsl.l}%), - hsl(300, ${hsl.s}%, ${hsl.l}%), - hsl(360, ${hsl.s}%, ${hsl.l}%) - )`; - }, [hsl.s, hsl.l]); - - const saturationGradient = useMemo(() => { - return `linear-gradient(to right, - hsl(${hsl.h}, 0%, ${hsl.l}%), - hsl(${hsl.h}, 100%, ${hsl.l}%) - )`; - }, [hsl.h, hsl.l]); - - const lightnessGradient = useMemo(() => { - return `linear-gradient(to right, - hsl(${hsl.h}, ${hsl.s}%, 0%), - hsl(${hsl.h}, ${hsl.s}%, 50%), - hsl(${hsl.h}, ${hsl.s}%, 100%) - )`; - }, [hsl.h, hsl.s]); - - return ( -
- } - badge={`${COLOR_PRESETS.length}`} - isOpen={isPresetsOpen} - onToggle={() => setIsPresetsOpen(!isPresetsOpen)} - > -
- {COLOR_PRESETS.map((preset, index) => ( -
- handlePresetSelect(preset)} - /> -
- ))} -
-
- -
-

- {t('admin.theme.customizeColors', 'Customize Colors')} -

- - } - badge={hexInput.toUpperCase()} - isOpen={isAccentOpen} - onToggle={() => setIsAccentOpen(!isAccentOpen)} - > -
-
-
-
- {hexInput.toUpperCase()} -
-
- - updateAccentFromHsl({ ...hsl, h })} - max={360} - gradient={hueGradient} - suffix="Β°" - /> - - updateAccentFromHsl({ ...hsl, s })} - max={100} - gradient={saturationGradient} - suffix="%" - /> - - updateAccentFromHsl({ ...hsl, l })} - max={100} - gradient={lightnessGradient} - suffix="%" - /> - -
- - handleHexInputChange(e.target.value)} - placeholder="#3b82f6" - maxLength={7} - className="input w-full font-mono text-sm uppercase" - /> -
-
- - - } - isOpen={isDarkOpen} - onToggle={() => setIsDarkOpen(!isDarkOpen)} - > -
- updateColor('darkBackground', c)} - /> - updateColor('darkSurface', c)} - /> - updateColor('darkText', c)} - /> - updateColor('darkTextSecondary', c)} - /> -
-
- - } - isOpen={isLightOpen} - onToggle={() => setIsLightOpen(!isLightOpen)} - > -
- updateColor('lightBackground', c)} - /> - updateColor('lightSurface', c)} - /> - updateColor('lightText', c)} - /> - updateColor('lightTextSecondary', c)} - /> -
-
- - } - isOpen={isStatusOpen} - onToggle={() => setIsStatusOpen(!isStatusOpen)} - > -
- updateColor('success', c)} - /> - updateColor('warning', c)} - /> - updateColor('error', c)} - /> -
-
-
- -
-

- {t('theme.preview', 'Preview')} -

-
- - - {t('theme.success', 'Success')} - {t('theme.warning', 'Warning')} - {t('theme.error', 'Error')} -
-
- - {hasChanges && ( -
- -
- )} -
- ); -} diff --git a/src/components/TicketNotificationBell.tsx b/src/components/TicketNotificationBell.tsx index c44a6f7..693f58d 100644 --- a/src/components/TicketNotificationBell.tsx +++ b/src/components/TicketNotificationBell.tsx @@ -1,12 +1,12 @@ import { useState, useRef, useEffect, useCallback } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router'; import { useTranslation } from 'react-i18next'; import { ticketNotificationsApi } from '../api/ticketNotifications'; import { useAuthStore } from '../store/auth'; import { useToast } from './Toast'; import { useWebSocket, WSMessage } from '../hooks/useWebSocket'; -import { useTelegramWebApp } from '../hooks/useTelegramWebApp'; +import { useTelegramSDK } from '../hooks/useTelegramSDK'; import type { TicketNotification } from '../types'; const BellIcon = () => ( @@ -37,7 +37,7 @@ export default function TicketNotificationBell({ isAdmin = false }: TicketNotifi const { showToast } = useToast(); const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); - const { isFullscreen, safeAreaInset, contentSafeAreaInset } = useTelegramWebApp(); + const { isFullscreen, safeAreaInset, contentSafeAreaInset } = useTelegramSDK(); // Calculate dropdown top position (account for fullscreen safe area + TG buttons) const dropdownTop = isFullscreen diff --git a/src/components/WebSocketNotifications.tsx b/src/components/WebSocketNotifications.tsx index ea8cc70..3baffb3 100644 --- a/src/components/WebSocketNotifications.tsx +++ b/src/components/WebSocketNotifications.tsx @@ -4,7 +4,7 @@ */ import { useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router'; import { useTranslation } from 'react-i18next'; import { useQueryClient } from '@tanstack/react-query'; import { useWebSocket, WSMessage } from '../hooks/useWebSocket'; diff --git a/src/components/admin/AdminBackButton.tsx b/src/components/admin/AdminBackButton.tsx index 7972d13..5dc084b 100644 --- a/src/components/admin/AdminBackButton.tsx +++ b/src/components/admin/AdminBackButton.tsx @@ -1,4 +1,4 @@ -import { Link } from 'react-router-dom'; +import { Link } from 'react-router'; import { usePlatform } from '@/platform'; import { BackIcon } from './icons'; diff --git a/src/components/admin/AdminLayout.tsx b/src/components/admin/AdminLayout.tsx deleted file mode 100644 index 526f5ae..0000000 --- a/src/components/admin/AdminLayout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type { ReactNode } from 'react'; - -interface AdminLayoutProps { - children: ReactNode; - className?: string; -} - -/** - * AdminLayout - wrapper for all admin pages. - * Animations removed to prevent black flash during transitions. - */ -export function AdminLayout({ children, className }: AdminLayoutProps) { - return
{children}
; -} diff --git a/src/components/admin/BrandingTab.tsx b/src/components/admin/BrandingTab.tsx index bcdd233..915fa29 100644 --- a/src/components/admin/BrandingTab.tsx +++ b/src/components/admin/BrandingTab.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { brandingApi, setCachedBranding } from '../../api/branding'; import { setCachedAnimationEnabled } from '../AnimatedBackground'; -import { setCachedFullscreenEnabled } from '../../hooks/useTelegramWebApp'; +import { setCachedFullscreenEnabled } from '../../hooks/useTelegramSDK'; import { UploadIcon, TrashIcon, PencilIcon, CheckIcon, CloseIcon } from './icons'; import { Toggle } from './Toggle'; diff --git a/src/components/admin/SettingsSidebar.tsx b/src/components/admin/SettingsSidebar.tsx deleted file mode 100644 index 5236c00..0000000 --- a/src/components/admin/SettingsSidebar.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { AdminBackButton, StarIcon, CloseIcon, MENU_SECTIONS } from './index'; - -interface SettingsSidebarProps { - activeSection: string; - setActiveSection: (section: string) => void; - mobileMenuOpen: boolean; - setMobileMenuOpen: (open: boolean) => void; - favoritesCount: number; -} - -export function SettingsSidebar({ - activeSection, - setActiveSection, - mobileMenuOpen, - setMobileMenuOpen, - favoritesCount, -}: SettingsSidebarProps) { - const { t } = useTranslation(); - - return ( - - ); -} diff --git a/src/components/admin/ThemeTab.tsx b/src/components/admin/ThemeTab.tsx index 380d851..42b319a 100644 --- a/src/components/admin/ThemeTab.tsx +++ b/src/components/admin/ThemeTab.tsx @@ -59,6 +59,8 @@ export function ThemeTab() { // Local draft state const [draftColors, setDraftColors] = useState(DEFAULT_THEME_COLORS); const savedColorsRef = useRef(DEFAULT_THEME_COLORS); + const draftColorsRef = useRef(draftColors); + draftColorsRef.current = draftColors; // Sync server data into draft and saved snapshot when it arrives useEffect(() => { @@ -79,14 +81,14 @@ export function ThemeTab() { }; // Only sync if saved snapshot matches current draft (no unsaved changes) if ( - colorsEqual(savedColorsRef.current, draftColors) || + colorsEqual(savedColorsRef.current, draftColorsRef.current) || colorsEqual(savedColorsRef.current, DEFAULT_THEME_COLORS) ) { setDraftColors(colors); } savedColorsRef.current = colors; } - }, [serverColors]); // eslint-disable-line react-hooks/exhaustive-deps + }, [serverColors]); const hasUnsavedChanges = !colorsEqual(draftColors, savedColorsRef.current); diff --git a/src/components/admin/index.ts b/src/components/admin/index.ts index 3bbeafd..4fc1815 100644 --- a/src/components/admin/index.ts +++ b/src/components/admin/index.ts @@ -1,6 +1,5 @@ // Components export * from './AdminBackButton'; -export * from './AdminLayout'; export * from './icons'; export * from './Toggle'; export * from './SettingInput'; @@ -10,7 +9,6 @@ export * from './BrandingTab'; export * from './ThemeTab'; export * from './FavoritesTab'; export * from './SettingsTab'; -export * from './SettingsSidebar'; export * from './SettingsMobileTabs'; export * from './SettingsSearch'; diff --git a/src/components/data-display/EmptyState/EmptyState.tsx b/src/components/data-display/EmptyState/EmptyState.tsx deleted file mode 100644 index 466d02c..0000000 --- a/src/components/data-display/EmptyState/EmptyState.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { forwardRef, type ReactNode } from 'react'; -import { motion } from 'framer-motion'; -import { cn } from '@/lib/utils'; -import { Button, type ButtonProps } from '@/components/primitives/Button'; -import { fadeIn, fadeInTransition } from '@/components/motion/transitions'; - -export interface EmptyStateProps { - icon?: ReactNode; - title: string; - description?: string; - action?: { - label: string; - onClick: () => void; - variant?: ButtonProps['variant']; - }; - className?: string; -} - -export const EmptyState = forwardRef( - ({ icon, title, description, action, className }, ref) => { - return ( - - {/* Icon */} - {icon && ( -
- {icon} -
- )} - - {/* Title */} -

{title}

- - {/* Description */} - {description &&

{description}

} - - {/* Action */} - {action && ( -
- -
- )} -
- ); - }, -); - -EmptyState.displayName = 'EmptyState'; diff --git a/src/components/data-display/EmptyState/index.ts b/src/components/data-display/EmptyState/index.ts deleted file mode 100644 index cbf85e5..0000000 --- a/src/components/data-display/EmptyState/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { EmptyState, type EmptyStateProps } from './EmptyState'; diff --git a/src/components/data-display/index.ts b/src/components/data-display/index.ts index d3e63c3..353156e 100644 --- a/src/components/data-display/index.ts +++ b/src/components/data-display/index.ts @@ -1,3 +1,2 @@ export * from './Card'; export * from './StatCard'; -export * from './EmptyState'; diff --git a/src/components/layout/AppShell/AppHeader.tsx b/src/components/layout/AppShell/AppHeader.tsx index 9a0a9b1..4799b62 100644 --- a/src/components/layout/AppShell/AppHeader.tsx +++ b/src/components/layout/AppShell/AppHeader.tsx @@ -1,4 +1,4 @@ -import { Link, useLocation } from 'react-router-dom'; +import { Link, useLocation } from 'react-router'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { useState, useEffect } from 'react'; diff --git a/src/components/layout/AppShell/AppShell.tsx b/src/components/layout/AppShell/AppShell.tsx index 7b4bf61..c7eeef6 100644 --- a/src/components/layout/AppShell/AppShell.tsx +++ b/src/components/layout/AppShell/AppShell.tsx @@ -1,26 +1,19 @@ -import { useEffect, useState, useCallback, useRef } from 'react'; -import { useLocation, useNavigate, Link } from 'react-router-dom'; +import { useEffect, useState } from 'react'; +import { useLocation, Link } from 'react-router'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '@/store/auth'; -import { useBackButton, useHaptic } from '@/platform'; +import { useHaptic } from '@/platform'; import { useTelegramSDK } from '@/hooks/useTelegramSDK'; import { useTheme } from '@/hooks/useTheme'; +import { useBranding } from '@/hooks/useBranding'; +import { useFeatureFlags } from '@/hooks/useFeatureFlags'; +import { useScrollRestoration } from '@/hooks/useScrollRestoration'; import { themeColorsApi } from '@/api/themeColors'; -import { referralApi } from '@/api/referral'; -import { wheelApi } from '@/api/wheel'; -import { contestsApi } from '@/api/contests'; -import { pollsApi } from '@/api/polls'; -import { - brandingApi, - getCachedBranding, - setCachedBranding, - preloadLogo, - isLogoPreloaded, -} from '@/api/branding'; -import { setCachedFullscreenEnabled } from '@/hooks/useTelegramSDK'; +import { isLogoPreloaded } from '@/api/branding'; import { cn } from '@/lib/utils'; +import { UI } from '@/config/constants'; import WebSocketNotifications from '@/components/WebSocketNotifications'; import SuccessNotificationModal from '@/components/SuccessNotificationModal'; @@ -176,9 +169,6 @@ const MoonIcon = ({ className }: { className?: string }) => ( ); -const FALLBACK_NAME = import.meta.env.VITE_APP_NAME || 'Cabinet'; -const FALLBACK_LOGO = import.meta.env.VITE_APP_LOGO || 'V'; - interface AppShellProps { children: React.ReactNode; } @@ -186,20 +176,17 @@ interface AppShellProps { export function AppShell({ children }: AppShellProps) { const { t } = useTranslation(); const location = useLocation(); - const navigate = useNavigate(); - const { isAdmin, isAuthenticated, logout } = useAuthStore(); - const { - isFullscreen, - safeAreaInset, - contentSafeAreaInset, - requestFullscreen, - isTelegramWebApp, - platform, - isMobile, - } = useTelegramSDK(); + const { isAdmin, logout } = useAuthStore(); + const { isFullscreen, safeAreaInset, contentSafeAreaInset, platform, isMobile } = + useTelegramSDK(); const haptic = useHaptic(); const { toggleTheme, isDark } = useTheme(); + // Extracted hooks + const { appName, logoLetter, hasCustomLogo, logoUrl } = useBranding(); + const { referralEnabled, wheelEnabled, hasContests, hasPolls } = useFeatureFlags(); + useScrollRestoration(); + // Theme toggle visibility const { data: enabledThemes } = useQuery({ queryKey: ['enabled-themes'], @@ -214,147 +201,6 @@ export function AppShell({ children }: AppShellProps) { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [isKeyboardOpen, setIsKeyboardOpen] = useState(false); - // Scroll position restoration for admin pages - const scrollPositions = useRef>({}); - - // Disable browser's automatic scroll restoration - useEffect(() => { - if ('scrollRestoration' in history) { - history.scrollRestoration = 'manual'; - } - }, []); - - // Continuously save scroll position for current path - useEffect(() => { - const currentPath = location.pathname; - - // Only track scroll for admin pages - if (!currentPath.startsWith('/admin')) return; - - const handleScroll = () => { - scrollPositions.current[currentPath] = window.scrollY; - }; - - // Save on scroll - window.addEventListener('scroll', handleScroll, { passive: true }); - - // Restore scroll position immediately (synchronous) - const savedPosition = scrollPositions.current[currentPath]; - if (savedPosition !== undefined && savedPosition > 0) { - // Immediate restore - window.scrollTo({ top: savedPosition, behavior: 'instant' }); - } - - return () => { - window.removeEventListener('scroll', handleScroll); - }; - }, [location.pathname]); - - // Branding - const { data: branding } = useQuery({ - queryKey: ['branding'], - queryFn: async () => { - const data = await brandingApi.getBranding(); - setCachedBranding(data); - preloadLogo(data); - return data; - }, - initialData: getCachedBranding() ?? undefined, - staleTime: 60000, - enabled: isAuthenticated, - }); - - const appName = branding ? branding.name : FALLBACK_NAME; - const logoLetter = branding?.logo_letter || FALLBACK_LOGO; - const hasCustomLogo = branding?.has_custom_logo || false; - const logoUrl = branding ? brandingApi.getLogoUrl(branding) : null; - - // Set document title - useEffect(() => { - document.title = appName || 'VPN'; - }, [appName]); - - // Update favicon - useEffect(() => { - if (!logoUrl) return; - - const link = - document.querySelector("link[rel*='icon']") || - document.createElement('link'); - link.type = 'image/x-icon'; - link.rel = 'shortcut icon'; - link.href = logoUrl; - document.head.appendChild(link); - }, [logoUrl]); - - // Fullscreen setting from server - const { data: fullscreenSetting } = useQuery({ - queryKey: ['fullscreen-enabled'], - queryFn: brandingApi.getFullscreenEnabled, - staleTime: 60000, - }); - - // Apply fullscreen setting when loaded from server - // Only apply on mobile Telegram (iOS/Android) - desktop doesn't need fullscreen - useEffect(() => { - if (!fullscreenSetting || !isTelegramWebApp) return; - - // Update cache for future app starts - setCachedFullscreenEnabled(fullscreenSetting.enabled); - - // Request fullscreen if enabled, not already fullscreen, and on mobile Telegram - if (fullscreenSetting.enabled && !isFullscreen && isMobile) { - requestFullscreen(); - } - }, [fullscreenSetting, isTelegramWebApp, isFullscreen, requestFullscreen, isMobile]); - - // Feature flags - const { data: referralTerms } = useQuery({ - queryKey: ['referral-terms'], - queryFn: referralApi.getReferralTerms, - enabled: isAuthenticated, - staleTime: 60000, - retry: false, - }); - - const { data: wheelConfig } = useQuery({ - queryKey: ['wheel-config'], - queryFn: wheelApi.getConfig, - enabled: isAuthenticated, - staleTime: 60000, - retry: false, - }); - - const { data: contestsCount } = useQuery({ - queryKey: ['contests-count'], - queryFn: contestsApi.getCount, - enabled: isAuthenticated, - staleTime: 60000, - retry: false, - }); - - const { data: pollsCount } = useQuery({ - queryKey: ['polls-count'], - queryFn: pollsApi.getCount, - enabled: isAuthenticated, - staleTime: 60000, - retry: false, - }); - - // BackButton for Telegram Mini App - // Don't show back button on main tab pages (bottom nav) - users navigate via tabs - const mainTabPaths = ['/', '/subscription', '/balance', '/referral', '/support', '/wheel']; - const isMainTabPage = mainTabPaths.includes(location.pathname); - const handleBack = useCallback(() => { - if (mobileMenuOpen) { - setMobileMenuOpen(false); - return; - } - navigate(-1); - }, [mobileMenuOpen, navigate]); - - useBackButton(isMainTabPage ? null : handleBack); - // Keyboard detection for hiding bottom nav useEffect(() => { const handleFocusIn = (e: FocusEvent) => { @@ -405,7 +251,8 @@ export function AppShell({ children }: AppShellProps) { // Calculate header height based on fullscreen mode (only on mobile Telegram) // On iOS: contentSafeAreaInset.top includes status bar + dynamic island + Telegram header // On Android: safeAreaInset.top only includes status bar, need to add Telegram header height (~48px) - const telegramHeaderHeight = platform === 'android' ? 48 : 45; + const telegramHeaderHeight = + platform === 'android' ? UI.TELEGRAM_HEADER_ANDROID_PX : UI.TELEGRAM_HEADER_IOS_PX; const headerHeight = isMobileFullscreen ? 64 + Math.max(safeAreaInset.top, contentSafeAreaInset.top) + telegramHeaderHeight : 64; @@ -465,7 +312,7 @@ export function AppShell({ children }: AppShellProps) { {item.label} ))} - {referralTerms?.is_enabled && ( + {referralEnabled && ( 0} - hasPolls={(pollsCount?.count ?? 0) > 0} + wheelEnabled={wheelEnabled} + referralEnabled={referralEnabled} + hasContests={hasContests} + hasPolls={hasPolls} /> {/* Desktop spacer */} @@ -559,8 +406,8 @@ export function AppShell({ children }: AppShellProps) { {/* Mobile Bottom Navigation */}
); diff --git a/src/components/layout/AppShell/Aurora.tsx b/src/components/layout/AppShell/Aurora.tsx index 415f7d5..e73f726 100644 --- a/src/components/layout/AppShell/Aurora.tsx +++ b/src/components/layout/AppShell/Aurora.tsx @@ -154,6 +154,14 @@ export function Aurora() { const background = isDark ? themeColors.darkBackground : themeColors.lightBackground; const surface = isDark ? themeColors.darkSurface : themeColors.lightSurface; + // Refs for initial color values (WebGL context shouldn't recreate on color change) + const backgroundRef = useRef(background); + backgroundRef.current = background; + const surfaceRef = useRef(surface); + surfaceRef.current = surface; + const accentRef = useRef(themeColors.accent); + accentRef.current = themeColors.accent; + // Initialize WebGL context once (only depends on isEnabled) useEffect(() => { if (!isEnabled || !containerRef.current) return; @@ -175,7 +183,11 @@ export function Aurora() { const geometry = new Triangle(gl); - const colorStops = generateColorStops(background, surface, themeColors.accent); + const colorStops = generateColorStops( + backgroundRef.current, + surfaceRef.current, + accentRef.current, + ); const colorStopsArray = colorStops .map((hex) => { const c = new Color(hex); @@ -238,7 +250,7 @@ export function Aurora() { rendererRef.current = null; programRef.current = null; }; - }, [isEnabled]); // eslint-disable-line react-hooks/exhaustive-deps + }, [isEnabled]); // Update color uniforms reactively without recreating WebGL context useEffect(() => { diff --git a/src/components/layout/AppShell/DesktopSidebar.tsx b/src/components/layout/AppShell/DesktopSidebar.tsx index ecebd2f..9b71433 100644 --- a/src/components/layout/AppShell/DesktopSidebar.tsx +++ b/src/components/layout/AppShell/DesktopSidebar.tsx @@ -1,4 +1,4 @@ -import { Link, useLocation } from 'react-router-dom'; +import { Link, useLocation } from 'react-router'; import { useTranslation } from 'react-i18next'; import { motion } from 'framer-motion'; import { useQuery } from '@tanstack/react-query'; diff --git a/src/components/layout/AppShell/MobileBottomNav.tsx b/src/components/layout/AppShell/MobileBottomNav.tsx index a51cc30..72463b7 100644 --- a/src/components/layout/AppShell/MobileBottomNav.tsx +++ b/src/components/layout/AppShell/MobileBottomNav.tsx @@ -1,4 +1,4 @@ -import { Link, useLocation } from 'react-router-dom'; +import { Link, useLocation } from 'react-router'; import { useTranslation } from 'react-i18next'; import { motion } from 'framer-motion'; diff --git a/src/components/layout/AppShell/MovingGradient.tsx b/src/components/layout/AppShell/MovingGradient.tsx deleted file mode 100644 index d53ec30..0000000 --- a/src/components/layout/AppShell/MovingGradient.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { motion } from 'framer-motion'; - -/** - * Animated moving gradient background. - * Uses CSS variables for colors to support theme switching. - * Lightweight - pure CSS gradients with Framer Motion animation. - */ -export function MovingGradient() { - return ( -
- {/* Base background */} -
- - {/* Animated gradient layer */} - - - {/* Second animated layer - moves in opposite direction */} - - - {/* Subtle noise texture overlay */} -
-
- ); -} diff --git a/src/components/motion/transitions.ts b/src/components/motion/transitions.ts index cb2aefc..8480b77 100644 --- a/src/components/motion/transitions.ts +++ b/src/components/motion/transitions.ts @@ -7,13 +7,6 @@ export const springTransition: Transition = { damping: 30, }; -// Smooth spring for larger movements -export const smoothSpring: Transition = { - type: 'spring', - stiffness: 300, - damping: 25, -}; - // Expo easing curve (Linear-style) export const easeOutExpo = [0.16, 1, 0.3, 1] as const; @@ -42,27 +35,6 @@ export const slideUpTransition: Transition = { ease: easeOutExpo, }; -// Slide down animation (for dropdowns, popovers) -export const slideDown: Variants = { - initial: { opacity: 0, y: -8 }, - animate: { opacity: 1, y: 0 }, - exit: { opacity: 0 }, -}; - -// Slide from right (for sheets, sidebars) -export const slideRight: Variants = { - initial: { opacity: 0, x: 20 }, - animate: { opacity: 1, x: 0 }, - exit: { opacity: 0, x: 20 }, -}; - -// Slide from left -export const slideLeft: Variants = { - initial: { opacity: 0, x: -20 }, - animate: { opacity: 1, x: 0 }, - exit: { opacity: 0, x: -20 }, -}; - // Scale animation (for modals, dialogs) export const scale: Variants = { initial: { opacity: 0, scale: 0.95 }, @@ -92,16 +64,6 @@ export const staggerContainer: Variants = { }, }; -// Fast stagger for dense lists -export const fastStaggerContainer: Variants = { - initial: {}, - animate: { - transition: { - staggerChildren: 0.03, - }, - }, -}; - // Stagger item (use with staggerContainer) // Exit is instant to avoid visual glitches in Telegram Mini App export const staggerItem: Variants = { diff --git a/src/components/navigation/CommandPalette/CommandPalette.tsx b/src/components/navigation/CommandPalette/CommandPalette.tsx index 6aceaa3..c949ec7 100644 --- a/src/components/navigation/CommandPalette/CommandPalette.tsx +++ b/src/components/navigation/CommandPalette/CommandPalette.tsx @@ -1,5 +1,5 @@ import { useCallback, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router'; import { useTranslation } from 'react-i18next'; import { AnimatePresence, motion } from 'framer-motion'; import * as DialogPrimitive from '@radix-ui/react-dialog'; diff --git a/src/components/primitives/Dialog/Dialog.tsx b/src/components/primitives/Dialog/Dialog.tsx index c1bfe23..ff88a50 100644 --- a/src/components/primitives/Dialog/Dialog.tsx +++ b/src/components/primitives/Dialog/Dialog.tsx @@ -10,6 +10,12 @@ import { import { cn } from '@/lib/utils'; import { backdrop, backdropTransition, scale, scaleTransition } from '../../motion/transitions'; +export { + Trigger as DialogTrigger, + Portal as DialogPortal, + Close as DialogClose, +} from '@radix-ui/react-dialog'; + // Close icon const CloseIcon = () => ( @@ -41,15 +47,6 @@ export const Dialog = ({ children, open, onOpenChange, ...props }: DialogProps) ); }; -// Trigger -export const DialogTrigger = DialogPrimitive.Trigger; - -// Portal -export const DialogPortal = DialogPrimitive.Portal; - -// Close -export const DialogClose = DialogPrimitive.Close; - // Overlay export type DialogOverlayProps = ComponentPropsWithoutRef; @@ -86,7 +83,7 @@ export const DialogContent = forwardRef( const { open } = useContext(DialogContext); return ( - + {open && ( <> @@ -131,7 +128,7 @@ export const DialogContent = forwardRef( )} - + ); }, ); diff --git a/src/components/primitives/DropdownMenu/DropdownMenu.tsx b/src/components/primitives/DropdownMenu/DropdownMenu.tsx index b923db7..78e8c67 100644 --- a/src/components/primitives/DropdownMenu/DropdownMenu.tsx +++ b/src/components/primitives/DropdownMenu/DropdownMenu.tsx @@ -5,6 +5,15 @@ import { cn } from '@/lib/utils'; import { usePlatform } from '@/platform'; import { dropdown, dropdownTransition } from '../../motion/transitions'; +export { + Root as DropdownMenu, + Trigger as DropdownMenuTrigger, + Group as DropdownMenuGroup, + Portal as DropdownMenuPortal, + Sub as DropdownMenuSub, + RadioGroup as DropdownMenuRadioGroup, +} from '@radix-ui/react-dropdown-menu'; + // Icons const CheckIcon = () => ( @@ -36,24 +45,6 @@ const DotIcon = () => ( ); -// Root -export const DropdownMenu = DropdownMenuPrimitive.Root; - -// Trigger -export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; - -// Group -export const DropdownMenuGroup = DropdownMenuPrimitive.Group; - -// Portal -export const DropdownMenuPortal = DropdownMenuPrimitive.Portal; - -// Sub -export const DropdownMenuSub = DropdownMenuPrimitive.Sub; - -// RadioGroup -export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; - // SubTrigger export interface DropdownMenuSubTriggerProps extends ComponentPropsWithoutRef< typeof DropdownMenuPrimitive.SubTrigger diff --git a/src/components/primitives/Popover/Popover.tsx b/src/components/primitives/Popover/Popover.tsx index 90b94e0..64f3e71 100644 --- a/src/components/primitives/Popover/Popover.tsx +++ b/src/components/primitives/Popover/Popover.tsx @@ -4,6 +4,13 @@ import { forwardRef, type ComponentPropsWithoutRef } from 'react'; import { cn } from '@/lib/utils'; import { dropdown, dropdownTransition } from '../../motion/transitions'; +export { + Root as Popover, + Trigger as PopoverTrigger, + Anchor as PopoverAnchor, + Close as PopoverClose, +} from '@radix-ui/react-popover'; + // Close icon const CloseIcon = () => ( @@ -17,18 +24,6 @@ const CloseIcon = () => ( ); -// Root -export const Popover = PopoverPrimitive.Root; - -// Trigger -export const PopoverTrigger = PopoverPrimitive.Trigger; - -// Anchor -export const PopoverAnchor = PopoverPrimitive.Anchor; - -// Close -export const PopoverClose = PopoverPrimitive.Close; - // Content export interface PopoverContentProps extends ComponentPropsWithoutRef< typeof PopoverPrimitive.Content diff --git a/src/components/primitives/Select/Select.tsx b/src/components/primitives/Select/Select.tsx index f7cfdf7..7b65634 100644 --- a/src/components/primitives/Select/Select.tsx +++ b/src/components/primitives/Select/Select.tsx @@ -5,6 +5,8 @@ import { cn } from '@/lib/utils'; import { usePlatform } from '@/platform'; import { dropdown, dropdownTransition } from '../../motion/transitions'; +export { Root as Select, Group as SelectGroup } from '@radix-ui/react-select'; + // Icons const ChevronDownIcon = () => ( @@ -30,9 +32,6 @@ const CheckIcon = () => ( ); -// Root -export const Select = SelectPrimitive.Root; - // Trigger export interface SelectTriggerProps extends ComponentPropsWithoutRef< typeof SelectPrimitive.Trigger @@ -149,9 +148,6 @@ export const SelectItem = forwardRef( SelectItem.displayName = 'SelectItem'; -// Group -export const SelectGroup = SelectPrimitive.Group; - // Label export type SelectLabelProps = ComponentPropsWithoutRef; diff --git a/src/components/primitives/Sheet/Sheet.tsx b/src/components/primitives/Sheet/Sheet.tsx index 1c29024..33cd6de 100644 --- a/src/components/primitives/Sheet/Sheet.tsx +++ b/src/components/primitives/Sheet/Sheet.tsx @@ -10,7 +10,6 @@ import { } from 'react'; import { cn } from '@/lib/utils'; import { usePlatform } from '@/platform'; -import { useBackButton } from '@/platform'; import { backdrop, backdropTransition, @@ -18,6 +17,12 @@ import { sheetTransition, } from '../../motion/transitions'; +export { + Trigger as SheetTrigger, + Portal as SheetPortal, + Close as SheetClose, +} from '@radix-ui/react-dialog'; + // Close icon const CloseIcon = () => ( @@ -79,15 +84,6 @@ export const Sheet = ({ children, open, onOpenChange, onClose, ...props }: Sheet ); }; -// Trigger -export const SheetTrigger = DialogPrimitive.Trigger; - -// Portal -export const SheetPortal = DialogPrimitive.Portal; - -// Close -export const SheetClose = DialogPrimitive.Close; - // Overlay export type SheetOverlayProps = ComponentPropsWithoutRef; @@ -138,9 +134,6 @@ export const SheetContent = forwardRef( const { open, onClose } = useContext(SheetContext); const { haptic } = usePlatform(); const dragControls = useDragControls(); - // Back button integration - useBackButton(open ? onClose : null); - const handleDragEnd = useCallback( (_: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => { const velocity = info.velocity.y; @@ -157,7 +150,7 @@ export const SheetContent = forwardRef( ); return ( - + {open && ( <> @@ -221,7 +214,7 @@ export const SheetContent = forwardRef( )} - + ); }, ); diff --git a/src/components/primitives/Tooltip/Tooltip.tsx b/src/components/primitives/Tooltip/Tooltip.tsx index 3d05860..2d0bd13 100644 --- a/src/components/primitives/Tooltip/Tooltip.tsx +++ b/src/components/primitives/Tooltip/Tooltip.tsx @@ -4,14 +4,11 @@ import { forwardRef, type ComponentPropsWithoutRef, type ReactNode } from 'react import { cn } from '@/lib/utils'; import { tooltip, tooltipTransition } from '../../motion/transitions'; -// Provider - wrap your app with this -export const TooltipProvider = TooltipPrimitive.Provider; - -// Root -export const Tooltip = TooltipPrimitive.Root; - -// Trigger -export const TooltipTrigger = TooltipPrimitive.Trigger; +export { + Provider as TooltipProvider, + Root as Tooltip, + Trigger as TooltipTrigger, +} from '@radix-ui/react-tooltip'; // Content export type TooltipContentProps = ComponentPropsWithoutRef; @@ -66,10 +63,10 @@ export const SimpleTooltip = ({ delayDuration = 200, className, }: SimpleTooltipProps) => ( - - {children} + + {children} {content} - + ); diff --git a/src/components/ui/BentoCard.tsx b/src/components/ui/BentoCard.tsx index 85097cf..ceeef2a 100644 --- a/src/components/ui/BentoCard.tsx +++ b/src/components/ui/BentoCard.tsx @@ -1,4 +1,4 @@ -import { Link } from 'react-router-dom'; +import { Link } from 'react-router'; import { forwardRef, useCallback } from 'react'; import { useHaptic } from '@/platform'; diff --git a/src/components/ui/Sheet.tsx b/src/components/ui/Sheet.tsx index 3ee4874..486a165 100644 --- a/src/components/ui/Sheet.tsx +++ b/src/components/ui/Sheet.tsx @@ -1,6 +1,5 @@ import { useEffect, useRef, useCallback, useState, type ReactNode } from 'react'; import { createPortal } from 'react-dom'; -import { useBackButton } from '@/platform'; import { useHaptic } from '@/platform'; export interface SheetProps { @@ -105,9 +104,6 @@ export function Sheet({ const haptic = useHaptic(); - // BackButton integration - useBackButton(isOpen ? onClose : null); - // Calculate current height based on snap point const currentHeight = `${snapPoints[currentSnapIndex] * 100}vh`; diff --git a/src/components/ui/Skeleton.tsx b/src/components/ui/Skeleton.tsx index 56f15c9..0de2d57 100644 --- a/src/components/ui/Skeleton.tsx +++ b/src/components/ui/Skeleton.tsx @@ -131,69 +131,3 @@ export function Skeleton({ return renderSkeleton(0); } - -// Convenience components for common use cases -export function TextSkeleton({ - lines = 1, - className = '', - lastLineWidth = '60%', -}: { - lines?: number; - className?: string; - lastLineWidth?: string; -}) { - return ( -
- {Array.from({ length: lines }).map((_, i) => ( - - ))} -
- ); -} - -export function AvatarSkeleton({ - size = 40, - className = '', -}: { - size?: number; - className?: string; -}) { - return ; -} - -export function CardSkeleton({ - count = 1, - className = '', -}: { - count?: number; - className?: string; -}) { - return ; -} - -export function ListSkeleton({ - count = 3, - className = '', -}: { - count?: number; - className?: string; -}) { - return ( -
- -
- ); -} - -export function BentoSkeleton({ - count = 1, - className = '', -}: { - count?: number; - className?: string; -}) { - return ; -} - -// Default export for backwards compatibility -export default BentoSkeleton; diff --git a/src/config/constants.ts b/src/config/constants.ts new file mode 100644 index 0000000..fa75d1e --- /dev/null +++ b/src/config/constants.ts @@ -0,0 +1,30 @@ +// Storage keys +export const STORAGE_KEYS = { + THEME: 'cabinet-theme', + ENABLED_THEMES: 'cabinet-enabled-themes', + FAVORITE_SETTINGS: 'admin_favorite_settings', + USER_THEME_PREFS: 'user_theme_preferences', +} as const; + +// WebSocket +export const WS = { + MAX_RECONNECT_ATTEMPTS: 5, + PING_INTERVAL_MS: 25000, + MAX_RECONNECT_DELAY_MS: 30000, +} as const; + +// UI +export const UI = { + RESEND_COOLDOWN_SEC: 60, + TELEGRAM_HEADER_ANDROID_PX: 48, + TELEGRAM_HEADER_IOS_PX: 45, +} as const; + +// API +export const API = { + TIMEOUT_MS: 30000, + BALANCE_STALE_TIME_MS: 30000, + TRAFFIC_CACHE_MS: 30000, + TRAFFIC_WARN_PERCENT: 70, + TRAFFIC_CRITICAL_PERCENT: 90, +} as const; diff --git a/src/data/colorPresets.ts b/src/data/colorPresets.ts index 01cdf32..964ca71 100644 --- a/src/data/colorPresets.ts +++ b/src/data/colorPresets.ts @@ -276,7 +276,3 @@ export const COLOR_PRESETS: ColorPreset[] = [ }, }, ]; - -export function getPresetById(id: string): ColorPreset | undefined { - return COLOR_PRESETS.find((preset) => preset.id === id); -} diff --git a/src/hooks/useBranding.ts b/src/hooks/useBranding.ts new file mode 100644 index 0000000..b68462d --- /dev/null +++ b/src/hooks/useBranding.ts @@ -0,0 +1,79 @@ +import { useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { useAuthStore } from '@/store/auth'; +import { useTelegramSDK, setCachedFullscreenEnabled } from '@/hooks/useTelegramSDK'; +import { + brandingApi, + getCachedBranding, + setCachedBranding, + preloadLogo, + isLogoPreloaded, +} from '@/api/branding'; + +const FALLBACK_NAME = import.meta.env.VITE_APP_NAME || 'Cabinet'; +const FALLBACK_LOGO = import.meta.env.VITE_APP_LOGO || 'V'; + +export function useBranding() { + const { isAuthenticated } = useAuthStore(); + const { isFullscreen, isTelegramWebApp, requestFullscreen, isMobile } = useTelegramSDK(); + + // Branding data + const { data: branding } = useQuery({ + queryKey: ['branding'], + queryFn: async () => { + const data = await brandingApi.getBranding(); + setCachedBranding(data); + preloadLogo(data); + return data; + }, + initialData: getCachedBranding() ?? undefined, + staleTime: 60000, + enabled: isAuthenticated, + }); + + const appName = branding ? branding.name : FALLBACK_NAME; + const logoLetter = branding?.logo_letter || FALLBACK_LOGO; + const hasCustomLogo = branding?.has_custom_logo || false; + const logoUrl = branding ? brandingApi.getLogoUrl(branding) : null; + + // Set document title + useEffect(() => { + document.title = appName || 'VPN'; + }, [appName]); + + // Update favicon + useEffect(() => { + if (!logoUrl) return; + + const link = + document.querySelector("link[rel*='icon']") || + document.createElement('link'); + link.type = 'image/x-icon'; + link.rel = 'shortcut icon'; + link.href = logoUrl; + document.head.appendChild(link); + }, [logoUrl]); + + // Fullscreen setting from server + const { data: fullscreenSetting } = useQuery({ + queryKey: ['fullscreen-enabled'], + queryFn: brandingApi.getFullscreenEnabled, + staleTime: 60000, + }); + + useEffect(() => { + if (!fullscreenSetting || !isTelegramWebApp) return; + setCachedFullscreenEnabled(fullscreenSetting.enabled); + if (fullscreenSetting.enabled && !isFullscreen && isMobile) { + requestFullscreen(); + } + }, [fullscreenSetting, isTelegramWebApp, isFullscreen, requestFullscreen, isMobile]); + + return { + appName, + logoLetter, + hasCustomLogo, + logoUrl, + isLogoPreloaded, + }; +} diff --git a/src/hooks/useCurrency.ts b/src/hooks/useCurrency.ts index 842f817..c58462d 100644 --- a/src/hooks/useCurrency.ts +++ b/src/hooks/useCurrency.ts @@ -1,3 +1,4 @@ +import { useCallback, useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { currencyApi, type ExchangeRates } from '../api/currency'; @@ -40,65 +41,93 @@ export function useCurrency() { const currencySymbol = t('common.currency'); // Format amount with currency conversion - const formatAmount = (rubAmount: number, decimals: number = 2): string => { - if (isRussian) { - return rubAmount.toFixed(decimals); - } + const formatAmount = useCallback( + (rubAmount: number, decimals: number = 2): string => { + if (isRussian) { + return rubAmount.toFixed(decimals); + } - // Convert to target currency - const convertedAmount = currencyApi.convertFromRub( - rubAmount, - targetCurrency as keyof ExchangeRates, - exchangeRates, - ); + // Convert to target currency + const convertedAmount = currencyApi.convertFromRub( + rubAmount, + targetCurrency as keyof ExchangeRates, + exchangeRates, + ); - // For IRR (Iranian Toman), use no decimals as amounts are large - if (targetCurrency === 'IRR') { - return Math.round(convertedAmount).toLocaleString('fa-IR'); - } + // For IRR (Iranian Toman), use no decimals as amounts are large + if (targetCurrency === 'IRR') { + return Math.round(convertedAmount).toLocaleString('fa-IR'); + } - return convertedAmount.toFixed(decimals); - }; + return convertedAmount.toFixed(decimals); + }, + [isRussian, targetCurrency, exchangeRates], + ); // Format amount with currency symbol - const formatWithCurrency = (rubAmount: number, decimals: number = 2): string => { - return `${formatAmount(rubAmount, decimals)} ${currencySymbol}`; - }; + const formatWithCurrency = useCallback( + (rubAmount: number, decimals: number = 2): string => { + return `${formatAmount(rubAmount, decimals)} ${currencySymbol}`; + }, + [formatAmount, currencySymbol], + ); // Format amount with + sign (for earnings/bonuses) - const formatPositive = (rubAmount: number, decimals: number = 2): string => { - return `+${formatAmount(rubAmount, decimals)} ${currencySymbol}`; - }; + const formatPositive = useCallback( + (rubAmount: number, decimals: number = 2): string => { + return `+${formatAmount(rubAmount, decimals)} ${currencySymbol}`; + }, + [formatAmount, currencySymbol], + ); // Get raw converted amount (for calculations) - const convertAmount = (rubAmount: number): number => { - if (isRussian) { - return rubAmount; - } - return currencyApi.convertFromRub( - rubAmount, - targetCurrency as keyof ExchangeRates, - exchangeRates, - ); - }; + const convertAmount = useCallback( + (rubAmount: number): number => { + if (isRussian) { + return rubAmount; + } + return currencyApi.convertFromRub( + rubAmount, + targetCurrency as keyof ExchangeRates, + exchangeRates, + ); + }, + [isRussian, targetCurrency, exchangeRates], + ); // Convert from user's currency back to rubles - const convertToRub = (amount: number): number => { - if (isRussian) { - return amount; - } - return currencyApi.convertToRub(amount, targetCurrency as keyof ExchangeRates, exchangeRates); - }; + const convertToRub = useCallback( + (amount: number): number => { + if (isRussian) { + return amount; + } + return currencyApi.convertToRub(amount, targetCurrency as keyof ExchangeRates, exchangeRates); + }, + [isRussian, targetCurrency, exchangeRates], + ); - return { - exchangeRates, - targetCurrency, - isRussian, - currencySymbol, - formatAmount, - formatWithCurrency, - formatPositive, - convertAmount, - convertToRub, - }; + return useMemo( + () => ({ + exchangeRates, + targetCurrency, + isRussian, + currencySymbol, + formatAmount, + formatWithCurrency, + formatPositive, + convertAmount, + convertToRub, + }), + [ + exchangeRates, + targetCurrency, + isRussian, + currencySymbol, + formatAmount, + formatWithCurrency, + formatPositive, + convertAmount, + convertToRub, + ], + ); } diff --git a/src/hooks/useFavoriteSettings.ts b/src/hooks/useFavoriteSettings.ts index ced7d33..be078d6 100644 --- a/src/hooks/useFavoriteSettings.ts +++ b/src/hooks/useFavoriteSettings.ts @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; +import { STORAGE_KEYS } from '../config/constants'; -const STORAGE_KEY = 'admin_favorite_settings'; +const STORAGE_KEY = STORAGE_KEYS.FAVORITE_SETTINGS; export function useFavoriteSettings() { const [favorites, setFavorites] = useState(() => { diff --git a/src/hooks/useFeatureFlags.ts b/src/hooks/useFeatureFlags.ts new file mode 100644 index 0000000..594e4a9 --- /dev/null +++ b/src/hooks/useFeatureFlags.ts @@ -0,0 +1,49 @@ +import { useQuery } from '@tanstack/react-query'; +import { useAuthStore } from '@/store/auth'; +import { referralApi } from '@/api/referral'; +import { wheelApi } from '@/api/wheel'; +import { contestsApi } from '@/api/contests'; +import { pollsApi } from '@/api/polls'; + +export function useFeatureFlags() { + const { isAuthenticated } = useAuthStore(); + + const { data: referralTerms } = useQuery({ + queryKey: ['referral-terms'], + queryFn: referralApi.getReferralTerms, + enabled: isAuthenticated, + staleTime: 60000, + retry: false, + }); + + const { data: wheelConfig } = useQuery({ + queryKey: ['wheel-config'], + queryFn: wheelApi.getConfig, + enabled: isAuthenticated, + staleTime: 60000, + retry: false, + }); + + const { data: contestsCount } = useQuery({ + queryKey: ['contests-count'], + queryFn: contestsApi.getCount, + enabled: isAuthenticated, + staleTime: 60000, + retry: false, + }); + + const { data: pollsCount } = useQuery({ + queryKey: ['polls-count'], + queryFn: pollsApi.getCount, + enabled: isAuthenticated, + staleTime: 60000, + retry: false, + }); + + return { + referralEnabled: referralTerms?.is_enabled, + wheelEnabled: wheelConfig?.is_enabled, + hasContests: (contestsCount?.count ?? 0) > 0, + hasPolls: (pollsCount?.count ?? 0) > 0, + }; +} diff --git a/src/hooks/useScrollRestoration.ts b/src/hooks/useScrollRestoration.ts new file mode 100644 index 0000000..4917a65 --- /dev/null +++ b/src/hooks/useScrollRestoration.ts @@ -0,0 +1,40 @@ +import { useEffect, useRef } from 'react'; +import { useLocation } from 'react-router'; + +/** + * Saves and restores scroll position for admin pages. + * Disables browser's automatic scroll restoration. + */ +export function useScrollRestoration() { + const location = useLocation(); + const scrollPositions = useRef>({}); + + // Disable browser's automatic scroll restoration + useEffect(() => { + if ('scrollRestoration' in history) { + history.scrollRestoration = 'manual'; + } + }, []); + + // Save/restore scroll for admin pages + useEffect(() => { + const currentPath = location.pathname; + + if (!currentPath.startsWith('/admin')) return; + + const handleScroll = () => { + scrollPositions.current[currentPath] = window.scrollY; + }; + + window.addEventListener('scroll', handleScroll, { passive: true }); + + const savedPosition = scrollPositions.current[currentPath]; + if (savedPosition !== undefined && savedPosition > 0) { + window.scrollTo({ top: savedPosition, behavior: 'instant' }); + } + + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, [location.pathname]); +} diff --git a/src/hooks/useTelegramDnd.ts b/src/hooks/useTelegramDnd.ts deleted file mode 100644 index 740953d..0000000 --- a/src/hooks/useTelegramDnd.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useCallback } from 'react'; -import { useTelegramSDK } from './useTelegramSDK'; - -/** - * Hook for drag-and-drop operations in Telegram Mini App. - * Note: Vertical swipes are now globally disabled at app init, - * so this hook just provides no-op callbacks for compatibility. - */ -export function useTelegramDnd() { - const { isTelegramWebApp } = useTelegramSDK(); - - const onDragStart = useCallback(() => { - // No-op: swipes are globally disabled - }, []); - - const onDragEnd = useCallback(() => { - // No-op: swipes are globally disabled - }, []); - - const onDragCancel = useCallback(() => { - // No-op: swipes are globally disabled - }, []); - - return { - onDragStart, - onDragEnd, - onDragCancel, - isTelegramWebApp, - }; -} diff --git a/src/hooks/useTelegramWebApp.ts b/src/hooks/useTelegramWebApp.ts deleted file mode 100644 index 7fdf0be..0000000 --- a/src/hooks/useTelegramWebApp.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @deprecated This hook is deprecated. Use useTelegramSDK instead. - * This file is kept for backward compatibility and re-exports from useTelegramSDK. - */ - -import { useTelegramSDK } from './useTelegramSDK'; - -// Re-export everything from useTelegramSDK for backward compatibility -export { - getCachedFullscreenEnabled, - setCachedFullscreenEnabled, - isInTelegramWebApp, - isTelegramMobile, -} from './useTelegramSDK'; - -/** - * @deprecated Use useTelegramSDK instead - * Hook for Telegram WebApp API integration - backward compatible wrapper - */ -export function useTelegramWebApp() { - const sdk = useTelegramSDK(); - - return { - isTelegramWebApp: sdk.isTelegramWebApp, - isFullscreen: sdk.isFullscreen, - isFullscreenSupported: sdk.isFullscreenSupported, - safeAreaInset: sdk.safeAreaInset, - contentSafeAreaInset: sdk.contentSafeAreaInset, - requestFullscreen: sdk.requestFullscreen, - exitFullscreen: sdk.exitFullscreen, - toggleFullscreen: sdk.toggleFullscreen, - disableVerticalSwipes: sdk.disableVerticalSwipes, - enableVerticalSwipes: sdk.enableVerticalSwipes, - webApp: null, - }; -} diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts index a13a51e..45d20c7 100644 --- a/src/hooks/useTheme.ts +++ b/src/hooks/useTheme.ts @@ -1,11 +1,12 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { EnabledThemes, DEFAULT_ENABLED_THEMES } from '../types/theme'; import { themeColorsApi } from '../api/themeColors'; +import { STORAGE_KEYS } from '../config/constants'; type Theme = 'dark' | 'light'; -const THEME_KEY = 'cabinet-theme'; -const ENABLED_THEMES_KEY = 'cabinet-enabled-themes'; +const THEME_KEY = STORAGE_KEYS.THEME; +const ENABLED_THEMES_KEY = STORAGE_KEYS.ENABLED_THEMES; // Fetch enabled themes from API async function fetchEnabledThemes(): Promise { @@ -82,18 +83,21 @@ export function useTheme() { return enabled.dark ? 'dark' : 'light'; }); + const themeRef = useRef(theme); + themeRef.current = theme; + // Fetch enabled themes on mount useEffect(() => { fetchEnabledThemes().then((data) => { setEnabledThemes(data); setIsLoading(false); // If current theme is disabled, switch to enabled one - if (!data[theme]) { + if (!data[themeRef.current]) { const newTheme = data.dark ? 'dark' : 'light'; setThemeState(newTheme); } }); - }, []); // eslint-disable-line react-hooks/exhaustive-deps + }, []); // Listen for localStorage changes (when admin updates enabled themes from other tabs) useEffect(() => { diff --git a/src/hooks/useThemeColors.ts b/src/hooks/useThemeColors.ts index 2bb5d58..1fb29d9 100644 --- a/src/hooks/useThemeColors.ts +++ b/src/hooks/useThemeColors.ts @@ -2,66 +2,7 @@ import { useEffect } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { themeColorsApi } from '../api/themeColors'; import { ThemeColors, DEFAULT_THEME_COLORS, SHADE_LEVELS, ColorPalette } from '../types/theme'; - -// Convert hex to RGB values -function hexToRgb(hex: string): { r: number; g: number; b: number } { - // Handle shorthand hex - if (hex.length === 4) { - hex = '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3]; - } - const r = parseInt(hex.slice(1, 3), 16); - const g = parseInt(hex.slice(3, 5), 16); - const b = parseInt(hex.slice(5, 7), 16); - return { r, g, b }; -} - -// Convert hex to HSL -function hexToHsl(hex: string): { h: number; s: number; l: number } { - const { r, g, b } = hexToRgb(hex); - const rNorm = r / 255; - const gNorm = g / 255; - const bNorm = b / 255; - - const max = Math.max(rNorm, gNorm, bNorm); - const min = Math.min(rNorm, gNorm, bNorm); - let h = 0; - let s = 0; - const l = (max + min) / 2; - - if (max !== min) { - const d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - - switch (max) { - case rNorm: - h = ((gNorm - bNorm) / d + (gNorm < bNorm ? 6 : 0)) / 6; - break; - case gNorm: - h = ((bNorm - rNorm) / d + 2) / 6; - break; - case bNorm: - h = ((rNorm - gNorm) / d + 4) / 6; - break; - } - } - - return { h: h * 360, s: s * 100, l: l * 100 }; -} - -// Convert HSL to RGB values -function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } { - s /= 100; - l /= 100; - - const a = s * Math.min(l, 1 - l); - const f = (n: number) => { - const k = (n + h / 30) % 12; - const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); - return Math.round(255 * color); - }; - - return { r: f(0), g: f(8), b: f(4) }; -} +import { hexToRgb, hexToHsl, hslToRgb } from '../utils/colorConversion'; // Convert RGB to string format for CSS variable function rgbToString(r: number, g: number, b: number): string { diff --git a/src/hooks/useUserThemePreferences.ts b/src/hooks/useUserThemePreferences.ts index bbf6e47..08bdf79 100644 --- a/src/hooks/useUserThemePreferences.ts +++ b/src/hooks/useUserThemePreferences.ts @@ -7,8 +7,9 @@ import { DEFAULT_USER_PREFERENCES, BORDER_RADIUS_VALUES, } from '../types/theme'; +import { STORAGE_KEYS } from '../config/constants'; -const STORAGE_KEY = 'user_theme_preferences'; +const STORAGE_KEY = STORAGE_KEYS.USER_THEME_PREFS; /** * Parse preferences from storage string diff --git a/src/main.tsx b/src/main.tsx index 5d53a22..05c79a7 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -20,6 +20,7 @@ import { isFullscreen, } from '@telegram-apps/sdk-react'; import { AppWithNavigator } from './AppWithNavigator'; +import { ErrorBoundary } from './components/ErrorBoundary'; import { initLogoPreload } from './api/branding'; import { getCachedFullscreenEnabled, isTelegramMobile } from './hooks/useTelegramSDK'; import './i18n'; @@ -100,8 +101,10 @@ const queryClient = new QueryClient({ ReactDOM.createRoot(document.getElementById('root')!).render( - - - + + + + + , ); diff --git a/src/pages/AdminApps.tsx b/src/pages/AdminApps.tsx index e80045c..fb7abe2 100644 --- a/src/pages/AdminApps.tsx +++ b/src/pages/AdminApps.tsx @@ -1,8 +1,7 @@ -import { useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { adminAppsApi } from '../api/adminApps'; -import { useBackButton } from '../platform/hooks/useBackButton'; import { usePlatform } from '../platform/hooks/usePlatform'; export default function AdminApps() { @@ -11,8 +10,6 @@ export default function AdminApps() { const queryClient = useQueryClient(); const { capabilities } = usePlatform(); - useBackButton(() => navigate('/admin')); - // RemnaWave status const { data: status } = useQuery({ queryKey: ['remnawave-status'], diff --git a/src/pages/AdminBanSystem.tsx b/src/pages/AdminBanSystem.tsx index 217788d..89f11d2 100644 --- a/src/pages/AdminBanSystem.tsx +++ b/src/pages/AdminBanSystem.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { banSystemApi, @@ -206,6 +206,8 @@ export default function AdminBanSystem() { const [report, setReport] = useState(null); const [health, setHealth] = useState(null); const [reportHours, setReportHours] = useState(24); + const reportHoursRef = useRef(reportHours); + reportHoursRef.current = reportHours; const [settingLoading, setSettingLoading] = useState(null); const [loading, setLoading] = useState(true); @@ -240,19 +242,7 @@ export default function AdminBanSystem() { const [searchQuery, setSearchQuery] = useState(''); const [actionLoading, setActionLoading] = useState(null); - useEffect(() => { - loadStatus(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (status?.enabled && status?.configured) { - loadTabData(activeTab); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeTab, status]); - - const loadStatus = async () => { + const loadStatus = useCallback(async () => { try { setLoading(true); const data = await banSystemApi.getStatus(); @@ -265,71 +255,84 @@ export default function AdminBanSystem() { } finally { setLoading(false); } - }; + }, [t]); - const loadTabData = async (tab: TabType) => { - try { - setLoading(true); - setError(null); + const loadTabData = useCallback( + async (tab: TabType) => { + try { + setLoading(true); + setError(null); - switch (tab) { - case 'dashboard': { - const statsData = await banSystemApi.getStats(); - setStats(statsData); - break; - } - case 'users': { - const usersData = await banSystemApi.getUsers({ limit: 50 }); - setUsers(usersData); - break; - } - case 'punishments': { - const punishmentsData = await banSystemApi.getPunishments(); - setPunishments(punishmentsData); - break; - } - case 'nodes': { - const nodesData = await banSystemApi.getNodes(); - setNodes(nodesData); - break; - } - case 'agents': { - const agentsData = await banSystemApi.getAgents(); - setAgents(agentsData); - break; - } - case 'violations': { - const violationsData = await banSystemApi.getTrafficViolations(); - setViolations(violationsData); - break; - } - case 'settings': { - const settingsData = await banSystemApi.getSettings(); - setSettings(settingsData); - break; - } - case 'traffic': { - const trafficData = await banSystemApi.getTraffic(); - setTraffic(trafficData); - break; - } - case 'reports': { - const reportData = await banSystemApi.getReport(reportHours); - setReport(reportData); - break; - } - case 'health': { - const healthData = await banSystemApi.getHealth(); - setHealth(healthData); - break; + switch (tab) { + case 'dashboard': { + const statsData = await banSystemApi.getStats(); + setStats(statsData); + break; + } + case 'users': { + const usersData = await banSystemApi.getUsers({ limit: 50 }); + setUsers(usersData); + break; + } + case 'punishments': { + const punishmentsData = await banSystemApi.getPunishments(); + setPunishments(punishmentsData); + break; + } + case 'nodes': { + const nodesData = await banSystemApi.getNodes(); + setNodes(nodesData); + break; + } + case 'agents': { + const agentsData = await banSystemApi.getAgents(); + setAgents(agentsData); + break; + } + case 'violations': { + const violationsData = await banSystemApi.getTrafficViolations(); + setViolations(violationsData); + break; + } + case 'settings': { + const settingsData = await banSystemApi.getSettings(); + setSettings(settingsData); + break; + } + case 'traffic': { + const trafficData = await banSystemApi.getTraffic(); + setTraffic(trafficData); + break; + } + case 'reports': { + const reportData = await banSystemApi.getReport(reportHoursRef.current); + setReport(reportData); + break; + } + case 'health': { + const healthData = await banSystemApi.getHealth(); + setHealth(healthData); + break; + } } + } catch { + setError(t('banSystem.loadError')); + } finally { + setLoading(false); } - } catch { - setError(t('banSystem.loadError')); - } finally { - setLoading(false); + }, + [t], + ); + + useEffect(() => { + loadStatus(); + }, [loadStatus]); + + useEffect(() => { + if (status?.enabled && status?.configured) { + loadTabData(activeTab); } - }; + }, [activeTab, status, loadTabData]); const handleSearch = async () => { if (!searchQuery.trim()) { @@ -403,8 +406,7 @@ export default function AdminBanSystem() { if (activeTab === 'reports' && status?.enabled) { loadTabData('reports'); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reportHours]); + }, [reportHours, activeTab, status, loadTabData]); const formatBytes = (bytes: number) => { if (bytes === 0) return '0 B'; diff --git a/src/pages/AdminBroadcastCreate.tsx b/src/pages/AdminBroadcastCreate.tsx index 0bc4f2d..3ee1dea 100644 --- a/src/pages/AdminBroadcastCreate.tsx +++ b/src/pages/AdminBroadcastCreate.tsx @@ -1,5 +1,5 @@ import { useState, useRef, useMemo } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { @@ -10,7 +10,6 @@ import { CombinedBroadcastCreateRequest, } from '../api/adminBroadcasts'; import { AdminBackButton } from '../components/admin'; -import { useBackButton } from '../platform/hooks/useBackButton'; // Icons const BroadcastIcon = () => ( @@ -119,8 +118,6 @@ export default function AdminBroadcastCreate() { const queryClient = useQueryClient(); const fileInputRef = useRef(null); - useBackButton(() => navigate('/admin/broadcasts')); - // Channel selection const [channel, setChannel] = useState('telegram'); diff --git a/src/pages/AdminBroadcastDetail.tsx b/src/pages/AdminBroadcastDetail.tsx index 244d725..3a715d4 100644 --- a/src/pages/AdminBroadcastDetail.tsx +++ b/src/pages/AdminBroadcastDetail.tsx @@ -1,9 +1,8 @@ -import { useParams, useNavigate } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { adminBroadcastsApi, type BroadcastChannel } from '../api/adminBroadcasts'; import { AdminBackButton } from '../components/admin'; -import { useBackButton } from '../platform/hooks/useBackButton'; // Icons @@ -159,8 +158,6 @@ export default function AdminBroadcastDetail() { const broadcastId = id ? parseInt(id, 10) : null; - useBackButton(() => navigate('/admin/broadcasts')); - // Fetch broadcast details const { data: broadcast, diff --git a/src/pages/AdminBroadcasts.tsx b/src/pages/AdminBroadcasts.tsx index f8fd05c..89e7958 100644 --- a/src/pages/AdminBroadcasts.tsx +++ b/src/pages/AdminBroadcasts.tsx @@ -1,9 +1,8 @@ import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { adminBroadcastsApi } from '../api/adminBroadcasts'; -import { useBackButton } from '../platform/hooks/useBackButton'; import { usePlatform } from '../platform/hooks/usePlatform'; // Icons @@ -131,9 +130,6 @@ export default function AdminBroadcasts() { const navigate = useNavigate(); const { capabilities } = usePlatform(); - // Use native Telegram back button in Mini App - useBackButton(() => navigate('/admin')); - const [page, setPage] = useState(0); const limit = 20; diff --git a/src/pages/AdminCampaignCreate.tsx b/src/pages/AdminCampaignCreate.tsx index e5d8566..5baf676 100644 --- a/src/pages/AdminCampaignCreate.tsx +++ b/src/pages/AdminCampaignCreate.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { @@ -11,8 +11,6 @@ import { } from '../api/campaigns'; import { AdminBackButton } from '../components/admin'; import { createNumberInputHandler, toNumber } from '../utils/inputHelpers'; -import { useBackButton } from '../platform/hooks/useBackButton'; - // Icons const CampaignIcon = () => ( @@ -156,8 +154,6 @@ export default function AdminCampaignCreate() { const navigate = useNavigate(); const queryClient = useQueryClient(); - useBackButton(() => navigate('/admin/campaigns')); - // Form state const [name, setName] = useState(''); const [startParameter, setStartParameter] = useState(''); diff --git a/src/pages/AdminCampaignEdit.tsx b/src/pages/AdminCampaignEdit.tsx index 6c50895..ae9478c 100644 --- a/src/pages/AdminCampaignEdit.tsx +++ b/src/pages/AdminCampaignEdit.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { @@ -12,7 +12,6 @@ import { import { AdminBackButton } from '../components/admin'; import { CheckIcon, CampaignIcon } from '../components/icons'; import { createNumberInputHandler, toNumber } from '../utils/inputHelpers'; -import { useBackButton } from '../platform/hooks/useBackButton'; // Bonus type config const bonusTypeConfig: Record< @@ -133,8 +132,6 @@ export default function AdminCampaignEdit() { const campaignId = parseInt(id || '0'); - useBackButton(() => navigate('/admin/campaigns')); - // Fetch campaign const { data: campaign, diff --git a/src/pages/AdminCampaignStats.tsx b/src/pages/AdminCampaignStats.tsx index 1c7403b..13494ee 100644 --- a/src/pages/AdminCampaignStats.tsx +++ b/src/pages/AdminCampaignStats.tsx @@ -1,10 +1,9 @@ import { useState } from 'react'; -import { useParams, useNavigate, Link } from 'react-router-dom'; +import { useParams, useNavigate, Link } from 'react-router'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { campaignsApi, CampaignBonusType } from '../api/campaigns'; import { AdminBackButton } from '../components/admin'; -import { useBackButton } from '../platform/hooks/useBackButton'; // Icons const CopyIcon = () => ( @@ -98,8 +97,6 @@ export default function AdminCampaignStats() { const [copied, setCopied] = useState(false); const [showUsers, setShowUsers] = useState(false); - useBackButton(() => navigate('/admin/campaigns')); - // Fetch stats const { data: stats, diff --git a/src/pages/AdminCampaigns.tsx b/src/pages/AdminCampaigns.tsx index 7ed0359..42c3c2e 100644 --- a/src/pages/AdminCampaigns.tsx +++ b/src/pages/AdminCampaigns.tsx @@ -1,11 +1,10 @@ import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import i18n from '../i18n'; import { campaignsApi, CampaignListItem, CampaignBonusType } from '../api/campaigns'; import { PlusIcon, EditIcon, TrashIcon, CheckIcon, XIcon, ChartIcon } from '../components/icons'; -import { useBackButton } from '../platform/hooks/useBackButton'; import { usePlatform } from '../platform/hooks/usePlatform'; // Bonus type labels and colors @@ -67,9 +66,6 @@ export default function AdminCampaigns() { const queryClient = useQueryClient(); const { capabilities } = usePlatform(); - // Use native Telegram back button in Mini App - useBackButton(() => navigate('/admin')); - const [deleteConfirm, setDeleteConfirm] = useState(null); // Queries diff --git a/src/pages/AdminDashboard.tsx b/src/pages/AdminDashboard.tsx index 6babd35..f9e523a 100644 --- a/src/pages/AdminDashboard.tsx +++ b/src/pages/AdminDashboard.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useState, useEffect, useCallback } from 'react'; +import { useNavigate } from 'react-router'; import { useTranslation } from 'react-i18next'; import { statsApi, @@ -10,7 +10,6 @@ import { type RecentPaymentsResponse, } from '../api/admin'; import { useCurrency } from '../hooks/useCurrency'; -import { useBackButton } from '../platform/hooks/useBackButton'; import { usePlatform } from '../platform/hooks/usePlatform'; // Icons - styled like main navigation @@ -365,9 +364,6 @@ export default function AdminDashboard() { const { formatAmount, currencySymbol } = useCurrency(); const { capabilities } = usePlatform(); - // Use native Telegram back button in Mini App - useBackButton(() => navigate('/admin')); - const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -380,7 +376,7 @@ export default function AdminDashboard() { const [payments, setPayments] = useState(null); const [referrersTab, setReferrersTab] = useState<'earnings' | 'invited'>('earnings'); - const fetchStats = async () => { + const fetchStats = useCallback(async () => { try { setLoading(true); setError(null); @@ -392,9 +388,9 @@ export default function AdminDashboard() { } finally { setLoading(false); } - }; + }, [t]); - const fetchExtendedStats = async () => { + const fetchExtendedStats = useCallback(async () => { try { const [referrersData, campaignsData, paymentsData] = await Promise.all([ statsApi.getTopReferrers(10), @@ -407,7 +403,7 @@ export default function AdminDashboard() { } catch (err) { console.error('Failed to load extended stats:', err); } - }; + }, []); useEffect(() => { fetchStats(); @@ -418,8 +414,7 @@ export default function AdminDashboard() { fetchExtendedStats(); }, 30000); return () => clearInterval(interval); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [fetchStats, fetchExtendedStats]); const handleRestartNode = async (uuid: string) => { try { diff --git a/src/pages/AdminEmailTemplatePreview.tsx b/src/pages/AdminEmailTemplatePreview.tsx index e19db99..da8fadc 100644 --- a/src/pages/AdminEmailTemplatePreview.tsx +++ b/src/pages/AdminEmailTemplatePreview.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { useLocation, useNavigate, useParams } from 'react-router-dom'; +import { useLocation, useNavigate, useParams } from 'react-router'; import { useMutation } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { adminEmailTemplatesApi } from '../api/adminEmailTemplates'; @@ -23,7 +23,11 @@ export default function AdminEmailTemplatePreview() { const state = location.state as PreviewState | null; // Preview mutation - const previewMutation = useMutation({ + const { + mutate: loadPreview, + isPending: previewPending, + isError: previewError, + } = useMutation({ mutationFn: () => { if (!type || !lang || !state) { throw new Error('Missing required data'); @@ -45,9 +49,8 @@ export default function AdminEmailTemplatePreview() { navigate('/admin/email-templates'); return; } - previewMutation.mutate(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [type, lang]); + loadPreview(); + }, [type, lang, state, navigate, loadPreview]); // Write preview HTML into iframe useEffect(() => { @@ -85,11 +88,11 @@ export default function AdminEmailTemplatePreview() { {/* Preview content */}
- {previewMutation.isPending ? ( + {previewPending ? (
- ) : previewMutation.isError ? ( + ) : previewError ? (

{t('common.error')}