diff --git a/web/package-lock.json b/web/package-lock.json index 7048f643..857e30ce 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-slot": "^1.2.3", + "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -120,6 +121,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", @@ -451,6 +453,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -474,6 +477,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2033,8 +2037,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", @@ -2155,6 +2158,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" @@ -2165,6 +2169,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -2205,6 +2210,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", @@ -2529,6 +2535,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2829,7 +2836,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, "node_modules/autoprefixer": { @@ -2885,6 +2891,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2952,6 +2969,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -2999,7 +3017,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3260,7 +3277,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -3632,7 +3648,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -3682,8 +3697,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", @@ -3698,7 +3712,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -3826,7 +3839,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3836,7 +3848,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3881,7 +3892,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3894,7 +3904,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4006,6 +4015,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", @@ -4066,6 +4076,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -4527,6 +4538,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -4563,7 +4594,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -4634,7 +4664,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4706,7 +4735,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4740,7 +4768,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -4834,7 +4861,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4906,7 +4932,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4919,7 +4944,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -4935,7 +4959,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -5567,6 +5590,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" } @@ -5595,6 +5619,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", @@ -5969,7 +5994,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -5988,7 +6012,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6020,7 +6043,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6030,7 +6052,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -6560,6 +6581,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -6713,6 +6735,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -6742,7 +6765,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -6758,7 +6780,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -6769,7 +6790,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -6782,8 +6802,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", @@ -6800,6 +6819,12 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6834,6 +6859,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" }, @@ -6845,6 +6871,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" @@ -8036,6 +8063,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -8252,6 +8280,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" @@ -8402,6 +8431,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", @@ -9006,6 +9036,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -9542,6 +9573,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -9955,6 +9987,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 31de80f9..b1bc84a7 100644 --- a/web/package.json +++ b/web/package.json @@ -16,6 +16,7 @@ "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-slot": "^1.2.3", + "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", diff --git a/web/src/components/RegisterPage.tsx b/web/src/components/RegisterPage.tsx index 4131ad2c..fb4154cb 100644 --- a/web/src/components/RegisterPage.tsx +++ b/web/src/components/RegisterPage.tsx @@ -76,9 +76,9 @@ export function RegisterPage() { setQrCodeURL(result.qrCodeURL || '') setStep('setup-otp') } else { + // Only business errors reach here (system/network errors shown via toast) const msg = result.message || t('registrationFailed', language) setError(msg) - toast.error(msg) } setLoading(false) @@ -298,36 +298,7 @@ export function RegisterPage() { color: 'var(--binance-red)', }} > -
- {t('passwordRequirements', language)} -
- setPasswordValid(isValid)} - /> + {error} )} diff --git a/web/src/components/TraderConfigModal.tsx b/web/src/components/TraderConfigModal.tsx index 2fa53a7f..94031343 100644 --- a/web/src/components/TraderConfigModal.tsx +++ b/web/src/components/TraderConfigModal.tsx @@ -115,10 +115,22 @@ export function TraderConfigModal({ useEffect(() => { const fetchConfig = async () => { try { - const response = await httpClient.get('/api/config') - const config = await response.json() - if (config.default_coins) { - setAvailableCoins(config.default_coins) + const result = await httpClient.get<{ default_coins?: string[] }>( + '/api/config' + ) + if (result.success && result.data?.default_coins) { + setAvailableCoins(result.data.default_coins) + } else { + // 使用默认币种列表 + setAvailableCoins([ + 'BTCUSDT', + 'ETHUSDT', + 'SOLUSDT', + 'BNBUSDT', + 'XRPUSDT', + 'DOGEUSDT', + 'ADAUSDT', + ]) } } catch (error) { console.error('Failed to fetch config:', error) @@ -141,10 +153,14 @@ export function TraderConfigModal({ useEffect(() => { const fetchPromptTemplates = async () => { try { - const response = await httpClient.get('/api/prompt-templates') - const data = await response.json() - if (data.templates) { - setPromptTemplates(data.templates) + const result = await httpClient.get<{ templates?: { name: string }[] }>( + '/api/prompt-templates' + ) + if (result.success && result.data?.templates) { + setPromptTemplates(result.data.templates) + } else { + // 使用默认模板列表 + setPromptTemplates([{ name: 'default' }, { name: 'aggressive' }]) } } catch (error) { console.error('Failed to fetch prompt templates:', error) @@ -194,30 +210,26 @@ export function TraderConfigModal({ setBalanceFetchError('') try { - const token = localStorage.getItem('auth_token') - if (!token) { - throw new Error('未登录,请先登录') + const result = await httpClient.get<{ + total_equity?: number + balance?: number + }>(`/api/account?trader_id=${traderData.trader_id}`) + + if (result.success && result.data) { + // total_equity = 当前账户净值(包含未实现盈亏) + // 这应该作为新的初始余额 + const currentBalance = + result.data.total_equity || result.data.balance || 0 + + setFormData((prev) => ({ ...prev, initial_balance: currentBalance })) + toast.success('已获取当前余额') + } else { + throw new Error(result.message || '获取余额失败') } - - const response = await httpClient.get( - `/api/account?trader_id=${traderData.trader_id}`, - { - Authorization: `Bearer ${token}`, - } - ) - - const data = await response.json() - - // total_equity = 当前账户净值(包含未实现盈亏) - // 这应该作为新的初始余额 - const currentBalance = data.total_equity || data.balance || 0 - - setFormData((prev) => ({ ...prev, initial_balance: currentBalance })) - toast.success('已获取当前余额') } catch (error) { console.error('获取余额失败:', error) setBalanceFetchError('获取余额失败,请检查网络连接') - toast.error('获取余额失败,请检查网络连接') + // Note: Network/system errors already shown via toast by httpClient } finally { setIsFetchingBalance(false) } diff --git a/web/src/contexts/AuthContext.tsx b/web/src/contexts/AuthContext.tsx index 9d1cfd1c..0464994e 100644 --- a/web/src/contexts/AuthContext.tsx +++ b/web/src/contexts/AuthContext.tsx @@ -1,6 +1,6 @@ import React, { createContext, useContext, useState, useEffect } from 'react' import { getSystemConfig } from '../lib/config' -import { reset401Flag } from '../lib/httpClient' +import { reset401Flag, httpClient } from '../lib/httpClient' interface User { id: string @@ -183,39 +183,36 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { password: string, betaCode?: string ) => { - try { - const requestBody: { - email: string - password: string - beta_code?: string - } = { email, password } - if (betaCode) { - requestBody.beta_code = betaCode + const requestBody: { + email: string + password: string + beta_code?: string + } = { email, password } + if (betaCode) { + requestBody.beta_code = betaCode + } + + const result = await httpClient.post<{ + user_id: string + otp_secret: string + qr_code_url: string + message: string + }>('/api/register', requestBody) + + if (result.success && result.data) { + return { + success: true, + userID: result.data.user_id, + otpSecret: result.data.otp_secret, + qrCodeURL: result.data.qr_code_url, + message: result.message || result.data.message, } + } - const response = await fetch('/api/register', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody), - }) - - const data = await response.json() - - if (response.ok) { - return { - success: true, - userID: data.user_id, - otpSecret: data.otp_secret, - qrCodeURL: data.qr_code_url, - message: data.message, - } - } else { - return { success: false, message: data.error } - } - } catch (error) { - return { success: false, message: '注册失败,请重试' } + // Only business errors reach here (system/network errors were intercepted) + return { + success: false, + message: result.message || 'Registration failed', } } diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 9b70edd4..7189a35a 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -18,117 +18,92 @@ import { httpClient } from './httpClient' const API_BASE = '/api' -// Helper function to get auth headers -function getAuthHeaders(): Record { - const token = localStorage.getItem('auth_token') - const headers: Record = { - 'Content-Type': 'application/json', - } - - if (token) { - headers['Authorization'] = `Bearer ${token}` - } - - return headers -} - export const api = { // AI交易员管理接口 async getTraders(): Promise { - const res = await httpClient.get(`${API_BASE}/my-traders`, getAuthHeaders()) - if (!res.ok) throw new Error('获取trader列表失败') - return res.json() + const result = await httpClient.get(`${API_BASE}/my-traders`) + if (!result.success) throw new Error('获取trader列表失败') + return result.data! }, // 获取公开的交易员列表(无需认证) async getPublicTraders(): Promise { - const res = await httpClient.get(`${API_BASE}/traders`) - if (!res.ok) throw new Error('获取公开trader列表失败') - return res.json() + const result = await httpClient.get(`${API_BASE}/traders`) + if (!result.success) throw new Error('获取公开trader列表失败') + return result.data! }, async createTrader(request: CreateTraderRequest): Promise { - const res = await httpClient.post( + const result = await httpClient.post( `${API_BASE}/traders`, - request, - getAuthHeaders() + request ) - if (!res.ok) throw new Error('创建交易员失败') - return res.json() + if (!result.success) throw new Error('创建交易员失败') + return result.data! }, async deleteTrader(traderId: string): Promise { - const res = await httpClient.delete( - `${API_BASE}/traders/${traderId}`, - getAuthHeaders() - ) - if (!res.ok) throw new Error('删除交易员失败') + const result = await httpClient.delete(`${API_BASE}/traders/${traderId}`) + if (!result.success) throw new Error('删除交易员失败') }, async startTrader(traderId: string): Promise { - const res = await httpClient.post( - `${API_BASE}/traders/${traderId}/start`, - undefined, - getAuthHeaders() + const result = await httpClient.post( + `${API_BASE}/traders/${traderId}/start` ) - if (!res.ok) throw new Error('启动交易员失败') + if (!result.success) throw new Error('启动交易员失败') }, async stopTrader(traderId: string): Promise { - const res = await httpClient.post( - `${API_BASE}/traders/${traderId}/stop`, - undefined, - getAuthHeaders() - ) - if (!res.ok) throw new Error('停止交易员失败') + const result = await httpClient.post(`${API_BASE}/traders/${traderId}/stop`) + if (!result.success) throw new Error('停止交易员失败') }, async updateTraderPrompt( traderId: string, customPrompt: string ): Promise { - const res = await httpClient.put( + const result = await httpClient.put( `${API_BASE}/traders/${traderId}/prompt`, - { custom_prompt: customPrompt }, - getAuthHeaders() + { custom_prompt: customPrompt } ) - if (!res.ok) throw new Error('更新自定义策略失败') + if (!result.success) throw new Error('更新自定义策略失败') }, async getTraderConfig(traderId: string): Promise { - const res = await httpClient.get( - `${API_BASE}/traders/${traderId}/config`, - getAuthHeaders() + const result = await httpClient.get( + `${API_BASE}/traders/${traderId}/config` ) - if (!res.ok) throw new Error('获取交易员配置失败') - return res.json() + if (!result.success) throw new Error('获取交易员配置失败') + return result.data! }, async updateTrader( traderId: string, request: CreateTraderRequest ): Promise { - const res = await httpClient.put( + const result = await httpClient.put( `${API_BASE}/traders/${traderId}`, - request, - getAuthHeaders() + request ) - if (!res.ok) throw new Error('更新交易员失败') - return res.json() + if (!result.success) throw new Error('更新交易员失败') + return result.data! }, // AI模型配置接口 async getModelConfigs(): Promise { - const res = await httpClient.get(`${API_BASE}/models`, getAuthHeaders()) - if (!res.ok) throw new Error('获取模型配置失败') - return res.json() + const result = await httpClient.get(`${API_BASE}/models`) + if (!result.success) throw new Error('获取模型配置失败') + return result.data! }, // 获取系统支持的AI模型列表(无需认证) async getSupportedModels(): Promise { - const res = await httpClient.get(`${API_BASE}/supported-models`) - if (!res.ok) throw new Error('获取支持的模型失败') - return res.json() + const result = await httpClient.get( + `${API_BASE}/supported-models` + ) + if (!result.success) throw new Error('获取支持的模型失败') + return result.data! }, async updateModelConfigs(request: UpdateModelConfigRequest): Promise { @@ -150,37 +125,31 @@ export const api = { ) // 发送加密数据 - const res = await httpClient.put( - `${API_BASE}/models`, - encryptedPayload, - getAuthHeaders() - ) - if (!res.ok) throw new Error('更新模型配置失败') + const result = await httpClient.put(`${API_BASE}/models`, encryptedPayload) + if (!result.success) throw new Error('更新模型配置失败') }, // 交易所配置接口 async getExchangeConfigs(): Promise { - const res = await httpClient.get(`${API_BASE}/exchanges`, getAuthHeaders()) - if (!res.ok) throw new Error('获取交易所配置失败') - return res.json() + const result = await httpClient.get(`${API_BASE}/exchanges`) + if (!result.success) throw new Error('获取交易所配置失败') + return result.data! }, // 获取系统支持的交易所列表(无需认证) async getSupportedExchanges(): Promise { - const res = await httpClient.get(`${API_BASE}/supported-exchanges`) - if (!res.ok) throw new Error('获取支持的交易所失败') - return res.json() + const result = await httpClient.get( + `${API_BASE}/supported-exchanges` + ) + if (!result.success) throw new Error('获取支持的交易所失败') + return result.data! }, async updateExchangeConfigs( request: UpdateExchangeConfigRequest ): Promise { - const res = await httpClient.put( - `${API_BASE}/exchanges`, - request, - getAuthHeaders() - ) - if (!res.ok) throw new Error('更新交易所配置失败') + const result = await httpClient.put(`${API_BASE}/exchanges`, request) + if (!result.success) throw new Error('更新交易所配置失败') }, // 使用加密传输更新交易所配置 @@ -205,12 +174,11 @@ export const api = { ) // 发送加密数据 - const res = await httpClient.put( + const result = await httpClient.put( `${API_BASE}/exchanges`, - encryptedPayload, - getAuthHeaders() + encryptedPayload ) - if (!res.ok) throw new Error('更新交易所配置失败') + if (!result.success) throw new Error('更新交易所配置失败') }, // 获取系统状态(支持trader_id) @@ -218,9 +186,9 @@ export const api = { const url = traderId ? `${API_BASE}/status?trader_id=${traderId}` : `${API_BASE}/status` - const res = await httpClient.get(url, getAuthHeaders()) - if (!res.ok) throw new Error('获取系统状态失败') - return res.json() + const result = await httpClient.get(url) + if (!result.success) throw new Error('获取系统状态失败') + return result.data! }, // 获取账户信息(支持trader_id) @@ -228,17 +196,10 @@ export const api = { const url = traderId ? `${API_BASE}/account?trader_id=${traderId}` : `${API_BASE}/account` - const res = await httpClient.request(url, { - cache: 'no-store', - headers: { - ...getAuthHeaders(), - 'Cache-Control': 'no-cache', - }, - }) - if (!res.ok) throw new Error('获取账户信息失败') - const data = await res.json() - console.log('Account data fetched:', data) - return data + const result = await httpClient.get(url) + if (!result.success) throw new Error('获取账户信息失败') + console.log('Account data fetched:', result.data) + return result.data! }, // 获取持仓列表(支持trader_id) @@ -246,9 +207,9 @@ export const api = { const url = traderId ? `${API_BASE}/positions?trader_id=${traderId}` : `${API_BASE}/positions` - const res = await httpClient.get(url, getAuthHeaders()) - if (!res.ok) throw new Error('获取持仓列表失败') - return res.json() + const result = await httpClient.get(url) + if (!result.success) throw new Error('获取持仓列表失败') + return result.data! }, // 获取决策日志(支持trader_id) @@ -256,9 +217,9 @@ export const api = { const url = traderId ? `${API_BASE}/decisions?trader_id=${traderId}` : `${API_BASE}/decisions` - const res = await httpClient.get(url, getAuthHeaders()) - if (!res.ok) throw new Error('获取决策日志失败') - return res.json() + const result = await httpClient.get(url) + if (!result.success) throw new Error('获取决策日志失败') + return result.data! }, // 获取最新决策(支持trader_id和limit参数) @@ -272,12 +233,11 @@ export const api = { } params.append('limit', limit.toString()) - const res = await httpClient.get( - `${API_BASE}/decisions/latest?${params}`, - getAuthHeaders() + const result = await httpClient.get( + `${API_BASE}/decisions/latest?${params}` ) - if (!res.ok) throw new Error('获取最新决策失败') - return res.json() + if (!result.success) throw new Error('获取最新决策失败') + return result.data! }, // 获取统计信息(支持trader_id) @@ -285,9 +245,9 @@ export const api = { const url = traderId ? `${API_BASE}/statistics?trader_id=${traderId}` : `${API_BASE}/statistics` - const res = await httpClient.get(url, getAuthHeaders()) - if (!res.ok) throw new Error('获取统计信息失败') - return res.json() + const result = await httpClient.get(url) + if (!result.success) throw new Error('获取统计信息失败') + return result.data! }, // 获取收益率历史数据(支持trader_id) @@ -295,32 +255,35 @@ export const api = { const url = traderId ? `${API_BASE}/equity-history?trader_id=${traderId}` : `${API_BASE}/equity-history` - const res = await httpClient.get(url, getAuthHeaders()) - if (!res.ok) throw new Error('获取历史数据失败') - return res.json() + const result = await httpClient.get(url) + if (!result.success) throw new Error('获取历史数据失败') + return result.data! }, // 批量获取多个交易员的历史数据(无需认证) async getEquityHistoryBatch(traderIds: string[]): Promise { - const res = await httpClient.post(`${API_BASE}/equity-history-batch`, { - trader_ids: traderIds, - }) - if (!res.ok) throw new Error('获取批量历史数据失败') - return res.json() + const result = await httpClient.post( + `${API_BASE}/equity-history-batch`, + { trader_ids: traderIds } + ) + if (!result.success) throw new Error('获取批量历史数据失败') + return result.data! }, // 获取前5名交易员数据(无需认证) async getTopTraders(): Promise { - const res = await httpClient.get(`${API_BASE}/top-traders`) - if (!res.ok) throw new Error('获取前5名交易员失败') - return res.json() + const result = await httpClient.get(`${API_BASE}/top-traders`) + if (!result.success) throw new Error('获取前5名交易员失败') + return result.data! }, // 获取公开交易员配置(无需认证) async getPublicTraderConfig(traderId: string): Promise { - const res = await httpClient.get(`${API_BASE}/trader/${traderId}/config`) - if (!res.ok) throw new Error('获取公开交易员配置失败') - return res.json() + const result = await httpClient.get( + `${API_BASE}/trader/${traderId}/config` + ) + if (!result.success) throw new Error('获取公开交易员配置失败') + return result.data! }, // 获取AI学习表现分析(支持trader_id) @@ -328,16 +291,18 @@ export const api = { const url = traderId ? `${API_BASE}/performance?trader_id=${traderId}` : `${API_BASE}/performance` - const res = await httpClient.get(url, getAuthHeaders()) - if (!res.ok) throw new Error('获取AI学习数据失败') - return res.json() + const result = await httpClient.get(url) + if (!result.success) throw new Error('获取AI学习数据失败') + return result.data! }, // 获取竞赛数据(无需认证) async getCompetition(): Promise { - const res = await httpClient.get(`${API_BASE}/competition`) - if (!res.ok) throw new Error('获取竞赛数据失败') - return res.json() + const result = await httpClient.get( + `${API_BASE}/competition` + ) + if (!result.success) throw new Error('获取竞赛数据失败') + return result.data! }, // 用户信号源配置接口 @@ -345,27 +310,23 @@ export const api = { coin_pool_url: string oi_top_url: string }> { - const res = await httpClient.get( - `${API_BASE}/user/signal-sources`, - getAuthHeaders() - ) - if (!res.ok) throw new Error('获取用户信号源配置失败') - return res.json() + const result = await httpClient.get<{ + coin_pool_url: string + oi_top_url: string + }>(`${API_BASE}/user/signal-sources`) + if (!result.success) throw new Error('获取用户信号源配置失败') + return result.data! }, async saveUserSignalSource( coinPoolUrl: string, oiTopUrl: string ): Promise { - const res = await httpClient.post( - `${API_BASE}/user/signal-sources`, - { - coin_pool_url: coinPoolUrl, - oi_top_url: oiTopUrl, - }, - getAuthHeaders() - ) - if (!res.ok) throw new Error('保存用户信号源配置失败') + const result = await httpClient.post(`${API_BASE}/user/signal-sources`, { + coin_pool_url: coinPoolUrl, + oi_top_url: oiTopUrl, + }) + if (!result.success) throw new Error('保存用户信号源配置失败') }, // 获取服务器IP(需要认证,用于白名单配置) @@ -373,8 +334,11 @@ export const api = { public_ip: string message: string }> { - const res = await httpClient.get(`${API_BASE}/server-ip`, getAuthHeaders()) - if (!res.ok) throw new Error('获取服务器IP失败') - return res.json() + const result = await httpClient.get<{ + public_ip: string + message: string + }>(`${API_BASE}/server-ip`) + if (!result.success) throw new Error('获取服务器IP失败') + return result.data! }, } diff --git a/web/src/lib/httpClient.ts b/web/src/lib/httpClient.ts index 079e6bdf..3c97cac7 100644 --- a/web/src/lib/httpClient.ts +++ b/web/src/lib/httpClient.ts @@ -1,18 +1,47 @@ /** - * HTTP Client with unified error handling and 401 interception + * HTTP Client with Axios * * Features: - * - Unified fetch wrapper + * - Axios-based unified request wrapper + * - Automatic error interception and toast notifications + * - Network errors and system errors are intercepted and shown via toast + * - Only business logic errors are returned to the caller * - Automatic 401 token expiration handling - * - Auth state cleanup on unauthorized - * - Automatic redirect to login page - * - Notification shown on login page after redirect */ +import axios, { AxiosInstance, AxiosError, AxiosResponse } from 'axios' +import { toast } from 'sonner' + +/** + * Business response format - only business errors reach the caller + */ +export interface ApiResponse { + success: boolean + data?: T + message?: string +} + +/** + * HTTP Client Class + */ export class HttpClient { - // Singleton flag to prevent duplicate 401 handling + private axiosInstance: AxiosInstance private static isHandling401 = false + constructor() { + // Create axios instance + this.axiosInstance = axios.create({ + baseURL: '/', + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, + }) + + // Setup interceptors + this.setupInterceptors() + } + /** * Reset 401 handling flag (call after successful login) */ @@ -21,137 +50,212 @@ export class HttpClient { } /** - * Response interceptor - handles common HTTP errors - * - * @param response - Fetch Response object - * @returns Response if successful - * @throws Error with user-friendly message + * Setup request and response interceptors */ - private async handleResponse(response: Response): Promise { - // Handle 401 Unauthorized - Token expired or invalid - if (response.status === 401) { - // Prevent duplicate 401 handling when multiple API calls fail simultaneously + private setupInterceptors(): void { + // Request interceptor - add auth token + this.axiosInstance.interceptors.request.use( + (config) => { + const token = localStorage.getItem('auth_token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => { + return Promise.reject(error) + } + ) + + // Response interceptor - handle errors + this.axiosInstance.interceptors.response.use( + (response: AxiosResponse) => { + // Success response - pass through + return response + }, + (error: AxiosError) => { + return this.handleError(error) + } + ) + } + + /** + * Handle different types of errors + * Network and system errors are intercepted and shown via toast + * Only business errors are returned to caller + */ + private async handleError(error: AxiosError): Promise { + // Network error (no response from server) + if (!error.response) { + toast.error('Network error - Please check your connection', { + description: 'Unable to reach the server', + }) + throw new Error('Network error') + } + + const { status } = error.response as AxiosResponse<{ + error?: string + message?: string + }> + + // Handle 401 Unauthorized + if (status === 401) { if (HttpClient.isHandling401) { - throw new Error('登录已过期,请重新登录') + throw new Error('Session expired') } - // Set flag to prevent race conditions HttpClient.isHandling401 = true - // Clean up local storage + // Clean up localStorage.removeItem('auth_token') localStorage.removeItem('auth_user') - // Notify global listeners (AuthContext will react to this) + // Notify global listeners window.dispatchEvent(new Event('unauthorized')) // Only redirect if not already on login page if (!window.location.pathname.includes('/login')) { - // Save current location for post-login redirect const returnUrl = window.location.pathname + window.location.search if (returnUrl !== '/login' && returnUrl !== '/') { sessionStorage.setItem('returnUrl', returnUrl) } - // Mark that user came from 401 (login page will show notification) sessionStorage.setItem('from401', 'true') - - // Redirect immediately to login page window.location.href = '/login' - // Return pending promise to prevent error from being caught by SWR/React - // The notification will be shown on the login page - return new Promise(() => {}) as Promise + // Return pending promise + return new Promise(() => {}) } - throw new Error('登录已过期,请重新登录') + throw new Error('Session expired') } - // Handle other common errors - if (response.status === 403) { - throw new Error('没有权限访问此资源') + // Handle 403 Forbidden - system error + if (status === 403) { + toast.error('Permission Denied', { + description: 'You do not have permission to access this resource', + }) + throw new Error('Permission denied') } - if (response.status === 404) { - throw new Error('请求的资源不存在') + // Handle 404 Not Found - system error + if (status === 404) { + toast.error('API Not Found', { + description: 'The requested endpoint does not exist (404)', + }) + throw new Error('API not found') } - if (response.status >= 500) { - throw new Error('服务器错误,请稍后重试') + // Handle 500+ Server Error - system error + if (status >= 500) { + toast.error('Server Error', { + description: 'Please try again later or contact support', + }) + throw new Error('Server error') } - return response + // 4xx errors (except 401/403/404) are business logic errors + // Return them to the caller for handling + return Promise.reject(error) + } + + /** + * Generic JSON request with standardized response + * System/network errors are already intercepted and shown via toast + * Only business errors are returned + */ + async request( + url: string, + options: { + method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' + data?: any + params?: any + headers?: Record + } = {} + ): Promise> { + try { + const response = await this.axiosInstance.request({ + url, + method: options.method || 'GET', + data: options.data, + params: options.params, + headers: options.headers, + }) + + // Success + return { + success: true, + data: response.data, + message: (response.data as any)?.message, + } + } catch (error) { + // If we get here, it's a business logic error (4xx except 401/403/404) + // System errors were already intercepted and toasted + if (axios.isAxiosError(error) && error.response) { + const errorData = error.response.data as any + return { + success: false, + message: errorData?.error || errorData?.message || 'Operation failed', + } + } + + // Network error or other exception (already toasted) + throw error + } } /** * GET request */ - async get(url: string, headers?: Record): Promise { - const response = await fetch(url, { - method: 'GET', - headers, - }) - return this.handleResponse(response) + async get( + url: string, + params?: any, + headers?: Record + ): Promise> { + return this.request(url, { method: 'GET', params, headers }) } /** * POST request */ - async post( + async post( url: string, - body?: any, + data?: any, headers?: Record - ): Promise { - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...headers, - }, - body: body ? JSON.stringify(body) : undefined, - }) - return this.handleResponse(response) + ): Promise> { + return this.request(url, { method: 'POST', data, headers }) } /** * PUT request */ - async put( + async put( url: string, - body?: any, + data?: any, headers?: Record - ): Promise { - const response = await fetch(url, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - ...headers, - }, - body: body ? JSON.stringify(body) : undefined, - }) - return this.handleResponse(response) + ): Promise> { + return this.request(url, { method: 'PUT', data, headers }) } /** * DELETE request */ - async delete( + async delete( url: string, headers?: Record - ): Promise { - const response = await fetch(url, { - method: 'DELETE', - headers, - }) - return this.handleResponse(response) + ): Promise> { + return this.request(url, { method: 'DELETE', headers }) } /** - * Generic request method for custom configurations + * PATCH request */ - async request(url: string, options: RequestInit = {}): Promise { - const response = await fetch(url, options) - return this.handleResponse(response) + async patch( + url: string, + data?: any, + headers?: Record + ): Promise> { + return this.request(url, { method: 'PATCH', data, headers }) } }