diff --git a/package-lock.json b/package-lock.json index ae63a25..98dbe18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,17 @@ "name": "tobeplayed-frontend", "version": "0.0.1", "dependencies": { + "@hookform/resolvers": "^3.10.0", "@mdi/font": "~7.4.47", "@tailwindcss/vite": "~4.2.2", "i18next": "~26.0.0", "i18next-http-backend": "~3.0.2", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-hook-form": "^7.73.1", "react-i18next": "~17.0.0", - "react-router-dom": "~7.13.2" + "react-router-dom": "~7.13.2", + "zod": "^3.25.76" }, "devDependencies": { "@vitejs/plugin-react": "^6.0.1", @@ -35,20 +38,20 @@ } }, "node_modules/@emnapi/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", - "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.0", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "license": "MIT", "optional": true, "dependencies": { @@ -56,15 +59,24 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", - "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -117,34 +129,36 @@ "license": "Apache-2.0" }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", - "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, "node_modules/@oxc-project/types": { - "version": "0.120.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", - "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", "cpu": [ "arm64" ], @@ -158,9 +172,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", "cpu": [ "arm64" ], @@ -174,9 +188,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz", - "integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", "cpu": [ "x64" ], @@ -190,9 +204,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz", - "integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", "cpu": [ "x64" ], @@ -206,9 +220,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz", - "integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", "cpu": [ "arm" ], @@ -222,9 +236,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", "cpu": [ "arm64" ], @@ -238,9 +252,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz", - "integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", "cpu": [ "arm64" ], @@ -254,9 +268,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", "cpu": [ "ppc64" ], @@ -270,9 +284,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", "cpu": [ "s390x" ], @@ -286,9 +300,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", "cpu": [ "x64" ], @@ -302,9 +316,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz", - "integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", "cpu": [ "x64" ], @@ -318,9 +332,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", "cpu": [ "arm64" ], @@ -334,25 +348,27 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz", - "integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", "cpu": [ "wasm32" ], "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" }, "engines": { - "node": ">=14.0.0" + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz", - "integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", "cpu": [ "arm64" ], @@ -366,9 +382,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz", - "integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", "cpu": [ "x64" ], @@ -800,12 +816,12 @@ } }, "node_modules/cross-fetch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", - "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", "license": "MIT", "dependencies": { - "node-fetch": "^2.6.12" + "node-fetch": "^2.7.0" } }, "node_modules/detect-libc": { @@ -939,12 +955,12 @@ } }, "node_modules/i18next-http-backend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz", - "integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.5.tgz", + "integrity": "sha512-QaWHnsxieEDcqKe+vo/RFqpiIFRi/KBqlOSPcUlvinBaISCeiTRCbtrazHAjtHtsLC66oDsROAH8frWkQzfMMQ==", "license": "MIT", "dependencies": { - "cross-fetch": "4.0.0" + "cross-fetch": "4.1.0" } }, "node_modules/jiti": { @@ -1278,9 +1294,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "funding": [ { "type": "opencollective", @@ -1333,6 +1349,22 @@ "react": "^19.2.4" } }, + "node_modules/react-hook-form": { + "version": "7.73.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.73.1.tgz", + "integrity": "sha512-VAfVYOPcx3piiEVQy95vyFmBwbVUsP/AUIN+mpFG8h11yshDd444nn0VyfaGWSRnhOLVgiDu7HIuBtAIzxn9dA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-i18next": { "version": "17.0.0", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.0.tgz", @@ -1399,13 +1431,13 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", - "integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.120.0", - "@rolldown/pluginutils": "1.0.0-rc.10" + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" @@ -1414,27 +1446,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.10", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", - "@rolldown/binding-darwin-x64": "1.0.0-rc.10", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", - "integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", "license": "MIT" }, "node_modules/scheduler": { @@ -1478,13 +1510,13 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -1547,16 +1579,16 @@ } }, "node_modules/vite": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", - "integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.10", - "tinyglobby": "^0.2.15" + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" }, "bin": { "vite": "bin/vite.js" @@ -1573,7 +1605,7 @@ "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", @@ -1647,6 +1679,15 @@ "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index ea45ce3..268f9bc 100644 --- a/package.json +++ b/package.json @@ -9,14 +9,17 @@ "preview": "vite preview" }, "dependencies": { + "@hookform/resolvers": "^3.10.0", "@mdi/font": "~7.4.47", "@tailwindcss/vite": "~4.2.2", "i18next": "~26.0.0", "i18next-http-backend": "~3.0.2", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-hook-form": "^7.73.1", "react-i18next": "~17.0.0", - "react-router-dom": "~7.13.2" + "react-router-dom": "~7.13.2", + "zod": "^3.25.76" }, "devDependencies": { "@vitejs/plugin-react": "^6.0.1", diff --git a/public/i18n/de.json b/public/i18n/de.json index 53f0e1d..1db8fe8 100644 --- a/public/i18n/de.json +++ b/public/i18n/de.json @@ -1,4 +1,4 @@ -{ +{ "IMPRINT": { "TITLE": "Impressum", "LEGAL": "Angaben gemäß § 5 DDG", @@ -9,9 +9,57 @@ "EMAIL": "Email", "PASSWORD": "Passwort", "SUBMIT": "Spiel betreten", + "NO_ACCOUNT": "Noch kein Konto?", "ERROR_EMAIL": "Gib eine gültige Email-Adresse ein mit '@' und mit '.'.", "ERROR_PASSWORD": "Das Passwort darf nicht leer sein." }, + "REGISTRATION": { + "TITLE": "Registrieren", + "EMAIL": "Email", + "PASSWORD": "Passwort", + "PASSWORD_CONFIRMATION": "Passwort bestätigen", + "INVITE_CODE": "Einladungscode", + "STEAM_ID": "Steam-ID", + "SUBMIT": "Konto erstellen", + "SUCCESS": "Konto erstellt. Weiterleitung zum Login …", + "HAVE_ACCOUNT": "Bereits ein Konto?", + "ERROR_GENERIC": "Registrierung fehlgeschlagen. Bitte erneut versuchen.", + "ERROR_EMAIL_IN_USE": "Diese Email-Adresse ist bereits registriert.", + "ERROR_INVALID_INVITE_CODE": "Der Einladungscode ist ungültig oder wurde bereits verwendet.", + "ERROR_STEAM_ID_IN_USE": "Diese Steam-ID ist bereits mit einem anderen Konto verknüpft." + }, + "SETTINGS": { + "TITLE": "Einstellungen", + "LOADING": "Einstellungen werden geladen …", + "ACCOUNT": "Konto", + "PREFERENCES": "Einstellungen", + "STEAM": "Steam", + "EMAIL": "Email", + "ROLE": "Rolle", + "CREATED_DATE": "Mitglied seit", + "LAST_LOGIN": "Letzter Login", + "DARK_THEME": "Dunkles Design", + "LANGUAGE": "Sprache", + "LANGUAGE_OPTIONS": { + "EN": "Englisch", + "DE": "Deutsch" + }, + "STEAM_ID": "Steam-ID", + "SAVE_STEAM_ID": "Steam-ID speichern", + "SAVED": "Einstellungen gespeichert.", + "ERROR_SAVE": "Einstellungen konnten nicht gespeichert werden." + }, + "VALIDATION": { + "EMAIL_REQUIRED": "Email ist erforderlich.", + "EMAIL_INVALID": "Gib eine gültige Email-Adresse ein.", + "PASSWORD_TOO_SHORT": "Passwort muss mindestens 8 Zeichen haben.", + "PASSWORD_CONFIRMATION_REQUIRED": "Bitte Passwort bestätigen.", + "PASSWORD_MISMATCH": "Die Passwörter stimmen nicht überein.", + "INVITE_CODE_REQUIRED": "Einladungscode ist erforderlich.", + "STEAM_ID_REQUIRED": "Steam-ID ist erforderlich.", + "STEAM_ID_INVALID": "Steam-ID muss eine positive Zahl sein.", + "LANGUAGE_INVALID": "Nicht unterstützte Sprache." + }, "INFO": { "ERROR": { "LOAD_PROFILE": "Fehler beim Laden der Profildaten.", @@ -29,4 +77,4 @@ "LOGIN": "Anmelden", "LOGOUT": "Abmelden" } -} \ No newline at end of file +} diff --git a/public/i18n/en.json b/public/i18n/en.json index 9a503ec..703bcdf 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -1,4 +1,4 @@ -{ +{ "IMPRINT": { "TITLE": "Imprint", "LEGAL": "Information according to § 5 TMG.", @@ -9,9 +9,57 @@ "EMAIL": "Email", "PASSWORD": "Password", "SUBMIT": "Enter the game", + "NO_ACCOUNT": "No account yet?", "ERROR_EMAIL": "Enter a valid email address with '@' and with '.'.", "ERROR_PASSWORD": "The password must have at least one character." }, + "REGISTRATION": { + "TITLE": "Register", + "EMAIL": "Email", + "PASSWORD": "Password", + "PASSWORD_CONFIRMATION": "Confirm password", + "INVITE_CODE": "Invite code", + "STEAM_ID": "Steam ID", + "SUBMIT": "Create account", + "SUCCESS": "Account created. Redirecting to login …", + "HAVE_ACCOUNT": "Already have an account?", + "ERROR_GENERIC": "Registration failed. Please try again.", + "ERROR_EMAIL_IN_USE": "This email address is already registered.", + "ERROR_INVALID_INVITE_CODE": "The invite code is invalid or has already been used.", + "ERROR_STEAM_ID_IN_USE": "This Steam ID is already linked to another account." + }, + "SETTINGS": { + "TITLE": "Settings", + "LOADING": "Loading settings …", + "ACCOUNT": "Account", + "PREFERENCES": "Preferences", + "STEAM": "Steam", + "EMAIL": "Email", + "ROLE": "Role", + "CREATED_DATE": "Member since", + "LAST_LOGIN": "Last login", + "DARK_THEME": "Dark theme", + "LANGUAGE": "Language", + "LANGUAGE_OPTIONS": { + "EN": "English", + "DE": "German" + }, + "STEAM_ID": "Steam ID", + "SAVE_STEAM_ID": "Save Steam ID", + "SAVED": "Settings saved.", + "ERROR_SAVE": "Could not save settings." + }, + "VALIDATION": { + "EMAIL_REQUIRED": "Email is required.", + "EMAIL_INVALID": "Enter a valid email address.", + "PASSWORD_TOO_SHORT": "Password must be at least 8 characters.", + "PASSWORD_CONFIRMATION_REQUIRED": "Please confirm your password.", + "PASSWORD_MISMATCH": "Passwords do not match.", + "INVITE_CODE_REQUIRED": "Invite code is required.", + "STEAM_ID_REQUIRED": "Steam ID is required.", + "STEAM_ID_INVALID": "Steam ID must be a positive number.", + "LANGUAGE_INVALID": "Unsupported language." + }, "INFO": { "ERROR": { "LOAD_PROFILE": "Error during loading of the user profile data.", @@ -29,4 +77,4 @@ "LOGIN": "Login", "LOGOUT": "Logout" } -} \ No newline at end of file +} diff --git a/src/App.jsx b/src/App.jsx index 39cacdc..50344ca 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -6,10 +6,13 @@ import LandingPage from './pages/LandingPage'; import LoginPage from './pages/LoginPage'; import ImprintPage from './pages/ImprintPage'; import Dashboard from "./pages/Dashboard.jsx"; +import RegistrationPage from './pages/RegistrationPage.jsx'; +import SettingsPage from './pages/SettingsPage.jsx'; +import {usePreferences} from './context/PreferencesContext.jsx'; function App() { const {t, i18n} = useTranslation(); - const [isDarkMode, setIsDarkMode] = useState(true); + const {isDarkMode, setDarkMode, setLanguage} = usePreferences(); const [isAuthenticated, setIsAuthenticated] = useState(false); // Hooks for routing @@ -21,21 +24,8 @@ function App() { setIsAuthenticated(!!token); // Converts the token string (or null) to a boolean }, [location]); - // Sync React state with the HTML class required by Tailwind CSS - useEffect(() => { - if (isDarkMode) { - document.documentElement.classList.add('dark'); - } else { - document.documentElement.classList.remove('dark'); - } - }, [isDarkMode]); - const toggleLanguage = () => { - if (i18n.language === 'de') { - i18n.changeLanguage('en').then(_ => console.log("Changed language to 'en'")); - } else { - i18n.changeLanguage('de').then(_ => console.log("Changed language to 'de'")); - } + setLanguage(i18n.language === 'de' ? 'en' : 'de'); }; const handleLogout = () => { @@ -56,14 +46,19 @@ function App() { {i18n.language.toUpperCase()} {isAuthenticated ? ( - + <> + + + + + ) : ( {t('MAIN.LOGIN')} @@ -76,7 +71,9 @@ function App() { }/> }/> + }/> }/> + }/> }/> diff --git a/src/context/PreferencesContext.jsx b/src/context/PreferencesContext.jsx new file mode 100644 index 0000000..7b43164 --- /dev/null +++ b/src/context/PreferencesContext.jsx @@ -0,0 +1,47 @@ +import {createContext, useContext, useEffect, useState} from 'react'; +import {useTranslation} from 'react-i18next'; + +const DARK_MODE_STORAGE_KEY = 'tbp_dark_mode'; + +const PreferencesContext = createContext(null); + +export function PreferencesProvider({children}) { + const {i18n} = useTranslation(); + + const [isDarkMode, setIsDarkMode] = useState(() => { + const stored = localStorage.getItem(DARK_MODE_STORAGE_KEY); + return stored === null ? true : stored === 'true'; + }); + + useEffect(() => { + if (isDarkMode) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + localStorage.setItem(DARK_MODE_STORAGE_KEY, String(isDarkMode)); + }, [isDarkMode]); + + const setLanguage = (lang) => i18n.changeLanguage(lang); + + return ( + + {children} + + ); +} + +export function usePreferences() { + const ctx = useContext(PreferencesContext); + if (!ctx) { + throw new Error('usePreferences must be used inside '); + } + return ctx; +} diff --git a/src/main.jsx b/src/main.jsx index 5480994..eb86d6b 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -2,6 +2,7 @@ import {StrictMode} from 'react'; import {createRoot} from 'react-dom/client'; import {BrowserRouter} from 'react-router-dom'; import App from './App.jsx'; +import {PreferencesProvider} from './context/PreferencesContext.jsx'; import './i18n'; import './style.css'; import '@mdi/font/css/materialdesignicons.css'; @@ -9,7 +10,9 @@ import '@mdi/font/css/materialdesignicons.css'; createRoot(document.getElementById('root')).render( - + + + ); \ No newline at end of file diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index 566a9de..433c81e 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -1,5 +1,5 @@ import React, {useEffect, useState} from 'react'; -import {useNavigate} from 'react-router-dom'; +import {Link, useNavigate} from 'react-router-dom'; import {useTranslation} from 'react-i18next'; const Dashboard = () => { @@ -83,9 +83,15 @@ const Dashboard = () => {
Tile 2 (Coming soon)
-
- Settings (Coming soon) -
+ + + + {t('SETTINGS.TITLE')} + + diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx index 66a6829..3179ee9 100644 --- a/src/pages/LoginPage.jsx +++ b/src/pages/LoginPage.jsx @@ -1,6 +1,6 @@ import {useTranslation} from 'react-i18next'; import {useState} from 'react'; -import {useNavigate} from 'react-router-dom'; +import {Link, useNavigate} from 'react-router-dom'; export default function LoginPage() { const {t} = useTranslation(); @@ -131,6 +131,13 @@ export default function LoginPage() { )} +

+ {t('LOGIN.NO_ACCOUNT')}{' '} + + {t('REGISTRATION.TITLE')} + +

+ ); diff --git a/src/pages/RegistrationPage.jsx b/src/pages/RegistrationPage.jsx new file mode 100644 index 0000000..611f738 --- /dev/null +++ b/src/pages/RegistrationPage.jsx @@ -0,0 +1,185 @@ +import {useState} from 'react'; +import {Link, useNavigate} from 'react-router-dom'; +import {useTranslation} from 'react-i18next'; +import {useForm} from 'react-hook-form'; +import {zodResolver} from '@hookform/resolvers/zod'; + +import {registrationSchema} from '../utils/validation.js'; + +const BACKEND_ERROR_KEY = { + EMAIL_ALREADY_IN_USE: 'REGISTRATION.ERROR_EMAIL_IN_USE', + INVALID_INVITATION_CODE: 'REGISTRATION.ERROR_INVALID_INVITE_CODE', + STEAM_ID_ALREADY_IN_USE: 'REGISTRATION.ERROR_STEAM_ID_IN_USE', +}; + +const inputClass = + 'w-full px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:outline-none'; +const inputErrorClass = + 'w-full px-4 py-2 border border-red-500 dark:border-red-500 rounded-lg bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-red-500 focus:outline-none'; + +export default function RegistrationPage() { + const {t} = useTranslation(); + const navigate = useNavigate(); + const [feedback, setFeedback] = useState({message: '', isError: false}); + + const { + register, + handleSubmit, + formState: {errors, isSubmitting}, + } = useForm({ + resolver: zodResolver(registrationSchema), + defaultValues: { + email: '', + password: '', + passwordConfirmation: '', + inviteCode: '', + steamId: '', + }, + }); + + const onSubmit = async (values) => { + setFeedback({message: '', isError: false}); + try { + const apiUrl = `${import.meta.env.VITE_API_URL}/api/registration/new`; + const response = await fetch(apiUrl, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + email: values.email, + password: values.password, + inviteCode: values.inviteCode, + steamId: values.steamId, + }), + }); + + if (response.ok) { + setFeedback({message: t('REGISTRATION.SUCCESS'), isError: false}); + setTimeout(() => navigate('/login'), 800); + return; + } + + const data = await response.json().catch(() => ({})); + const mappedKey = BACKEND_ERROR_KEY[data.code]; + setFeedback({ + message: mappedKey ? t(mappedKey) : data.message || t('REGISTRATION.ERROR_GENERIC'), + isError: true, + }); + } catch (err) { + console.error('Registration API error:', err); + setFeedback({message: t('INFO.ERROR.NETWORK_FAILURE'), isError: true}); + } + }; + + const renderError = (field) => + errors[field] && ( +

{t(errors[field].message)}

+ ); + + return ( +
+
+

+ {t('REGISTRATION.TITLE')} +

+ +
+
+ + + {renderError('email')} +
+ +
+ + + {renderError('password')} +
+ +
+ + + {renderError('passwordConfirmation')} +
+ +
+ + + {renderError('inviteCode')} +
+ +
+ + + {renderError('steamId')} +
+ + +
+ + {feedback.message && ( +
+ {feedback.message} +
+ )} + +

+ {t('REGISTRATION.HAVE_ACCOUNT')}{' '} + + {t('MAIN.LOGIN')} + +

+
+
+ ); +} diff --git a/src/pages/SettingsPage.jsx b/src/pages/SettingsPage.jsx new file mode 100644 index 0000000..5c9d74f --- /dev/null +++ b/src/pages/SettingsPage.jsx @@ -0,0 +1,334 @@ +import {useEffect, useState} from 'react'; +import {useNavigate} from 'react-router-dom'; +import {useTranslation} from 'react-i18next'; +import {useForm} from 'react-hook-form'; +import {zodResolver} from '@hookform/resolvers/zod'; + +import {settingsSchema} from '../utils/validation.js'; +import {usePreferences} from '../context/PreferencesContext.jsx'; + +const BACKEND_ERROR_KEY = { + STEAM_ID_ALREADY_IN_USE: 'REGISTRATION.ERROR_STEAM_ID_IN_USE', +}; + +const SUPPORTED_LANGUAGES = ['en', 'de']; + +const inputClass = + 'w-full px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:outline-none'; +const inputErrorClass = + 'w-full px-4 py-2 border border-red-500 dark:border-red-500 rounded-lg bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-red-500 focus:outline-none'; +const readOnlyClass = + 'w-full px-4 py-2 border border-slate-200 dark:border-slate-700 rounded-lg bg-slate-100 dark:bg-slate-900 text-slate-500 dark:text-slate-400 cursor-not-allowed'; + +const formatDate = (value, locale) => { + if (!value) return '—'; + try { + return new Date(value).toLocaleString(locale); + } catch { + return value; + } +}; + +export default function SettingsPage() { + const {t, i18n} = useTranslation(); + const navigate = useNavigate(); + const {isDarkMode, setDarkMode, setLanguage} = usePreferences(); + + const [profile, setProfile] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [loadError, setLoadError] = useState(null); + const [feedback, setFeedback] = useState({message: '', isError: false}); + + const { + register, + handleSubmit, + reset, + formState: {errors, isSubmitting}, + } = useForm({ + resolver: zodResolver(settingsSchema), + defaultValues: {useDarkTheme: true, language: 'en', steamId: ''}, + }); + + useEffect(() => { + const token = localStorage.getItem('tbp_jwt'); + if (!token) { + navigate('/login'); + return; + } + + const load = async () => { + try { + const response = await fetch(`${import.meta.env.VITE_API_URL}/api/users/me?v=1`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (response.status === 401) { + localStorage.removeItem('tbp_jwt'); + navigate('/login'); + return; + } + + if (!response.ok) { + setLoadError(t('INFO.ERROR.LOAD_PROFILE')); + return; + } + + const data = await response.json(); + setProfile(data); + reset({ + useDarkTheme: !!data.settings?.useDarkTheme, + language: data.settings?.language || 'en', + steamId: data.steamId ? String(data.steamId) : '', + }); + } catch (err) { + console.error('Settings load error:', err); + setLoadError(t('INFO.ERROR.NETWORK_FAILURE')); + } finally { + setIsLoading(false); + } + }; + + load(); + }, [navigate, reset, t]); + + const persistSettings = async (payload) => { + const token = localStorage.getItem('tbp_jwt'); + if (!token) { + navigate('/login'); + return {ok: false}; + } + + try { + const response = await fetch(`${import.meta.env.VITE_API_URL}/api/users/settings`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (response.status === 401) { + localStorage.removeItem('tbp_jwt'); + navigate('/login'); + return {ok: false}; + } + + if (!response.ok) { + const data = await response.json().catch(() => ({})); + const mappedKey = BACKEND_ERROR_KEY[data.code]; + setFeedback({ + message: mappedKey ? t(mappedKey) : data.message || t('SETTINGS.ERROR_SAVE'), + isError: true, + }); + return {ok: false}; + } + + setFeedback({message: t('SETTINGS.SAVED'), isError: false}); + return {ok: true}; + } catch (err) { + console.error('Settings save error:', err); + setFeedback({message: t('INFO.ERROR.NETWORK_FAILURE'), isError: true}); + return {ok: false}; + } + }; + + const handleDarkThemeToggle = async () => { + const next = !isDarkMode; + setDarkMode(next); // apply on the fly + await persistSettings({ + useDarkTheme: next, + language: i18n.language, + steamId: profile?.steamId ?? 0, + }); + }; + + const handleLanguageChange = async (event) => { + const next = event.target.value; + setLanguage(next); // apply on the fly + await persistSettings({ + useDarkTheme: isDarkMode, + language: next, + steamId: profile?.steamId ?? 0, + }); + }; + + const onSubmitSteamId = async (values) => { + const result = await persistSettings({ + useDarkTheme: isDarkMode, + language: i18n.language, + steamId: values.steamId, + }); + if (result.ok) { + setProfile((p) => (p ? {...p, steamId: values.steamId} : p)); + } + }; + + const renderError = (field) => + errors[field] && ( +

{t(errors[field].message)}

+ ); + + if (isLoading) { + return ( +
+ {t('SETTINGS.LOADING')} +
+ ); + } + + if (loadError) { + return ( +
+ {loadError} +
+ ); + } + + return ( +
+
+
+

+ {t('SETTINGS.TITLE')} +

+ + {/* Read-only account info */} +
+

+ {t('SETTINGS.ACCOUNT')} +

+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + {/* Preferences — live */} +
+

+ {t('SETTINGS.PREFERENCES')} +

+ +
+ + +
+ +
+ + +
+
+ + {/* Steam ID — explicit save */} +
+

+ {t('SETTINGS.STEAM')} +

+
+
+ + + {renderError('steamId')} +
+ +
+
+ + {feedback.message && ( +
+ {feedback.message} +
+ )} +
+
+
+ ); +} diff --git a/src/utils/validation.js b/src/utils/validation.js new file mode 100644 index 0000000..46784d9 --- /dev/null +++ b/src/utils/validation.js @@ -0,0 +1,44 @@ +import {z} from 'zod'; + +// Error messages are i18n keys — translate them in components via t(error.message) + +const emailField = z + .string() + .min(1, 'VALIDATION.EMAIL_REQUIRED') + .email('VALIDATION.EMAIL_INVALID'); + +const passwordField = z + .string() + .min(8, 'VALIDATION.PASSWORD_TOO_SHORT'); + +const inviteCodeField = z + .string() + .min(1, 'VALIDATION.INVITE_CODE_REQUIRED'); + +// Backend stores steamId as a primitive long; Steam IDs are positive 64-bit integers. +const steamIdField = z + .union([z.string(), z.number()]) + .transform((v) => (typeof v === 'string' ? v.trim() : String(v))) + .refine((v) => v.length > 0, {message: 'VALIDATION.STEAM_ID_REQUIRED'}) + .refine((v) => /^\d+$/.test(v), {message: 'VALIDATION.STEAM_ID_INVALID'}) + .transform((v) => Number(v)) + .refine((n) => Number.isFinite(n) && n > 0, {message: 'VALIDATION.STEAM_ID_INVALID'}); + +export const registrationSchema = z + .object({ + email: emailField, + password: passwordField, + passwordConfirmation: z.string().min(1, 'VALIDATION.PASSWORD_CONFIRMATION_REQUIRED'), + inviteCode: inviteCodeField, + steamId: steamIdField, + }) + .refine((d) => d.password === d.passwordConfirmation, { + path: ['passwordConfirmation'], + message: 'VALIDATION.PASSWORD_MISMATCH', + }); + +export const settingsSchema = z.object({ + useDarkTheme: z.boolean(), + language: z.enum(['en', 'de'], {message: 'VALIDATION.LANGUAGE_INVALID'}), + steamId: steamIdField, +});