diff --git a/client/index.html b/client/index.html index e60adad..e398811 100644 --- a/client/index.html +++ b/client/index.html @@ -8,6 +8,13 @@ href="/src/assets/SlimeArtTransparent.png" /> + + + + + + + SMAASH diff --git a/client/package-lock.json b/client/package-lock.json index 40f3df1..6401133 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -17,9 +17,14 @@ "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.18", + "@tanstack/query-sync-storage-persister": "^5.90.24", + "@tanstack/react-query": "^5.90.21", + "@tanstack/react-query-devtools": "^5.91.3", + "@tanstack/react-query-persist-client": "^5.90.24", "@vscode/codicons": "^0.0.44", "add": "^2.0.6", "avatar": "^0.1.0", + "axios": "^1.13.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "combobox": "^1.0.1", @@ -31,6 +36,7 @@ "react": "^19.2.0", "react-colorful": "^5.6.1", "react-dom": "^19.2.0", + "react-google-recaptcha-v3": "^1.11.0", "react-markdown": "^10.1.0", "react-resizable-panels": "^4.6.5", "react-router-dom": "^7.13.0", @@ -42,6 +48,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@tanstack/react-virtual": "^3.13.21", "@types/node": "^24.10.9", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", @@ -51,6 +58,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "rollup-plugin-visualizer": "^7.0.1", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", "typescript-eslint": "^8.0.0", @@ -5867,6 +5875,132 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.93.0.tgz", + "integrity": "sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-persist-client-core": { + "version": "5.92.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.92.1.tgz", + "integrity": "sha512-XGzB1lulFrGc8UwQnMI12r71R7ock/XOZvDaz3Fu3xrxCFwLHuFcABAOkIolS/6hFHe0pRdsBRXd4Q8ECqiCug==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-sync-storage-persister": { + "version": "5.90.24", + "resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-5.90.24.tgz", + "integrity": "sha512-/L0XnUNe+XanPpnTka9J/5M8Xj/6Ap1zre1fVLN/HsoMlLrFiqDijwMiwvwbSuaCyVUnH8Kh8ECXX5fPZtEsBg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20", + "@tanstack/query-persist-client-core": "5.92.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.91.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.91.3.tgz", + "integrity": "sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.93.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.90.20", + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-persist-client": { + "version": "5.90.24", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.90.24.tgz", + "integrity": "sha512-FkfU37vHq61Efr/qGiz+CUNmGfCky1jjsaZFuS5MsWwA9vPHudCwmdirgyTx+RfcQxyHON904q/pc48zrIEhxg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-persist-client-core": "5.92.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.90.21", + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.21.tgz", + "integrity": "sha512-SYXFrmrbPgXBvf+HsOsKhFgqSe4M6B29VHOsX9Jih9TlNkNkDWx0hWMiMLUghMEzyUz772ndzdEeCEBx+3GIZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.21" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.21", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.21.tgz", + "integrity": "sha512-ww+fmLHyCbPSf7JNbWZP3g7wl6SdNo3ah5Aiw+0e9FDErkVHLKprYUrwTm7dF646FtEkN/KkAKPYezxpmvOjxw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@ts-morph/common": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", @@ -6534,6 +6668,12 @@ "node": ">= 0.6" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/avatar": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/avatar/-/avatar-0.1.0.tgz", @@ -6551,6 +6691,17 @@ "url4data": "^0.1.0" } }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-plugin-react-compiler": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", @@ -7025,6 +7176,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/combobox": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/combobox/-/combobox-1.0.1.tgz", @@ -7367,6 +7530,15 @@ "integrity": "sha512-zpqiCT8bODLu3QSmLLic8xJnYWBFjOSu/fBCm189oAiTtPq/PSanNACKZDS7kgSyCJY7P+IcODzlIogBK/9RBg==", "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7579,6 +7751,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -8316,6 +8503,63 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/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/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -8859,6 +9103,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -8934,6 +9193,15 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/hono": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.0.tgz", @@ -11702,6 +11970,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/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", @@ -12022,6 +12296,25 @@ "react": "^19.2.4" } }, + "node_modules/react-google-recaptcha-v3": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/react-google-recaptcha-v3/-/react-google-recaptcha-v3-1.11.0.tgz", + "integrity": "sha512-kLQqpz/77m8+trpBwzqcxNtvWZYoZ/YO6Vm2cVTHW8hs80BWUfDpC7RDwuAvpswwtSYApWfaSpIDFWAIBNIYxQ==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "react": "^16.3 || ^17.0 || ^18.0 || ^19.0", + "react-dom": "^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-markdown": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", @@ -12422,6 +12715,121 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-visualizer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-7.0.1.tgz", + "integrity": "sha512-UJUT4+1Ho4OcWmPYU3sYXgUqI8B8Ayfe06MX7y0qCJ1K8aGoKtR/NDd/2nZqM7ADkrzny+I99Ul7GgyoiVNAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "open": "^11.0.0", + "picomatch": "^4.0.2", + "source-map": "^0.7.4", + "yargs": "^18.0.0" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "rolldown": "1.x || ^1.0.0-beta || ^1.0.0-rc", + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", diff --git a/client/package.json b/client/package.json index a8d1939..68c94c4 100644 --- a/client/package.json +++ b/client/package.json @@ -19,9 +19,14 @@ "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.18", + "@tanstack/query-sync-storage-persister": "^5.90.24", + "@tanstack/react-query": "^5.90.21", + "@tanstack/react-query-devtools": "^5.91.3", + "@tanstack/react-query-persist-client": "^5.90.24", "@vscode/codicons": "^0.0.44", "add": "^2.0.6", "avatar": "^0.1.0", + "axios": "^1.13.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "combobox": "^1.0.1", @@ -33,6 +38,7 @@ "react": "^19.2.0", "react-colorful": "^5.6.1", "react-dom": "^19.2.0", + "react-google-recaptcha-v3": "^1.11.0", "react-markdown": "^10.1.0", "react-resizable-panels": "^4.6.5", "react-router-dom": "^7.13.0", @@ -44,6 +50,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@tanstack/react-virtual": "^3.13.21", "@types/node": "^24.10.9", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", @@ -53,6 +60,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "rollup-plugin-visualizer": "^7.0.1", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", "typescript-eslint": "^8.0.0", diff --git a/client/src/App.css b/client/src/App.css index b9d355d..05d0a2f 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1,8 +1,9 @@ #root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; + width: 100%; + min-height: 100vh; + margin: 0; + padding: 0; + text-align: initial; } .logo { diff --git a/client/src/RootLayout.tsx b/client/src/RootLayout.tsx index 2bc887e..300c34e 100644 --- a/client/src/RootLayout.tsx +++ b/client/src/RootLayout.tsx @@ -1,10 +1,44 @@ import { Outlet } from "react-router-dom"; +import { QueryClient } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; +import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; import { AuthProvider } from "./context/AuthProvider"; import { SettingsProvider } from "./components/pages/profileDependents/settings/settingsLogic/SettingsContext"; import { NavbarProvider } from "./context/NavbarContext"; import { ColorProvider } from "./components/pages/profileDependents/settings/settingsLogic/color/ColorProvider"; import { ProfileProvider } from "@/components/forms/addNewProfile/ProfilesContext"; import { Wrapper } from "./Wrapper"; +import { Suspense } from "react"; + +/** + * Create a single QueryClient instance for the entire app. + * This is initialized outside the component to avoid recreating it on every render. + */ +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 2 * 60 * 1000, // 2 minutes (reduced from 5) + gcTime: 10 * 60 * 1000, // 10 minutes (garbage collection) + retry: 1, + refetchOnWindowFocus: true, + refetchOnReconnect: true, + networkMode: "online", + }, + mutations: { + retry: 1, + networkMode: "online", + }, + }, +}); + +/** + * Create a persister for localStorage caching. + * This enables offline support and faster app startup. + */ +const persister = createSyncStoragePersister({ + storage: window.localStorage, +}); /** * Root layout rendered by React Router. All providers live here so every @@ -12,18 +46,32 @@ import { Wrapper } from "./Wrapper"; */ export function RootLayout() { return ( - - - - - - - - - - - - - + + + + + + + + +
+ + } + > + +
+
+
+
+
+
+
+ +
); } diff --git a/client/src/Wrapper.tsx b/client/src/Wrapper.tsx index f1a4b78..08f1d34 100644 --- a/client/src/Wrapper.tsx +++ b/client/src/Wrapper.tsx @@ -1,7 +1,12 @@ -import { useContext, useRef } from "react"; +import { useContext } from "react"; import { ColorContext } from "./components/pages/profileDependents/settings/settingsLogic/color/ColorContext"; import { useSettings } from "./components/pages/profileDependents/settings/settingsLogic/SettingsContext"; -import { useColorAnimation } from "./lib/miscAnimations/ColorInterpolation"; +import { + getAverageHexColor, + getTextColor, + lightenHexColor, + toRgbaColor, +} from "./lib/utils"; interface WrapperProps { children: React.ReactNode; @@ -10,31 +15,43 @@ interface WrapperProps { export function Wrapper({ children }: WrapperProps) { const context = useContext(ColorContext); const { settings } = useSettings(); - const wrapperRef = useRef(null); const colorLeft = context?.colorLeft || "#616161"; const colorMiddle = context?.colorMiddle || "#000000"; const colorRight = context?.colorRight || "#616161"; const currentGradient = `linear-gradient(to right, ${colorLeft}, ${colorMiddle}, ${colorRight})`; + const themeAverage = getAverageHexColor([colorLeft, colorMiddle, colorRight]); + const themeAccent = lightenHexColor( + themeAverage, + settings.useDarkMode ? 0.08 : 0.02, + ); + const themeAccentHover = lightenHexColor( + themeAverage, + settings.useDarkMode ? 0.22 : 0.14, + ); + const themeAccentSoft = toRgbaColor( + themeAverage, + settings.useDarkMode ? 0.32 : 0.25, + ); + const themeNavBorder = themeAverage; + const themeNavShadow = toRgbaColor( + lightenHexColor(themeAverage, settings.useDarkMode ? 0.25 : 0.16), + settings.useDarkMode ? 0.42 : 0.34, + ); - // Use the animation hook for smooth color transitions - useColorAnimation({ - elementRef: wrapperRef as React.RefObject, - gradient: currentGradient, - duration: 0.6, - useAnimation: settings.useAnimations, - }); + const textColor = getTextColor(settings.useLiquidGlass, settings.useDarkMode); return (
{children} diff --git a/client/src/components/forms/LoginForm.tsx b/client/src/components/forms/LoginForm.tsx index 63422db..6cc3c40 100644 --- a/client/src/components/forms/LoginForm.tsx +++ b/client/src/components/forms/LoginForm.tsx @@ -12,7 +12,7 @@ import { Input } from "../ui/input"; import { Link, useNavigate } from "react-router-dom"; import React, { useEffect } from "react"; import { AuthContext } from "@/context/AuthContext"; -import { apiLogin } from "@/hooks/useApi"; +import { useLoginMutation } from "@/hooks/useQueryHooks"; export function LoginForm({ className, @@ -20,40 +20,67 @@ export function LoginForm({ }: React.ComponentProps<"div">) { const [password, setPassword] = React.useState(""); const [email, setEmail] = React.useState(""); - const [error, setError] = React.useState(""); - const { isLoggedIn, setIsLoggedIn, setUserId } = + const { isLoggedIn, setIsLoggedIn, setUserId, setIsAdmin } = React.useContext(AuthContext); const navigate = useNavigate(); + const loginMutation = useLoginMutation(); - // Calls the centralized login API; on success stores the user id in context. - const Login = async () => { - setError(""); - try { - const { data, ok } = await apiLogin({ email, password }); - - if (ok) { - console.log("Login successful"); - - if (data?.id !== undefined && data?.id !== null) { - setUserId(BigInt(data.id)); - } + const parseUserId = (value: unknown): bigint | null => { + if (typeof value === "bigint") return value; + if (typeof value === "number" && Number.isFinite(value)) { + return BigInt(Math.trunc(value)); + } + if (typeof value === "string" && value.trim() !== "") { + try { + return BigInt(value); + } catch { + return null; + } + } + return null; + }; - setIsLoggedIn(true); - } else { - setError("Login failed"); - setIsLoggedIn(false); + const parseRoleId = (value: unknown): number | null => { + if (typeof value === "number" && Number.isFinite(value)) { + return Math.trunc(value); + } + if (typeof value === "string" && value.trim() !== "") { + const parsed = Number(value); + if (Number.isFinite(parsed)) { + return Math.trunc(parsed); } - } catch (err) { - console.error(err); - setError("An error occurred"); - setIsLoggedIn(false); } + return null; }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - await Login(); + + // Reset stale auth state first so a previous account cannot leak through. + setIsLoggedIn(false); + setUserId(null); + setIsAdmin(false); + + try { + const response = await loginMutation.mutateAsync({ email, password }); + + const parsedUserId = parseUserId(response?.id); + if (parsedUserId === null) { + return; + } + + const parsedRoleId = parseRoleId( + response?.role_id ?? response?.roleId ?? response?.role?.id, + ); + + console.log("Login successful"); + setUserId(parsedUserId); + setIsAdmin(parsedRoleId === 1); + setIsLoggedIn(true); + } catch { + // Error is handled by mutation state + } }; useEffect(() => { @@ -63,8 +90,8 @@ export function LoginForm({ }, [navigate, isLoggedIn]); return ( -
- +
+ Login to your account @@ -75,7 +102,9 @@ export function LoginForm({
- Email + + Email +
- Password + + Password + setPassword(e.target.value)} required + disabled={loginMutation.isPending} /> - {error &&

{error}

} + {loginMutation.isError && ( +

+ {String(loginMutation.error?.response?.data) || + "Login failed"} +

+ )} - Don't have an account?{" "} diff --git a/client/src/components/forms/PasswordResetForm.tsx b/client/src/components/forms/PasswordResetForm.tsx index 000c438..8638ca2 100644 --- a/client/src/components/forms/PasswordResetForm.tsx +++ b/client/src/components/forms/PasswordResetForm.tsx @@ -1,28 +1,23 @@ -import { cn } from "@/lib/utils" -import { Button } from "../ui/button" +import { cn } from "@/lib/utils"; +import { Button } from "../ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, -} from "../ui/card" -import { - Field, - FieldDescription, - FieldGroup, - FieldLabel, -} from "../ui/field" -import { Input } from "../ui/input" -import { Link } from "react-router-dom" +} from "../ui/card"; +import { Field, FieldDescription, FieldGroup, FieldLabel } from "../ui/field"; +import { Input } from "../ui/input"; +import { Link } from "react-router-dom"; export function PasswordResetForm({ className, ...props }: React.ComponentProps<"div">) { return ( -
- +
+ Recover your account @@ -33,7 +28,9 @@ export function PasswordResetForm({ - Email + + Email + - + - Don't have an account? Sign up + Don't have an account?{" "} + Sign up @@ -52,5 +52,5 @@ export function PasswordResetForm({
- ) + ); } diff --git a/client/src/components/forms/ProfileSelectorForm.tsx b/client/src/components/forms/ProfileSelectorForm.tsx index 44225c8..d8ccd8a 100644 --- a/client/src/components/forms/ProfileSelectorForm.tsx +++ b/client/src/components/forms/ProfileSelectorForm.tsx @@ -1,15 +1,77 @@ -import { cn } from "@/lib/utils"; +import { + cn, + getLiquidGlassClasses, + getLiquidGlassTextShadow, +} from "@/lib/utils"; import { useSettings } from "../pages/profileDependents/settings/settingsLogic/SettingsContext"; +import type { SettingsState } from "../pages/profileDependents/settings/settingsLogic/SettingsContext"; import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { Label } from "../ui/label"; import { Plus, Trash2, Play } from "lucide-react"; import { Button } from "../ui/button"; -import { useState } from "react"; +import { useState, useMemo, memo } from "react"; import { useNavigate } from "react-router-dom"; import { AddNewProfile } from "./addNewProfile/AddNewProfile"; import { useProfiles } from "./addNewProfile/useProfiles"; import * as motion from "motion/react-client"; +const ProfileAvatar = memo(function ProfileAvatar({ + profile, + isManaging, + settings, + onProfileClick, +}: { + profile: { name: string; avatar: string }; + isManaging: boolean; + settings: SettingsState; + onProfileClick: (name: string) => void; +}) { + return ( +
+ + onProfileClick(profile.name)} + className={`text-white cursor-pointer ${settings.useLiquidGlass ? `${getLiquidGlassClasses(settings.useLiquidGlass, settings.useDarkMode)} border-2 ${getLiquidGlassTextShadow(settings.useLiquidGlass, settings.useDarkMode)} ${isManaging ? "border-red-400" : settings.useDarkMode ? "border-black/40" : "border-white/30"}` : `${isManaging ? "border-red-500" : "border-(--theme-accent)"} border-2 bg-amber-200`}`} + > + + + {isManaging ? ( + + ) : ( + + )} + + + {profile.name + ? profile.name + .split(" ") + .map((n) => n[0]) + .slice(0, 2) + .join("") + : "NA"} + + + + {profile.name} + + +
+ ); +}); + export function ProfileSelectorForm() { const { settings } = useSettings(); const { profiles, removeProfile, selectProfile } = useProfiles(); @@ -18,25 +80,27 @@ export function ProfileSelectorForm() { const [isManaging, setIsManaging] = useState(false); const navigate = useNavigate(); - const handleProfileClick = async (name: string) => { - if (isManaging) { - try { - await removeProfile(name); - } catch (error) { - console.error("Failed to delete profile:", error); + const handleProfileClick = useMemo( + () => async (name: string) => { + if (isManaging) { + try { + await removeProfile(name); + } catch (error) { + console.error("Failed to delete profile:", error); + } + return; } - return; - } - // set the selected profile in context so other pages can render it - selectProfile(name); - navigate("/app/releases"); - }; + selectProfile(name); + navigate("/app/releases"); + }, + [isManaging, removeProfile, selectProfile, navigate], + ); return (
@@ -48,53 +112,13 @@ export function ProfileSelectorForm() {
{profiles.map((p) => ( -
- - handleProfileClick(p.name)} - className={`text-white cursor-pointer ${settings.useLiquidGlass ? `bg-white/30 backdrop-blur-lg border-2 shadow-sm shadow-white/20[text-shadow:0_2px_4px_rgba(163,163,163,0.8)] ${isManaging ? "border-red-400" : "border-white/30"}` : `${isManaging ? "border-red-500" : "border-green-500"} border-2 bg-amber-200`} `} - > - - - {isManaging ? ( - - ) : ( - - )} - - - {p.name - ? p.name - .split(" ") - .map((n) => n[0]) - .slice(0, 2) - .join("") - : "NA"} - - - - - {p.name} - - -
+ ))} {profileCount < 5 && ( Add New @@ -123,7 +147,7 @@ export function ProfileSelectorForm() { diff --git a/client/src/components/forms/SignUpForm.tsx b/client/src/components/forms/SignUpForm.tsx index e549d33..b34fa8c 100644 --- a/client/src/components/forms/SignUpForm.tsx +++ b/client/src/components/forms/SignUpForm.tsx @@ -13,120 +13,194 @@ import { FieldLabel, } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; import { Link, useNavigate } from "react-router-dom"; import React from "react"; import { generateRandomUsername } from "@/lib/GenerateRandomUsername"; -import { apiSignup } from "@/hooks/useApi"; +import { useSignupMutation } from "@/hooks/useQueryHooks"; +import { + GoogleReCaptcha, + GoogleReCaptchaProvider, +} from "react-google-recaptcha-v3"; -export function SignupForm({ ...props }: React.ComponentProps) { +export function SignupForm({ + className, + ...props +}: React.ComponentProps<"div">) { + const recaptchaSiteKey = "6LeA2IQsAAAAAAK7ljf7tDqBjwR_rm5uDAzGbr8S"; + const captchaEnabled = recaptchaSiteKey.trim().length > 0; const [password, setPassword] = React.useState(""); const [confirmPassword, setConfirmPassword] = React.useState(""); const [username, setUsername] = React.useState(""); const [email, setEmail] = React.useState(""); - const [error, setError] = React.useState(""); + const [captchaToken, setCaptchaToken] = React.useState(null); + const [validationError, setValidationError] = React.useState(""); const navigate = useNavigate(); const randomUsername = generateRandomUsername(); - - // Calls the centralized signup API; navigates to login on success. - const signup = async () => { - try { - const { ok } = await apiSignup({ email, password, username }); - if (ok) { - console.log("Signup successful"); - navigate("/app/login"); - } else { - setError("Signup failed"); - } - } catch (err) { - console.error(err); - setError("An error occurred"); - } - }; + const signupMutation = useSignupMutation(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + + // Validate username length + if (username.length < 3) { + setValidationError("Username must be at least 3 characters long"); + return; + } + if (password !== confirmPassword) { - setError("Passwords do not match"); + setValidationError("Passwords do not match"); return; } if (password.length < 8) { - setError("Password must be at least 8 characters long"); + setValidationError("Password must be at least 8 characters long"); + return; + } + if (captchaEnabled && !captchaToken) { + setValidationError("Captcha verification failed. Please try again."); return; } - setError(""); - await signup(); + setValidationError(""); + + try { + await signupMutation.mutateAsync({ email, password, username }); + console.log("Signup successful"); + navigate("/app/login"); + } catch (err) { + const error = err as { response?: { data?: string; status?: number } }; + console.error("Signup error:", err); + console.error("Error response:", error?.response); + console.error("Error data:", error?.response?.data); + console.error("Error status:", error?.response?.status); + + // Display backend error to user + if (error?.response?.data) { + setValidationError(error.response.data.toString()); + } + } }; return ( - - - Create an account - - Enter your information below to create your account - - - - - - - Username - setUsername(e.target.value)} - required - /> - - - Email - setEmail(e.target.value)} - required - /> - - - Password - setPassword(e.target.value)} - required - /> - - Must be at least 8 characters long. - - - - - Confirm Password - - setConfirmPassword(e.target.value)} - required - /> - Please confirm your password. - - {error &&

{error}

} - - - - Already have an account? Sign in - - -
- -
-
+
+ + + Create an account + + Enter your information below to create your account + + + +
+ + + + Username + + setUsername(e.target.value)} + disabled={signupMutation.isPending} + required + /> + + Must be at least 3 characters long. + + + + + Email + + setEmail(e.target.value)} + disabled={signupMutation.isPending} + required + /> + + + + Password + + setPassword(e.target.value)} + disabled={signupMutation.isPending} + required + /> + + Must be at least 8 characters long. + + + + + Confirm Password + + setConfirmPassword(e.target.value)} + disabled={signupMutation.isPending} + required + /> + Please confirm your password. + + {validationError &&

{validationError}

} + {signupMutation.isError && ( +

Signup failed

+ )} + + {captchaEnabled ? ( + + { + setCaptchaToken(token); + }} + /> + + {captchaToken + ? "Captcha verified" + : "Verifying captcha..."} + + + ) : ( + + Captcha key is empty. Captcha is currently disabled. + + )} + + + + + Already have an account? Sign in + + +
+
+
+
+
); } diff --git a/client/src/components/forms/addNewProfile/AddNewProfile.tsx b/client/src/components/forms/addNewProfile/AddNewProfile.tsx index 0b0bd4f..745d9ea 100644 --- a/client/src/components/forms/addNewProfile/AddNewProfile.tsx +++ b/client/src/components/forms/addNewProfile/AddNewProfile.tsx @@ -25,6 +25,7 @@ export function AddNewProfile({ open, onOpenChange }: AddNewProfileProps) { const { addProfile, profiles } = useProfiles(); const [username, setUsername] = useState(""); const [error, setError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); // Generate a username that's not already in `profiles`. const generateUniqueUsername = () => { @@ -57,8 +58,21 @@ export function AddNewProfile({ open, onOpenChange }: AddNewProfileProps) { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + if (isSubmitting) return; + + const normalizedUsername = username.trim(); + if (!normalizedUsername) { + setError("Username is required."); + return; + } + + if (normalizedUsername.length > 20) { + setError("Username must be 20 characters or less."); + return; + } + // Validate uniqueness on submit - if (profiles.some((p) => p.name === username)) { + if (profiles.some((p) => p.name === normalizedUsername)) { setError( "That username is already in use. Please choose a different one.", ); @@ -66,9 +80,11 @@ export function AddNewProfile({ open, onOpenChange }: AddNewProfileProps) { setUsername(generateUniqueUsername()); return; } + try { + setIsSubmitting(true); setError(null); - await addProfile({ name: username, avatar: profilePicture }); + await addProfile({ name: normalizedUsername, avatar: profilePicture }); onOpenChange(false); // Prepare a fresh suggestion for the next time the dialog opens. setUsername(""); @@ -76,6 +92,8 @@ export function AddNewProfile({ open, onOpenChange }: AddNewProfileProps) { } catch (err) { console.error("Failed to add profile:", err); setError("Failed to save profile. Please try again."); + } finally { + setIsSubmitting(false); } }; @@ -92,12 +110,15 @@ export function AddNewProfile({ open, onOpenChange }: AddNewProfileProps) { - + { setUsername(e.target.value); if (error) setError(null); @@ -110,7 +131,9 @@ export function AddNewProfile({ open, onOpenChange }: AddNewProfileProps) {
) : null} - + - + - + diff --git a/client/src/components/forms/addNewProfile/ProfilesContext.tsx b/client/src/components/forms/addNewProfile/ProfilesContext.tsx index c86a4e6..2b03a1a 100644 --- a/client/src/components/forms/addNewProfile/ProfilesContext.tsx +++ b/client/src/components/forms/addNewProfile/ProfilesContext.tsx @@ -8,12 +8,22 @@ import React, { import type { Profile, ProfileContextType } from "./ProfilesTypes"; import { AuthContext } from "@/context/AuthContext"; import { - apiAddProfile, - apiGetProfilesByUserId, - apiDeleteProfile, -} from "@/hooks/useApi"; - -const ProfileContext = createContext(undefined); + useProfilesQuery, + useAddProfileMutation, + useUpdateProfileMutation, + useDeleteProfileMutation, + type ProfileResponse, +} from "@/hooks/useQueryHooks"; + +const defaultProfileContext: ProfileContextType = { + profiles: [], + addProfile: async () => {}, + removeProfile: async () => {}, + selectedProfile: null, + selectProfile: () => {}, +}; + +const ProfileContext = createContext(defaultProfileContext); export { ProfileContext }; @@ -58,55 +68,43 @@ export function ProfileProvider({ children }: { children: React.ReactNode }) { // Posts to the server via the centralized API, then updates local state on success. const addProfile = async (profile: Profile) => { - try { - if (userId === null) { - throw new Error("User is not logged in"); - } - - const { data, ok, status } = await apiAddProfile({ - display_name: profile.name, - user_id: Number(userId), - }); + if (!numUserId) { + throw new Error("User is not logged in"); + } - if (!ok) { - throw new Error(`Failed to add profile: ${status}`); - } + await addProfileMutation.mutateAsync({ + display_name: profile.name, + user_id: numUserId, + }); - // The server may return the created profile with a `name` field. - // If it does, use it; otherwise fall back to the profile we sent. - const toAdd: Profile = data?.name - ? { name: data.name, avatar: data.avatar ?? profile.avatar } - : profile; - setProfiles((prev) => [...prev, toAdd]); - } catch (error) { - console.error("Error adding profile:", error); - // Keep UI stable; do not optimistic-update. Optionally, surface error to caller by rethrowing. - // Rethrow so callers can react if they awaited addProfile. - throw error; - } + // No return needed - mutations handle cache invalidation }; const removeProfile = async (name: string) => { const profile = profiles.find((p) => p.name === name); - if (!profile) return; + if (!profile || !profile.id) return; - // If the profile has a server-side id, delete it from the backend first. - if (profile.id != null) { - try { - const { ok, status } = await apiDeleteProfile(profile.id); - if (!ok) { - throw new Error(`Failed to delete profile: ${status}`); - } - } catch (error) { - console.error("Error deleting profile:", error); - throw error; - } - } + try { + // Rename to tombstone first (without optimistic rename in UI), + // then delete optimistically so tombstone text never flashes. + const tombstoneName = + `del_${profile.id}_${Date.now().toString(36)}`.slice(0, 20); + + await updateProfileMutation.mutateAsync({ + profileId: profile.id, + payload: { + id: profile.id, + display_name: tombstoneName, + coins: profile.coins ?? 0, + }, + optimistic: false, + invalidateAfterSuccess: false, + }); - setProfiles((prev) => prev.filter((p) => p.name !== name)); - // If the deleted profile was selected, clear the selection. - if (selectedProfile?.name === name) { - setSelectedProfile(null); + await deleteProfileMutation.mutateAsync(profile.id); + } catch (error) { + console.error("Error deleting profile:", error); + throw error; } }; diff --git a/client/src/components/forms/addNewProfile/ProfilesTypes.ts b/client/src/components/forms/addNewProfile/ProfilesTypes.ts index 19dc0eb..c02f58d 100644 --- a/client/src/components/forms/addNewProfile/ProfilesTypes.ts +++ b/client/src/components/forms/addNewProfile/ProfilesTypes.ts @@ -2,6 +2,7 @@ export interface Profile { id?: number; name: string; avatar: string; + coins?: number; } export interface ProfileContextType { diff --git a/client/src/components/forms/addNewProfile/useProfiles.ts b/client/src/components/forms/addNewProfile/useProfiles.ts index 9a42661..bce91be 100644 --- a/client/src/components/forms/addNewProfile/useProfiles.ts +++ b/client/src/components/forms/addNewProfile/useProfiles.ts @@ -1,10 +1,6 @@ import { useContext } from "react"; -import { ProfileContext } from "./ProfilesContext"; +import { ProfileContext } from "@/components/forms/addNewProfile/ProfilesContext"; export function useProfiles() { - const context = useContext(ProfileContext); - if (!context) { - throw new Error("useProfiles must be used within ProfileProvider"); - } - return context; + return useContext(ProfileContext); } diff --git a/client/src/components/nav/AccountMenu.tsx b/client/src/components/nav/AccountMenu.tsx index b7d14cf..0f026ee 100644 --- a/client/src/components/nav/AccountMenu.tsx +++ b/client/src/components/nav/AccountMenu.tsx @@ -14,29 +14,45 @@ import { useContext } from "react"; import { useNavbarContext } from "@/context/NavbarContextUtils"; import { useSettings } from "../pages/profileDependents/settings/settingsLogic/SettingsContext"; import { AuthContext } from "@/context/AuthContext"; -import { apiLogout } from "@/hooks/useApi"; +import { useLogoutMutation } from "@/hooks/useQueryHooks"; import { m } from "motion/react"; +import { + getBackgroundClasses, + getSubtextColor, + getTextColor, + getTextShadow, +} from "@/lib/utils"; export default function AccountMenu() { const { setIsDropdownHovering, setIsDropdownOpen } = useNavbarContext(); const { settings } = useSettings(); - const { setIsLoggedIn, setUserId } = useContext(AuthContext); + const { setIsLoggedIn, setUserId, setIsAdmin } = useContext(AuthContext); const navigate = useNavigate(); - // placeholder flag to toggle dark styling; replace with real theme flag as needed - const isDark = false; + const logoutMutation = useLogoutMutation(); + const textColor = getTextColor(settings.useLiquidGlass, settings.useDarkMode); + const subtextColor = getSubtextColor( + settings.useLiquidGlass, + settings.useDarkMode, + ); + const textShadow = getTextShadow( + settings.useLiquidGlass, + settings.useDarkMode, + ); + const dropdownBackground = getBackgroundClasses( + settings.useLiquidGlass, + settings.useDarkMode, + "strong", + ); - // Calls the centralized logout API to end the session. + // Calls the React Query logout mutation to end the session and clear cache const logout = async () => { try { - const { ok } = await apiLogout(); - if (ok) { - console.log("Logout successful"); - setIsLoggedIn(false); - setUserId(null); - navigate("/app/login"); - } else { - console.error("Logout failed"); - } + await logoutMutation.mutateAsync(); + console.log("Logout successful"); + setIsLoggedIn(false); + setUserId(null); + setIsAdmin(false); + navigate("/app/login"); } catch (error) { console.error("Logout failed:", error); } @@ -51,15 +67,15 @@ export default function AccountMenu() { e.preventDefault()} onInteractOutside={(e) => e.preventDefault()} > - Create new News Article + + Create new News Article + @@ -93,6 +136,7 @@ export function AddNews({ onCreate }: { onCreate?: (post: NewsPost) => void }) { setTitle((e.target as HTMLInputElement).value)} + className={inputClass} /> @@ -107,13 +151,13 @@ export function AddNews({ onCreate }: { onCreate?: (post: NewsPost) => void }) { className="w-full" > - + Image Settings -