diff --git a/web/package-lock.json b/web/package-lock.json index 9407dc46..b1545cf4 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,6 +8,7 @@ "name": "nofx-web", "version": "1.0.0", "dependencies": { + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-slot": "^1.2.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -17,7 +18,9 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-password-checklist": "^1.8.1", + "react-router-dom": "^7.9.5", "recharts": "^2.15.2", + "sonner": "^1.5.0", "swr": "^2.2.5", "tailwind-merge": "^3.3.1", "zustand": "^5.0.2" @@ -117,6 +120,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -448,6 +452,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -471,18 +476,20 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" @@ -493,12 +500,13 @@ }, "node_modules/@esbuild/android-arm": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.11.tgz", "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -509,12 +517,13 @@ }, "node_modules/@esbuild/android-arm64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -525,12 +534,13 @@ }, "node_modules/@esbuild/android-x64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.11.tgz", "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -557,12 +567,13 @@ }, "node_modules/@esbuild/darwin-x64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -573,12 +584,13 @@ }, "node_modules/@esbuild/freebsd-arm64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -589,12 +601,13 @@ }, "node_modules/@esbuild/freebsd-x64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -605,12 +618,13 @@ }, "node_modules/@esbuild/linux-arm": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -621,12 +635,13 @@ }, "node_modules/@esbuild/linux-arm64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -637,12 +652,13 @@ }, "node_modules/@esbuild/linux-ia32": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -653,12 +669,13 @@ }, "node_modules/@esbuild/linux-loong64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -669,12 +686,13 @@ }, "node_modules/@esbuild/linux-mips64el": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -685,12 +703,13 @@ }, "node_modules/@esbuild/linux-ppc64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -701,12 +720,13 @@ }, "node_modules/@esbuild/linux-riscv64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -717,12 +737,13 @@ }, "node_modules/@esbuild/linux-s390x": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -733,12 +754,13 @@ }, "node_modules/@esbuild/linux-x64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -749,12 +771,13 @@ }, "node_modules/@esbuild/netbsd-arm64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -765,12 +788,13 @@ }, "node_modules/@esbuild/netbsd-x64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -781,12 +805,13 @@ }, "node_modules/@esbuild/openbsd-arm64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -797,12 +822,13 @@ }, "node_modules/@esbuild/openbsd-x64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -813,12 +839,13 @@ }, "node_modules/@esbuild/openharmony-arm64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openharmony" @@ -829,12 +856,13 @@ }, "node_modules/@esbuild/sunos-x64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -845,12 +873,13 @@ }, "node_modules/@esbuild/win32-arm64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -861,12 +890,13 @@ }, "node_modules/@esbuild/win32-ia32": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -877,12 +907,13 @@ }, "node_modules/@esbuild/win32-x64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1139,7 +1170,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=18" } @@ -1151,7 +1181,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/type": "^3.0.9" @@ -1175,7 +1204,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@inquirer/ansi": "^1.0.1", "@inquirer/figures": "^1.0.14", @@ -1205,7 +1233,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=8" } @@ -1217,7 +1244,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1234,8 +1260,7 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@inquirer/core/node_modules/string-width": { "version": "4.2.3", @@ -1244,7 +1269,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -1261,7 +1285,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -1276,7 +1299,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -1293,7 +1315,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=18" } @@ -1305,7 +1326,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=18" }, @@ -1387,7 +1407,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", @@ -1441,8 +1460,7 @@ "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@open-draft/logger": { "version": "0.3.0", @@ -1451,7 +1469,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" @@ -1463,8 +1480,7 @@ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", @@ -1489,6 +1505,40 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -1504,6 +1554,213 @@ } } }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -1522,6 +1779,91 @@ } } }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1530,12 +1872,13 @@ }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1543,12 +1886,13 @@ }, "node_modules/@rollup/rollup-android-arm64": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1569,12 +1913,13 @@ }, "node_modules/@rollup/rollup-darwin-x64": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1582,12 +1927,13 @@ }, "node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -1595,12 +1941,13 @@ }, "node_modules/@rollup/rollup-freebsd-x64": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -1608,12 +1955,13 @@ }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1621,12 +1969,13 @@ }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1634,12 +1983,13 @@ }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1647,12 +1997,13 @@ }, "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1660,12 +2011,13 @@ }, "node_modules/@rollup/rollup-linux-loong64-gnu": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1673,12 +2025,13 @@ }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1686,12 +2039,13 @@ }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1699,12 +2053,13 @@ }, "node_modules/@rollup/rollup-linux-riscv64-musl": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1712,12 +2067,13 @@ }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1725,12 +2081,13 @@ }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1738,12 +2095,13 @@ }, "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1751,12 +2109,13 @@ }, "node_modules/@rollup/rollup-openharmony-arm64": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openharmony" @@ -1764,12 +2123,13 @@ }, "node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1777,12 +2137,13 @@ }, "node_modules/@rollup/rollup-win32-ia32-msvc": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1790,12 +2151,13 @@ }, "node_modules/@rollup/rollup-win32-x64-gnu": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1803,12 +2165,13 @@ }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1895,8 +2258,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2017,6 +2379,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "devOptional": true, + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2026,7 +2389,8 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, + "devOptional": true, + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -2037,8 +2401,7 @@ "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.3", @@ -2076,6 +2439,7 @@ "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.3", "@typescript-eslint/types": "8.46.3", @@ -2400,6 +2764,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2516,6 +2881,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmmirror.com/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -2811,6 +3188,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -3089,7 +3467,6 @@ "dev": true, "license": "ISC", "optional": true, - "peer": true, "engines": { "node": ">= 12" } @@ -3101,7 +3478,6 @@ "dev": true, "license": "ISC", "optional": true, - "peer": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -3118,7 +3494,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=8" } @@ -3130,7 +3505,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3147,8 +3521,7 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", @@ -3157,7 +3530,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -3174,7 +3546,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -3189,7 +3560,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -3274,10 +3644,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "dev": true, "license": "MIT", - "optional": true, - "peer": true, "engines": { "node": ">=18" } @@ -3628,6 +3995,12 @@ "node": ">=6" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -3658,8 +4031,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -3982,6 +4354,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4042,6 +4415,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -4672,7 +5046,6 @@ "dev": true, "license": "ISC", "optional": true, - "peer": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -4715,6 +5088,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -4836,7 +5218,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -4940,8 +5321,7 @@ "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/hermes-estree": { "version": "0.25.1", @@ -5344,8 +5724,7 @@ "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/is-number": { "version": "7.0.0", @@ -5576,6 +5955,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -5604,6 +5984,7 @@ "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.1.0", "data-urls": "^5.0.0", @@ -5978,7 +6359,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -6124,7 +6504,6 @@ "hasInstallScript": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.40.0", @@ -6170,7 +6549,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tldts-core": "^7.0.17" }, @@ -6184,8 +6562,7 @@ "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/msw/node_modules/tough-cookie": { "version": "6.0.0", @@ -6194,7 +6571,6 @@ "dev": true, "license": "BSD-3-Clause", "optional": true, - "peer": true, "dependencies": { "tldts": "^7.0.5" }, @@ -6209,7 +6585,6 @@ "dev": true, "license": "ISC", "optional": true, - "peer": true, "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -6449,8 +6824,7 @@ "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/own-keys": { "version": "1.0.1", @@ -6587,8 +6961,7 @@ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/pathe": { "version": "1.1.2", @@ -6685,6 +7058,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -6838,6 +7212,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -6867,7 +7242,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -6883,7 +7257,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -6894,7 +7267,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -6907,8 +7279,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/prop-types": { "version": "15.8.1", @@ -6959,6 +7330,7 @@ "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==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -6970,6 +7342,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -7001,6 +7374,91 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.9.5", + "resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.9.5.tgz", + "integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.5", + "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.9.5.tgz", + "integrity": "sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.5" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-smooth": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", @@ -7015,6 +7473,28 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -7146,7 +7626,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7204,8 +7683,7 @@ "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/reusify": { "version": "1.1.0", @@ -7387,6 +7865,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -7585,6 +8069,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/sonner": { + "version": "1.7.4", + "resolved": "https://registry.npmmirror.com/sonner/-/sonner-1.7.4.tgz", + "integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -7608,7 +8102,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 0.8" } @@ -7640,8 +8133,7 @@ "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/string-argv": { "version": "0.3.2", @@ -8080,6 +8572,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -8220,7 +8713,6 @@ "dev": true, "license": "(MIT OR CC0-1.0)", "optional": true, - "peer": true, "engines": { "node": ">=16" }, @@ -8311,6 +8803,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8345,7 +8838,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "funding": { "url": "https://github.com/sponsors/kettanaito" } @@ -8390,6 +8882,49 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -8430,6 +8965,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -8524,7 +9060,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" @@ -8541,7 +9077,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/android-arm": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" @@ -8558,7 +9094,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/android-arm64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" @@ -8575,7 +9111,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/android-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" @@ -8609,7 +9145,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" @@ -8626,7 +9162,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" @@ -8643,7 +9179,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" @@ -8660,7 +9196,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/linux-arm": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" @@ -8677,7 +9213,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" @@ -8694,7 +9230,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" @@ -8711,7 +9247,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" @@ -8728,7 +9264,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" @@ -8745,7 +9281,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" @@ -8762,7 +9298,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" @@ -8779,7 +9315,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" @@ -8796,7 +9332,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/linux-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" @@ -8813,7 +9349,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" @@ -8830,7 +9366,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" @@ -8847,7 +9383,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" @@ -8864,7 +9400,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" @@ -8881,7 +9417,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" @@ -8898,7 +9434,7 @@ }, "node_modules/vite-node/node_modules/@esbuild/win32-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" @@ -9034,6 +9570,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -9109,7 +9646,7 @@ }, "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" @@ -9126,7 +9663,7 @@ }, "node_modules/vitest/node_modules/@esbuild/android-arm": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" @@ -9143,7 +9680,7 @@ }, "node_modules/vitest/node_modules/@esbuild/android-arm64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" @@ -9160,7 +9697,7 @@ }, "node_modules/vitest/node_modules/@esbuild/android-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" @@ -9194,7 +9731,7 @@ }, "node_modules/vitest/node_modules/@esbuild/darwin-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" @@ -9211,7 +9748,7 @@ }, "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" @@ -9228,7 +9765,7 @@ }, "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" @@ -9245,7 +9782,7 @@ }, "node_modules/vitest/node_modules/@esbuild/linux-arm": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" @@ -9262,7 +9799,7 @@ }, "node_modules/vitest/node_modules/@esbuild/linux-arm64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" @@ -9279,7 +9816,7 @@ }, "node_modules/vitest/node_modules/@esbuild/linux-ia32": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" @@ -9296,7 +9833,7 @@ }, "node_modules/vitest/node_modules/@esbuild/linux-loong64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" @@ -9313,7 +9850,7 @@ }, "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" @@ -9330,7 +9867,7 @@ }, "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" @@ -9347,7 +9884,7 @@ }, "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" @@ -9364,7 +9901,7 @@ }, "node_modules/vitest/node_modules/@esbuild/linux-s390x": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" @@ -9381,7 +9918,7 @@ }, "node_modules/vitest/node_modules/@esbuild/linux-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" @@ -9398,7 +9935,7 @@ }, "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" @@ -9415,7 +9952,7 @@ }, "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" @@ -9432,7 +9969,7 @@ }, "node_modules/vitest/node_modules/@esbuild/sunos-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" @@ -9449,7 +9986,7 @@ }, "node_modules/vitest/node_modules/@esbuild/win32-arm64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" @@ -9466,7 +10003,7 @@ }, "node_modules/vitest/node_modules/@esbuild/win32-ia32": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" @@ -9483,7 +10020,7 @@ }, "node_modules/vitest/node_modules/@esbuild/win32-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" @@ -9570,6 +10107,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -9952,7 +10490,6 @@ "dev": true, "license": "ISC", "optional": true, - "peer": true, "engines": { "node": ">=10" } @@ -9983,7 +10520,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -10004,7 +10540,6 @@ "dev": true, "license": "ISC", "optional": true, - "peer": true, "engines": { "node": ">=12" } @@ -10016,7 +10551,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=8" } @@ -10027,8 +10561,7 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", @@ -10037,7 +10570,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -10054,7 +10586,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -10082,7 +10613,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=18" }, @@ -10096,6 +10626,7 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/web/package.json b/web/package.json index 0825e2c1..31de80f9 100644 --- a/web/package.json +++ b/web/package.json @@ -14,6 +14,7 @@ "test": "vitest run" }, "dependencies": { + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-slot": "^1.2.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -23,7 +24,9 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-password-checklist": "^1.8.1", + "react-router-dom": "^7.9.5", "recharts": "^2.15.2", + "sonner": "^1.5.0", "swr": "^2.2.5", "tailwind-merge": "^3.3.1", "zustand": "^5.0.2" diff --git a/web/src/App.tsx b/web/src/App.tsx index 566e7338..eec52d01 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,1270 +1,52 @@ -import { useEffect, useState } from 'react' -import useSWR from 'swr' -import { api } from './lib/api' -import { EquityChart } from './components/EquityChart' -import { AITradersPage } from './components/AITradersPage' -import { LoginPage } from './components/LoginPage' -import { RegisterPage } from './components/RegisterPage' -import { ResetPasswordPage } from './components/ResetPasswordPage' -import { CompetitionPage } from './components/CompetitionPage' -import { LandingPage } from './pages/LandingPage' -import { FAQPage } from './pages/FAQPage' -import HeaderBar from './components/landing/HeaderBar' -import AILearning from './components/AILearning' -import { LanguageProvider, useLanguage } from './contexts/LanguageContext' -import { AuthProvider, useAuth } from './contexts/AuthContext' -import { t, type Language } from './i18n/translations' +import { RouterProvider } from 'react-router-dom' +import { LanguageProvider } from './contexts/LanguageContext' +import { AuthProvider } from './contexts/AuthContext' +import { ConfirmDialogProvider } from './components/ConfirmDialog' +import { router } from './routes' import { useSystemConfig } from './hooks/useSystemConfig' -import { AlertTriangle } from 'lucide-react' -import type { - SystemStatus, - AccountInfo, - Position, - DecisionRecord, - Statistics, - TraderInfo, -} from './types' +import { useAuth } from './contexts/AuthContext' +import { useLanguage } from './contexts/LanguageContext' +import { t } from './i18n/translations' -type Page = 'competition' | 'traders' | 'trader' +function LoadingScreen() { + const { language } = useLanguage() -// 获取友好的AI模型名称 -function getModelDisplayName(modelId: string): string { - switch (modelId.toLowerCase()) { - case 'deepseek': - return 'DeepSeek' - case 'qwen': - return 'Qwen' - case 'claude': - return 'Claude' - default: - return modelId.toUpperCase() - } + return ( +
+
+ NoFx Logo +

{t('loading', language)}

+
+
+ ) } -function App() { - const { language, setLanguage } = useLanguage() - const { user, token, logout, isLoading } = useAuth() +function AppContent() { + const { isLoading } = useAuth() const { loading: configLoading } = useSystemConfig() - const [route, setRoute] = useState(window.location.pathname) - - // 从URL路径读取初始页面状态(支持刷新保持页面) - const getInitialPage = (): Page => { - const path = window.location.pathname - const hash = window.location.hash.slice(1) // 去掉 # - - if (path === '/traders' || hash === 'traders') return 'traders' - if (path === '/dashboard' || hash === 'trader' || hash === 'details') - return 'trader' - return 'competition' // 默认为竞赛页面 - } - - const [currentPage, setCurrentPage] = useState(getInitialPage()) - const [selectedTraderId, setSelectedTraderId] = useState() - const [lastUpdate, setLastUpdate] = useState('--:--:--') - - // 监听URL变化,同步页面状态 - useEffect(() => { - const handleRouteChange = () => { - const path = window.location.pathname - const hash = window.location.hash.slice(1) - - if (path === '/traders' || hash === 'traders') { - setCurrentPage('traders') - } else if ( - path === '/dashboard' || - hash === 'trader' || - hash === 'details' - ) { - setCurrentPage('trader') - } else if ( - path === '/competition' || - hash === 'competition' || - hash === '' - ) { - setCurrentPage('competition') - } - setRoute(path) - } - - window.addEventListener('hashchange', handleRouteChange) - window.addEventListener('popstate', handleRouteChange) - return () => { - window.removeEventListener('hashchange', handleRouteChange) - window.removeEventListener('popstate', handleRouteChange) - } - }, []) - - // 切换页面时更新URL hash (当前通过按钮直接调用setCurrentPage,这个函数暂时保留用于未来扩展) - // const navigateToPage = (page: Page) => { - // setCurrentPage(page); - // window.location.hash = page === 'competition' ? '' : 'trader'; - // }; - - // 获取trader列表(仅在用户登录时) - const { data: traders, error: tradersError } = useSWR( - user && token ? 'traders' : null, - api.getTraders, - { - refreshInterval: 10000, - shouldRetryOnError: false, // 避免在后端未运行时无限重试 - } - ) - - // 当获取到traders后,设置默认选中第一个 - useEffect(() => { - if (traders && traders.length > 0 && !selectedTraderId) { - setSelectedTraderId(traders[0].trader_id) - } - }, [traders, selectedTraderId]) - - // 如果在trader页面,获取该trader的数据 - const { data: status } = useSWR( - currentPage === 'trader' && selectedTraderId - ? `status-${selectedTraderId}` - : null, - () => api.getStatus(selectedTraderId), - { - refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存) - revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求 - dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求 - } - ) - - const { data: account } = useSWR( - currentPage === 'trader' && selectedTraderId - ? `account-${selectedTraderId}` - : null, - () => api.getAccount(selectedTraderId), - { - refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存) - revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求 - dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求 - } - ) - - const { data: positions } = useSWR( - currentPage === 'trader' && selectedTraderId - ? `positions-${selectedTraderId}` - : null, - () => api.getPositions(selectedTraderId), - { - refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存) - revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求 - dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求 - } - ) - - const { data: decisions } = useSWR( - currentPage === 'trader' && selectedTraderId - ? `decisions/latest-${selectedTraderId}` - : null, - () => api.getLatestDecisions(selectedTraderId), - { - refreshInterval: 30000, // 30秒刷新(决策更新频率较低) - revalidateOnFocus: false, - dedupingInterval: 20000, - } - ) - - const { data: stats } = useSWR( - currentPage === 'trader' && selectedTraderId - ? `statistics-${selectedTraderId}` - : null, - () => api.getStatistics(selectedTraderId), - { - refreshInterval: 30000, // 30秒刷新(统计数据更新频率较低) - revalidateOnFocus: false, - dedupingInterval: 20000, - } - ) - - useEffect(() => { - if (account) { - const now = new Date().toLocaleTimeString() - setLastUpdate(now) - } - }, [account]) - - const selectedTrader = traders?.find((t) => t.trader_id === selectedTraderId) - - // Handle routing - useEffect(() => { - const handlePopState = () => { - setRoute(window.location.pathname) - } - window.addEventListener('popstate', handlePopState) - return () => window.removeEventListener('popstate', handlePopState) - }, []) - - // Set current page based on route for consistent navigation state - useEffect(() => { - if (route === '/competition') { - setCurrentPage('competition') - } else if (route === '/traders') { - setCurrentPage('traders') - } else if (route === '/dashboard') { - setCurrentPage('trader') - } - }, [route]) // Show loading spinner while checking auth or config if (isLoading || configLoading) { - return ( -
-
- NoFx Logo -

{t('loading', language)}

-
-
- ) + return } - // Handle specific routes regardless of authentication - if (route === '/login') { - return - } - if (route === '/register') { - return - } - if (route === '/faq') { - return - } - if (route === '/reset-password') { - return - } - if (route === '/competition') { - return ( -
- { - console.log('Competition page onPageChange called with:', page) - console.log('Current route:', route, 'Current page:', currentPage) - - if (page === 'competition') { - console.log('Navigating to competition') - window.history.pushState({}, '', '/competition') - setRoute('/competition') - setCurrentPage('competition') - } else if (page === 'traders') { - console.log('Navigating to traders') - window.history.pushState({}, '', '/traders') - setRoute('/traders') - setCurrentPage('traders') - } else if (page === 'trader') { - console.log('Navigating to trader/dashboard') - window.history.pushState({}, '', '/dashboard') - setRoute('/dashboard') - setCurrentPage('trader') - } else if (page === 'faq') { - console.log('Navigating to faq') - window.history.pushState({}, '', '/faq') - setRoute('/faq') - } - - console.log( - 'After navigation - route:', - route, - 'currentPage:', - currentPage - ) - }} - /> -
- -
-
- ) - } - - // Show landing page for root route - if (route === '/' || route === '') { - return - } - - // Show main app for authenticated users on other routes - if (!user || !token) { - // Default to landing page when not authenticated and no specific route - return - } - - return ( -
- { - console.log('Main app onPageChange called with:', page) - - if (page === 'competition') { - window.history.pushState({}, '', '/competition') - setRoute('/competition') - setCurrentPage('competition') - } else if (page === 'traders') { - window.history.pushState({}, '', '/traders') - setRoute('/traders') - setCurrentPage('traders') - } else if (page === 'trader') { - window.history.pushState({}, '', '/dashboard') - setRoute('/dashboard') - setCurrentPage('trader') - } else if (page === 'faq') { - window.history.pushState({}, '', '/faq') - setRoute('/faq') - } - }} - /> - - {/* Main Content */} -
- {currentPage === 'competition' ? ( - - ) : currentPage === 'traders' ? ( - { - setSelectedTraderId(traderId) - window.history.pushState({}, '', '/dashboard') - setRoute('/dashboard') - setCurrentPage('trader') - }} - /> - ) : ( - { - window.history.pushState({}, '', '/traders') - setRoute('/traders') - setCurrentPage('traders') - }} - /> - )} -
- - {/* Footer */} - -
- ) + return } -// Trader Details Page Component -function TraderDetailsPage({ - selectedTrader, - status, - account, - positions, - decisions, - lastUpdate, - language, - traders, - tradersError, - selectedTraderId, - onTraderSelect, - onNavigateToTraders, -}: { - selectedTrader?: TraderInfo - traders?: TraderInfo[] - tradersError?: Error - selectedTraderId?: string - onTraderSelect: (traderId: string) => void - onNavigateToTraders: () => void - status?: SystemStatus - account?: AccountInfo - positions?: Position[] - decisions?: DecisionRecord[] - stats?: Statistics - lastUpdate: string - language: Language -}) { - // If API failed with error, show empty state (likely backend not running) - if (tradersError) { - return ( -
-
- {/* Icon */} -
- - - -
- - {/* Title */} -

