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 (
+
+
+
+
{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 (
-
-
-
-
{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 */}
-
- {t('goToTradersPage', language)}
-
-
-
- )
- }
-
- // 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 */}
-
- {t('goToTradersPage', language)}
-
-
-
- )
- }
-
- // 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)}:
-
- onTraderSelect(e.target.value)}
- className="rounded px-3 py-2 text-sm font-medium cursor-pointer transition-colors"
- style={{
- background: '#1E2329',
- border: '1px solid #2B3139',
- color: '#EAECEF',
- }}
- >
- {traders.map((trader) => (
-
- {trader.trader_name}
-
- ))}
-
-
- )}
-
-
-
- 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 ? (
-
-
-
-
-
- {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)}
-
-
-
-
- {positions.map((pos, i) => (
-
-
- {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 && (
-
-
setShowInputPrompt(!showInputPrompt)}
- className="flex items-center gap-2 text-sm transition-colors"
- style={{ color: '#60a5fa' }}
- >
-
- 📥 {t('inputPrompt', language)}
-
-
- {showInputPrompt
- ? t('collapse', language)
- : t('expand', language)}
-
-
- {showInputPrompt && (
-
- {decision.input_prompt}
-
- )}
-
- )}
-
- {/* AI Chain of Thought - Collapsible */}
- {decision.cot_trace && (
-
-
setShowCoT(!showCoT)}
- className="flex items-center gap-2 text-sm transition-colors"
- style={{ color: '#F0B90B' }}
- >
-
- 📤 {t('aiThinking', language)}
-
-
- {showCoT ? t('collapse', language) : t('expand', language)}
-
-
- {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: 禁止换行,超出横向滚动 */}
+
onTraderSelect?.(trader.trader_id)}
+ onClick={() => {
+ if (onTraderSelect) {
+ onTraderSelect(trader.trader_id)
+ } else {
+ navigate(`/dashboard?trader=${trader.trader_id}`)
+ }
+ }}
className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 flex items-center gap-1 whitespace-nowrap"
style={{
background: 'rgba(99, 102, 241, 0.1)',
@@ -1069,7 +1116,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
handleEditTrader(trader.trader_id)}
disabled={trader.is_running}
- className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
+ className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap flex items-center gap-1"
style={{
background: trader.is_running
? 'rgba(132, 142, 156, 0.1)'
@@ -1077,7 +1124,8 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
color: trader.is_running ? '#848E9C' : '#FFC107',
}}
>
- ✏️ {t('edit', language)}
+
+ {t('edit', language)}
setCopiedIP(false), 2000)
+ toast.success(t('ipCopied', language))
} else {
// 降级方案: 使用传统的 execCommand 方法
const textArea = document.createElement('textarea')
@@ -1804,6 +1853,7 @@ function ExchangeConfigModal({
if (successful) {
setCopiedIP(true)
setTimeout(() => setCopiedIP(false), 2000)
+ toast.success(t('ipCopied', language))
} else {
throw new Error('复制命令执行失败')
}
@@ -1814,7 +1864,7 @@ function ExchangeConfigModal({
} catch (err) {
console.error('复制失败:', err)
// 显示错误提示
- alert(t('copyIPFailed', language) || `复制失败: ${ip}\n请手动复制此IP地址`)
+ toast.error(t('copyIPFailed', language) || `复制失败: ${ip}\n请手动复制此IP地址`)
}
}
diff --git a/web/src/components/ConfirmDialog.tsx b/web/src/components/ConfirmDialog.tsx
new file mode 100644
index 00000000..c2d9b711
--- /dev/null
+++ b/web/src/components/ConfirmDialog.tsx
@@ -0,0 +1,123 @@
+import React, {
+ createContext,
+ useContext,
+ useState,
+ useCallback,
+ useEffect,
+} from 'react'
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogTitle,
+} from './ui/alert-dialog'
+import { setGlobalConfirm } from '../lib/notify'
+
+interface ConfirmOptions {
+ title?: string
+ message: string
+ okText?: string
+ cancelText?: string
+}
+
+interface ConfirmDialogContextType {
+ confirm: (options: ConfirmOptions) => Promise
+}
+
+const ConfirmDialogContext = createContext<
+ ConfirmDialogContextType | undefined
+>(undefined)
+
+export function useConfirmDialog() {
+ const context = useContext(ConfirmDialogContext)
+ if (!context) {
+ throw new Error(
+ 'useConfirmDialog must be used within ConfirmDialogProvider'
+ )
+ }
+ return context
+}
+
+interface ConfirmState {
+ isOpen: boolean
+ title?: string
+ message: string
+ okText: string
+ cancelText: string
+ resolve?: (value: boolean) => void
+}
+
+export function ConfirmDialogProvider({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ const [state, setState] = useState({
+ isOpen: false,
+ message: '',
+ okText: '确认',
+ cancelText: '取消',
+ })
+
+ const confirm = useCallback((options: ConfirmOptions): Promise => {
+ return new Promise((resolve) => {
+ setState({
+ isOpen: true,
+ title: options.title,
+ message: options.message,
+ okText: options.okText || '确认',
+ cancelText: options.cancelText || '取消',
+ resolve,
+ })
+ })
+ }, [])
+
+ // 注册全局 confirm 函数
+ useEffect(() => {
+ setGlobalConfirm(confirm)
+ }, [confirm])
+
+ const handleClose = useCallback((result: boolean) => {
+ setState((prev) => {
+ prev.resolve?.(result)
+ return {
+ ...prev,
+ isOpen: false,
+ }
+ })
+ }, [])
+
+ return (
+
+ {children}
+ !open && handleClose(false)}
+ >
+
+
+ {state.title && (
+
+ {state.title}
+
+ )}
+
+ {state.message}
+
+
+
+ handleClose(false)}>
+ {state.cancelText}
+
+ handleClose(true)}>
+ {state.okText}
+
+
+
+
+
+ )
+}
diff --git a/web/src/components/Container.tsx b/web/src/components/Container.tsx
new file mode 100644
index 00000000..00ae716d
--- /dev/null
+++ b/web/src/components/Container.tsx
@@ -0,0 +1,40 @@
+import { ReactNode, CSSProperties } from 'react'
+
+interface ContainerProps {
+ children: ReactNode
+ className?: string
+ as?: 'div' | 'main' | 'header' | 'section'
+ style?: CSSProperties
+ /** 是否充满宽度(取消 max-width) */
+ fluid?: boolean
+ /** 是否取消水平内边距 */
+ noPadding?: boolean
+ /** 自定义最大宽度类(默认 max-w-[1920px]) */
+ maxWidthClass?: string
+}
+
+/**
+ * 统一的容器组件,确保所有页面元素使用一致的最大宽度和内边距
+ * - max-width: 1920px
+ * - padding: 24px (mobile) -> 32px (tablet) -> 48px (desktop)
+ */
+export function Container({
+ children,
+ className = '',
+ as: Component = 'div',
+ style,
+ fluid = false,
+ noPadding = false,
+ maxWidthClass = 'max-w-[1920px]',
+}: ContainerProps) {
+ const maxWidth = fluid ? 'w-full' : maxWidthClass
+ const padding = noPadding ? 'px-0' : 'px-6 sm:px-8 lg:px-12'
+ return (
+
+ {children}
+
+ )
+}
diff --git a/web/src/components/DevToastController.tsx b/web/src/components/DevToastController.tsx
new file mode 100644
index 00000000..912e3723
--- /dev/null
+++ b/web/src/components/DevToastController.tsx
@@ -0,0 +1,116 @@
+///
+
+import { useState } from 'react'
+import { confirmToast, notify } from '../lib/notify'
+
+const toastOptions = [
+ 'message',
+ 'success',
+ 'info',
+ 'warning',
+ 'error',
+ 'custom',
+] as const
+
+type ToastType = (typeof toastOptions)[number]
+
+const customRenderer = () => (
+
+
Sonner 自定义通知
+
+ 这是一个通过 `notify.custom` 渲染的测试 Toast
+
+
+)
+
+export function DevToastController() {
+ const [type, setType] = useState('success')
+ const [message, setMessage] = useState('来自 Dev 控制器的测试通知')
+ const [duration, setDuration] = useState(2200)
+
+ if (!import.meta.env.DEV) {
+ return null
+ }
+
+ const triggerToast = async () => {
+ switch (type) {
+ case 'message':
+ notify.message(message, { duration })
+ break
+ case 'success':
+ notify.success(message, { duration })
+ break
+ case 'info':
+ notify.info(message, { duration })
+ break
+ case 'warning':
+ notify.warning(message, { duration })
+ break
+ case 'error':
+ notify.error(message, { duration })
+ break
+ case 'custom':
+ notify.custom(() => customRenderer(), { duration })
+ break
+ }
+ }
+
+ const triggerConfirm = async () => {
+ const confirmed = await confirmToast(message, {
+ okText: '继续',
+ cancelText: '取消',
+ })
+ if (confirmed) {
+ notify.success('确认按钮已点击', { duration: 2000 })
+ } else {
+ notify.message('已取消确认逻辑', { duration: 2000 })
+ }
+ }
+
+ return (
+
+
+ Dev Sonner 控制器
+ 仅在 dev 模式可见
+
+
+
+ 类型
+ setType(event.target.value as ToastType)}
+ >
+ {toastOptions.map((option) => (
+
+ {option}
+
+ ))}
+
+
+
+ 文案
+ setMessage(event.target.value)}
+ placeholder="输入通知/确认文案"
+ />
+
+
+ 持续(ms)
+ setDuration(Number(event.target.value))}
+ />
+
+
+ 触发通知
+ 触发确认
+
+
+
+ )
+}
+
+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 (
+
+
+ {/* Logo */}
+
+
+
+ NOFX
+
+
+ Agentic Trading OS
+
+
+
+ {/* Desktop Menu */}
+
+ {/* Left Side - Navigation Tabs */}
+
+ {isLoggedIn ? (
+ // Main app navigation when logged in
+ <>
+
{
+ navigate('/competition')
+ }}
+ className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
+ style={{
+ color:
+ currentPage === 'competition'
+ ? 'var(--brand-yellow)'
+ : 'var(--brand-light-gray)',
+ padding: '8px 16px',
+ borderRadius: '8px',
+ position: 'relative',
+ }}
+ onMouseEnter={(e) => {
+ if (currentPage !== 'competition') {
+ e.currentTarget.style.color = 'var(--brand-yellow)'
+ }
+ }}
+ onMouseLeave={(e) => {
+ if (currentPage !== 'competition') {
+ e.currentTarget.style.color = 'var(--brand-light-gray)'
+ }
+ }}
+ >
+ {/* Background for selected state */}
+ {currentPage === 'competition' && (
+
+ )}
+
+ {t('realtimeNav', language)}
+
+
+
{
+ navigate('/traders')
+ }}
+ className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
+ style={{
+ color:
+ currentPage === 'traders'
+ ? 'var(--brand-yellow)'
+ : 'var(--brand-light-gray)',
+ padding: '8px 16px',
+ borderRadius: '8px',
+ position: 'relative',
+ }}
+ onMouseEnter={(e) => {
+ if (currentPage !== 'traders') {
+ e.currentTarget.style.color = 'var(--brand-yellow)'
+ }
+ }}
+ onMouseLeave={(e) => {
+ if (currentPage !== 'traders') {
+ e.currentTarget.style.color = 'var(--brand-light-gray)'
+ }
+ }}
+ >
+ {/* Background for selected state */}
+ {currentPage === 'traders' && (
+
+ )}
+
+ {t('configNav', language)}
+
+
+
{
+ navigate('/dashboard')
+ }}
+ className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
+ style={{
+ color:
+ currentPage === 'trader'
+ ? 'var(--brand-yellow)'
+ : 'var(--brand-light-gray)',
+ padding: '8px 16px',
+ borderRadius: '8px',
+ position: 'relative',
+ }}
+ onMouseEnter={(e) => {
+ if (currentPage !== 'trader') {
+ e.currentTarget.style.color = 'var(--brand-yellow)'
+ }
+ }}
+ onMouseLeave={(e) => {
+ if (currentPage !== 'trader') {
+ e.currentTarget.style.color = 'var(--brand-light-gray)'
+ }
+ }}
+ >
+ {/* Background for selected state */}
+ {currentPage === 'trader' && (
+
+ )}
+
+ {t('dashboardNav', language)}
+
+
+
{
+ if (onPageChange) {
+ onPageChange('faq')
+ } else {
+ navigate('/faq')
+ }
+ }}
+ className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
+ style={{
+ color:
+ currentPage === 'faq'
+ ? 'var(--brand-yellow)'
+ : 'var(--brand-light-gray)',
+ padding: '8px 16px',
+ borderRadius: '8px',
+ position: 'relative',
+ }}
+ onMouseEnter={(e) => {
+ if (currentPage !== 'faq') {
+ e.currentTarget.style.color = 'var(--brand-yellow)'
+ }
+ }}
+ onMouseLeave={(e) => {
+ if (currentPage !== 'faq') {
+ e.currentTarget.style.color = 'var(--brand-light-gray)'
+ }
+ }}
+ >
+ {/* Background for selected state */}
+ {currentPage === 'faq' && (
+
+ )}
+
+ {t('faqNav', language)}
+
+ >
+ ) : (
+ // Landing page navigation when not logged in
+ <>
+
{
+ if (currentPage !== 'competition') {
+ e.currentTarget.style.color = 'var(--brand-yellow)'
+ }
+ }}
+ onMouseLeave={(e) => {
+ if (currentPage !== 'competition') {
+ e.currentTarget.style.color = 'var(--brand-light-gray)'
+ }
+ }}
+ >
+ {/* Background for selected state */}
+ {currentPage === 'competition' && (
+
+ )}
+
+ {t('realtimeNav', language)}
+
+
+
{
+ if (currentPage !== 'faq') {
+ e.currentTarget.style.color = 'var(--brand-yellow)'
+ }
+ }}
+ onMouseLeave={(e) => {
+ if (currentPage !== 'faq') {
+ e.currentTarget.style.color = 'var(--brand-light-gray)'
+ }
+ }}
+ >
+ {/* Background for selected state */}
+ {currentPage === 'faq' && (
+
+ )}
+
+ {t('faqNav', language)}
+
+ >
+ )}
+
+
+ {/* Right Side - Original Navigation Items and Login */}
+
+ {/* Only show original navigation items on home page */}
+ {isHomePage &&
+ [
+ { key: 'features', label: t('features', language) },
+ { key: 'howItWorks', label: t('howItWorks', language) },
+ { key: 'GitHub', label: 'GitHub' },
+ { key: 'community', label: t('community', language) },
+ ].map((item) => (
+
+ {item.label}
+
+
+ ))}
+
+ {/* User Info and Actions */}
+ {isLoggedIn && user ? (
+
+ {/* User Info with Dropdown */}
+
+
setUserDropdownOpen(!userDropdownOpen)}
+ className="flex items-center gap-2 px-3 py-2 rounded transition-colors"
+ style={{
+ background: 'var(--panel-bg)',
+ border: '1px solid var(--panel-border)',
+ }}
+ onMouseEnter={(e) =>
+ (e.currentTarget.style.background =
+ 'rgba(255, 255, 255, 0.05)')
+ }
+ onMouseLeave={(e) =>
+ (e.currentTarget.style.background = 'var(--panel-bg)')
+ }
+ >
+
+ {user.email[0].toUpperCase()}
+
+
+ {user.email}
+
+
+
+
+ {userDropdownOpen && (
+
+
+
+ {t('loggedInAs', language)}
+
+
+ {user.email}
+
+
+ {onLogout && (
+
{
+ onLogout()
+ setUserDropdownOpen(false)
+ }}
+ className="w-full px-3 py-2 text-sm font-semibold transition-colors hover:opacity-80 text-center"
+ style={{
+ background: 'var(--binance-red-bg)',
+ color: 'var(--binance-red)',
+ }}
+ >
+ {t('exitLogin', language)}
+
+ )}
+
+ )}
+
+
+ ) : (
+ /* Show login/register buttons when not logged in and not on login/register pages */
+ currentPage !== 'login' &&
+ currentPage !== 'register' && (
+
+ )
+ )}
+
+ {/* Language Toggle - Always at the rightmost */}
+
+
setLanguageDropdownOpen(!languageDropdownOpen)}
+ className="flex items-center gap-2 px-3 py-2 rounded transition-colors"
+ style={{ color: 'var(--brand-light-gray)' }}
+ onMouseEnter={(e) =>
+ (e.currentTarget.style.background =
+ 'rgba(255, 255, 255, 0.05)')
+ }
+ onMouseLeave={(e) =>
+ (e.currentTarget.style.background = 'transparent')
+ }
+ >
+
+ {language === 'zh' ? '🇨🇳' : '🇺🇸'}
+
+
+
+
+ {languageDropdownOpen && (
+
+ {
+ onLanguageChange?.('zh')
+ setLanguageDropdownOpen(false)
+ }}
+ className={`w-full flex items-center gap-2 px-3 py-2 transition-colors ${
+ language === 'zh' ? '' : 'hover:opacity-80'
+ }`}
+ style={{
+ color: 'var(--brand-light-gray)',
+ background:
+ language === 'zh'
+ ? 'rgba(240, 185, 11, 0.1)'
+ : 'transparent',
+ }}
+ >
+ 🇨🇳
+ 中文
+
+ {
+ onLanguageChange?.('en')
+ setLanguageDropdownOpen(false)
+ }}
+ className={`w-full flex items-center gap-2 px-3 py-2 transition-colors ${
+ language === 'en' ? '' : 'hover:opacity-80'
+ }`}
+ style={{
+ color: 'var(--brand-light-gray)',
+ background:
+ language === 'en'
+ ? 'rgba(240, 185, 11, 0.1)'
+ : 'transparent',
+ }}
+ >
+ 🇺🇸
+ English
+
+
+ )}
+
+
+
+
+ {/* Mobile Menu Button */}
+ setMobileMenuOpen(!mobileMenuOpen)}
+ className="md:hidden"
+ style={{ color: 'var(--brand-light-gray)' }}
+ whileTap={{ scale: 0.9 }}
+ >
+ {mobileMenuOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* Mobile Menu */}
+
+
+ {/* New Navigation Tabs */}
+ {isLoggedIn ? (
+
{
+ console.log(
+ '移动端 实时 button clicked, onPageChange:',
+ onPageChange
+ )
+ onPageChange?.('competition')
+ setMobileMenuOpen(false)
+ }}
+ className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
+ style={{
+ color:
+ currentPage === 'competition'
+ ? 'var(--brand-yellow)'
+ : 'var(--brand-light-gray)',
+ padding: '12px 16px',
+ borderRadius: '8px',
+ position: 'relative',
+ width: '100%',
+ textAlign: 'left',
+ }}
+ >
+ {/* Background for selected state */}
+ {currentPage === 'competition' && (
+
+ )}
+
+ {t('realtimeNav', language)}
+
+ ) : (
+
+ {/* Background for selected state */}
+ {currentPage === 'competition' && (
+
+ )}
+
+ {t('realtimeNav', language)}
+
+ )}
+ {/* Only show 配置 and 看板 when logged in */}
+ {isLoggedIn && (
+ <>
+
{
+ if (onPageChange) {
+ onPageChange('traders')
+ } else {
+ navigate('/traders')
+ }
+ setMobileMenuOpen(false)
+ }}
+ className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 hover:text-yellow-500"
+ style={{
+ color:
+ currentPage === 'traders'
+ ? 'var(--brand-yellow)'
+ : 'var(--brand-light-gray)',
+ padding: '12px 16px',
+ borderRadius: '8px',
+ position: 'relative',
+ width: '100%',
+ textAlign: 'left',
+ }}
+ >
+ {/* Background for selected state */}
+ {currentPage === 'traders' && (
+
+ )}
+
+ {t('configNav', language)}
+
+
{
+ if (onPageChange) {
+ onPageChange('trader')
+ } else {
+ navigate('/dashboard')
+ }
+ setMobileMenuOpen(false)
+ }}
+ className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 hover:text-yellow-500"
+ style={{
+ color:
+ currentPage === 'trader'
+ ? 'var(--brand-yellow)'
+ : 'var(--brand-light-gray)',
+ padding: '12px 16px',
+ borderRadius: '8px',
+ position: 'relative',
+ width: '100%',
+ textAlign: 'left',
+ }}
+ >
+ {/* Background for selected state */}
+ {currentPage === 'trader' && (
+
+ )}
+
+ {t('dashboardNav', language)}
+
+
{
+ if (onPageChange) {
+ onPageChange('faq')
+ } else {
+ navigate('/faq')
+ }
+ setMobileMenuOpen(false)
+ }}
+ className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 hover:text-yellow-500"
+ style={{
+ color:
+ currentPage === 'faq'
+ ? 'var(--brand-yellow)'
+ : 'var(--brand-light-gray)',
+ padding: '12px 16px',
+ borderRadius: '8px',
+ position: 'relative',
+ width: '100%',
+ textAlign: 'left',
+ }}
+ >
+ {/* Background for selected state */}
+ {currentPage === 'faq' && (
+
+ )}
+
+ {t('faqNav', language)}
+
+ >
+ )}
+
+ {/* Original Navigation Items - Only on home page */}
+ {isHomePage &&
+ [
+ { key: 'features', label: t('features', language) },
+ { key: 'howItWorks', label: t('howItWorks', language) },
+ { key: 'GitHub', label: 'GitHub' },
+ { key: 'community', label: t('community', language) },
+ ].map((item) => (
+
+ {item.label}
+
+ ))}
+
+ {/* Language Toggle */}
+
+
+
+ {t('language', language)}:
+
+
+
+ {
+ onLanguageChange?.('zh')
+ setMobileMenuOpen(false)
+ }}
+ className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${
+ language === 'zh'
+ ? 'bg-yellow-500 text-black'
+ : 'text-gray-400 hover:text-white'
+ }`}
+ >
+ 🇨🇳
+ 中文
+
+ {
+ onLanguageChange?.('en')
+ setMobileMenuOpen(false)
+ }}
+ className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${
+ language === 'en'
+ ? 'bg-yellow-500 text-black'
+ : 'text-gray-400 hover:text-white'
+ }`}
+ >
+ 🇺🇸
+ English
+
+
+
+
+ {/* User info and logout for mobile when logged in */}
+ {isLoggedIn && user && (
+
+
+
+ {user.email[0].toUpperCase()}
+
+
+
+ {t('loggedInAs', language)}
+
+
+ {user.email}
+
+
+
+ {onLogout && (
+
{
+ onLogout()
+ setMobileMenuOpen(false)
+ }}
+ className="w-full px-4 py-2 rounded text-sm font-semibold transition-colors text-center"
+ style={{
+ background: 'var(--binance-red-bg)',
+ color: 'var(--binance-red)',
+ }}
+ >
+ {t('exitLogin', language)}
+
+ )}
+
+ )}
+
+ {/* Show login/register buttons when not logged in and not on login/register pages */}
+ {!isLoggedIn &&
+ currentPage !== 'login' &&
+ currentPage !== 'register' && (
+
+ )}
+
+
+
+ )
+}
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
-
-
- {step === 'login' ? '请输入您的邮箱和密码' : '请输入两步验证码'}
-
+
+
+ {/* Logo */}
+
+
+
-
- {/* Login Form */}
-
- {error && (
-
- {error}
-
- )}
-
-
+ {adminMode ? (
+
)}
+
+ {/* Register Link */}
+ {!adminMode && (
+
+
+ 还没有账户?{' '}
+ navigate('/register')}
+ className="font-semibold hover:underline transition-colors"
+ style={{ color: 'var(--brand-yellow)' }}
+ >
+ 立即注册
+
+
+
+ )}
)
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 */}
-
-
-
-
-
- {t('appTitle', language)}
-
-
- {step === 'register' && t('registerTitle', language)}
- {step === 'setup-otp' && t('setupTwoFactor', language)}
- {step === 'verify-otp' && t('verifyOTP', language)}
-
+
+
+ {/* 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' && (
-
-
-
- {t('email', language)}
-
+ {/* Registration Form */}
+
+ {step === 'register' && (
+
+
+
+ {t('email', language)}
+
+ setEmail(e.target.value)}
+ placeholder={t('emailPlaceholder', language)}
+ required
+ />
+
+
+
+
+ {t('password', language)}
+
+
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
/>
-
-
-
-
e.preventDefault()}
+ onClick={() => setShowPassword((v) => !v)}
+ className="absolute inset-y-0 right-2 w-8 h-10 flex items-center justify-center rounded bg-transparent p-0 m-0 border-0 outline-none focus:outline-none focus:ring-0 appearance-none cursor-pointer btn-icon"
+ style={{ color: 'var(--text-secondary)' }}
>
- {t('password', language)}
-
-
- setPassword(e.target.value)}
- className="pr-10"
- placeholder={t('passwordPlaceholder', language)}
- required
- />
- e.preventDefault()}
- onClick={() => setShowPassword((v) => !v)}
- className="absolute inset-y-0 right-2 w-8 h-10 flex items-center justify-center rounded bg-transparent p-0 m-0 border-0 outline-none focus:outline-none focus:ring-0 appearance-none cursor-pointer btn-icon"
- style={{ color: 'var(--text-secondary)' }}
- >
- {showPassword ? : }
-
-
+ {showPassword ?
:
}
+
+
-
-
+
+ {t('confirmPassword', language)}
+
+
+
- {/* 密码规则清单(通过才允许提交) */}
+ {/* 密码规则清单(通过才允许提交) */}
+
+ {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位字母数字组成,区分大小写
-
-
- )}
+
+ {loading
+ ? t('loading', language)
+ : t('registerButton', language)}
+
+
+ )}
- {error && (
-
+
+
📱
+
+ {t('setupTwoFactor', language)}
+
+
+ {t('setupTwoFactorDesc', language)}
+
+
+
+
+
- )}
+ {t('authStep1Title', language)}
+
+
+ {t('authStep1Desc', language)}
+
+
+
+
+ {t('authStep2Title', language)}
+
+
+ {t('authStep2Desc', language)}
+
+
+ {qrCodeURL && (
+
+
+ {t('qrCodeHint', language)}
+
+
+
+
+
+ )}
+
+
+
+ {t('otpSecret', language)}
+
+
+
+ {otpSecret}
+
+ copyToClipboard(otpSecret)}
+ className="px-2 py-1 text-xs rounded"
+ style={{
+ background: 'var(--brand-yellow)',
+ color: 'var(--brand-black)',
+ }}
+ >
+ {t('copy', language)}
+
+
+
+
+
+
+
+ {t('authStep3Title', language)}
+
+
+ {t('authStep3Desc', language)}
+
+
+
+
+
+ {t('setupCompleteContinue', language)}
+
+
+ )}
+
+ {step === 'verify-otp' && (
+
+
+
🔐
+
+ {t('enterOTPCode', language)}
+
+ {t('completeRegistrationSubtitle', language)}
+
+
+
+
+
+ {t('otpCode', 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}
+
+ )}
+
+
+
setStep('setup-otp')}
+ className="flex-1 px-4 py-2 rounded text-sm font-semibold"
+ style={{
+ background: 'var(--panel-bg-hover)',
+ color: 'var(--text-secondary)',
+ }}
+ >
+ {t('back', language)}
+
{loading
? t('loading', language)
- : t('registerButton', language)}
-
-
- )}
-
- {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)}
-
-
-
-
-
- )}
-
-
-
- {t('otpSecret', language)}
-
-
-
- {otpSecret}
-
- copyToClipboard(otpSecret)}
- className="px-2 py-1 text-xs rounded"
- style={{
- background: 'var(--brand-yellow)',
- color: 'var(--brand-black)',
- }}
- >
- {t('copy', language)}
-
-
-
-
-
-
-
- {t('authStep3Title', language)}
-
-
- {t('authStep3Desc', language)}
-
-
-
-
-
- {t('setupCompleteContinue', language)}
+ : t('completeRegistration', language)}
- )}
-
- {step === 'verify-otp' && (
-
-
-
🔐
-
- {t('enterOTPCode', language)}
-
- {t('completeRegistrationSubtitle', language)}
-
-
-
-
-
- {t('otpCode', 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}
-
- )}
-
-
- setStep('setup-otp')}
- className="flex-1 px-4 py-2 rounded text-sm font-semibold"
- style={{
- background: 'var(--panel-bg-hover)',
- color: 'var(--text-secondary)',
- }}
- >
- {t('back', language)}
-
-
- {loading
- ? t('loading', language)
- : t('completeRegistration', language)}
-
-
-
- )}
-
-
- {/* Login Link */}
- {step === 'register' && (
-
-
- 已有账户?{' '}
- {
- window.history.pushState({}, '', '/login')
- window.dispatchEvent(new PopStateEvent('popstate'))
- }}
- className="font-semibold hover:underline transition-colors"
- style={{ color: 'var(--brand-yellow)' }}
- >
- 立即登录
-
-
-
+
)}
+
+ {/* Login Link */}
+ {step === 'register' && (
+
+
+ 已有账户?{' '}
+ navigate('/login')}
+ className="font-semibold hover:underline transition-colors"
+ style={{ color: 'var(--brand-yellow)' }}
+ >
+ 立即登录
+
+
+
+ )}
)
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 (
-
-
-
- {/* Logo */}
-
-
-
- NOFX
-
-
- Agentic Trading OS
-
-
-
- {/* Desktop Menu */}
-
- {/* Left Side - Navigation Tabs */}
-
- {isLoggedIn ? (
- // Main app navigation when logged in
- <>
-
{
- console.log(
- '实时 button clicked, onPageChange:',
- onPageChange
- )
- onPageChange?.('competition')
- }}
- className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
- style={{
- color:
- currentPage === 'competition'
- ? 'var(--brand-yellow)'
- : 'var(--brand-light-gray)',
- padding: '8px 16px',
- borderRadius: '8px',
- position: 'relative',
- }}
- onMouseEnter={(e) => {
- if (currentPage !== 'competition') {
- e.currentTarget.style.color = 'var(--brand-yellow)'
- }
- }}
- onMouseLeave={(e) => {
- if (currentPage !== 'competition') {
- e.currentTarget.style.color = 'var(--brand-light-gray)'
- }
- }}
- >
- {/* Background for selected state */}
- {currentPage === 'competition' && (
-
- )}
-
- {t('realtimeNav', language)}
-
-
-
{
- console.log(
- '配置 button clicked, onPageChange:',
- onPageChange
- )
- onPageChange?.('traders')
- }}
- className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
- style={{
- color:
- currentPage === 'traders'
- ? 'var(--brand-yellow)'
- : 'var(--brand-light-gray)',
- padding: '8px 16px',
- borderRadius: '8px',
- position: 'relative',
- }}
- onMouseEnter={(e) => {
- if (currentPage !== 'traders') {
- e.currentTarget.style.color = 'var(--brand-yellow)'
- }
- }}
- onMouseLeave={(e) => {
- if (currentPage !== 'traders') {
- e.currentTarget.style.color = 'var(--brand-light-gray)'
- }
- }}
- >
- {/* Background for selected state */}
- {currentPage === 'traders' && (
-
- )}
-
- {t('configNav', language)}
-
-
-
{
- console.log(
- '看板 button clicked, onPageChange:',
- onPageChange
- )
- onPageChange?.('trader')
- }}
- className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
- style={{
- color:
- currentPage === 'trader'
- ? 'var(--brand-yellow)'
- : 'var(--brand-light-gray)',
- padding: '8px 16px',
- borderRadius: '8px',
- position: 'relative',
- }}
- onMouseEnter={(e) => {
- if (currentPage !== 'trader') {
- e.currentTarget.style.color = 'var(--brand-yellow)'
- }
- }}
- onMouseLeave={(e) => {
- if (currentPage !== 'trader') {
- e.currentTarget.style.color = 'var(--brand-light-gray)'
- }
- }}
- >
- {/* Background for selected state */}
- {currentPage === 'trader' && (
-
- )}
-
- {t('dashboardNav', language)}
-
-
-
{
- console.log(
- 'FAQ button clicked, onPageChange:',
- onPageChange
- )
- onPageChange?.('faq')
- }}
- className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
- style={{
- color:
- currentPage === 'faq'
- ? 'var(--brand-yellow)'
- : 'var(--brand-light-gray)',
- padding: '8px 16px',
- borderRadius: '8px',
- position: 'relative',
- }}
- onMouseEnter={(e) => {
- if (currentPage !== 'faq') {
- e.currentTarget.style.color = 'var(--brand-yellow)'
- }
- }}
- onMouseLeave={(e) => {
- if (currentPage !== 'faq') {
- e.currentTarget.style.color = 'var(--brand-light-gray)'
- }
- }}
- >
- {/* Background for selected state */}
- {currentPage === 'faq' && (
-
- )}
-
- {t('faqNav', language)}
-
- >
- ) : (
- // Landing page navigation when not logged in
- <>
-
{
- if (currentPage !== 'competition') {
- e.currentTarget.style.color = 'var(--brand-yellow)'
- }
- }}
- onMouseLeave={(e) => {
- if (currentPage !== 'competition') {
- e.currentTarget.style.color = 'var(--brand-light-gray)'
- }
- }}
- >
- {/* Background for selected state */}
- {currentPage === 'competition' && (
-
- )}
-
- {t('realtimeNav', language)}
-
-
-
{
- if (currentPage !== 'faq') {
- e.currentTarget.style.color = 'var(--brand-yellow)'
- }
- }}
- onMouseLeave={(e) => {
- if (currentPage !== 'faq') {
- e.currentTarget.style.color = 'var(--brand-light-gray)'
- }
- }}
- >
- {/* Background for selected state */}
- {currentPage === 'faq' && (
-
- )}
-
- {t('faqNav', language)}
-
- >
- )}
-
-
- {/* Right Side - Original Navigation Items and Login */}
-
- {/* Only show original navigation items on home page */}
- {isHomePage &&
- [
- { key: 'features', label: t('features', language) },
- { key: 'howItWorks', label: t('howItWorks', language) },
- { key: 'GitHub', label: 'GitHub' },
- { key: 'community', label: t('community', language) },
- ].map((item) => (
-
- {item.label}
-
-
- ))}
-
- {/* User Info and Actions */}
- {isLoggedIn && user ? (
-
- {/* User Info with Dropdown */}
-
-
setUserDropdownOpen(!userDropdownOpen)}
- className="flex items-center gap-2 px-3 py-2 rounded transition-colors"
- style={{
- background: 'var(--panel-bg)',
- border: '1px solid var(--panel-border)',
- }}
- onMouseEnter={(e) =>
- (e.currentTarget.style.background =
- 'rgba(255, 255, 255, 0.05)')
- }
- onMouseLeave={(e) =>
- (e.currentTarget.style.background = 'var(--panel-bg)')
- }
- >
-
- {user.email[0].toUpperCase()}
-
-
- {user.email}
-
-
-
-
- {userDropdownOpen && (
-
-
-
- {t('loggedInAs', language)}
-
-
- {user.email}
-
-
- {onLogout && (
-
{
- onLogout()
- setUserDropdownOpen(false)
- }}
- className="w-full px-3 py-2 text-sm font-semibold transition-colors hover:opacity-80 text-center"
- style={{
- background: 'var(--binance-red-bg)',
- color: 'var(--binance-red)',
- }}
- >
- {t('exitLogin', language)}
-
- )}
-
- )}
-
-
- ) : (
- /* Show login/register buttons when not logged in and not on login/register pages */
- currentPage !== 'login' &&
- currentPage !== 'register' && (
-
- )
- )}
-
- {/* Language Toggle - Always at the rightmost */}
-
-
setLanguageDropdownOpen(!languageDropdownOpen)}
- className="flex items-center gap-2 px-3 py-2 rounded transition-colors"
- style={{ color: 'var(--brand-light-gray)' }}
- onMouseEnter={(e) =>
- (e.currentTarget.style.background =
- 'rgba(255, 255, 255, 0.05)')
- }
- onMouseLeave={(e) =>
- (e.currentTarget.style.background = 'transparent')
- }
- >
-
- {language === 'zh' ? '🇨🇳' : '🇺🇸'}
-
-
-
-
- {languageDropdownOpen && (
-
- {
- onLanguageChange?.('zh')
- setLanguageDropdownOpen(false)
- }}
- className={`w-full flex items-center gap-2 px-3 py-2 transition-colors ${
- language === 'zh' ? '' : 'hover:opacity-80'
- }`}
- style={{
- color: 'var(--brand-light-gray)',
- background:
- language === 'zh'
- ? 'rgba(240, 185, 11, 0.1)'
- : 'transparent',
- }}
- >
- 🇨🇳
- 中文
-
- {
- onLanguageChange?.('en')
- setLanguageDropdownOpen(false)
- }}
- className={`w-full flex items-center gap-2 px-3 py-2 transition-colors ${
- language === 'en' ? '' : 'hover:opacity-80'
- }`}
- style={{
- color: 'var(--brand-light-gray)',
- background:
- language === 'en'
- ? 'rgba(240, 185, 11, 0.1)'
- : 'transparent',
- }}
- >
- 🇺🇸
- English
-
-
- )}
-
-
-
-
- {/* Mobile Menu Button */}
-
setMobileMenuOpen(!mobileMenuOpen)}
- className="md:hidden"
- style={{ color: 'var(--brand-light-gray)' }}
- whileTap={{ scale: 0.9 }}
- >
- {mobileMenuOpen ? (
-
- ) : (
-
- )}
-
-
-
-
- {/* Mobile Menu */}
-
-
- {/* New Navigation Tabs */}
- {isLoggedIn ? (
-
{
- console.log(
- '移动端 实时 button clicked, onPageChange:',
- onPageChange
- )
- onPageChange?.('competition')
- setMobileMenuOpen(false)
- }}
- className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
- style={{
- color:
- currentPage === 'competition'
- ? 'var(--brand-yellow)'
- : 'var(--brand-light-gray)',
- padding: '12px 16px',
- borderRadius: '8px',
- position: 'relative',
- width: '100%',
- textAlign: 'left',
- }}
- >
- {/* Background for selected state */}
- {currentPage === 'competition' && (
-
- )}
-
- {t('realtimeNav', language)}
-
- ) : (
-
- {/* Background for selected state */}
- {currentPage === 'competition' && (
-
- )}
-
- {t('realtimeNav', language)}
-
- )}
- {/* Only show 配置 and 看板 when logged in */}
- {isLoggedIn && (
- <>
-
{
- console.log(
- '移动端 配置 button clicked, onPageChange:',
- onPageChange
- )
- onPageChange?.('traders')
- setMobileMenuOpen(false)
- }}
- className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 hover:text-yellow-500"
- style={{
- color:
- currentPage === 'traders'
- ? 'var(--brand-yellow)'
- : 'var(--brand-light-gray)',
- padding: '12px 16px',
- borderRadius: '8px',
- position: 'relative',
- width: '100%',
- textAlign: 'left',
- }}
- >
- {/* Background for selected state */}
- {currentPage === 'traders' && (
-
- )}
-
- {t('configNav', language)}
-
-
{
- console.log(
- '移动端 看板 button clicked, onPageChange:',
- onPageChange
- )
- onPageChange?.('trader')
- setMobileMenuOpen(false)
- }}
- className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 hover:text-yellow-500"
- style={{
- color:
- currentPage === 'trader'
- ? 'var(--brand-yellow)'
- : 'var(--brand-light-gray)',
- padding: '12px 16px',
- borderRadius: '8px',
- position: 'relative',
- width: '100%',
- textAlign: 'left',
- }}
- >
- {/* Background for selected state */}
- {currentPage === 'trader' && (
-
- )}
-
- {t('dashboardNav', language)}
-
-
{
- console.log(
- '移动端 FAQ button clicked, onPageChange:',
- onPageChange
- )
- onPageChange?.('faq')
- setMobileMenuOpen(false)
- }}
- className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 hover:text-yellow-500"
- style={{
- color:
- currentPage === 'faq'
- ? 'var(--brand-yellow)'
- : 'var(--brand-light-gray)',
- padding: '12px 16px',
- borderRadius: '8px',
- position: 'relative',
- width: '100%',
- textAlign: 'left',
- }}
- >
- {/* Background for selected state */}
- {currentPage === 'faq' && (
-
- )}
-
- {t('faqNav', language)}
-
- >
- )}
-
- {/* Original Navigation Items - Only on home page */}
- {isHomePage &&
- [
- { key: 'features', label: t('features', language) },
- { key: 'howItWorks', label: t('howItWorks', language) },
- { key: 'GitHub', label: 'GitHub' },
- { key: 'community', label: t('community', language) },
- ].map((item) => (
-
- {item.label}
-
- ))}
-
- {/* Language Toggle */}
-
-
-
- {t('language', language)}:
-
-
-
- {
- onLanguageChange?.('zh')
- setMobileMenuOpen(false)
- }}
- className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${
- language === 'zh'
- ? 'bg-yellow-500 text-black'
- : 'text-gray-400 hover:text-white'
- }`}
- >
- 🇨🇳
- 中文
-
- {
- onLanguageChange?.('en')
- setMobileMenuOpen(false)
- }}
- className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${
- language === 'en'
- ? 'bg-yellow-500 text-black'
- : 'text-gray-400 hover:text-white'
- }`}
- >
- 🇺🇸
- English
-
-
-
-
- {/* User info and logout for mobile when logged in */}
- {isLoggedIn && user && (
-
-
-
- {user.email[0].toUpperCase()}
-
-
-
- {t('loggedInAs', language)}
-
-
- {user.email}
-
-
-
- {onLogout && (
-
{
- onLogout()
- setMobileMenuOpen(false)
- }}
- className="w-full px-4 py-2 rounded text-sm font-semibold transition-colors text-center"
- style={{
- background: 'var(--binance-red-bg)',
- color: 'var(--binance-red)',
- }}
- >
- {t('exitLogin', language)}
-
- )}
-
- )}
-
- {/* Show login/register buttons when not logged in and not on login/register pages */}
- {!isLoggedIn &&
- currentPage !== 'login' &&
- currentPage !== 'register' && (
-
- )}
-
-
-
- )
-}
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 */}
+
+
+ {/* Logo */}
+
+
+
+ NOFX
+
+
+
+ {/* Language Selector */}
+
+ setLanguage(language === 'zh' ? 'en' : 'zh')}
+ className="px-3 py-1.5 rounded text-sm font-medium transition-colors"
+ style={{
+ background: '#1E2329',
+ border: '1px solid #2B3139',
+ color: '#EAECEF',
+ }}
+ >
+ {language === 'zh' ? 'English' : '中文'}
+
+
+
+
+
+ {/* 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 (
+
+
{
+ // React Router handles navigation now
+ }}
+ />
+
+ {/* Main Content */}
+
+ {children || }
+
+
+ {/* Footer */}
+
+
+ )
+}
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 */}
-
-
- )
+ 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)}
+
+
navigate('/traders')}
+ className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105 active:scale-95"
+ style={{
+ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
+ color: '#0B0E11',
+ boxShadow: '0 4px 12px rgba(240, 185, 11, 0.3)',
+ }}
+ >
+ {t('goToTradersPage', language)}
+
+
+
+ )
+ }
+
+ // If traders is loaded and empty, show empty state
+ if (traders && traders.length === 0) {
+ return (
+
+
+
+
+ {t('dashboardEmptyTitle', language)}
+
+
+ {t('dashboardEmptyDescription', language)}
+
+
navigate('/traders')}
+ className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105 active:scale-95"
+ style={{
+ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
+ color: '#0B0E11',
+ boxShadow: '0 4px 12px rgba(240, 185, 11, 0.3)',
+ }}
+ >
+ {t('goToTradersPage', 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)}:
+
+ handleTraderSelect(e.target.value)}
+ className="rounded px-3 py-2 text-sm font-medium cursor-pointer transition-colors"
+ style={{
+ background: '#1E2329',
+ border: '1px solid #2B3139',
+ color: '#EAECEF',
+ }}
+ >
+ {traders.map((trader) => (
+
+ {trader.trader_name}
+
+ ))}
+
+
+ )}
+
+
+
+ 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 ? (
+
+
+
+
+
+ {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)}
+
+
+
+
+ {positions.map((pos, i) => (
+
+
+ {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 && (
+
+
setShowInputPrompt(!showInputPrompt)}
+ className="flex items-center gap-2 text-sm transition-colors"
+ style={{ color: '#60a5fa' }}
+ >
+
+ {t('inputPrompt', language)}
+
+
+ {showInputPrompt
+ ? t('collapse', language)
+ : t('expand', language)}
+
+
+ {showInputPrompt && (
+
+ {decision.input_prompt}
+
+ )}
+
+ )}
+
+ {/* AI Chain of Thought - Collapsible */}
+ {decision.cot_trace && (
+
+
setShowCoT(!showCoT)}
+ className="flex items-center gap-2 text-sm transition-colors"
+ style={{ color: '#F0B90B' }}
+ >
+
+ {' '}
+ {stripLeadingIcons(t('aiThinking', language))}
+
+
+ {showCoT ? t('collapse', language) : t('expand', language)}
+
+
+ {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: ,
+ },
+])