diff --git a/package-lock.json b/package-lock.json index f1ea550..98587de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,17 +10,19 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@mui/icons-material": "^9.1.1", - "@mui/material": "^9.1.2", + "@mui/icons-material": "^9.2.0", + "@mui/material": "^9.2.0", + "@reduxjs/toolkit": "^2.12.0", "notistack": "^3.0.2", - "react": "^19.2.7", - "react-dom": "^19.2.7", - "react-router-dom": "^7.18.0" + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-redux": "^9.3.0", + "react-router-dom": "^7.18.2" }, "devDependencies": { - "@vitejs/plugin-react": "^6.0.2", + "@vitejs/plugin-react": "6.0.2", "qrcode": "^1.5.4", - "vite": "^8.0.16", + "vite": "^8.2.0", "vite-plugin-pwa": "^1.3.0" } }, @@ -1580,21 +1582,21 @@ } }, "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", + "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", + "@emnapi/wasi-threads": "2.0.1", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", + "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", "dev": true, "license": "MIT", "optional": true, @@ -1603,9 +1605,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", + "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", "dev": true, "license": "MIT", "optional": true, @@ -1827,9 +1829,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.1.2.tgz", - "integrity": "sha512-ZMufoA/YFOEVp48lskcAOTlQYwpdBk4Z++4yUgPDEfuLHIpxBx9g+urGmIBKOtr+7M0ZlYfCxSvrJpEE/S32sg==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.2.0.tgz", + "integrity": "sha512-+XMav+ZaXkZKUFUgzjrfMEedfyJKxxviAske2q8N8CWDMeqZdDU2lWMkiUPiB388hGaDqhwvOAwkrsc/pUyp8g==", "license": "MIT", "funding": { "type": "opencollective", @@ -1837,9 +1839,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.1.1.tgz", - "integrity": "sha512-OXhm9DajemStb58AumM06DuPhHTa3XD36TFD4yf6WtJyNRO5DfEZbbnHlBg/US2Y2oOXwM/XurMTBOD6L/YYZw==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.2.0.tgz", + "integrity": "sha512-VgBd3z7Qc3vd/thcNSMC03nHRh/U4DzMUd+1dRyJTbm/hGo7+N6N4GDuJZDNHa6LZhhwG6Cu1X3DNvrVv8sNag==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.2" @@ -1852,7 +1854,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^9.1.1", + "@mui/material": "^9.2.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -1863,16 +1865,16 @@ } }, "node_modules/@mui/material": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.1.2.tgz", - "integrity": "sha512-CN2U1etAL+6qZT2XjJR1Ibv7nyE2wBN3/28b5XpXjQFMtBKNlD45wQupODfJrm9PLanJ1DefocHWIQZ5PkSipQ==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.2.0.tgz", + "integrity": "sha512-+YTRSgGKGrrRo2XJZXs7JRA6qHoHWvNtxyqxnrRJTBmIuLOUpxxh7m4G9lF4tWberxGFY+EqkkRPgJCl+fSMJg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.2", - "@mui/core-downloads-tracker": "^9.1.2", - "@mui/system": "^9.1.2", + "@mui/core-downloads-tracker": "^9.2.0", + "@mui/system": "^9.2.0", "@mui/types": "^9.1.1", - "@mui/utils": "^9.1.1", + "@mui/utils": "^9.2.0", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", @@ -1891,7 +1893,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^9.1.1", + "@mui/material-pigment-css": "^9.2.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1912,13 +1914,13 @@ } }, "node_modules/@mui/private-theming": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.1.1.tgz", - "integrity": "sha512-oH6c+d6sJ1CZT0Vg2/fHdUQ5zvo9Pn+f+WWk0tlQliHqqIRdN32DZ7UxjalW3LUj4OkHbdWR31biWuLxK9i7Cg==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.2.0.tgz", + "integrity": "sha512-w9wpyDxGPGnAACPB2hKhCDmILJIAvQxrfjUbIAEa0AznX1rOjaz5N+yB1uuw8ixnJcpEh/tPbD9oEe19wcWPHw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.2", - "@mui/utils": "^9.1.1", + "@mui/utils": "^9.2.0", "prop-types": "^15.8.1" }, "engines": { @@ -1973,16 +1975,16 @@ } }, "node_modules/@mui/system": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.1.2.tgz", - "integrity": "sha512-oJxyyummOR6nV8ODF/yugasJ//pSsQxxfYCE9q9RU2Hef0f5RRzJ75M9zr5NvHDhzhGgrPstkaNrJtmcuz/Pdg==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.2.0.tgz", + "integrity": "sha512-YvUJwKoGVtbnOm2PyPi5TvX2d1rOA6sqSpEWVs4WmXNIaFTuYmNUaVdU2o1NKUEe31URnD3E8ZVUMcsLQXwcYg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.2", - "@mui/private-theming": "^9.1.1", + "@mui/private-theming": "^9.2.0", "@mui/styled-engine": "^9.1.1", "@mui/types": "^9.1.1", - "@mui/utils": "^9.1.1", + "@mui/utils": "^9.2.0", "clsx": "^2.1.1", "csstype": "^3.2.3", "prop-types": "^15.8.1" @@ -2030,9 +2032,9 @@ } }, "node_modules/@mui/utils": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.1.1.tgz", - "integrity": "sha512-qSNfnkzZMptaaWFFklpDf4NPJztgwsMDVfM/sSDt+wq4ssYSBhLYwwjuB6eS/+p2IUYbeRzHluzXbw0Zn7aI4A==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.2.0.tgz", + "integrity": "sha512-OsUH5zhlSOM4xmLl53+agug1M1UyWb4zxFxWQCqwKTKUeQPvTENtg3JhrroBD2qpCLKsX5W/DYGERJ4mBUbc8g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.2", @@ -2060,28 +2062,31 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", - "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.1.tgz", + "integrity": "sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.2" + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" } }, "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", + "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", "dev": true, "license": "MIT", "funding": { @@ -2098,10 +2103,36 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.1.tgz", + "integrity": "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==", "cpu": [ "arm64" ], @@ -2116,9 +2147,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.1.tgz", + "integrity": "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==", "cpu": [ "arm64" ], @@ -2133,9 +2164,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.1.tgz", + "integrity": "sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==", "cpu": [ "x64" ], @@ -2150,9 +2181,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.1.tgz", + "integrity": "sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==", "cpu": [ "x64" ], @@ -2167,9 +2198,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.1.tgz", + "integrity": "sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==", "cpu": [ "arm" ], @@ -2184,9 +2215,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.1.tgz", + "integrity": "sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==", "cpu": [ "arm64" ], @@ -2204,9 +2235,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.1.tgz", + "integrity": "sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==", "cpu": [ "arm64" ], @@ -2224,9 +2255,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.1.tgz", + "integrity": "sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==", "cpu": [ "ppc64" ], @@ -2244,9 +2275,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.1.tgz", + "integrity": "sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==", "cpu": [ "s390x" ], @@ -2264,9 +2295,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.1.tgz", + "integrity": "sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==", "cpu": [ "x64" ], @@ -2284,9 +2315,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.1.tgz", + "integrity": "sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==", "cpu": [ "x64" ], @@ -2304,9 +2335,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.1.tgz", + "integrity": "sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==", "cpu": [ "arm64" ], @@ -2321,28 +2352,25 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", - "cpu": [ - "wasm32" - ], + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.1.tgz", + "integrity": "sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "@emnapi/core": "2.0.0-alpha.3", + "@emnapi/runtime": "2.0.0-alpha.3", + "@napi-rs/wasm-runtime": "^1.2.0" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.1.tgz", + "integrity": "sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==", "cpu": [ "arm64" ], @@ -2357,9 +2385,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.1.tgz", + "integrity": "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==", "cpu": [ "x64" ], @@ -2889,6 +2917,18 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@trickfilm400/rollup-plugin-off-main-thread": { "version": "3.0.0-pre1", "resolved": "https://registry.npmjs.org/@trickfilm400/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-3.0.0-pre1.tgz", @@ -2906,9 +2946,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, @@ -2968,6 +3008,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", @@ -4406,6 +4452,16 @@ "dev": true, "license": "ISC" }, + "node_modules/immer": { + "version": "11.1.15", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.15.tgz", + "integrity": "sha512-VrNANlmnWQnh5COXIIOQXM9oOJw7naGKlBT74ZOOR6lpVXc3gFEu9FJLDFcpCJ2j+NWr8TIwtWD//T6ZX6TKiQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4993,9 +5049,9 @@ } }, "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -5009,23 +5065,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "cpu": [ "arm64" ], @@ -5044,9 +5100,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", "cpu": [ "arm64" ], @@ -5065,9 +5121,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", "cpu": [ "x64" ], @@ -5086,9 +5142,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", "cpu": [ "x64" ], @@ -5107,9 +5163,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", "cpu": [ "arm" ], @@ -5128,9 +5184,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", "cpu": [ "arm64" ], @@ -5152,9 +5208,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", "cpu": [ "arm64" ], @@ -5176,9 +5232,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", "cpu": [ "x64" ], @@ -5200,9 +5256,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", "cpu": [ "x64" ], @@ -5224,9 +5280,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", "cpu": [ "arm64" ], @@ -5245,9 +5301,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", "cpu": [ "x64" ], @@ -5373,9 +5429,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -5648,9 +5704,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -5681,9 +5737,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -5701,7 +5757,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5768,36 +5824,59 @@ } }, "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.7" + "react": "^19.2.8" } }, "node_modules/react-is": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", - "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-router": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", - "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -5817,12 +5896,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz", - "integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", "license": "MIT", "dependencies": { - "react-router": "7.18.0" + "react-router": "7.18.2" }, "engines": { "node": ">=20.0.0" @@ -5848,6 +5927,21 @@ "react-dom": ">=16.6.0" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5977,6 +6071,12 @@ "dev": true, "license": "ISC" }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -6008,13 +6108,13 @@ } }, "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.1.tgz", + "integrity": "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.133.0", + "@oxc-project/types": "=0.142.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -6024,21 +6124,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" + "@rolldown/binding-android-arm64": "1.2.1", + "@rolldown/binding-darwin-arm64": "1.2.1", + "@rolldown/binding-darwin-x64": "1.2.1", + "@rolldown/binding-freebsd-x64": "1.2.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.1", + "@rolldown/binding-linux-arm64-gnu": "1.2.1", + "@rolldown/binding-linux-arm64-musl": "1.2.1", + "@rolldown/binding-linux-ppc64-gnu": "1.2.1", + "@rolldown/binding-linux-s390x-gnu": "1.2.1", + "@rolldown/binding-linux-x64-gnu": "1.2.1", + "@rolldown/binding-linux-x64-musl": "1.2.1", + "@rolldown/binding-openharmony-arm64": "1.2.1", + "@rolldown/binding-wasm32-wasi": "1.2.1", + "@rolldown/binding-win32-arm64-msvc": "1.2.1", + "@rolldown/binding-win32-x64-msvc": "1.2.1" } }, "node_modules/rollup": { @@ -6866,17 +6966,26 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", + "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.23", + "rolldown": "~1.2.0", "tinyglobby": "^0.2.17" }, "bin": { @@ -6893,7 +7002,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", + "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", diff --git a/package.json b/package.json index df5c970..2701444 100644 --- a/package.json +++ b/package.json @@ -16,17 +16,19 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@mui/icons-material": "^9.1.1", - "@mui/material": "^9.1.2", + "@mui/icons-material": "^9.2.0", + "@mui/material": "^9.2.0", + "@reduxjs/toolkit": "^2.12.0", "notistack": "^3.0.2", - "react": "^19.2.7", - "react-dom": "^19.2.7", - "react-router-dom": "^7.18.0" + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-redux": "^9.3.0", + "react-router-dom": "^7.18.2" }, "devDependencies": { - "@vitejs/plugin-react": "^6.0.2", + "@vitejs/plugin-react": "6.0.2", "qrcode": "^1.5.4", - "vite": "^8.0.16", + "vite": "^8.2.0", "vite-plugin-pwa": "^1.3.0" } } diff --git a/src/App.jsx b/src/App.jsx index 1582b4c..a0d92cc 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,8 +1,9 @@ -import { lazy, Suspense } from 'react'; +import { Component, lazy, Suspense } from 'react'; +import { connect } from 'react-redux'; import Box from '@mui/material/Box'; import CircularProgress from '@mui/material/CircularProgress'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; -import { useAuth } from './auth/useAuth'; +import { selectAuthStatus, selectNeedsCompanySetup } from './store/authSlice'; import LoginPage from './components/LoginPage'; import CompanySetupDialog from './components/CompanySetupDialog'; import AppShell from './components/AppShell'; @@ -36,60 +37,69 @@ function PageFallback() { ); } -export default function App() { - const { status, needsCompanySetup } = useAuth(); +class App extends Component { + render() { + const { status, needsCompanySetup } = this.props; + + if (status === 'loading') { + return ( + + + + ); + } + + if (status !== 'authed') { + return ; + } + + if (needsCompanySetup) { + return ; + } - if (status === 'loading') { return ( - - - + + }> + + }> + } /> + } /> + } /> + }> + } /> + } /> + } /> + } /> + + }> + } /> + } /> + + }> + } /> + } /> + } /> + + } /> + + + + ); } - - if (status !== 'authed') { - return ; - } - - if (needsCompanySetup) { - return ; - } - - return ( - - }> - - }> - } /> - } /> - } /> - }> - } /> - } /> - } /> - } /> - - }> - } /> - } /> - - }> - } /> - } /> - } /> - - } /> - - - - - ); } + +const mapStateToProps = (state) => ({ + status: selectAuthStatus(state), + needsCompanySetup: selectNeedsCompanySetup(state), +}); + +export default connect(mapStateToProps)(App); diff --git a/src/auth/AuthProvider.jsx b/src/auth/AuthProvider.jsx deleted file mode 100644 index 95418bc..0000000 --- a/src/auth/AuthProvider.jsx +++ /dev/null @@ -1,115 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { useSnackbar } from 'notistack'; -import { auth, company } from '../api'; -import { AuthContext } from './context'; - -function applyUser(session, setUser, setStatus) { - const nextUser = session?.user ?? null; - setUser(nextUser); - setStatus(nextUser ? 'authed' : 'anon'); - return nextUser; -} - -export default function AuthProvider({ children }) { - const { enqueueSnackbar } = useSnackbar(); - const [user, setUser] = useState(null); - const [googleClientId, setGoogleClientId] = useState(null); - const [status, setStatus] = useState('loading'); // loading | authed | anon - const [companyReady, setCompanyReady] = useState(false); - - useEffect(() => { - let active = true; - (async () => { - try { - const session = await auth.me(); - if (!active) return; - const nextUser = applyUser(session, setUser, setStatus); - setCompanyReady(nextUser?.selectedCompanyId != null); - setGoogleClientId(session.googleClientId); - if (session.forbidden) { - enqueueSnackbar(session.message, { variant: 'warning' }); - } - } catch (err) { - if (!active) return; - setStatus('anon'); - setCompanyReady(false); - enqueueSnackbar(`Session check failed: ${err.message}`, { - variant: 'error', - }); - } - })(); - return () => { - active = false; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const loginWithGoogle = useCallback(async (credential) => { - const { session } = await auth.loginWithGoogle(credential); - if (!session?.user) { - setUser(null); - setStatus('anon'); - setCompanyReady(false); - enqueueSnackbar( - `Login ok but session cookie not accepted (/api/auth/me still unauthorized)`, - { variant: 'error', autoHideDuration: 12000 }, - ); - return; - } - const nextUser = applyUser(session, setUser, setStatus); - setCompanyReady(nextUser?.selectedCompanyId != null); - }, [enqueueSnackbar]); - - const selectCompany = useCallback(async (companyId) => { - const data = await company.select(companyId); - if (data.user) setUser(data.user); - setCompanyReady(true); - }, []); - - const createCompany = useCallback(async ({ name, iso639, taxrate, reducedtaxrate }) => { - const data = await company.create({ name, iso639, taxrate, reducedtaxrate }); - if (data.user) { - setUser(data.user); - setCompanyReady(true); - return; - } - if (data.company?.id) { - const selected = await company.select(data.company.id); - if (selected.user) setUser(selected.user); - setCompanyReady(true); - } - }, []); - - const logout = useCallback(async () => { - await auth.logout(); - setUser(null); - setStatus('anon'); - setCompanyReady(false); - window.history.replaceState(null, '', '/'); - }, []); - - const value = useMemo( - () => ({ - user, - googleClientId, - status, - loginWithGoogle, - logout, - selectCompany, - createCompany, - needsCompanySetup: status === 'authed' && !companyReady, - }), - [ - user, - googleClientId, - status, - companyReady, - loginWithGoogle, - logout, - selectCompany, - createCompany, - ], - ); - - return {children}; -} diff --git a/src/auth/context.js b/src/auth/context.js deleted file mode 100644 index a3e3c7d..0000000 --- a/src/auth/context.js +++ /dev/null @@ -1,3 +0,0 @@ -import { createContext } from 'react'; - -export const AuthContext = createContext(null); diff --git a/src/auth/useAuth.js b/src/auth/useAuth.js deleted file mode 100644 index cfcfe48..0000000 --- a/src/auth/useAuth.js +++ /dev/null @@ -1,8 +0,0 @@ -import { useContext } from 'react'; -import { AuthContext } from './context'; - -export function useAuth() { - const ctx = useContext(AuthContext); - if (!ctx) throw new Error('useAuth must be used within an AuthProvider'); - return ctx; -} diff --git a/src/components/AuthBootstrap.jsx b/src/components/AuthBootstrap.jsx new file mode 100644 index 0000000..88571b4 --- /dev/null +++ b/src/components/AuthBootstrap.jsx @@ -0,0 +1,30 @@ +import { Component } from 'react'; +import { connect } from 'react-redux'; +import { enqueueSnackbar } from 'notistack'; +import { bootstrapSession } from '../store/authSlice'; + +class AuthBootstrap extends Component { + componentDidMount() { + this.props + .bootstrapSession() + .unwrap() + .then((session) => { + if (session.forbidden) { + enqueueSnackbar(session.message, { variant: 'warning' }); + } + }) + .catch((err) => { + enqueueSnackbar(`Session check failed: ${err.message}`, { variant: 'error' }); + }); + } + + render() { + return this.props.children; + } +} + +const mapDispatchToProps = (dispatch) => ({ + bootstrapSession: () => dispatch(bootstrapSession()), +}); + +export default connect(null, mapDispatchToProps)(AuthBootstrap); diff --git a/src/components/CompanySettingsDialog.jsx b/src/components/CompanySettingsDialog.jsx index 7c10fc5..254e9e0 100644 --- a/src/components/CompanySettingsDialog.jsx +++ b/src/components/CompanySettingsDialog.jsx @@ -1,4 +1,5 @@ -import { useEffect, useState } from 'react'; +import { Component } from 'react'; +import { connect } from 'react-redux'; import Alert from '@mui/material/Alert'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -14,72 +15,75 @@ import Stack from '@mui/material/Stack'; import TextField from '@mui/material/TextField'; import ToggleButton from '@mui/material/ToggleButton'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; -import { useSnackbar } from 'notistack'; +import { enqueueSnackbar } from 'notistack'; import { company } from '../api'; -import { useAuth } from '../auth/useAuth'; +import { selectAuthUser } from '../store/authSlice'; import { ISO639_OPTIONS, companyLanguageToIso639, parseTaxRate, } from '../utils/companyForm'; -export default function CompanySettingsDialog({ open, onClose }) { - const { user } = useAuth(); - const { enqueueSnackbar } = useSnackbar(); - const [loading, setLoading] = useState(false); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(null); - const [name, setName] = useState(''); - const [iso639, setIso639] = useState('de'); - const [taxrate, setTaxrate] = useState(''); - const [reducedtaxrate, setReducedtaxrate] = useState(''); - - const companyId = user?.selectedCompanyId; - const parsedTaxrate = parseTaxRate(taxrate); - const parsedReducedtaxrate = parseTaxRate(reducedtaxrate); - const canSave = - name.trim() !== '' && - ISO639_OPTIONS.includes(iso639) && - parsedTaxrate != null && - parsedReducedtaxrate != null; - - useEffect(() => { - if (!open || !companyId) return; - - let active = true; - (async () => { - setLoading(true); - setError(null); - try { - const data = await company.getById(companyId); - if (!active) return; - const item = data.company; - setName(item.name ?? ''); - setIso639(companyLanguageToIso639(item)); - setTaxrate(item.taxrate != null ? String(item.taxrate) : ''); - setReducedtaxrate(item.reducedtaxrate != null ? String(item.reducedtaxrate) : ''); - } catch (err) { - if (!active) return; - setError(err.message); - } finally { - if (active) setLoading(false); - } - })(); - - return () => { - active = false; +class CompanySettingsDialog extends Component { + constructor(props) { + super(props); + this.state = { + loading: false, + busy: false, + error: null, + name: '', + iso639: 'de', + taxrate: '', + reducedtaxrate: '', }; - }, [open, companyId]); + this.handleSubmit = this.handleSubmit.bind(this); + } - const handleSubmit = async (event) => { + componentDidUpdate(prevProps) { + const { open, user } = this.props; + const companyId = user?.selectedCompanyId; + if (open && !prevProps.open && companyId) { + void this.loadCompany(companyId); + } + } + + async loadCompany(companyId) { + this.setState({ loading: true, error: null }); + try { + const data = await company.getById(companyId); + const item = data.company; + this.setState({ + name: item.name ?? '', + iso639: companyLanguageToIso639(item), + taxrate: item.taxrate != null ? String(item.taxrate) : '', + reducedtaxrate: item.reducedtaxrate != null ? String(item.reducedtaxrate) : '', + }); + } catch (err) { + this.setState({ error: err.message }); + } finally { + this.setState({ loading: false }); + } + } + + async handleSubmit(event) { event.preventDefault(); + const { user, onClose } = this.props; + const { name, iso639, taxrate, reducedtaxrate } = this.state; + const companyId = user?.selectedCompanyId; + const parsedTaxrate = parseTaxRate(taxrate); + const parsedReducedtaxrate = parseTaxRate(reducedtaxrate); + const canSave = + name.trim() !== '' && + ISO639_OPTIONS.includes(iso639) && + parsedTaxrate != null && + parsedReducedtaxrate != null; + if (!companyId || !canSave) { - setError('Bitte alle Pflichtfelder ausfüllen.'); + this.setState({ error: 'Bitte alle Pflichtfelder ausfüllen.' }); return; } - setBusy(true); - setError(null); + this.setState({ busy: true, error: null }); try { await company.update(companyId, { name: name.trim(), @@ -90,106 +94,124 @@ export default function CompanySettingsDialog({ open, onClose }) { enqueueSnackbar('Unternehmen gespeichert.', { variant: 'success' }); onClose(); } catch (err) { - setError(err.message); + this.setState({ error: err.message }); } finally { - setBusy(false); + this.setState({ busy: false }); } - }; + } - return ( - - Unternehmen bearbeiten - - - {error ? ( - - {error} - - ) : null} + render() { + const { open, onClose } = this.props; + const { loading, busy, error, name, iso639, taxrate, reducedtaxrate } = this.state; + const parsedTaxrate = parseTaxRate(taxrate); + const parsedReducedtaxrate = parseTaxRate(reducedtaxrate); + const canSave = + name.trim() !== '' && + ISO639_OPTIONS.includes(iso639) && + parsedTaxrate != null && + parsedReducedtaxrate != null; - {loading ? ( - - - - ) : ( - - setName(e.target.value)} - autoComplete="organization" - autoFocus - /> + return ( + + Unternehmen bearbeiten + + + {error ? ( + + {error} + + ) : null} - - Sprache - { - if (value) setIso639(value); - }} - > - Deutsch - English - - - - + {loading ? ( + + + + ) : ( + setTaxrate(e.target.value)} - error={taxrate !== '' && parsedTaxrate == null} - slotProps={{ - input: { - endAdornment: %, - }, - htmlInput: { min: 0, max: 100, step: 0.01, required: true }, - }} - /> - setReducedtaxrate(e.target.value)} - error={reducedtaxrate !== '' && parsedReducedtaxrate == null} - slotProps={{ - input: { - endAdornment: %, - }, - htmlInput: { min: 0, max: 100, step: 0.01, required: true }, - }} + onChange={(e) => this.setState({ name: e.target.value })} + autoComplete="organization" + autoFocus /> + + + Sprache + { + if (value) this.setState({ iso639: value }); + }} + > + Deutsch + English + + + + + this.setState({ taxrate: e.target.value })} + error={taxrate !== '' && parsedTaxrate == null} + slotProps={{ + input: { + endAdornment: %, + }, + htmlInput: { min: 0, max: 100, step: 0.01, required: true }, + }} + /> + this.setState({ reducedtaxrate: e.target.value })} + error={reducedtaxrate !== '' && parsedReducedtaxrate == null} + slotProps={{ + input: { + endAdornment: %, + }, + htmlInput: { min: 0, max: 100, step: 0.01, required: true }, + }} + /> + - - )} - - - - - - - - ); + )} + + + + + + + + ); + } } + +const mapStateToProps = (state) => ({ + user: selectAuthUser(state), +}); + +export default connect(mapStateToProps)(CompanySettingsDialog); diff --git a/src/components/CompanySetupDialog.jsx b/src/components/CompanySetupDialog.jsx index d5c7acd..e9a29ff 100644 --- a/src/components/CompanySetupDialog.jsx +++ b/src/components/CompanySetupDialog.jsx @@ -1,4 +1,5 @@ -import { useCallback, useEffect, useState } from 'react'; +import { Component } from 'react'; +import { connect } from 'react-redux'; import Alert from '@mui/material/Alert'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -20,225 +21,256 @@ import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import Typography from '@mui/material/Typography'; import BusinessIcon from '@mui/icons-material/Business'; import { company } from '../api'; -import { useAuth } from '../auth/useAuth'; +import { createCompany, logout, selectCompany } from '../store/authSlice'; import { defaultIso639, ISO639_OPTIONS, parseTaxRate, } from '../utils/companyForm'; -export default function CompanySetupDialog() { - const { createCompany, selectCompany, logout } = useAuth(); - const [companies, setCompanies] = useState([]); - const [loading, setLoading] = useState(true); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(null); - const [name, setName] = useState(''); - const [iso639, setIso639] = useState(defaultIso639); - const [taxrate, setTaxrate] = useState('19'); - const [reducedtaxrate, setReducedtaxrate] = useState('7'); +class CompanySetupDialog extends Component { + constructor(props) { + super(props); + this.state = { + companies: [], + loading: true, + busy: false, + error: null, + name: '', + iso639: defaultIso639(), + taxrate: '19', + reducedtaxrate: '7', + }; + this.handleSelect = this.handleSelect.bind(this); + this.handleCreate = this.handleCreate.bind(this); + this.handleLogout = this.handleLogout.bind(this); + } - const hasCompanies = companies.length > 0; + componentDidMount() { + void this.loadCompanies(); + } - const loadCompanies = useCallback(async () => { - setLoading(true); + async loadCompanies() { + this.setState({ loading: true }); try { const data = await company.list(); - setCompanies(data.companies ?? []); + this.setState({ companies: data.companies ?? [] }); } catch { - setCompanies([]); + this.setState({ companies: [] }); } finally { - setLoading(false); + this.setState({ loading: false }); } - }, []); + } - useEffect(() => { - void loadCompanies(); - }, [loadCompanies]); - - const parsedTaxrate = parseTaxRate(taxrate); - const parsedReducedtaxrate = parseTaxRate(reducedtaxrate); - const canCreate = - name.trim() !== '' && - ISO639_OPTIONS.includes(iso639) && - parsedTaxrate != null && - parsedReducedtaxrate != null; - - const handleSelect = async (companyId) => { - setBusy(true); - setError(null); + async handleSelect(companyId) { + this.setState({ busy: true, error: null }); try { - await selectCompany(companyId); + await this.props.selectCompany(companyId).unwrap(); } catch (err) { - setError(err.message); + this.setState({ error: err.message }); } finally { - setBusy(false); + this.setState({ busy: false }); } - }; + } - const handleCreate = async (event) => { + async handleCreate(event) { event.preventDefault(); + const { name, iso639, taxrate, reducedtaxrate } = this.state; + const parsedTaxrate = parseTaxRate(taxrate); + const parsedReducedtaxrate = parseTaxRate(reducedtaxrate); + const canCreate = + name.trim() !== '' && + ISO639_OPTIONS.includes(iso639) && + parsedTaxrate != null && + parsedReducedtaxrate != null; + if (!canCreate) { - setError('Bitte alle Pflichtfelder ausfüllen.'); + this.setState({ error: 'Bitte alle Pflichtfelder ausfüllen.' }); return; } - setBusy(true); - setError(null); + this.setState({ busy: true, error: null }); try { - await createCompany({ - name: name.trim(), - iso639, - taxrate: parsedTaxrate, - reducedtaxrate: parsedReducedtaxrate, - }); + await this.props + .createCompany({ + name: name.trim(), + iso639, + taxrate: parsedTaxrate, + reducedtaxrate: parsedReducedtaxrate, + }) + .unwrap(); } catch (err) { - setError(err.message); + this.setState({ error: err.message }); } finally { - setBusy(false); + this.setState({ busy: false }); } - }; + } - return ( - - - {loading - ? 'Unternehmen' - : hasCompanies - ? 'Unternehmen auswählen' - : 'Unternehmen anlegen'} - - - {!loading ? ( - - {hasCompanies - ? 'Wählen Sie ein bestehendes Unternehmen oder legen Sie ein neues an.' - : 'Legen Sie Ihr Unternehmen an, um fortzufahren.'} - - ) : null} + handleLogout() { + void this.props.logout(); + } - {error ? ( - - {error} - - ) : null} + render() { + const { companies, loading, busy, error, name, iso639, taxrate, reducedtaxrate } = + this.state; + const hasCompanies = companies.length > 0; + const parsedTaxrate = parseTaxRate(taxrate); + const parsedReducedtaxrate = parseTaxRate(reducedtaxrate); + const canCreate = + name.trim() !== '' && + ISO639_OPTIONS.includes(iso639) && + parsedTaxrate != null && + parsedReducedtaxrate != null; - {loading ? ( - - - - ) : ( - - {hasCompanies ? ( - - - {companies.map((item) => ( - handleSelect(item.id)} - > - - - - ))} - - - ) : null} + return ( + + + {loading + ? 'Unternehmen' + : hasCompanies + ? 'Unternehmen auswählen' + : 'Unternehmen anlegen'} + + + {!loading ? ( + + {hasCompanies + ? 'Wählen Sie ein bestehendes Unternehmen oder legen Sie ein neues an.' + : 'Legen Sie Ihr Unternehmen an, um fortzufahren.'} + + ) : null} - {hasCompanies ? oder : null} + {error ? ( + + {error} + + ) : null} - - - {hasCompanies ? ( - Neues Unternehmen anlegen - ) : null} - - setName(e.target.value)} - autoComplete="organization" - autoFocus={!hasCompanies} - /> - - - Sprache - { - if (value) setIso639(value); - }} - > - Deutsch - English - - - - - setTaxrate(e.target.value)} - error={taxrate !== '' && parsedTaxrate == null} - slotProps={{ - input: { - endAdornment: %, - }, - htmlInput: { min: 0, max: 100, step: 0.01, required: true }, - }} - /> - setReducedtaxrate(e.target.value)} - error={reducedtaxrate !== '' && parsedReducedtaxrate == null} - slotProps={{ - input: { - endAdornment: %, - }, - htmlInput: { min: 0, max: 100, step: 0.01, required: true }, - }} - /> - - - - + {loading ? ( + + + ) : ( + + {hasCompanies ? ( + + + {companies.map((item) => ( + this.handleSelect(item.id)} + > + + + + ))} + + + ) : null} - - - )} - - - ); + {hasCompanies ? oder : null} + + + + {hasCompanies ? ( + Neues Unternehmen anlegen + ) : null} + + this.setState({ name: e.target.value })} + autoComplete="organization" + autoFocus={!hasCompanies} + /> + + + Sprache + { + if (value) this.setState({ iso639: value }); + }} + > + Deutsch + English + + + + + this.setState({ taxrate: e.target.value })} + error={taxrate !== '' && parsedTaxrate == null} + slotProps={{ + input: { + endAdornment: %, + }, + htmlInput: { min: 0, max: 100, step: 0.01, required: true }, + }} + /> + this.setState({ reducedtaxrate: e.target.value })} + error={reducedtaxrate !== '' && parsedReducedtaxrate == null} + slotProps={{ + input: { + endAdornment: %, + }, + htmlInput: { min: 0, max: 100, step: 0.01, required: true }, + }} + /> + + + + + + + + + )} + + + ); + } } + +const mapDispatchToProps = (dispatch) => ({ + createCompany: (payload) => dispatch(createCompany(payload)), + selectCompany: (companyId) => dispatch(selectCompany(companyId)), + logout: () => dispatch(logout()), +}); + +export default connect(null, mapDispatchToProps)(CompanySetupDialog); diff --git a/src/components/LoginPage.jsx b/src/components/LoginPage.jsx index 78b9f3b..53027bd 100644 --- a/src/components/LoginPage.jsx +++ b/src/components/LoginPage.jsx @@ -1,13 +1,10 @@ -import { useState } from 'react'; +import { Component } from 'react'; +import { connect } from 'react-redux'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; import GoogleIcon from '@mui/icons-material/Google'; -import { useSnackbar } from 'notistack'; -import { useAuth } from '../auth/useAuth'; +import { enqueueSnackbar } from 'notistack'; +import { loginWithGoogle, selectAuthStatus, selectGoogleClientId } from '../store/authSlice'; import { abortGoogleSignIn, FEDCM_BLOCKED_MESSAGE, @@ -17,12 +14,15 @@ import { requestGoogleIdToken, } from '../auth/googleSignIn'; -export default function LoginPage() { - const { loginWithGoogle, googleClientId, status } = useAuth(); - const { enqueueSnackbar } = useSnackbar(); - const [busy, setBusy] = useState(false); +class LoginPage extends Component { + constructor(props) { + super(props); + this.state = { busy: false }; + this.handleLogin = this.handleLogin.bind(this); + } - const handleLogin = async () => { + async handleLogin() { + const { googleClientId } = this.props; abortGoogleSignIn(); try { const idToken = await requestGoogleIdToken(googleClientId, { @@ -33,8 +33,8 @@ export default function LoginPage() { }); }, }); - setBusy(true); - await loginWithGoogle(idToken); + this.setState({ busy: true }); + await this.props.loginWithGoogle(idToken).unwrap(); } catch (err) { if (err instanceof GoogleSignInCancelledError) return; logError('[login] Google sign-in failed', { @@ -64,32 +64,47 @@ export default function LoginPage() { .join(' · '); enqueueSnackbar(`Login failed: ${detail}`, { variant: 'error' }); } finally { - setBusy(false); + this.setState({ busy: false }); } - }; + } - const loading = status === 'loading'; - const ready = Boolean(googleClientId); + render() { + const { busy } = this.state; + const { status, googleClientId } = this.props; + const loading = status === 'loading'; + const ready = Boolean(googleClientId); - return ( - - - - ); + + + ); + } } + +const mapStateToProps = (state) => ({ + status: selectAuthStatus(state), + googleClientId: selectGoogleClientId(state), +}); + +const mapDispatchToProps = (dispatch) => ({ + loginWithGoogle: (credential) => dispatch(loginWithGoogle(credential)), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(LoginPage); diff --git a/src/components/UserMenu.jsx b/src/components/UserMenu.jsx index d1b1344..1c731ee 100644 --- a/src/components/UserMenu.jsx +++ b/src/components/UserMenu.jsx @@ -1,4 +1,5 @@ -import { useState } from 'react'; +import { Component } from 'react'; +import { connect } from 'react-redux'; import Avatar from '@mui/material/Avatar'; import Box from '@mui/material/Box'; import IconButton from '@mui/material/IconButton'; @@ -8,62 +9,95 @@ import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import LogoutIcon from '@mui/icons-material/Logout'; import BusinessIcon from '@mui/icons-material/Business'; -import { useAuth } from '../auth/useAuth'; +import { logout, selectAuthUser } from '../store/authSlice'; import CompanySettingsDialog from './CompanySettingsDialog'; -// Menu open/close state is local; toggling it only re-renders this component. -export default function UserMenu() { - const { user, logout } = useAuth(); - const [anchorEl, setAnchorEl] = useState(null); - const [settingsOpen, setSettingsOpen] = useState(false); - const open = Boolean(anchorEl); +class UserMenu extends Component { + constructor(props) { + super(props); + this.state = { + anchorEl: null, + settingsOpen: false, + }; + this.handleOpenMenu = this.handleOpenMenu.bind(this); + this.handleCloseMenu = this.handleCloseMenu.bind(this); + this.handleOpenSettings = this.handleOpenSettings.bind(this); + this.handleCloseSettings = this.handleCloseSettings.bind(this); + this.handleLogout = this.handleLogout.bind(this); + } - return ( - <> - - setAnchorEl(e.currentTarget)} - onMouseDown={(e) => e.preventDefault()} - sx={{ p: 0, width: 32, height: 32, borderRadius: '50%' }} + handleOpenMenu(event) { + this.setState({ anchorEl: event.currentTarget }); + } + + handleCloseMenu() { + this.setState({ anchorEl: null }); + } + + handleOpenSettings() { + this.setState({ anchorEl: null, settingsOpen: true }); + } + + handleCloseSettings() { + this.setState({ settingsOpen: false }); + } + + handleLogout() { + this.setState({ anchorEl: null }); + void this.props.logout(); + } + + render() { + const { user } = this.props; + const { anchorEl, settingsOpen } = this.state; + const open = Boolean(anchorEl); + + return ( + <> + + e.preventDefault()} + sx={{ p: 0, width: 32, height: 32, borderRadius: '50%' }} + > + + {user?.email?.[0]?.toUpperCase()} + + + + - - {user?.email?.[0]?.toUpperCase()} - - - - setAnchorEl(null)} - anchorOrigin={{ vertical: 'top', horizontal: 'left' }} - transformOrigin={{ vertical: 'bottom', horizontal: 'left' }} - > - {user?.email} - { - setAnchorEl(null); - setSettingsOpen(true); - }} - > - - - - Unternehmen - - { - setAnchorEl(null); - logout(); - }} - > - - - - Sign out - - - setSettingsOpen(false)} /> - - ); + {user?.email} + + + + + Unternehmen + + + + + + Sign out + + + + + ); + } } + +const mapStateToProps = (state) => ({ + user: selectAuthUser(state), +}); + +const mapDispatchToProps = (dispatch) => ({ + logout: () => dispatch(logout()), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(UserMenu); diff --git a/src/main.jsx b/src/main.jsx index 8910f86..ee701dc 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,26 +1,30 @@ import { createRoot } from 'react-dom/client'; import CssBaseline from '@mui/material/CssBaseline'; import { ThemeProvider } from '@mui/material/styles'; +import { Provider } from 'react-redux'; import { SnackbarProvider } from 'notistack'; import { registerSW } from 'virtual:pwa-register'; -import AuthProvider from './auth/AuthProvider'; +import AuthBootstrap from './components/AuthBootstrap'; import App from './App'; +import { store } from './store'; import theme from './theme'; import './index.css'; registerSW({ immediate: true }); createRoot(document.getElementById('root')).render( - - - - - - - - , + + + + + + + + + + , ); diff --git a/src/store/authSlice.js b/src/store/authSlice.js new file mode 100644 index 0000000..a7d65fe --- /dev/null +++ b/src/store/authSlice.js @@ -0,0 +1,93 @@ +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import { auth, company } from '../api'; + +export const bootstrapSession = createAsyncThunk('auth/bootstrap', async () => auth.me()); + +export const loginWithGoogle = createAsyncThunk('auth/login', async (credential) => { + const { session } = await auth.loginWithGoogle(credential); + if (!session?.user) { + throw new Error('Login ok but session cookie not accepted (/api/auth/me still unauthorized)'); + } + return session; +}); + +export const selectCompany = createAsyncThunk('auth/selectCompany', async (companyId) => { + const data = await company.select(companyId); + return data.user ?? { selectedCompanyId: companyId }; +}); + +export const createCompany = createAsyncThunk('auth/createCompany', async (payload) => { + const data = await company.create(payload); + if (data.user) return data.user; + if (data.company?.id) { + const selected = await company.select(data.company.id); + return selected.user ?? { selectedCompanyId: data.company.id }; + } + throw new Error('Unternehmen angelegt, aber keine Benutzerdaten erhalten.'); +}); + +export const logout = createAsyncThunk('auth/logout', async () => { + await auth.logout(); + window.history.replaceState(null, '', '/'); +}); + +const initialState = { + user: null, + googleClientId: null, + status: 'loading', + companyReady: false, +}; + +const authSlice = createSlice({ + name: 'auth', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(bootstrapSession.fulfilled, (state, action) => { + const session = action.payload; + state.user = session.user ?? null; + state.googleClientId = session.googleClientId ?? null; + state.status = session.user ? 'authed' : 'anon'; + state.companyReady = session.user?.selectedCompanyId != null; + }) + .addCase(bootstrapSession.rejected, (state) => { + state.user = null; + state.status = 'anon'; + state.companyReady = false; + }) + .addCase(loginWithGoogle.fulfilled, (state, action) => { + const session = action.payload; + state.user = session.user; + state.googleClientId = session.googleClientId ?? state.googleClientId; + state.status = 'authed'; + state.companyReady = session.user?.selectedCompanyId != null; + }) + .addCase(loginWithGoogle.rejected, (state) => { + state.user = null; + state.status = 'anon'; + state.companyReady = false; + }) + .addCase(selectCompany.fulfilled, (state, action) => { + state.user = { ...state.user, ...action.payload }; + state.companyReady = true; + }) + .addCase(createCompany.fulfilled, (state, action) => { + state.user = { ...state.user, ...action.payload }; + state.companyReady = true; + }) + .addCase(logout.fulfilled, (state) => { + state.user = null; + state.status = 'anon'; + state.companyReady = false; + }); + }, +}); + +export const selectAuthStatus = (state) => state.auth.status; +export const selectAuthUser = (state) => state.auth.user; +export const selectGoogleClientId = (state) => state.auth.googleClientId; +export const selectNeedsCompanySetup = (state) => + state.auth.status === 'authed' && !state.auth.companyReady; + +export default authSlice.reducer; diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..e7b1251 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,8 @@ +import { configureStore } from '@reduxjs/toolkit'; +import authReducer from './authSlice'; + +export const store = configureStore({ + reducer: { + auth: authReducer, + }, +});