- {t('dashboardEmptyTitle', language)} -

- - {/* Description */} -

- {t('dashboardEmptyDescription', language)} -

- - {/* CTA Button */} - -
-
- ) - } - - // If traders is loaded and empty, show empty state - if (traders && traders.length === 0) { - return ( -
-
- {/* Icon */} -
- - - -
- - {/* Title */} -

- {t('dashboardEmptyTitle', language)} -

- - {/* Description */} -

- {t('dashboardEmptyDescription', language)} -

- - {/* CTA Button */} - -
-
- ) - } - - // If traders is still loading or selectedTrader is not ready, show skeleton - if (!selectedTrader) { - return ( -
- {/* Loading Skeleton - Binance Style */} -
-
-
-
-
-
-
-
-
- {[1, 2, 3, 4].map((i) => ( -
-
-
-
- ))} -
-
-
-
-
-
- ) - } - - return ( -
- {/* Trader Header */} -
-
-

- - 🤖 - - {selectedTrader.trader_name} -

- - {/* Trader Selector */} - {traders && traders.length > 0 && ( -
- - {t('switchTrader', language)}: - - -
- )} -
-
- - AI Model:{' '} - - {getModelDisplayName( - selectedTrader.ai_model.split('_').pop() || - selectedTrader.ai_model - )} - - - {status && ( - <> - - Cycles: {status.call_count} - - Runtime: {status.runtime_minutes} min - - )} -
-
- - {/* Debug Info */} - {account && ( -
-
- 🔄 Last Update: {lastUpdate} | Total Equity:{' '} - {account?.total_equity?.toFixed(2) || '0.00'} | Available:{' '} - {account?.available_balance?.toFixed(2) || '0.00'} | P&L:{' '} - {account?.total_pnl?.toFixed(2) || '0.00'} ( - {account?.total_pnl_pct?.toFixed(2) || '0.00'}%) -
-
- )} - - {/* Account Overview */} -
- 0} - /> - - = 0 ? '+' : ''}${account?.total_pnl?.toFixed(2) || '0.00'} USDT`} - change={account?.total_pnl_pct || 0} - positive={(account?.total_pnl ?? 0) >= 0} - /> - -
- - {/* 主要内容区:左右分屏 */} -
- {/* 左侧:图表 + 持仓 */} -
- {/* Equity Chart */} -
- -
- - {/* Current Positions */} -
-
-

- 📈 {t('currentPositions', language)} -

- {positions && positions.length > 0 && ( -
- {positions.length} {t('active', language)} -
- )} -
- {positions && positions.length > 0 ? ( -
- - - - - - - - - - - - - - - - {positions.map((pos, i) => ( - - - - - - - - - - - - ))} - -
- {t('symbol', language)} - - {t('side', language)} - - {t('entryPrice', language)} - - {t('markPrice', language)} - - {t('quantity', language)} - - {t('positionValue', language)} - - {t('leverage', language)} - - {t('unrealizedPnL', language)} - - {t('liqPrice', language)} -
- {pos.symbol} - - - {t( - pos.side === 'long' ? 'long' : 'short', - language - )} - - - {pos.entry_price.toFixed(4)} - - {pos.mark_price.toFixed(4)} - - {pos.quantity.toFixed(4)} - - {(pos.quantity * pos.mark_price).toFixed(2)} USDT - - {pos.leverage}x - - = 0 ? '#0ECB81' : '#F6465D', - fontWeight: 'bold', - }} - > - {pos.unrealized_pnl >= 0 ? '+' : ''} - {pos.unrealized_pnl.toFixed(2)} ( - {pos.unrealized_pnl_pct.toFixed(2)}%) - - - {pos.liquidation_price.toFixed(4)} -
-
- ) : ( -
-
📊
-
- {t('noPositions', language)} -
-
- {t('noActivePositions', language)} -
-
- )} -
-
- {/* 左侧结束 */} - - {/* 右侧:Recent Decisions - 卡片容器 */} -
- {/* 标题 */} -
-
- 🧠 -
-
-

- {t('recentDecisions', language)} -

- {decisions && decisions.length > 0 && ( -
- {t('lastCycles', language, { count: decisions.length })} -
- )} -
-
- - {/* 决策列表 - 可滚动 */} -
- {decisions && decisions.length > 0 ? ( - decisions.map((decision, i) => ( - - )) - ) : ( -
-
🧠
-
- {t('noDecisionsYet', language)} -
-
- {t('aiDecisionsWillAppear', language)} -
-
- )} -
-
- {/* 右侧结束 */} -
- - {/* AI Learning & Performance Analysis */} -
- -
-
- ) -} - -// Stat Card Component - Binance Style Enhanced -function StatCard({ - title, - value, - change, - positive, - subtitle, -}: { - title: string - value: string - change?: number - positive?: boolean - subtitle?: string -}) { - return ( -
-
- {title} -
-
- {value} -
- {change !== undefined && ( -
-
- {positive ? '▲' : '▼'} {positive ? '+' : ''} - {change.toFixed(2)}% -
-
- )} - {subtitle && ( -
- {subtitle} -
- )} -
- ) -} - -// Decision Card Component with CoT Trace - Binance Style -function DecisionCard({ - decision, - language, -}: { - decision: DecisionRecord - language: Language -}) { - const [showInputPrompt, setShowInputPrompt] = useState(false) - const [showCoT, setShowCoT] = useState(false) - - return ( -
- {/* Header */} -
-
-
- {t('cycle', language)} #{decision.cycle_number} -
-
- {new Date(decision.timestamp).toLocaleString()} -
-
-
- {t(decision.success ? 'success' : 'failed', language)} -
-
- - {/* Input Prompt - Collapsible */} - {decision.input_prompt && ( -
- - {showInputPrompt && ( -
- {decision.input_prompt} -
- )} -
- )} - - {/* AI Chain of Thought - Collapsible */} - {decision.cot_trace && ( -
- - {showCoT && ( -
- {decision.cot_trace} -
- )} -
- )} - - {/* Decisions Actions */} - {decision.decisions && decision.decisions.length > 0 && ( -
- {decision.decisions.map((action, j) => ( -
- - {action.symbol} - - - {action.action} - - {action.leverage > 0 && ( - {action.leverage}x - )} - {action.price > 0 && ( - - @{action.price.toFixed(4)} - - )} - - {action.success ? '✓' : '✗'} - - {action.error && ( - - {action.error} - - )} -
- ))} -
- )} - - {/* Account State Summary */} - {decision.account_state && ( -
- - 净值: {decision.account_state.total_balance.toFixed(2)} USDT - - - 可用: {decision.account_state.available_balance.toFixed(2)} USDT - - - 保证金率: {decision.account_state.margin_used_pct.toFixed(1)}% - - 持仓: {decision.account_state.position_count} - - {t('candidateCoins', language)}:{' '} - {decision.candidate_coins?.length || 0} - -
- )} - - {/* Candidate Coins Warning */} - {decision.candidate_coins && decision.candidate_coins.length === 0 && ( -
- -
-
- ⚠️ {t('candidateCoinsZeroWarning', language)} -
-
-
{t('possibleReasons', language)}
-
    -
  • {t('coinPoolApiNotConfigured', language)}
  • -
  • {t('apiConnectionTimeout', language)}
  • -
  • {t('noCustomCoinsAndApiFailed', language)}
  • -
-
- {t('solutions', language)} -
-
    -
  • {t('setCustomCoinsInConfig', language)}
  • -
  • {t('orConfigureCorrectApiUrl', language)}
  • -
  • {t('orDisableCoinPoolOptions', language)}
  • -
-
-
-
- )} - - {/* Execution Logs */} - {decision.execution_log && decision.execution_log.length > 0 && ( -
- {decision.execution_log.map((log, k) => ( -
- {log} -
- ))} -
- )} - - {/* Error Message */} - {decision.error_message && ( -
- ❌ {decision.error_message} -
- )} -
- ) -} - -// Wrap App with providers -export default function AppWithProviders() { +export default function App() { return ( - + + + ) diff --git a/web/src/components/AILearning.tsx b/web/src/components/AILearning.tsx index 75793cd2..a10f8f14 100644 --- a/web/src/components/AILearning.tsx +++ b/web/src/components/AILearning.tsx @@ -1,6 +1,7 @@ import useSWR from 'swr' import { useLanguage } from '../contexts/LanguageContext' import { t } from '../i18n/translations' +import { stripLeadingIcons } from '../lib/text' import { api } from '../lib/api' import { Brain, @@ -78,7 +79,9 @@ export default function AILearning({ traderId }: AILearningProps) { className="rounded p-6" style={{ background: '#1E2329', border: '1px solid #2B3139' }} > -
{t('loadingError', language)}
+
+ {stripLeadingIcons(t('loadingError', language))} +
) } @@ -695,7 +698,7 @@ export default function AILearning({ traderId }: AILearningProps) { style={{ color: '#E0E7FF' }} > {' '} - {t('symbolPerformance', language)} + {stripLeadingIcons(t('symbolPerformance', language))}
- {t('howAILearns', language)} + {stripLeadingIcons(t('howAILearns', language))}
diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index 32252ad1..10b06dd0 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' import useSWR from 'swr' import { api } from '../lib/api' import type { @@ -29,7 +30,10 @@ import { BookOpen, HelpCircle, Radio, + Pencil, } from 'lucide-react' +import { confirmToast } from '../lib/notify' +import { toast } from 'sonner' // 获取友好的AI模型名称 function getModelDisplayName(modelId: string): string { @@ -58,6 +62,7 @@ interface AITradersPageProps { export function AITradersPage({ onTraderSelect }: AITradersPageProps) { const { language } = useLanguage() const { user, token } = useAuth() + const navigate = useNavigate() const [showCreateModal, setShowCreateModal] = useState(false) const [showEditModal, setShowEditModal] = useState(false) const [showModelModal, setShowModelModal] = useState(false) @@ -220,21 +225,25 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { const exchange = allExchanges?.find((e) => e.id === data.exchange_id) if (!model?.enabled) { - alert(t('modelNotConfigured', language)) + toast.error(t('modelNotConfigured', language)) return } if (!exchange?.enabled) { - alert(t('exchangeNotConfigured', language)) + toast.error(t('exchangeNotConfigured', language)) return } - await api.createTrader(data) + await toast.promise(api.createTrader(data), { + loading: '正在创建…', + success: '创建成功', + error: '创建失败', + }) setShowCreateModal(false) mutateTraders() } catch (error) { console.error('Failed to create trader:', error) - alert(t('createTraderFailed', language)) + toast.error(t('createTraderFailed', language)) } } @@ -245,7 +254,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { setShowEditModal(true) } catch (error) { console.error('Failed to fetch trader config:', error) - alert(t('getTraderConfigFailed', language)) + toast.error(t('getTraderConfigFailed', language)) } } @@ -257,12 +266,12 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { const exchange = enabledExchanges?.find((e) => e.id === data.exchange_id) if (!model) { - alert(t('modelConfigNotExist', language)) + toast.error(t('modelConfigNotExist', language)) return } if (!exchange) { - alert(t('exchangeConfigNotExist', language)) + toast.error(t('exchangeConfigNotExist', language)) return } @@ -282,39 +291,58 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { use_oi_top: data.use_oi_top, } - await api.updateTrader(editingTrader.trader_id, request) + await toast.promise(api.updateTrader(editingTrader.trader_id, request), { + loading: '正在保存…', + success: '保存成功', + error: '保存失败', + }) setShowEditModal(false) setEditingTrader(null) mutateTraders() } catch (error) { console.error('Failed to update trader:', error) - alert(t('updateTraderFailed', language)) + toast.error(t('updateTraderFailed', language)) } } const handleDeleteTrader = async (traderId: string) => { - if (!confirm(t('confirmDeleteTrader', language))) return + { + const ok = await confirmToast(t('confirmDeleteTrader', language)) + if (!ok) return + } try { - await api.deleteTrader(traderId) + await toast.promise(api.deleteTrader(traderId), { + loading: '正在删除…', + success: '删除成功', + error: '删除失败', + }) mutateTraders() } catch (error) { console.error('Failed to delete trader:', error) - alert(t('deleteTraderFailed', language)) + toast.error(t('deleteTraderFailed', language)) } } const handleToggleTrader = async (traderId: string, running: boolean) => { try { if (running) { - await api.stopTrader(traderId) + await toast.promise(api.stopTrader(traderId), { + loading: '正在停止…', + success: '已停止', + error: '停止失败', + }) } else { - await api.startTrader(traderId) + await toast.promise(api.startTrader(traderId), { + loading: '正在启动…', + success: '已启动', + error: '启动失败', + }) } mutateTraders() } catch (error) { console.error('Failed to toggle trader:', error) - alert(t('operationFailed', language)) + toast.error(t('operationFailed', language)) } } @@ -353,19 +381,16 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { if (config.checkInUse(config.id)) { const usingTraders = config.getUsingTraders(config.id) const traderNames = usingTraders.map((t) => t.trader_name).join(', ') - alert( - t(config.cannotDeleteKey, language) + - '\n\n' + - t('tradersUsing', language) + - ': ' + - traderNames + - '\n\n' + - t('pleaseDeleteTradersFirst', language) + toast.error( + `${t(config.cannotDeleteKey, language)} · ${t('tradersUsing', language)}: ${traderNames} · ${t('pleaseDeleteTradersFirst', language)}` ) return } - if (!confirm(t(config.confirmDeleteKey, language))) return + { + const ok = await confirmToast(t(config.confirmDeleteKey, language)) + if (!ok) return + } try { const updatedItems = @@ -374,7 +399,11 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { ) || [] const request = config.buildRequest(updatedItems) - await config.updateApi(request) + await toast.promise(config.updateApi(request), { + loading: '正在更新配置…', + success: '配置已更新', + error: '更新配置失败', + }) // 重新获取用户配置以确保数据同步 const refreshedItems = await config.refreshApi() @@ -383,7 +412,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { config.closeModal() } catch (error) { console.error(`Failed to delete ${config.type} config:`, error) - alert(t(config.errorKey, language)) + toast.error(t(config.errorKey, language)) } } @@ -445,7 +474,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { const modelToUpdate = existingModel || supportedModels?.find((m) => m.id === modelId) if (!modelToUpdate) { - alert(t('modelNotExist', language)) + toast.error(t('modelNotExist', language)) return } @@ -489,7 +518,11 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { ), } - await api.updateModelConfigs(request) + await toast.promise(api.updateModelConfigs(request), { + loading: '正在更新模型配置…', + success: '模型配置已更新', + error: '更新模型配置失败', + }) // 重新获取用户配置以确保数据同步 const refreshedModels = await api.getModelConfigs() @@ -499,7 +532,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { setEditingModel(null) } catch (error) { console.error('Failed to save model config:', error) - alert(t('saveConfigFailed', language)) + toast.error(t('saveConfigFailed', language)) } } @@ -569,7 +602,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { (e) => e.id === exchangeId ) if (!exchangeToUpdate) { - alert(t('exchangeNotExist', language)) + toast.error(t('exchangeNotExist', language)) return } @@ -629,7 +662,11 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { ), } - await api.updateExchangeConfigsEncrypted(request) + await toast.promise(api.updateExchangeConfigsEncrypted(request), { + loading: '正在更新交易所配置…', + success: '交易所配置已更新', + error: '更新交易所配置失败', + }) // 重新获取用户配置以确保数据同步 const refreshedExchanges = await api.getExchangeConfigs() @@ -639,7 +676,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { setEditingExchange(null) } catch (error) { console.error('Failed to save exchange config:', error) - alert(t('saveConfigFailed', language)) + toast.error(t('saveConfigFailed', language)) } } @@ -658,12 +695,16 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { oiTopUrl: string ) => { try { - await api.saveUserSignalSource(coinPoolUrl, oiTopUrl) + await toast.promise(api.saveUserSignalSource(coinPoolUrl, oiTopUrl), { + loading: '正在保存…', + success: '保存成功', + error: '保存失败', + }) setUserSignalSource({ coinPoolUrl, oiTopUrl }) setShowSignalSourceModal(false) } catch (error) { console.error('Failed to save signal source:', error) - alert(t('saveSignalSourceFailed', language)) + toast.error(t('saveSignalSourceFailed', language)) } } @@ -1025,9 +1066,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
{/* Status */}
-
+ {/*
{t('status', language)} -
+
*/}
- {/* Actions */} -
+ {/* Actions: 禁止换行,超出横向滚动 */} +
+ +
+
+
+ ) +} + +export default DevToastController diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx index e39731c1..02b7320f 100644 --- a/web/src/components/Header.tsx +++ b/web/src/components/Header.tsx @@ -1,5 +1,6 @@ import { useLanguage } from '../contexts/LanguageContext' import { t } from '../i18n/translations' +import { Container } from './Container' interface HeaderProps { simple?: boolean // For login/register pages @@ -10,7 +11,7 @@ export function Header({ simple = false }: HeaderProps) { return (
-
+
{/* Left - Logo and Title */}
@@ -58,7 +59,7 @@ export function Header({ simple = false }: HeaderProps) {
-
+
) } diff --git a/web/src/components/HeaderBar.tsx b/web/src/components/HeaderBar.tsx new file mode 100644 index 00000000..3c1870c4 --- /dev/null +++ b/web/src/components/HeaderBar.tsx @@ -0,0 +1,921 @@ +import { useState, useEffect, useRef } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { motion } from 'framer-motion' +import { Menu, X, ChevronDown } from 'lucide-react' +import { t, type Language } from '../i18n/translations' +import { Container } from './Container' + +interface HeaderBarProps { + onLoginClick?: () => void + isLoggedIn?: boolean + isHomePage?: boolean + currentPage?: string + language?: Language + onLanguageChange?: (lang: Language) => void + user?: { email: string } | null + onLogout?: () => void + onPageChange?: (page: string) => void +} + +export default function HeaderBar({ + isLoggedIn = false, + isHomePage = false, + currentPage, + language = 'zh' as Language, + onLanguageChange, + user, + onLogout, + onPageChange, +}: HeaderBarProps) { + const navigate = useNavigate() + const [mobileMenuOpen, setMobileMenuOpen] = useState(false) + const [languageDropdownOpen, setLanguageDropdownOpen] = useState(false) + const [userDropdownOpen, setUserDropdownOpen] = useState(false) + const dropdownRef = useRef(null) + const userDropdownRef = useRef(null) + + // Close dropdown when clicking outside + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setLanguageDropdownOpen(false) + } + if ( + userDropdownRef.current && + !userDropdownRef.current.contains(event.target as Node) + ) { + setUserDropdownOpen(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) + + return ( + + ) +} diff --git a/web/src/components/LoginPage.tsx b/web/src/components/LoginPage.tsx index abf63959..e498d91d 100644 --- a/web/src/components/LoginPage.tsx +++ b/web/src/components/LoginPage.tsx @@ -1,14 +1,16 @@ import React, { useState } from 'react' +import { useNavigate } from 'react-router-dom' import { useAuth } from '../contexts/AuthContext' import { useLanguage } from '../contexts/LanguageContext' import { t } from '../i18n/translations' -import HeaderBar from './landing/HeaderBar' import { Eye, EyeOff } from 'lucide-react' import { Input } from './ui/input' +import { toast } from 'sonner' export function LoginPage() { const { language } = useLanguage() const { login, loginAdmin, verifyOTP } = useAuth() + const navigate = useNavigate() const [step, setStep] = useState<'login' | 'otp'>('login') const [email, setEmail] = useState('') const [password, setPassword] = useState('') @@ -26,7 +28,9 @@ export function LoginPage() { setLoading(true) const result = await loginAdmin(adminPassword) if (!result.success) { - setError(result.message || t('loginFailed', language)) + const msg = result.message || t('loginFailed', language) + setError(msg) + toast.error(msg) } setLoading(false) } @@ -44,7 +48,9 @@ export function LoginPage() { setStep('otp') } } else { - setError(result.message || t('loginFailed', language)) + const msg = result.message || t('loginFailed', language) + setError(msg) + toast.error(msg) } setLoading(false) @@ -58,7 +64,9 @@ export function LoginPage() { const result = await verifyOTP(userID, otpCode) if (!result.success) { - setError(result.message || t('verificationFailed', language)) + const msg = result.message || t('verificationFailed', language) + setError(msg) + toast.error(msg) } // 成功的话AuthContext会自动处理登录状态 @@ -66,286 +74,259 @@ export function LoginPage() { } return ( -
- {}} - isLoggedIn={false} - isHomePage={false} - currentPage="login" - language={language} - onLanguageChange={() => {}} - onPageChange={(page) => { - console.log('LoginPage onPageChange called with:', page) - if (page === 'competition') { - window.location.href = '/competition' - } - }} - /> - -
-
- {/* Logo */} -
-
- NoFx Logo -
-

- 登录 NOFX -

-

- {step === 'login' ? '请输入您的邮箱和密码' : '请输入两步验证码'} -

+
+
+ {/* Logo */} +
+
+ NoFx Logo
- - {/* Login Form */} -
- {adminMode ? ( -
-
- - setAdminPassword(e.target.value)} - className="w-full px-3 py-2 rounded" - style={{ - background: 'var(--brand-black)', - border: '1px solid var(--panel-border)', - color: 'var(--brand-light-gray)', - }} - placeholder="请输入管理员密码" - required - /> -
+ 登录 NOFX + +

+ {step === 'login' ? '请输入您的邮箱和密码' : '请输入两步验证码'} +

+
- {error && ( -
- {error} -
- )} - - - - ) : step === 'login' ? ( -
-
- + {error} +
+ )} + + +
+ ) : step === 'login' ? ( +
+
+ + setEmail(e.target.value)} + placeholder={t('emailPlaceholder', language)} + required + /> +
+ +
+ +
setEmail(e.target.value)} - placeholder={t('emailPlaceholder', language)} + type={showPassword ? 'text' : 'password'} + value={password} + onChange={(e) => setPassword(e.target.value)} + className="pr-10" + placeholder={t('passwordPlaceholder', language)} required /> -
- -
- -
- setPassword(e.target.value)} - className="pr-10" - placeholder={t('passwordPlaceholder', language)} - required - /> - -
-
- -
-
- - {error && ( -
- {error} -
- )} - - - - ) : ( -
-
-
📱
-

- {t('scanQRCodeInstructions', language)} -
- {t('enterOTPCode', language)} -

-
- -
- - - setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6)) - } - className="w-full px-3 py-2 rounded text-center text-2xl font-mono" - style={{ - background: 'var(--brand-black)', - border: '1px solid var(--panel-border)', - color: 'var(--brand-light-gray)', - }} - placeholder={t('otpPlaceholder', language)} - maxLength={6} - required - /> -
- - {error && ( -
- {error} -
- )} - -
-
-
- )} -
+
+ +
+
- {/* Register Link */} - {!adminMode && ( -
-

- 还没有账户?{' '} - + + ) : ( +

+
+
📱
+

+ {t('scanQRCodeInstructions', language)} +
+ {t('enterOTPCode', language)} +

+
+ +
+ + + setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6)) + } + className="w-full px-3 py-2 rounded text-center text-2xl font-mono" + style={{ + background: 'var(--brand-black)', + border: '1px solid var(--panel-border)', + color: 'var(--brand-light-gray)', + }} + placeholder={t('otpPlaceholder', language)} + maxLength={6} + required + /> +
+ + {error && ( +
+ {error} +
+ )} + +
+ -

-
+ +
+ )}
+ + {/* Register Link */} + {!adminMode && ( +
+

+ 还没有账户?{' '} + +

+
+ )}
) diff --git a/web/src/components/RegisterPage.tsx b/web/src/components/RegisterPage.tsx index 4c1b6275..c7b1c451 100644 --- a/web/src/components/RegisterPage.tsx +++ b/web/src/components/RegisterPage.tsx @@ -1,9 +1,11 @@ import React, { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' import { useAuth } from '../contexts/AuthContext' import { useLanguage } from '../contexts/LanguageContext' import { t } from '../i18n/translations' import { getSystemConfig } from '../lib/config' -import HeaderBar from './landing/HeaderBar' +import { toast } from 'sonner' +import { copyWithToast } from '../lib/clipboard' import { Eye, EyeOff } from 'lucide-react' import { Input } from './ui/input' import PasswordChecklist from 'react-password-checklist' @@ -11,6 +13,7 @@ import PasswordChecklist from 'react-password-checklist' export function RegisterPage() { const { language } = useLanguage() const { register, completeRegistration } = useAuth() + const navigate = useNavigate() const [step, setStep] = useState<'register' | 'setup-otp' | 'verify-otp'>( 'register' ) @@ -66,7 +69,9 @@ export function RegisterPage() { setQrCodeURL(result.qrCodeURL || '') setStep('setup-otp') } else { - setError(result.message || t('registrationFailed', language)) + const msg = result.message || t('registrationFailed', language) + setError(msg) + toast.error(msg) } setLoading(false) @@ -84,7 +89,9 @@ export function RegisterPage() { const result = await completeRegistration(userID, otpCode) if (!result.success) { - setError(result.message || t('registrationFailed', language)) + const msg = result.message || t('registrationFailed', language) + setError(msg) + toast.error(msg) } // 成功的话AuthContext会自动处理登录状态 @@ -92,141 +99,197 @@ export function RegisterPage() { } const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text) + copyWithToast(text) } return ( -
- {}} - onPageChange={(page) => { - console.log('RegisterPage onPageChange called with:', page) - if (page === 'competition') { - window.location.href = '/competition' - } - }} - /> - -
-
- {/* Logo */} -
-
- NoFx Logo -
-

- {t('appTitle', language)} -

-

- {step === 'register' && t('registerTitle', language)} - {step === 'setup-otp' && t('setupTwoFactor', language)} - {step === 'verify-otp' && t('verifyOTP', language)} -

+
+
+ {/* Logo */} +
+
+ NoFx Logo
+

+ {t('appTitle', language)} +

+

+ {step === 'register' && t('registerTitle', language)} + {step === 'setup-otp' && t('setupTwoFactor', language)} + {step === 'verify-otp' && t('verifyOTP', language)} +

+
- {/* Registration Form */} -
- {step === 'register' && ( -
-
- + {/* Registration Form */} +
+ {step === 'register' && ( + +
+ + setEmail(e.target.value)} + placeholder={t('emailPlaceholder', language)} + required + /> +
+ +
+ +
setEmail(e.target.value)} - placeholder={t('emailPlaceholder', language)} + type={showPassword ? 'text' : 'password'} + value={password} + onChange={(e) => setPassword(e.target.value)} + className="pr-10" + placeholder={t('passwordPlaceholder', language)} required /> -
- -
- -
- setPassword(e.target.value)} - className="pr-10" - placeholder={t('passwordPlaceholder', language)} - required - /> - -
+ {showPassword ? : } +
+
-
-
+
- {/* 密码规则清单(通过才允许提交) */} + {/* 密码规则清单(通过才允许提交) */} +
+ {t('passwordRequirements', language)} +
+ setPasswordValid(isValid)} + /> +
+ + {betaMode && ( +
+ + + setBetaCode( + e.target.value.replace(/[^a-z0-9]/gi, '').toLowerCase() + ) + } + className="w-full px-3 py-2 rounded font-mono" + style={{ + background: '#0B0E11', + border: '1px solid #2B3139', + color: '#EAECEF', + }} + placeholder="请输入6位内测码" + maxLength={6} + required={betaMode} + /> +

+ 内测码由6位字母数字组成,区分大小写 +

+
+ )} + + {error && ( +
setPasswordValid(isValid)} />
+ )} - {betaMode && ( -
- - - setBetaCode( - e.target.value - .replace(/[^a-z0-9]/gi, '') - .toLowerCase() - ) - } - className="w-full px-3 py-2 rounded font-mono" - style={{ - background: '#0B0E11', - border: '1px solid #2B3139', - color: '#EAECEF', - }} - placeholder="请输入6位内测码" - maxLength={6} - required={betaMode} - /> -

- 内测码由6位字母数字组成,区分大小写 -

-
- )} + + + )} - {error && ( -
+
+
📱
+

+ {t('setupTwoFactor', language)} +

+

+ {t('setupTwoFactorDesc', language)} +

+
+ +
+
+

- {error} -

- )} + {t('authStep1Title', language)} +

+

+ {t('authStep1Desc', language)} +

+
+
+

+ {t('authStep2Title', language)} +

+

+ {t('authStep2Desc', language)} +

+ + {qrCodeURL && ( +
+

+ {t('qrCodeHint', language)} +

+
+ QR Code +
+
+ )} + +
+

+ {t('otpSecret', language)} +

+
+ + {otpSecret} + + +
+
+
+ +
+

+ {t('authStep3Title', language)} +

+

+ {t('authStep3Desc', language)} +

+
+
+ + +
+ )} + + {step === 'verify-otp' && ( +
+
+
🔐
+

+ {t('enterOTPCode', language)} +
+ {t('completeRegistrationSubtitle', language)} +

+
+ +
+ + + setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6)) + } + className="w-full px-3 py-2 rounded text-center text-2xl font-mono" + style={{ + background: 'var(--brand-black)', + border: '1px solid var(--panel-border)', + color: 'var(--brand-light-gray)', + }} + placeholder={t('otpPlaceholder', language)} + maxLength={6} + required + /> +
+ + {error && ( +
+ {error} +
+ )} + +
+ - - )} - - {step === 'setup-otp' && ( -
-
-
📱
-

- {t('setupTwoFactor', language)} -

-

- {t('setupTwoFactorDesc', language)} -

-
- -
-
-

- {t('authStep1Title', language)} -

-

- {t('authStep1Desc', language)} -

-
- -
-

- {t('authStep2Title', language)} -

-

- {t('authStep2Desc', language)} -

- - {qrCodeURL && ( -
-

- {t('qrCodeHint', language)} -

-
- QR Code -
-
- )} - -
-

- {t('otpSecret', language)} -

-
- - {otpSecret} - - -
-
-
- -
-

- {t('authStep3Title', language)} -

-

- {t('authStep3Desc', language)} -

-
-
- -
- )} - - {step === 'verify-otp' && ( -
-
-
🔐
-

- {t('enterOTPCode', language)} -
- {t('completeRegistrationSubtitle', language)} -

-
- -
- - - setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6)) - } - className="w-full px-3 py-2 rounded text-center text-2xl font-mono" - style={{ - background: 'var(--brand-black)', - border: '1px solid var(--panel-border)', - color: 'var(--brand-light-gray)', - }} - placeholder={t('otpPlaceholder', language)} - maxLength={6} - required - /> -
- - {error && ( -
- {error} -
- )} - -
- - -
-
- )} -
- - {/* Login Link */} - {step === 'register' && ( -
-

- 已有账户?{' '} - -

-
+ )}
+ + {/* Login Link */} + {step === 'register' && ( +
+

+ 已有账户?{' '} + +

+
+ )}
) diff --git a/web/src/components/ResetPasswordPage.tsx b/web/src/components/ResetPasswordPage.tsx index 6cf2cef5..2504c9c8 100644 --- a/web/src/components/ResetPasswordPage.tsx +++ b/web/src/components/ResetPasswordPage.tsx @@ -6,6 +6,7 @@ import { Header } from './Header' import { ArrowLeft, KeyRound, Eye, EyeOff } from 'lucide-react' import PasswordChecklist from 'react-password-checklist' import { Input } from './ui/input' +import { toast } from 'sonner' export function ResetPasswordPage() { const { language } = useLanguage() @@ -38,13 +39,16 @@ export function ResetPasswordPage() { if (result.success) { setSuccess(true) + toast.success(t('resetPasswordSuccess', language) || '重置成功') // 3秒后跳转到登录页面 setTimeout(() => { window.history.pushState({}, '', '/login') window.dispatchEvent(new PopStateEvent('popstate')) }, 3000) } else { - setError(result.message || t('resetPasswordFailed', language)) + const msg = result.message || t('resetPasswordFailed', language) + setError(msg) + toast.error(msg) } setLoading(false) diff --git a/web/src/components/TraderConfigModal.tsx b/web/src/components/TraderConfigModal.tsx index db343285..f0655213 100644 --- a/web/src/components/TraderConfigModal.tsx +++ b/web/src/components/TraderConfigModal.tsx @@ -2,6 +2,8 @@ import { useState, useEffect } from 'react' import type { AIModel, Exchange, CreateTraderRequest } from '../types' import { useLanguage } from '../contexts/LanguageContext' import { t } from '../i18n/translations' +import { toast } from 'sonner' +import { Pencil, Plus, X as IconX } from 'lucide-react' // 提取下划线后面的名称部分 function getShortName(fullName: string): string { @@ -217,12 +219,11 @@ export function TraderConfigModal({ const currentBalance = data.total_equity || data.balance || 0 setFormData((prev) => ({ ...prev, initial_balance: currentBalance })) - - // 显示成功提示 - console.log('已获取当前余额:', currentBalance) + toast.success('已获取当前余额') } catch (error) { console.error('获取余额失败:', error) setBalanceFetchError('获取余额失败,请检查网络连接') + toast.error('获取余额失败,请检查网络连接') } finally { setIsFetchingBalance(false) } @@ -249,7 +250,11 @@ export function TraderConfigModal({ initial_balance: formData.initial_balance, scan_interval_minutes: formData.scan_interval_minutes, } - await onSave(saveData) + await toast.promise(onSave(saveData), { + loading: '正在保存…', + success: '保存成功', + error: '保存失败', + }) onClose() } catch (error) { console.error('保存失败:', error) @@ -268,8 +273,12 @@ export function TraderConfigModal({ {/* Header */}
-
- {isEditMode ? '✏️' : '➕'} +
+ {isEditMode ? ( + + ) : ( + + )}

@@ -284,7 +293,7 @@ export function TraderConfigModal({ onClick={onClose} className="w-8 h-8 rounded-lg text-[#848E9C] hover:text-[#EAECEF] hover:bg-[#2B3139] transition-colors flex items-center justify-center" > - ✕ +

diff --git a/web/src/components/TraderConfigViewModal.tsx b/web/src/components/TraderConfigViewModal.tsx index febf115b..3df872fe 100644 --- a/web/src/components/TraderConfigViewModal.tsx +++ b/web/src/components/TraderConfigViewModal.tsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import { toast } from 'sonner' import type { TraderConfigData } from '../types' // 提取下划线后面的名称部分 @@ -27,8 +28,10 @@ export function TraderConfigViewModal({ await navigator.clipboard.writeText(text) setCopiedField(fieldName) setTimeout(() => setCopiedField(null), 2000) + toast.success('已复制到剪贴板') } catch (error) { console.error('Failed to copy:', error) + toast.error('复制失败,请手动复制') } } diff --git a/web/src/components/TwoStageKeyModal.tsx b/web/src/components/TwoStageKeyModal.tsx index 82d8a8f0..97e856dd 100644 --- a/web/src/components/TwoStageKeyModal.tsx +++ b/web/src/components/TwoStageKeyModal.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { t, type Language } from '../i18n/translations' +import { toast } from 'sonner' const DEFAULT_LENGTH = 64 @@ -99,12 +100,14 @@ export function TwoStageKeyModal({ ...obfuscationLog, `Stage 1: ${new Date().toISOString()} - Auto copied obfuscation`, ]) + toast.success('已复制混淆字符串到剪贴板') } catch { setClipboardStatus('failed') setObfuscationLog([ ...obfuscationLog, `Stage 1: ${new Date().toISOString()} - Auto copy failed, manual required`, ]) + toast.error('复制失败,请手动复制混淆字符串') } } else { setClipboardStatus('failed') @@ -112,6 +115,7 @@ export function TwoStageKeyModal({ ...obfuscationLog, `Stage 1: ${new Date().toISOString()} - Clipboard API not available`, ]) + toast('当前浏览器不支持自动复制,请手动复制') } setTimeout(() => { diff --git a/web/src/components/faq/FAQLayout.tsx b/web/src/components/faq/FAQLayout.tsx index b4388427..a2367fcf 100644 --- a/web/src/components/faq/FAQLayout.tsx +++ b/web/src/components/faq/FAQLayout.tsx @@ -1,5 +1,6 @@ import { useState, useMemo } from 'react' import { HelpCircle } from 'lucide-react' +import { Container } from '../Container' import { t, type Language } from '../../i18n/translations' import { FAQSearchBar } from './FAQSearchBar' import { FAQSidebar } from './FAQSidebar' @@ -57,7 +58,7 @@ export function FAQLayout({ language }: FAQLayoutProps) { } return ( -
+ {/* Page Header */}
@@ -176,6 +177,6 @@ export function FAQLayout({ language }: FAQLayoutProps) {
-
+ ) } diff --git a/web/src/components/landing/HeaderBar.tsx b/web/src/components/landing/HeaderBar.tsx deleted file mode 100644 index 527891c4..00000000 --- a/web/src/components/landing/HeaderBar.tsx +++ /dev/null @@ -1,932 +0,0 @@ -import { useState, useEffect, useRef } from 'react' -import { motion } from 'framer-motion' -import { Menu, X, ChevronDown } from 'lucide-react' -import { t, type Language } from '../../i18n/translations' - -interface HeaderBarProps { - onLoginClick?: () => void - isLoggedIn?: boolean - isHomePage?: boolean - currentPage?: string - language?: Language - onLanguageChange?: (lang: Language) => void - user?: { email: string } | null - onLogout?: () => void - onPageChange?: (page: string) => void -} - -export default function HeaderBar({ - isLoggedIn = false, - isHomePage = false, - currentPage, - language = 'zh' as Language, - onLanguageChange, - user, - onLogout, - onPageChange, -}: HeaderBarProps) { - const [mobileMenuOpen, setMobileMenuOpen] = useState(false) - const [languageDropdownOpen, setLanguageDropdownOpen] = useState(false) - const [userDropdownOpen, setUserDropdownOpen] = useState(false) - const dropdownRef = useRef(null) - const userDropdownRef = useRef(null) - - // Close dropdown when clicking outside - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if ( - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) - ) { - setLanguageDropdownOpen(false) - } - if ( - userDropdownRef.current && - !userDropdownRef.current.contains(event.target as Node) - ) { - setUserDropdownOpen(false) - } - } - - document.addEventListener('mousedown', handleClickOutside) - return () => { - document.removeEventListener('mousedown', handleClickOutside) - } - }, []) - - return ( - - ) -} diff --git a/web/src/components/ui/alert-dialog.tsx b/web/src/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..75d9fe26 --- /dev/null +++ b/web/src/components/ui/alert-dialog.tsx @@ -0,0 +1,142 @@ +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' +import { cn } from '../../lib/cn' + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = 'AlertDialogHeader' + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = 'AlertDialogFooter' + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/web/src/index.css b/web/src/index.css index f2f7e744..7028a6bd 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -200,6 +200,69 @@ body { border-bottom: 1px solid var(--panel-border); } +/* Sonner (toast) - Binance theme overrides */ +.sonner-toaster { + z-index: 9999; +} + +.nofx-toast { + background: #0b0e11 !important; + border: 1px solid var(--panel-border) !important; + color: var(--text-primary) !important; + box-shadow: var(--shadow-lg) !important; + border-radius: 6px !important; +} + +.nofx-toast .sonner-title { + color: var(--text-primary) !important; + font-weight: 700; +} + +.nofx-toast .sonner-description { + color: var(--text-secondary) !important; +} + +/* Success / Error / Warning tint */ +.nofx-toast[data-type='success'] { + background: #0b0e11 !important; + border-color: var(--binance-green) !important; + border-left: 3px solid var(--binance-green) !important; +} +.nofx-toast[data-type='success'] .sonner-title, +.nofx-toast[data-type='success'] .sonner-description { + color: var(--binance-green) !important; +} + +.nofx-toast[data-type='error'] { + background: #0b0e11 !important; + border-color: var(--binance-red) !important; + border-left: 3px solid var(--binance-red) !important; +} +.nofx-toast[data-type='error'] .sonner-title, +.nofx-toast[data-type='error'] .sonner-description { + color: var(--binance-red) !important; +} + +.nofx-toast[data-type='warning'], +.nofx-toast[data-type='info'] { + background: #0b0e11 !important; + border-color: var(--binance-yellow) !important; + border-left: 3px solid var(--binance-yellow) !important; +} +.nofx-toast[data-type='warning'] .sonner-title, +.nofx-toast[data-type='warning'] .sonner-description, +.nofx-toast[data-type='info'] .sonner-title, +.nofx-toast[data-type='info'] .sonner-description { + color: var(--binance-yellow) !important; +} + +.nofx-toast .sonner-close-button { + color: var(--text-secondary) !important; +} +.nofx-toast .sonner-close-button:hover { + color: var(--text-primary) !important; +} + /* Monospace numbers */ .mono { font-family: 'IBM Plex Mono', 'Courier New', monospace; @@ -235,6 +298,113 @@ button:disabled { box-shadow: var(--shadow-sm); } +.dev-toast-controller { + position: fixed; + right: 18px; + bottom: 18px; + width: min(320px, 85vw); + background: rgba(11, 14, 17, 0.9); + border: 1px solid var(--panel-border); + border-radius: 12px; + padding: 16px; + color: var(--text-secondary); + box-shadow: 0 25px 60px rgba(0, 0, 0, 0.65); + backdrop-filter: blur(16px); + font-size: 0.85rem; + z-index: 9999; +} + +.dev-toast-controller__header { + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 12px; +} + +.dev-toast-controller__header small { + font-size: 0.7rem; + color: var(--text-tertiary); +} + +.dev-toast-controller__content { + display: flex; + flex-direction: column; + gap: 10px; +} + +.dev-toast-controller__label { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 0.8rem; + color: var(--text-secondary); +} + +.dev-toast-controller__label select, +.dev-toast-controller__label input { + width: 100%; + border: 1px solid var(--panel-border); + border-radius: 6px; + padding: 6px 10px; + background: var(--panel-bg); + color: var(--text-primary); + font-size: 0.9rem; +} + +.dev-toast-controller__actions { + display: flex; + gap: 8px; + justify-content: space-between; +} + +.dev-toast-controller__actions button { + flex: 1; + cursor: pointer; + border-radius: 999px; + padding: 8px 10px; + border: none; + font-weight: 600; + font-size: 0.85rem; + transition: transform 0.2s ease; +} + +.dev-toast-controller__actions button:first-child { + background: rgba(240, 185, 11, 0.15); + color: var(--binance-yellow); + border: 1px solid rgba(240, 185, 11, 0.4); +} + +.dev-toast-controller__actions button:last-child { + background: rgba(132, 142, 156, 0.15); + color: var(--text-secondary); + border: 1px solid var(--panel-border); +} + +.dev-toast-controller__actions button:hover:not(:disabled) { + transform: translateY(-1px); +} + +.dev-custom-toast { + padding: 12px 18px; + border-radius: 12px; + background: linear-gradient(135deg, #f0b90b, #df8c0c); + color: #0a0a0a; + font-weight: 600; +} + +.dev-custom-title { + margin: 0; + font-size: 1rem; +} + +.dev-custom-body { + margin: 0; + font-size: 0.85rem; + opacity: 0.8; +} + .binance-card:hover { border-color: var(--panel-border-hover); box-shadow: var(--shadow-md); diff --git a/web/src/layouts/AuthLayout.tsx b/web/src/layouts/AuthLayout.tsx new file mode 100644 index 00000000..b86bf270 --- /dev/null +++ b/web/src/layouts/AuthLayout.tsx @@ -0,0 +1,56 @@ +import { ReactNode } from 'react' +import { Outlet, Link } from 'react-router-dom' +import { Container } from '../components/Container' +import { useLanguage } from '../contexts/LanguageContext' + +interface AuthLayoutProps { + children?: ReactNode +} + +export default function AuthLayout({ children }: AuthLayoutProps) { + const { language, setLanguage } = useLanguage() + + return ( +
+ {/* Simple Header with Logo and Language Selector */} + + + {/* Content with top padding to avoid overlap with fixed header */} +
{children || }
+
+ ) +} diff --git a/web/src/layouts/MainLayout.tsx b/web/src/layouts/MainLayout.tsx new file mode 100644 index 00000000..244025a9 --- /dev/null +++ b/web/src/layouts/MainLayout.tsx @@ -0,0 +1,97 @@ +import { ReactNode } from 'react' +import { Outlet, useLocation } from 'react-router-dom' +import HeaderBar from '../components/HeaderBar' +import { Container } from '../components/Container' +import { useLanguage } from '../contexts/LanguageContext' +import { useAuth } from '../contexts/AuthContext' +import { t } from '../i18n/translations' + +interface MainLayoutProps { + children?: ReactNode +} + +export default function MainLayout({ children }: MainLayoutProps) { + const { language, setLanguage } = useLanguage() + const { user, logout } = useAuth() + const location = useLocation() + + // 根据路径自动判断当前页面 + const getCurrentPage = (): 'competition' | 'traders' | 'trader' | 'faq' => { + if (location.pathname === '/faq') return 'faq' + if (location.pathname === '/traders') return 'traders' + if (location.pathname === '/dashboard') return 'trader' + if (location.pathname === '/competition') return 'competition' + return 'competition' // 默认 + } + + return ( + + ) +} diff --git a/web/src/lib/clipboard.ts b/web/src/lib/clipboard.ts new file mode 100644 index 00000000..1a95cef3 --- /dev/null +++ b/web/src/lib/clipboard.ts @@ -0,0 +1,30 @@ +import { notify } from './notify' + +/** + * 复制文本到剪贴板,并显示轻量提示。 + */ +export async function copyWithToast(text: string, successMsg = '已复制') { + try { + if (navigator?.clipboard?.writeText) { + await navigator.clipboard.writeText(text) + } else { + // 兼容降级:创建临时文本域执行复制 + const el = document.createElement('textarea') + el.value = text + el.style.position = 'fixed' + el.style.left = '-9999px' + document.body.appendChild(el) + el.select() + document.execCommand('copy') + document.body.removeChild(el) + } + notify.success(successMsg) + return true + } catch (err) { + console.error('Clipboard copy failed:', err) + notify.error('复制失败') + return false + } +} + +export default { copyWithToast } diff --git a/web/src/lib/httpClient.ts b/web/src/lib/httpClient.ts index 9097c416..15ebc16c 100644 --- a/web/src/lib/httpClient.ts +++ b/web/src/lib/httpClient.ts @@ -8,6 +8,8 @@ * - Automatic redirect to login page */ +import { toast } from 'sonner' + export class HttpClient { // Singleton flag to prevent duplicate 401 handling private static isHandling401 = false @@ -23,52 +25,7 @@ export class HttpClient { * Show login required notification to user */ private showLoginRequiredNotification(): void { - // Create notification element - const notification = document.createElement('div') - notification.style.cssText = ` - position: fixed; - top: 20px; - left: 50%; - transform: translateX(-50%); - background: linear-gradient(135deg, #F0B90B 0%, #FCD535 100%); - color: #0B0E11; - padding: 16px 24px; - border-radius: 8px; - font-size: 16px; - font-weight: bold; - z-index: 10000; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - animation: slideDown 0.3s ease-out; - ` - notification.textContent = '⚠️ 登录已过期,请先登录' - - // Add slide down animation - const style = document.createElement('style') - style.textContent = ` - @keyframes slideDown { - from { - opacity: 0; - transform: translateX(-50%) translateY(-20px); - } - to { - opacity: 1; - transform: translateX(-50%) translateY(0); - } - } - ` - document.head.appendChild(style) - - // Add to page - document.body.appendChild(notification) - - // Auto remove after animation - setTimeout(() => { - notification.style.animation = 'slideDown 0.3s ease-out reverse' - setTimeout(() => { - document.body.removeChild(notification) - document.head.removeChild(style) - }, 300) - }, 1800) + toast.warning('登录已过期,请先登录', { duration: 1800 }) } /** diff --git a/web/src/lib/notify.tsx b/web/src/lib/notify.tsx new file mode 100644 index 00000000..5589d3c8 --- /dev/null +++ b/web/src/lib/notify.tsx @@ -0,0 +1,87 @@ +import { toast } from 'sonner' +import type { ReactNode } from 'react' + +export interface ConfirmOptions { + title?: string + message?: string + okText?: string + cancelText?: string +} + +// 全局 confirm 函数的引用,将在 ConfirmDialogProvider 中设置 +let globalConfirm: + | ((options: ConfirmOptions & { message: string }) => Promise) + | null = null + +export function setGlobalConfirm( + confirmFn: (options: ConfirmOptions & { message: string }) => Promise +) { + globalConfirm = confirmFn +} + +// 确认对话框函数,使用 shadcn AlertDialog +export function confirmToast( + message: string, + options: ConfirmOptions = {} +): Promise { + if (!globalConfirm) { + console.error('ConfirmDialogProvider not initialized') + return Promise.resolve(false) + } + + return globalConfirm({ + message, + ...options, + }) +} + +// 统一通知封装,避免组件直接依赖 sonner +type Message = string | ReactNode + +function message(msg: Message, options?: Parameters[1]) { + return toast(msg as any, options) +} + +function success(msg: Message, options?: Parameters[1]) { + return toast.success(msg as any, options) +} + +function error(msg: Message, options?: Parameters[1]) { + return toast.error(msg as any, options) +} + +function info(msg: Message, options?: Parameters[1]) { + return toast.info?.(msg as any, options) ?? toast(msg as any, options) +} + +function warning(msg: Message, options?: Parameters[1]) { + return toast.warning?.(msg as any, options) ?? toast(msg as any, options) +} + +function custom( + renderer: Parameters[0], + options?: Parameters[1] +) { + return toast.custom(renderer, options) +} + +function dismiss(id?: string | number) { + return toast.dismiss(id as any) +} + +function promise(p: Promise | (() => Promise), msgs: any) { + return toast.promise(p as any, msgs as any) +} + +export const notify = { + message, + success, + error, + info, + warning, + custom, + dismiss, + promise, +} + +export default { confirmToast, notify } diff --git a/web/src/lib/text.ts b/web/src/lib/text.ts new file mode 100644 index 00000000..f8fb5487 --- /dev/null +++ b/web/src/lib/text.ts @@ -0,0 +1,28 @@ +/** + * 文本工具 + * + * stripLeadingIcons: 去掉翻译文案或标题前面用于装饰的 Emoji/符号, + * 以便在组件里自行放置图标时不重复显示。 + */ + +/** + * 去掉开头的装饰性 Emoji/符号以及随后的分隔符(空格/冒号/点号等)。 + */ +export function stripLeadingIcons(input: string | undefined | null): string { + if (!input) return '' + let s = String(input) + + // 1) 去除常见的 Emoji/符号块(箭头、杂项符号、几何图形、表情等) + // 覆盖常见范围,兼容性好于使用 Unicode 属性类。 + s = s.replace( + /^[\s\u2190-\u21FF\u2300-\u23FF\u2460-\u24FF\u25A0-\u25FF\u2600-\u27BF\u2B00-\u2BFF\u1F000-\u1FAFF]+/u, + '' + ) + + // 2) 去掉开头可能残留的分隔符(空格、连字符、冒号、居中点等) + s = s.replace(/^[\s\-:•·]+/, '') + + return s.trim() +} + +export default { stripLeadingIcons } diff --git a/web/src/main.tsx b/web/src/main.tsx index c4fc9bba..2bed5575 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,10 +1,26 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.tsx' +import { Toaster } from 'sonner' import './index.css' ReactDOM.createRoot(document.getElementById('root')!).render( + ) diff --git a/web/src/pages/FAQPage.tsx b/web/src/pages/FAQPage.tsx index bd74f5c2..c766230b 100644 --- a/web/src/pages/FAQPage.tsx +++ b/web/src/pages/FAQPage.tsx @@ -1,17 +1,10 @@ -import HeaderBar from '../components/landing/HeaderBar' import { FAQLayout } from '../components/faq/FAQLayout' import { useLanguage } from '../contexts/LanguageContext' -import { useAuth } from '../contexts/AuthContext' -import { useSystemConfig } from '../hooks/useSystemConfig' -import { t } from '../i18n/translations' /** * FAQ 页面 * - * 这个页面只是组件的集合,负责: - * - 组装 HeaderBar 和 FAQLayout - * - 提供全局状态(语言、用户、系统配置) - * - 处理页面级别的导航 + * HeaderBar 和 Footer 现在由 MainLayout 提供 * * 所有 FAQ 相关的逻辑都在子组件中: * - FAQLayout: 整体布局和搜索逻辑 @@ -22,54 +15,7 @@ import { t } from '../i18n/translations' * FAQ 数据配置在 data/faqData.ts */ export function FAQPage() { - const { language, setLanguage } = useLanguage() - const { user, logout } = useAuth() - useSystemConfig() // Load system config but don't use it + const { language } = useLanguage() - return ( -
- { - if (page === 'competition') { - window.history.pushState({}, '', '/competition') - window.location.href = '/competition' - } else if (page === 'traders') { - window.history.pushState({}, '', '/traders') - window.location.href = '/traders' - } else if (page === 'trader') { - window.history.pushState({}, '', '/dashboard') - window.location.href = '/dashboard' - } else if (page === 'faq') { - window.history.pushState({}, '', '/faq') - window.location.href = '/faq' - } - }} - /> - - - - {/* Footer */} -
-
-

{t('footerTitle', language)}

-

{t('footerWarning', language)}

-
-
-
- ) + return } diff --git a/web/src/pages/LandingPage.tsx b/web/src/pages/LandingPage.tsx index 4135ee60..f53a1cd5 100644 --- a/web/src/pages/LandingPage.tsx +++ b/web/src/pages/LandingPage.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { motion } from 'framer-motion' import { ArrowRight } from 'lucide-react' -import HeaderBar from '../components/landing/HeaderBar' +import HeaderBar from '../components/HeaderBar' import HeroSection from '../components/landing/HeroSection' import AboutSection from '../components/landing/AboutSection' import FeaturesSection from '../components/landing/FeaturesSection' diff --git a/web/src/pages/TraderDashboard.tsx b/web/src/pages/TraderDashboard.tsx new file mode 100644 index 00000000..9a0a3cc4 --- /dev/null +++ b/web/src/pages/TraderDashboard.tsx @@ -0,0 +1,942 @@ +import { useEffect, useState } from 'react' +import { useNavigate, useSearchParams } from 'react-router-dom' +import useSWR from 'swr' +import { api } from '../lib/api' +import { EquityChart } from '../components/EquityChart' +import AILearning from '../components/AILearning' +import { useLanguage } from '../contexts/LanguageContext' +import { useAuth } from '../contexts/AuthContext' +import { t, type Language } from '../i18n/translations' +import { + AlertTriangle, + Bot, + Brain, + RefreshCw, + TrendingUp, + PieChart, + Inbox, + Send, + Check, + X, + XCircle, +} from 'lucide-react' +import { stripLeadingIcons } from '../lib/text' +import type { + SystemStatus, + AccountInfo, + Position, + DecisionRecord, + Statistics, + TraderInfo, +} from '../types' + +// 获取友好的AI模型名称 +function getModelDisplayName(modelId: string): string { + switch (modelId.toLowerCase()) { + case 'deepseek': + return 'DeepSeek' + case 'qwen': + return 'Qwen' + case 'claude': + return 'Claude' + default: + return modelId.toUpperCase() + } +} + +export default function TraderDashboard() { + const { language } = useLanguage() + const { user, token } = useAuth() + const navigate = useNavigate() + const [searchParams, setSearchParams] = useSearchParams() + const [selectedTraderId, setSelectedTraderId] = useState( + searchParams.get('trader') || undefined + ) + const [lastUpdate, setLastUpdate] = useState('--:--:--') + + // 获取trader列表(仅在用户登录时) + const { data: traders, error: tradersError } = useSWR( + user && token ? 'traders' : null, + api.getTraders, + { + refreshInterval: 10000, + shouldRetryOnError: false, + } + ) + + // 当获取到traders后,设置默认选中第一个 + useEffect(() => { + if (traders && traders.length > 0 && !selectedTraderId) { + const firstTraderId = traders[0].trader_id + setSelectedTraderId(firstTraderId) + setSearchParams({ trader: firstTraderId }) + } + }, [traders, selectedTraderId, setSearchParams]) + + // 更新URL参数 + const handleTraderSelect = (traderId: string) => { + setSelectedTraderId(traderId) + setSearchParams({ trader: traderId }) + } + + // 如果在trader页面,获取该trader的数据 + const { data: status } = useSWR( + selectedTraderId ? `status-${selectedTraderId}` : null, + () => api.getStatus(selectedTraderId), + { + refreshInterval: 15000, + revalidateOnFocus: false, + dedupingInterval: 10000, + } + ) + + const { data: account } = useSWR( + selectedTraderId ? `account-${selectedTraderId}` : null, + () => api.getAccount(selectedTraderId), + { + refreshInterval: 15000, + revalidateOnFocus: false, + dedupingInterval: 10000, + } + ) + + const { data: positions } = useSWR( + selectedTraderId ? `positions-${selectedTraderId}` : null, + () => api.getPositions(selectedTraderId), + { + refreshInterval: 15000, + revalidateOnFocus: false, + dedupingInterval: 10000, + } + ) + + const { data: decisions } = useSWR( + selectedTraderId ? `decisions/latest-${selectedTraderId}` : null, + () => api.getLatestDecisions(selectedTraderId), + { + refreshInterval: 30000, + revalidateOnFocus: false, + dedupingInterval: 20000, + } + ) + + const { data: stats } = useSWR( + selectedTraderId ? `statistics-${selectedTraderId}` : null, + () => api.getStatistics(selectedTraderId), + { + refreshInterval: 30000, + revalidateOnFocus: false, + dedupingInterval: 20000, + } + ) + + // Avoid unused variable warning + void stats + + useEffect(() => { + if (account) { + const now = new Date().toLocaleTimeString() + setLastUpdate(now) + } + }, [account]) + + const selectedTrader = traders?.find((t) => t.trader_id === selectedTraderId) + + // If API failed with error, show empty state + if (tradersError) { + return ( +
+
+
+ + + +
+

+ {t('dashboardEmptyTitle', language)} +

+

+ {t('dashboardEmptyDescription', language)} +

+ +
+
+ ) + } + + // If traders is loaded and empty, show empty state + if (traders && traders.length === 0) { + return ( +
+
+
+ + + +
+

+ {t('dashboardEmptyTitle', language)} +

+

+ {t('dashboardEmptyDescription', language)} +

+ +
+
+ ) + } + + // If traders is still loading or selectedTrader is not ready, show skeleton + if (!selectedTrader) { + return ( +
+
+
+
+
+
+
+
+
+
+ {[1, 2, 3, 4].map((i) => ( +
+
+
+
+ ))} +
+
+
+
+
+
+ ) + } + + return ( +
+ {/* Trader Header */} +
+
+

+ + + + {selectedTrader.trader_name} +

+ + {/* Trader Selector */} + {traders && traders.length > 0 && ( +
+ + {t('switchTrader', language)}: + + +
+ )} +
+
+ + AI Model:{' '} + + {getModelDisplayName( + selectedTrader.ai_model.split('_').pop() || + selectedTrader.ai_model + )} + + + {status && ( + <> + + Cycles: {status.call_count} + + Runtime: {status.runtime_minutes} min + + )} +
+
+ + {/* Debug Info */} + {account && ( +
+
+ + Last Update: {lastUpdate} | Total Equity:{' '} + {account?.total_equity?.toFixed(2) || '0.00'} | Available:{' '} + {account?.available_balance?.toFixed(2) || '0.00'} | P&L:{' '} + {account?.total_pnl?.toFixed(2) || '0.00'} ( + {account?.total_pnl_pct?.toFixed(2) || '0.00'}%) +
+
+ )} + + {/* Account Overview */} +
+ 0} + /> + + = 0 ? '+' : ''}${account?.total_pnl?.toFixed(2) || '0.00'} USDT`} + change={account?.total_pnl_pct || 0} + positive={(account?.total_pnl ?? 0) >= 0} + /> + +
+ + {/* 主要内容区:左右分屏 */} +
+ {/* 左侧:图表 + 持仓 */} +
+ {/* Equity Chart */} +
+ +
+ + {/* Current Positions */} +
+
+

+ + {t('currentPositions', language)} +

+ {positions && positions.length > 0 && ( +
+ {positions.length} {t('active', language)} +
+ )} +
+ {positions && positions.length > 0 ? ( +
+ + + + + + + + + + + + + + + + {positions.map((pos, i) => ( + + + + + + + + + + + + ))} + +
+ {t('symbol', language)} + + {t('side', language)} + + {t('entryPrice', language)} + + {t('markPrice', language)} + + {t('quantity', language)} + + {t('positionValue', language)} + + {t('leverage', language)} + + {t('unrealizedPnL', language)} + + {t('liqPrice', language)} +
+ {pos.symbol} + + + {t( + pos.side === 'long' ? 'long' : 'short', + language + )} + + + {pos.entry_price.toFixed(4)} + + {pos.mark_price.toFixed(4)} + + {pos.quantity.toFixed(4)} + + {(pos.quantity * pos.mark_price).toFixed(2)} USDT + + {pos.leverage}x + + = 0 ? '#0ECB81' : '#F6465D', + fontWeight: 'bold', + }} + > + {pos.unrealized_pnl >= 0 ? '+' : ''} + {pos.unrealized_pnl.toFixed(2)} ( + {pos.unrealized_pnl_pct.toFixed(2)}%) + + + {pos.liquidation_price.toFixed(4)} +
+
+ ) : ( +
+
+ +
+
+ {t('noPositions', language)} +
+
+ {t('noActivePositions', language)} +
+
+ )} +
+
+ + {/* 右侧:Recent Decisions */} +
+
+
+ +
+
+

+ {t('recentDecisions', language)} +

+ {decisions && decisions.length > 0 && ( +
+ {t('lastCycles', language, { count: decisions.length })} +
+ )} +
+
+ +
+ {decisions && decisions.length > 0 ? ( + decisions.map((decision, i) => ( + + )) + ) : ( +
+
+ +
+
+ {t('noDecisionsYet', language)} +
+
+ {t('aiDecisionsWillAppear', language)} +
+
+ )} +
+
+
+ + {/* AI Learning & Performance Analysis */} +
+ +
+
+ ) +} + +// Stat Card Component +function StatCard({ + title, + value, + change, + positive, + subtitle, +}: { + title: string + value: string + change?: number + positive?: boolean + subtitle?: string +}) { + return ( +
+
+ {title} +
+
+ {value} +
+ {change !== undefined && ( +
+
+ {positive ? '▲' : '▼'} {positive ? '+' : ''} + {change.toFixed(2)}% +
+
+ )} + {subtitle && ( +
+ {subtitle} +
+ )} +
+ ) +} + +// Decision Card Component +function DecisionCard({ + decision, + language, +}: { + decision: DecisionRecord + language: Language +}) { + const [showInputPrompt, setShowInputPrompt] = useState(false) + const [showCoT, setShowCoT] = useState(false) + + return ( +
+ {/* Header */} +
+
+
+ {t('cycle', language)} #{decision.cycle_number} +
+
+ {new Date(decision.timestamp).toLocaleString()} +
+
+
+ {t(decision.success ? 'success' : 'failed', language)} +
+
+ + {/* Input Prompt - Collapsible */} + {decision.input_prompt && ( +
+ + {showInputPrompt && ( +
+ {decision.input_prompt} +
+ )} +
+ )} + + {/* AI Chain of Thought - Collapsible */} + {decision.cot_trace && ( +
+ + {showCoT && ( +
+ {decision.cot_trace} +
+ )} +
+ )} + + {/* Decisions Actions */} + {decision.decisions && decision.decisions.length > 0 && ( +
+ {decision.decisions.map((action, j) => ( +
+ + {action.symbol} + + + {action.action} + + {action.leverage > 0 && ( + {action.leverage}x + )} + {action.price > 0 && ( + + @{action.price.toFixed(4)} + + )} + + {action.success ? ( + + ) : ( + + )} + + {action.error && ( + + {action.error} + + )} +
+ ))} +
+ )} + + {/* Account State Summary */} + {decision.account_state && ( +
+ + 净值: {decision.account_state.total_balance.toFixed(2)} USDT + + + 可用: {decision.account_state.available_balance.toFixed(2)} USDT + + + 保证金率: {decision.account_state.margin_used_pct.toFixed(1)}% + + 持仓: {decision.account_state.position_count} + + {t('candidateCoins', language)}:{' '} + {decision.candidate_coins?.length || 0} + +
+ )} + + {/* Candidate Coins Warning */} + {decision.candidate_coins && decision.candidate_coins.length === 0 && ( +
+ +
+
+ {t('candidateCoinsZeroWarning', language)} +
+
+
{t('possibleReasons', language)}
+
    +
  • {t('coinPoolApiNotConfigured', language)}
  • +
  • {t('apiConnectionTimeout', language)}
  • +
  • {t('noCustomCoinsAndApiFailed', language)}
  • +
+
+ {t('solutions', language)} +
+
    +
  • {t('setCustomCoinsInConfig', language)}
  • +
  • {t('orConfigureCorrectApiUrl', language)}
  • +
  • {t('orDisableCoinPoolOptions', language)}
  • +
+
+
+
+ )} + + {/* Execution Logs */} + {decision.execution_log && decision.execution_log.length > 0 && ( +
+ {decision.execution_log.map((log, k) => ( +
+ {log} +
+ ))} +
+ )} + + {/* Error Message */} + {decision.error_message && ( +
+ {decision.error_message} +
+ )} +
+ ) +} diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx new file mode 100644 index 00000000..b49164cf --- /dev/null +++ b/web/src/routes/index.tsx @@ -0,0 +1,62 @@ +import { createBrowserRouter, Navigate } from 'react-router-dom' +import MainLayout from '../layouts/MainLayout' +import AuthLayout from '../layouts/AuthLayout' +import { LandingPage } from '../pages/LandingPage' +import { FAQPage } from '../pages/FAQPage' +import { LoginPage } from '../components/LoginPage' +import { RegisterPage } from '../components/RegisterPage' +import { ResetPasswordPage } from '../components/ResetPasswordPage' +import { CompetitionPage } from '../components/CompetitionPage' +import { AITradersPage } from '../components/AITradersPage' +import TraderDashboard from '../pages/TraderDashboard' + +export const router = createBrowserRouter([ + { + path: '/', + element: , + }, + // Auth routes - using AuthLayout + { + element: , + children: [ + { + path: '/login', + element: , + }, + { + path: '/register', + element: , + }, + { + path: '/reset-password', + element: , + }, + ], + }, + // Main app routes - using MainLayout with nested routes + { + element: , + children: [ + { + path: '/faq', + element: , + }, + { + path: '/competition', + element: , + }, + { + path: '/traders', + element: , + }, + { + path: '/dashboard', + element: , + }, + ], + }, + { + path: '*', + element: , + }, +])