diff --git a/moneymgr_backend/docker/dex/dex.config.yaml b/moneymgr_backend/docker/dex/dex.config.yaml
index 5d4a047..0b9d381 100644
--- a/moneymgr_backend/docker/dex/dex.config.yaml
+++ b/moneymgr_backend/docker/dex/dex.config.yaml
@@ -22,5 +22,5 @@ staticClients:
- id: foo
secret: bar
redirectURIs:
- - http://localhost:5173/web/oidc_cb
+ - http://localhost:5173/oidc_cb
name: Project
diff --git a/moneymgr_backend/src/app_config.rs b/moneymgr_backend/src/app_config.rs
index 5d19b01..fca7d52 100644
--- a/moneymgr_backend/src/app_config.rs
+++ b/moneymgr_backend/src/app_config.rs
@@ -72,7 +72,7 @@ pub struct AppConfig {
pub oidc_client_secret: String,
/// OpenID login redirect URL
- #[arg(long, env, default_value = "APP_ORIGIN/web/oidc_cb")]
+ #[arg(long, env, default_value = "APP_ORIGIN/oidc_cb")]
oidc_redirect_url: String,
/// S3 Bucket name
diff --git a/moneymgr_web/.env b/moneymgr_web/.env
new file mode 100644
index 0000000..38c6b7b
--- /dev/null
+++ b/moneymgr_web/.env
@@ -0,0 +1 @@
+VITE_APP_BACKEND=http://localhost:8000/api
diff --git a/moneymgr_web/.env.production b/moneymgr_web/.env.production
new file mode 100644
index 0000000..89f62f8
--- /dev/null
+++ b/moneymgr_web/.env.production
@@ -0,0 +1 @@
+VITE_APP_BACKEND=/api
diff --git a/moneymgr_web/README.md b/moneymgr_web/README.md
index 40ede56..f02ed1e 100644
--- a/moneymgr_web/README.md
+++ b/moneymgr_web/README.md
@@ -1,54 +1,2 @@
-# React + TypeScript + Vite
-
-This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
-
-Currently, two official plugins are available:
-
-- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
-- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
-
-## Expanding the ESLint configuration
-
-If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
-
-```js
-export default tseslint.config({
- extends: [
- // Remove ...tseslint.configs.recommended and replace with this
- ...tseslint.configs.recommendedTypeChecked,
- // Alternatively, use this for stricter rules
- ...tseslint.configs.strictTypeChecked,
- // Optionally, add this for stylistic rules
- ...tseslint.configs.stylisticTypeChecked,
- ],
- languageOptions: {
- // other options...
- parserOptions: {
- project: ['./tsconfig.node.json', './tsconfig.app.json'],
- tsconfigRootDir: import.meta.dirname,
- },
- },
-})
-```
-
-You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
-
-```js
-// eslint.config.js
-import reactX from 'eslint-plugin-react-x'
-import reactDom from 'eslint-plugin-react-dom'
-
-export default tseslint.config({
- plugins: {
- // Add the react-x and react-dom plugins
- 'react-x': reactX,
- 'react-dom': reactDom,
- },
- rules: {
- // other rules...
- // Enable its recommended typescript rules
- ...reactX.configs['recommended-typescript'].rules,
- ...reactDom.configs.recommended.rules,
- },
-})
-```
+# MoneyManager web
+Application built using Vite + React + MUI
diff --git a/moneymgr_web/eslint.config.js b/moneymgr_web/eslint.config.js
index 092408a..583a4f6 100644
--- a/moneymgr_web/eslint.config.js
+++ b/moneymgr_web/eslint.config.js
@@ -1,28 +1,48 @@
-import js from '@eslint/js'
-import globals from 'globals'
-import reactHooks from 'eslint-plugin-react-hooks'
-import reactRefresh from 'eslint-plugin-react-refresh'
-import tseslint from 'typescript-eslint'
+import js from "@eslint/js";
+import globals from "globals";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
+import tseslint from "typescript-eslint";
+import reactX from "eslint-plugin-react-x";
+import reactDom from "eslint-plugin-react-dom";
export default tseslint.config(
- { ignores: ['dist'] },
+ { ignores: ["dist"] },
{
- extends: [js.configs.recommended, ...tseslint.configs.recommended],
- files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ // Remove ...tseslint.configs.recommended and replace with this
+ // ...tseslint.configs.recommendedTypeChecked,
+ // Alternatively, use this for stricter rules
+ ...tseslint.configs.strictTypeChecked,
+ // Optionally, add this for stylistic rules
+ ...tseslint.configs.stylisticTypeChecked,
+ ],
+ files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
+ // other options...
+ parserOptions: {
+ project: ["./tsconfig.node.json", "./tsconfig.app.json"],
+ tsconfigRootDir: import.meta.dirname,
+ },
},
plugins: {
- 'react-hooks': reactHooks,
- 'react-refresh': reactRefresh,
+ "react-hooks": reactHooks,
+ "react-refresh": reactRefresh,
+ "react-x": reactX,
+ "react-dom": reactDom,
},
rules: {
...reactHooks.configs.recommended.rules,
- 'react-refresh/only-export-components': [
- 'warn',
+ "react-refresh/only-export-components": [
+ "warn",
{ allowConstantExport: true },
],
+
+ ...reactX.configs["recommended-typescript"].rules,
+ ...reactDom.configs.recommended.rules,
},
- },
-)
+ }
+);
diff --git a/moneymgr_web/index.html b/moneymgr_web/index.html
index e4b78ea..37f21fb 100644
--- a/moneymgr_web/index.html
+++ b/moneymgr_web/index.html
@@ -2,9 +2,9 @@
-
+
- Vite + React + TS
+ Money manager
diff --git a/moneymgr_web/package-lock.json b/moneymgr_web/package-lock.json
index c07ebb0..85fbb70 100644
--- a/moneymgr_web/package-lock.json
+++ b/moneymgr_web/package-lock.json
@@ -8,8 +8,20 @@
"name": "moneymgr_web",
"version": "0.0.0",
"dependencies": {
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.0",
+ "@fontsource/roboto": "^5.2.5",
+ "@mdi/js": "^7.4.47",
+ "@mdi/react": "^1.6.1",
+ "@mui/icons-material": "^6.4.8",
+ "@mui/material": "^6.4.8",
+ "@mui/x-data-grid": "^7.28.0",
+ "@mui/x-date-pickers": "^7.28.0",
+ "dayjs": "^1.11.13",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "react-router": "^7.3.0",
+ "react-router-dom": "^7.3.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
@@ -17,8 +29,10 @@
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0",
+ "eslint-plugin-react-dom": "^1.35.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
+ "eslint-plugin-react-x": "^1.35.0",
"globals": "^15.15.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
@@ -43,7 +57,6 @@
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.25.9",
@@ -99,7 +112,6 @@
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz",
"integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.26.10",
@@ -133,7 +145,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
"integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.25.9",
@@ -175,7 +186,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -185,7 +195,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -219,7 +228,6 @@
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz",
"integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.10"
@@ -263,11 +271,22 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.26.10",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
+ "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
+ "license": "MIT",
+ "dependencies": {
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
@@ -282,7 +301,6 @@
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz",
"integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
@@ -301,7 +319,6 @@
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -311,7 +328,6 @@
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz",
"integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
@@ -321,6 +337,158 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@emotion/babel-plugin": {
+ "version": "11.13.5",
+ "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
+ "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.16.7",
+ "@babel/runtime": "^7.18.3",
+ "@emotion/hash": "^0.9.2",
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/serialize": "^1.3.3",
+ "babel-plugin-macros": "^3.1.0",
+ "convert-source-map": "^1.5.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-root": "^1.1.0",
+ "source-map": "^0.5.7",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/cache": {
+ "version": "11.14.0",
+ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
+ "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/sheet": "^1.4.0",
+ "@emotion/utils": "^1.4.2",
+ "@emotion/weak-memoize": "^0.4.0",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+ "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz",
+ "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/memoize": "^0.9.0"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
+ "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/react": {
+ "version": "11.14.0",
+ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
+ "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.13.5",
+ "@emotion/cache": "^11.14.0",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
+ "@emotion/utils": "^1.4.2",
+ "@emotion/weak-memoize": "^0.4.0",
+ "hoist-non-react-statics": "^3.3.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/serialize": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
+ "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/hash": "^0.9.2",
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/unitless": "^0.10.0",
+ "@emotion/utils": "^1.4.2",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@emotion/sheet": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
+ "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/styled": {
+ "version": "11.14.0",
+ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz",
+ "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.13.5",
+ "@emotion/is-prop-valid": "^1.3.0",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
+ "@emotion/utils": "^1.4.2"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.0.0-rc.0",
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
+ "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
+ "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@emotion/utils": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
+ "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/weak-memoize": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
+ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
+ "license": "MIT"
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
@@ -788,6 +956,130 @@
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
+ "node_modules/@eslint-react/ast": {
+ "version": "1.35.0",
+ "resolved": "https://registry.npmjs.org/@eslint-react/ast/-/ast-1.35.0.tgz",
+ "integrity": "sha512-ULE2vnV+5dK2TTksx+6ZKo5fiAa3iBL06WX4dThbXvtbCuluf3CTxt7RL7mOmk7gG4O7kmDXQnIzPlBMkK0Jyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-react/eff": "1.35.0",
+ "@typescript-eslint/types": "^8.26.1",
+ "@typescript-eslint/typescript-estree": "^8.26.1",
+ "@typescript-eslint/utils": "^8.26.1",
+ "string-ts": "^2.2.1",
+ "ts-pattern": "^5.6.2"
+ },
+ "engines": {
+ "bun": ">=1.0.15",
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@eslint-react/core": {
+ "version": "1.35.0",
+ "resolved": "https://registry.npmjs.org/@eslint-react/core/-/core-1.35.0.tgz",
+ "integrity": "sha512-r5OiZNI4/OeeXb5ruQFuq9mbRmeR2ry0lQ2gca0I/B+fhgxT4kFC8H+sQwIvIlomdXE5+GgkNlqZGLWAKfMrDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-react/ast": "1.35.0",
+ "@eslint-react/eff": "1.35.0",
+ "@eslint-react/jsx": "1.35.0",
+ "@eslint-react/shared": "1.35.0",
+ "@eslint-react/var": "1.35.0",
+ "@typescript-eslint/scope-manager": "^8.26.1",
+ "@typescript-eslint/type-utils": "^8.26.1",
+ "@typescript-eslint/types": "^8.26.1",
+ "@typescript-eslint/utils": "^8.26.1",
+ "birecord": "^0.1.1",
+ "ts-pattern": "^5.6.2"
+ },
+ "engines": {
+ "bun": ">=1.0.15",
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@eslint-react/eff": {
+ "version": "1.35.0",
+ "resolved": "https://registry.npmjs.org/@eslint-react/eff/-/eff-1.35.0.tgz",
+ "integrity": "sha512-ixfgCirM4dYVXwWe9frBtHKDii575ypxfFCf5U7+mEDvSk4itoy7jz+H+Gb4XzsQ/LxZPJFFzxFvY+LhIHhdMQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.0.15",
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@eslint-react/jsx": {
+ "version": "1.35.0",
+ "resolved": "https://registry.npmjs.org/@eslint-react/jsx/-/jsx-1.35.0.tgz",
+ "integrity": "sha512-y2rtFriOwuUFpSWRqKJ/hD+p/s4M0AD8H7SIKjFr5RkA10qV4vHs4CmEWJaNKNV9NaF7nW5+osJh23lVw+Y0NA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-react/ast": "1.35.0",
+ "@eslint-react/eff": "1.35.0",
+ "@eslint-react/var": "1.35.0",
+ "@typescript-eslint/scope-manager": "^8.26.1",
+ "@typescript-eslint/types": "^8.26.1",
+ "@typescript-eslint/utils": "^8.26.1",
+ "ts-pattern": "^5.6.2"
+ },
+ "engines": {
+ "bun": ">=1.0.15",
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@eslint-react/shared": {
+ "version": "1.35.0",
+ "resolved": "https://registry.npmjs.org/@eslint-react/shared/-/shared-1.35.0.tgz",
+ "integrity": "sha512-s8wPoL64ULNcl7OajEMr1pvMza3NxIhFxd9O5kGbJnXt7I8cbsWdT4WFOqZNjWA5/FtWaXdg4+mfOzZXbVWAaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-react/eff": "1.35.0",
+ "@typescript-eslint/utils": "^8.26.1",
+ "picomatch": "^4.0.2",
+ "ts-pattern": "^5.6.2"
+ },
+ "engines": {
+ "bun": ">=1.0.15",
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@eslint-react/shared/node_modules/picomatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/@eslint-react/var": {
+ "version": "1.35.0",
+ "resolved": "https://registry.npmjs.org/@eslint-react/var/-/var-1.35.0.tgz",
+ "integrity": "sha512-IkHkUTsTEciTwDkwwTwO72lVrBP8mnC3rESaAVZoD45fSY1X0yAC+GCvscfmpNEl4yFEr7DbjGLVFY0Szfvg6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-react/ast": "1.35.0",
+ "@eslint-react/eff": "1.35.0",
+ "@typescript-eslint/scope-manager": "^8.26.1",
+ "@typescript-eslint/types": "^8.26.1",
+ "@typescript-eslint/utils": "^8.26.1",
+ "string-ts": "^2.2.1",
+ "ts-pattern": "^5.6.2"
+ },
+ "engines": {
+ "bun": ">=1.0.15",
+ "node": ">=18.18.0"
+ }
+ },
"node_modules/@eslint/config-array": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz",
@@ -897,6 +1189,15 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@fontsource/roboto": {
+ "version": "5.2.5",
+ "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.5.tgz",
+ "integrity": "sha512-70r2UZ0raqLn5W+sPeKhqlf8wGvUXFWlofaDlcbt/S3d06+17gXKr3VNqDODB0I1ASme3dGT5OJj9NABt7OTZQ==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -967,7 +1268,6 @@
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
@@ -982,7 +1282,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -992,7 +1291,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -1002,20 +1300,387 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@mdi/js": {
+ "version": "7.4.47",
+ "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz",
+ "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@mdi/react": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.6.1.tgz",
+ "integrity": "sha512-4qZeDcluDFGFTWkHs86VOlHkm6gnKaMql13/gpIcUQ8kzxHgpj31NuCkD8abECVfbULJ3shc7Yt4HJ6Wu6SN4w==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.7.2"
+ }
+ },
+ "node_modules/@mui/core-downloads-tracker": {
+ "version": "6.4.8",
+ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.8.tgz",
+ "integrity": "sha512-vjP4+A1ybyCRhDZC7r5EPWu/gLseFZxaGyPdDl94vzVvk6Yj6gahdaqcjbhkaCrJjdZj90m3VioltWPAnWF/zw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ }
+ },
+ "node_modules/@mui/icons-material": {
+ "version": "6.4.8",
+ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.8.tgz",
+ "integrity": "sha512-LKGWiLWRyoOw3dWxZQ+lV//mK+4DVTTAiLd2ljmJdD6XV0rDB8JFKjRD9nyn9cJAU5XgWnii7ZR3c93ttUnMKg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.26.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@mui/material": "^6.4.8",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/material": {
+ "version": "6.4.8",
+ "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.8.tgz",
+ "integrity": "sha512-5S9UTjKZZBd9GfbcYh/nYfD9cv6OXmj5Y7NgKYfk7JcSoshp8/pW5zP4wecRiroBSZX8wcrywSgogpVNO+5W0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.26.0",
+ "@mui/core-downloads-tracker": "^6.4.8",
+ "@mui/system": "^6.4.8",
+ "@mui/types": "~7.2.24",
+ "@mui/utils": "^6.4.8",
+ "@popperjs/core": "^2.11.8",
+ "@types/react-transition-group": "^4.4.12",
+ "clsx": "^2.1.1",
+ "csstype": "^3.1.3",
+ "prop-types": "^15.8.1",
+ "react-is": "^19.0.0",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@mui/material-pigment-css": "^6.4.8",
+ "@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"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@mui/material-pigment-css": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/private-theming": {
+ "version": "6.4.8",
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.8.tgz",
+ "integrity": "sha512-sWwQoNSn6elsPTAtSqCf+w5aaGoh7AASURNmpy+QTTD/zwJ0Jgwt0ZaaP6mXq2IcgHxYnYloM/+vJgHPMkRKTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.26.0",
+ "@mui/utils": "^6.4.8",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/styled-engine": {
+ "version": "6.4.8",
+ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.8.tgz",
+ "integrity": "sha512-oyjx1b1FvUCI85ZMO4trrjNxGm90eLN3Ohy0AP/SqK5gWvRQg1677UjNf7t6iETOKAleHctJjuq0B3aXO2gtmw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.26.0",
+ "@emotion/cache": "^11.13.5",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/sheet": "^1.4.0",
+ "csstype": "^3.1.3",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.4.1",
+ "@emotion/styled": "^11.3.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/system": {
+ "version": "6.4.8",
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.8.tgz",
+ "integrity": "sha512-gV7iBHoqlsIenU2BP0wq14BefRoZcASZ/4LeyuQglayBl+DfLX5rEd3EYR3J409V2EZpR0NOM1LATAGlNk2cyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.26.0",
+ "@mui/private-theming": "^6.4.8",
+ "@mui/styled-engine": "^6.4.8",
+ "@mui/types": "~7.2.24",
+ "@mui/utils": "^6.4.8",
+ "clsx": "^2.1.1",
+ "csstype": "^3.1.3",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/types": {
+ "version": "7.2.24",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz",
+ "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/utils": {
+ "version": "6.4.8",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.8.tgz",
+ "integrity": "sha512-C86gfiZ5BfZ51KqzqoHi1WuuM2QdSKoFhbkZeAfQRB+jCc4YNhhj11UXFVMMsqBgZ+Zy8IHNJW3M9Wj/LOwRXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.26.0",
+ "@mui/types": "~7.2.24",
+ "@types/prop-types": "^15.7.14",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-is": "^19.0.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/x-data-grid": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.28.0.tgz",
+ "integrity": "sha512-rOAUB0m1kL2hmgodScJu5AI0AjbVBJtG7erRZ3IhDyk73oRRlgnKttWNks9iIuVCNxXbCbBkvH06rqxgkkuCsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.25.7",
+ "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta",
+ "@mui/x-internals": "7.28.0",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "reselect": "^5.1.1",
+ "use-sync-external-store": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta",
+ "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/x-date-pickers": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.28.0.tgz",
+ "integrity": "sha512-m1bfkZLOw3cMogeh6q92SjykVmLzfptnz3ZTgAlFKV7UBnVFuGUITvmwbgTZ1Mz3FmLVnGUQYUpZWw0ZnoghNA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.25.7",
+ "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta",
+ "@mui/x-internals": "7.28.0",
+ "@types/react-transition-group": "^4.4.11",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta",
+ "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta",
+ "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
+ "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0",
+ "dayjs": "^1.10.7",
+ "luxon": "^3.0.2",
+ "moment": "^2.29.4",
+ "moment-hijri": "^2.1.2 || ^3.0.0",
+ "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "date-fns": {
+ "optional": true
+ },
+ "date-fns-jalali": {
+ "optional": true
+ },
+ "dayjs": {
+ "optional": true
+ },
+ "luxon": {
+ "optional": true
+ },
+ "moment": {
+ "optional": true
+ },
+ "moment-hijri": {
+ "optional": true
+ },
+ "moment-jalaali": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/x-internals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.28.0.tgz",
+ "integrity": "sha512-p4GEp/09bLDumktdIMiw+OF4p+pJOOjTG0VUvzNxjbHB9GxbBKoMcHrmyrURqoBnQpWIeFnN/QAoLMFSpfwQbw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.25.7",
+ "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1054,6 +1719,16 @@
"node": ">= 8"
}
},
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.36.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.36.0.tgz",
@@ -1365,6 +2040,12 @@
"@babel/types": "^7.20.7"
}
},
+ "node_modules/@types/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@@ -1379,11 +2060,22 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/parse-json": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
+ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.14",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
+ "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
+ "license": "MIT"
+ },
"node_modules/@types/react": {
"version": "19.0.11",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.11.tgz",
"integrity": "sha512-vrdxRZfo9ALXth6yPfV16PYTLZwsUWhVjjC+DkfE5t1suNSbBrWC9YqSuuxJZ8Ps6z1o2ycRpIqzZJIgklq4Tw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
@@ -1399,6 +2091,15 @@
"@types/react": "^19.0.0"
}
},
+ "node_modules/@types/react-transition-group": {
+ "version": "4.4.12",
+ "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
+ "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.26.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz",
@@ -1701,6 +2402,21 @@
"dev": true,
"license": "Python-2.0"
},
+ "node_modules/babel-plugin-macros": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
+ "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "cosmiconfig": "^7.0.0",
+ "resolve": "^1.19.0"
+ },
+ "engines": {
+ "node": ">=10",
+ "npm": ">=6"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1708,6 +2424,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/birecord": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/birecord/-/birecord-0.1.1.tgz",
+ "integrity": "sha512-VUpsf/qykW0heRlC8LooCq28Kxn3mAqKohhDG/49rrsQ1dT1CXyj/pgXS+5BSRzFTR/3DyIBOqQOrGyZOh71Aw==",
+ "dev": true,
+ "license": "(MIT OR Apache-2.0)"
+ },
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -1769,7 +2492,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -1813,6 +2535,15 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1833,6 +2564,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/compare-versions": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
+ "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1847,6 +2585,40 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
+ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/cosmiconfig": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+ "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/cosmiconfig/node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1866,14 +2638,18 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.13",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
+ "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -1894,6 +2670,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.120",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.120.tgz",
@@ -1901,6 +2687,15 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
"node_modules/esbuild": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
@@ -1956,7 +2751,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -2026,6 +2820,43 @@
}
}
},
+ "node_modules/eslint-plugin-react-dom": {
+ "version": "1.35.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-dom/-/eslint-plugin-react-dom-1.35.0.tgz",
+ "integrity": "sha512-haa5YpJwAaGTAZXKCYhvkm5lWATSnVySs4qDit89BCCkc9Y5OzQY6Bi9z+dHYtYmyig3v1l4R4ZlBFjnFRp9bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-react/ast": "1.35.0",
+ "@eslint-react/core": "1.35.0",
+ "@eslint-react/eff": "1.35.0",
+ "@eslint-react/jsx": "1.35.0",
+ "@eslint-react/shared": "1.35.0",
+ "@eslint-react/var": "1.35.0",
+ "@typescript-eslint/scope-manager": "^8.26.1",
+ "@typescript-eslint/types": "^8.26.1",
+ "@typescript-eslint/utils": "^8.26.1",
+ "compare-versions": "^6.1.1",
+ "string-ts": "^2.2.1",
+ "ts-pattern": "^5.6.2"
+ },
+ "engines": {
+ "bun": ">=1.0.15",
+ "node": ">=18.18.0"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": "^4.9.5 || ^5.3.3"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": false
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/eslint-plugin-react-hooks": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
@@ -2049,6 +2880,48 @@
"eslint": ">=8.40"
}
},
+ "node_modules/eslint-plugin-react-x": {
+ "version": "1.35.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-x/-/eslint-plugin-react-x-1.35.0.tgz",
+ "integrity": "sha512-nkC0lLFmZqnOYZNoNHPZCqjNtCkj5Ep5kRe7Uh5hmTWO6+QeYO9eOOZdBfNXhSarcsz2G8ah7ZEII3lov765cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-react/ast": "1.35.0",
+ "@eslint-react/core": "1.35.0",
+ "@eslint-react/eff": "1.35.0",
+ "@eslint-react/jsx": "1.35.0",
+ "@eslint-react/shared": "1.35.0",
+ "@eslint-react/var": "1.35.0",
+ "@typescript-eslint/scope-manager": "^8.26.1",
+ "@typescript-eslint/type-utils": "^8.26.1",
+ "@typescript-eslint/types": "^8.26.1",
+ "@typescript-eslint/utils": "^8.26.1",
+ "compare-versions": "^6.1.1",
+ "string-ts": "^2.2.1",
+ "ts-pattern": "^5.6.2"
+ },
+ "engines": {
+ "bun": ">=1.0.15",
+ "node": ">=18.18.0"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "ts-api-utils": "^2.0.1",
+ "typescript": "^4.9.5 || ^5.3.3"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": false
+ },
+ "ts-api-utils": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/eslint-scope": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
@@ -2230,6 +3103,12 @@
"node": ">=8"
}
},
+ "node_modules/find-root": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
+ "license": "MIT"
+ },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -2283,6 +3162,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -2336,6 +3224,33 @@
"node": ">=8"
}
},
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2350,7 +3265,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
@@ -2373,6 +3287,27 @@
"node": ">=0.8.19"
}
},
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "license": "MIT"
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2417,7 +3352,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@@ -2437,7 +3371,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
- "dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
@@ -2453,6 +3386,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "license": "MIT"
+ },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -2504,6 +3443,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "license": "MIT"
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -2527,6 +3472,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -2578,7 +3535,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@@ -2614,6 +3570,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -2668,7 +3633,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
@@ -2677,6 +3641,24 @@
"node": ">=6"
}
},
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -2697,11 +3679,25 @@
"node": ">=8"
}
},
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -2756,6 +3752,23 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -2808,6 +3821,12 @@
"react": "^19.0.0"
}
},
+ "node_modules/react-is": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
+ "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==",
+ "license": "MIT"
+ },
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@@ -2818,11 +3837,98 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.3.0.tgz",
+ "integrity": "sha512-466f2W7HIWaNXTKM5nHTqNxLrHTyXybm7R0eBlVSt0k/u55tTCDO194OIx/NrYD4TS5SXKTNekXfT37kMKUjgw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/cookie": "^0.6.0",
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0",
+ "turbo-stream": "2.4.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.3.0.tgz",
+ "integrity": "sha512-z7Q5FTiHGgQfEurX/FBinkOXhWREJIAB2RiU24lvcBa82PxUpwqvs/PAXb9lJyPjTs2jrl6UkLvCZVGJPeNuuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.3.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/react-transition-group": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
+ "license": "MIT"
+ },
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+ "license": "MIT"
+ },
+ "node_modules/resolve": {
+ "version": "1.22.10",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+ "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -2918,6 +4024,12 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
+ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
+ "license": "MIT"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -2941,6 +4053,15 @@
"node": ">=8"
}
},
+ "node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2951,6 +4072,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/string-ts": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/string-ts/-/string-ts-2.2.1.tgz",
+ "integrity": "sha512-Q2u0gko67PLLhbte5HmPfdOjNvUKbKQM+mCNQae6jE91DmoFHY6HH9GcdqCeNx87DZ2KKjiFxmA0R/42OneGWw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -2964,6 +4092,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/stylis": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
+ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
+ "license": "MIT"
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -2977,6 +4111,18 @@
"node": ">=8"
}
},
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -3003,6 +4149,19 @@
"typescript": ">=4.8.4"
}
},
+ "node_modules/ts-pattern": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.6.2.tgz",
+ "integrity": "sha512-d4IxJUXROL5NCa3amvMg6VQW2HVtZYmUTPfvVtO7zJWGYLJ+mry9v2OmYm+z67aniQoQ8/yFNadiEwtNS9qQiw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/turbo-stream": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
+ "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
+ "license": "ISC"
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -3094,6 +4253,15 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
+ "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/vite": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz",
@@ -3199,6 +4367,21 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/yaml": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
+ "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
+ "dev": true,
+ "license": "ISC",
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/moneymgr_web/package.json b/moneymgr_web/package.json
index 6c75cc5..9bf3f31 100644
--- a/moneymgr_web/package.json
+++ b/moneymgr_web/package.json
@@ -10,8 +10,20 @@
"preview": "vite preview"
},
"dependencies": {
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.0",
+ "@fontsource/roboto": "^5.2.5",
+ "@mdi/js": "^7.4.47",
+ "@mdi/react": "^1.6.1",
+ "@mui/icons-material": "^6.4.8",
+ "@mui/material": "^6.4.8",
+ "@mui/x-data-grid": "^7.28.0",
+ "@mui/x-date-pickers": "^7.28.0",
+ "dayjs": "^1.11.13",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "react-router": "^7.3.0",
+ "react-router-dom": "^7.3.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
@@ -19,8 +31,10 @@
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0",
+ "eslint-plugin-react-dom": "^1.35.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
+ "eslint-plugin-react-x": "^1.35.0",
"globals": "^15.15.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
diff --git a/moneymgr_web/public/vite.svg b/moneymgr_web/public/vite.svg
deleted file mode 100644
index e7b8dfb..0000000
--- a/moneymgr_web/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/moneymgr_web/src/App.css b/moneymgr_web/src/App.css
deleted file mode 100644
index b9d355d..0000000
--- a/moneymgr_web/src/App.css
+++ /dev/null
@@ -1,42 +0,0 @@
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
-}
-
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
-}
-
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
- }
-}
-
-.card {
- padding: 2em;
-}
-
-.read-the-docs {
- color: #888;
-}
diff --git a/moneymgr_web/src/App.tsx b/moneymgr_web/src/App.tsx
index 3d7ded3..cd616db 100644
--- a/moneymgr_web/src/App.tsx
+++ b/moneymgr_web/src/App.tsx
@@ -1,35 +1,60 @@
-import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from '/vite.svg'
-import './App.css'
+import React from "react";
+import {
+ Route,
+ RouterProvider,
+ createBrowserRouter,
+ createRoutesFromElements,
+} from "react-router-dom";
+import { AuthApi } from "./api/AuthApi";
+import { ServerApi } from "./api/ServerApi";
+import { HomeRoute } from "./routes/HomeRoute";
+import { NotFoundRoute } from "./routes/NotFound";
+import { LoginRoute } from "./routes/auth/LoginRoute";
+import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
+import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
+import { BaseLoginPage } from "./widgets/BaseLoginPage";
-function App() {
- const [count, setCount] = useState(0)
-
- return (
- <>
-
- Vite + React
-
-
-
- Edit src/App.tsx
and save to test HMR
-
-
-
- Click on the Vite and React logos to learn more
-
- >
- )
+interface AuthContext {
+ signedIn: boolean;
+ setSignedIn: (signedIn: boolean) => void;
}
-export default App
+const AuthContextK = React.createContext(null);
+
+export function App() {
+ const [signedIn, setSignedIn] = React.useState(AuthApi.SignedIn);
+
+ const context: AuthContext = {
+ signedIn: signedIn,
+ setSignedIn: (s) => setSignedIn(s),
+ };
+
+ const router = createBrowserRouter(
+ createRoutesFromElements(
+ signedIn || ServerApi.Config.auth_disabled ? (
+ }>
+ } />
+
+ } />
+
+ ) : (
+ }>
+ } />
+ } />
+ } />
+
+ )
+ ),
+ { basename: import.meta.env.VITE_APP_BASENAME }
+ );
+
+ return (
+
+
+
+ );
+}
+
+export function useAuth(): AuthContext {
+ return React.useContext(AuthContextK)!;
+}
diff --git a/moneymgr_web/src/api/ApiClient.ts b/moneymgr_web/src/api/ApiClient.ts
new file mode 100644
index 0000000..213ddbc
--- /dev/null
+++ b/moneymgr_web/src/api/ApiClient.ts
@@ -0,0 +1,177 @@
+import { AuthApi } from "./AuthApi";
+
+interface RequestParams {
+ uri: string;
+ method: "GET" | "POST" | "DELETE" | "PATCH" | "PUT";
+ allowFail?: boolean;
+ jsonData?: any;
+ formData?: FormData;
+ upProgress?: (progress: number) => void;
+ downProgress?: (e: { progress: number; total: number }) => void;
+}
+
+interface APIResponse {
+ data: any;
+ status: number;
+}
+
+export class ApiError extends Error {
+ constructor(message: string, public code: number, public data: any) {
+ super(`HTTP status: ${code}\nMessage: ${message}\nData=${data}`);
+ }
+}
+
+export class APIClient {
+ /**
+ * Get backend URL
+ */
+ static backendURL(): string {
+ const URL = import.meta.env.VITE_APP_BACKEND ?? "";
+ if (URL.length === 0) throw new Error("Backend URL undefined!");
+ return URL;
+ }
+
+ /**
+ * Check out whether the backend is accessed through
+ * HTTPS or not
+ */
+ static IsBackendSecure(): boolean {
+ return this.backendURL().startsWith("https");
+ }
+
+ /**
+ * Perform a request on the backend
+ */
+ static async exec(args: RequestParams): Promise {
+ let body: string | undefined | FormData = undefined;
+ let headers: any = {};
+
+ // JSON request
+ if (args.jsonData) {
+ headers["Content-Type"] = "application/json";
+ body = JSON.stringify(args.jsonData);
+ }
+
+ // Form data request
+ else if (args.formData) {
+ body = args.formData;
+ }
+
+ const url = this.backendURL() + args.uri;
+
+ let data;
+ let status: number;
+
+ // Make the request with XMLHttpRequest
+ if (args.upProgress) {
+ const res: XMLHttpRequest = await new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.upload.addEventListener("progress", (e) =>
+ args.upProgress!(e.loaded / e.total)
+ );
+ xhr.addEventListener("load", () => resolve(xhr));
+ xhr.addEventListener("error", () =>
+ reject(new Error("File upload failed"))
+ );
+ xhr.addEventListener("abort", () =>
+ reject(new Error("File upload aborted"))
+ );
+ xhr.addEventListener("timeout", () =>
+ reject(new Error("File upload timeout"))
+ );
+ xhr.open(args.method, url, true);
+ xhr.withCredentials = true;
+ for (const key in headers) {
+ if (headers.hasOwnProperty(key))
+ xhr.setRequestHeader(key, headers[key]);
+ }
+ xhr.send(body);
+ });
+
+ status = res.status;
+ if (res.responseType === "json") data = JSON.parse(res.responseText);
+ else data = res.response;
+ }
+
+ // Make the request with fetch
+ else {
+ const res = await fetch(url, {
+ method: args.method,
+ body: body,
+ headers: headers,
+ credentials: "include",
+ });
+
+ // Process response
+ // JSON response
+ if (res.headers.get("content-type") === "application/json")
+ data = await res.json();
+ // Text / XML response
+ else if (
+ ["application/xml", "text/plain"].includes(
+ res.headers.get("content-type") ?? ""
+ )
+ )
+ data = await res.text();
+ // Binary file, tracking download progress
+ else if (res.body !== null && args.downProgress) {
+ // Track download progress
+ const contentEncoding = res.headers.get("content-encoding");
+ const contentLength = contentEncoding
+ ? null
+ : res.headers.get("content-length");
+
+ const total = parseInt(contentLength ?? "0", 10);
+ let loaded = 0;
+
+ const resInt = new Response(
+ new ReadableStream({
+ start(controller) {
+ const reader = res.body!.getReader();
+
+ const read = async () => {
+ try {
+ const ret = await reader.read();
+ if (ret.done) {
+ controller.close();
+ return;
+ }
+ loaded += ret.value.byteLength;
+ args.downProgress!({ progress: loaded, total });
+ controller.enqueue(ret.value);
+ read();
+ } catch (e) {
+ console.error(e);
+ controller.error(e);
+ }
+ };
+
+ read();
+ },
+ })
+ );
+
+ data = await resInt.blob();
+ }
+
+ // Do not track progress (binary file)
+ else data = await res.blob();
+
+ status = res.status;
+ }
+
+ // Handle expired tokens
+ if (status === 412) {
+ AuthApi.UnsetAuthenticated();
+ window.location.href = import.meta.env.VITE_APP_BASENAME;
+ }
+
+ if (!args.allowFail && (status < 200 || status > 299))
+ throw new ApiError("Request failed!", status, data);
+
+ return {
+ data: data,
+ status: status,
+ };
+ }
+}
diff --git a/moneymgr_web/src/api/AuthApi.ts b/moneymgr_web/src/api/AuthApi.ts
new file mode 100644
index 0000000..eb4b105
--- /dev/null
+++ b/moneymgr_web/src/api/AuthApi.ts
@@ -0,0 +1,87 @@
+import { APIClient } from "./ApiClient";
+
+export interface AuthInfo {
+ id: number;
+ time_create: number;
+ time_update: number;
+ name: string;
+ email: string;
+}
+
+const TokenStateKey = "auth-state";
+
+export class AuthApi {
+ /**
+ * Check out whether user is signed in or not
+ */
+ static get SignedIn(): boolean {
+ return localStorage.getItem(TokenStateKey) !== null;
+ }
+
+ /**
+ * Mark user as authenticated
+ */
+ static SetAuthenticated() {
+ localStorage.setItem(TokenStateKey, "");
+ }
+
+ /**
+ * Un-mark user as authenticated
+ */
+ static UnsetAuthenticated() {
+ localStorage.removeItem(TokenStateKey);
+ }
+
+ /**
+ * Start OpenID login
+ */
+ static async StartOpenIDLogin(): Promise<{ url: string }> {
+ return (
+ await APIClient.exec({
+ uri: "/auth/start_oidc",
+ method: "GET",
+ })
+ ).data;
+ }
+
+ /**
+ * Finish OpenID login
+ */
+ static async FinishOpenIDLogin(code: string, state: string): Promise {
+ await APIClient.exec({
+ uri: "/auth/finish_oidc",
+ method: "POST",
+ jsonData: { code: code, state: state },
+ });
+
+ this.SetAuthenticated();
+ }
+
+ /**
+ * Get auth information
+ */
+ static async GetAuthInfo(): Promise {
+ return (
+ await APIClient.exec({
+ uri: "/auth/info",
+ method: "GET",
+ })
+ ).data;
+ }
+
+ /**
+ * Sign out
+ */
+ static async SignOut(): Promise {
+ try {
+ await APIClient.exec({
+ uri: "/auth/sign_out",
+ method: "GET",
+ });
+ } catch (e) {
+ console.error("Failed to sign out user on API!", e);
+ }
+
+ this.UnsetAuthenticated();
+ }
+}
diff --git a/moneymgr_web/src/api/ServerApi.ts b/moneymgr_web/src/api/ServerApi.ts
new file mode 100644
index 0000000..104da9d
--- /dev/null
+++ b/moneymgr_web/src/api/ServerApi.ts
@@ -0,0 +1,38 @@
+import { APIClient } from "./ApiClient";
+
+export interface ServerConfig {
+ auth_disabled: boolean;
+ oidc_provider_name: string;
+ constraints: ServerConstraints;
+}
+
+export interface ServerConstraints {}
+
+export interface LenConstraint {
+ min: number;
+ max: number;
+}
+
+let config: ServerConfig | null = null;
+
+export class ServerApi {
+ /**
+ * Get server configuration
+ */
+ static async LoadConfig(): Promise {
+ config = (
+ await APIClient.exec({
+ uri: "/server/config",
+ method: "GET",
+ })
+ ).data;
+ }
+
+ /**
+ * Get cached configuration
+ */
+ static get Config(): ServerConfig {
+ if (config === null) throw new Error("Missing configuration!");
+ return config;
+ }
+}
diff --git a/moneymgr_web/src/assets/react.svg b/moneymgr_web/src/assets/react.svg
deleted file mode 100644
index 6c87de9..0000000
--- a/moneymgr_web/src/assets/react.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/moneymgr_web/src/hooks/context_providers/AlertDialogProvider.tsx b/moneymgr_web/src/hooks/context_providers/AlertDialogProvider.tsx
new file mode 100644
index 0000000..d0d996a
--- /dev/null
+++ b/moneymgr_web/src/hooks/context_providers/AlertDialogProvider.tsx
@@ -0,0 +1,68 @@
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ DialogTitle,
+} from "@mui/material";
+import React, { PropsWithChildren } from "react";
+
+type AlertContext = (message: string, title?: string) => Promise;
+
+const AlertContextK = React.createContext(null);
+
+export function AlertDialogProvider(p: PropsWithChildren): React.ReactElement {
+ const [open, setOpen] = React.useState(false);
+
+ const [title, setTitle] = React.useState(undefined);
+ const [message, setMessage] = React.useState("");
+
+ const cb = React.useRef void)>(null);
+
+ const handleClose = () => {
+ setOpen(false);
+
+ if (cb.current !== null) cb.current();
+ cb.current = null;
+ };
+
+ const hook: AlertContext = (message, title) => {
+ setTitle(title);
+ setMessage(message);
+ setOpen(true);
+
+ return new Promise((res) => {
+ cb.current = res;
+ });
+ };
+
+ return (
+ <>
+ {p.children}
+
+
+ >
+ );
+}
+
+export function useAlert(): AlertContext {
+ return React.useContext(AlertContextK)!;
+}
diff --git a/moneymgr_web/src/hooks/context_providers/ConfirmDialogProvider.tsx b/moneymgr_web/src/hooks/context_providers/ConfirmDialogProvider.tsx
new file mode 100644
index 0000000..8b52c9a
--- /dev/null
+++ b/moneymgr_web/src/hooks/context_providers/ConfirmDialogProvider.tsx
@@ -0,0 +1,88 @@
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ DialogTitle,
+} from "@mui/material";
+import React, { PropsWithChildren } from "react";
+
+type ConfirmContext = (
+ message: string,
+ title?: string,
+ confirmButton?: string
+) => Promise;
+
+const ConfirmContextK = React.createContext(null);
+
+export function ConfirmDialogProvider(
+ p: PropsWithChildren
+): React.ReactElement {
+ const [open, setOpen] = React.useState(false);
+
+ const [title, setTitle] = React.useState(undefined);
+ const [message, setMessage] = React.useState("");
+ const [confirmButton, setConfirmButton] = React.useState(
+ undefined
+ );
+
+ const cb = React.useRef void)>(null);
+
+ const handleClose = (confirm: boolean) => {
+ setOpen(false);
+
+ if (cb.current !== null) cb.current(confirm);
+ cb.current = null;
+ };
+
+ const hook: ConfirmContext = (message, title, confirmButton) => {
+ setTitle(title);
+ setMessage(message);
+ setConfirmButton(confirmButton);
+ setOpen(true);
+
+ return new Promise((res) => {
+ cb.current = res;
+ });
+ };
+
+ const keyUp = (e: React.KeyboardEvent) => {
+ if (e.code === "Enter") handleClose(true);
+ };
+
+ return (
+ <>
+
+ {p.children}
+
+
+
+ >
+ );
+}
+
+export function useConfirm(): ConfirmContext {
+ return React.useContext(ConfirmContextK)!;
+}
diff --git a/moneymgr_web/src/hooks/context_providers/DarkThemeProvider.tsx b/moneymgr_web/src/hooks/context_providers/DarkThemeProvider.tsx
new file mode 100644
index 0000000..ab04b23
--- /dev/null
+++ b/moneymgr_web/src/hooks/context_providers/DarkThemeProvider.tsx
@@ -0,0 +1,57 @@
+import { ThemeProvider, createTheme } from "@mui/material/styles";
+import React from "react";
+import { PropsWithChildren } from "react";
+import { frFR as dataGridFr } from "@mui/x-data-grid/locales";
+
+const localStorageKey = "dark-theme";
+
+const darkTheme = createTheme(
+ {
+ palette: {
+ mode: "dark",
+ },
+ },
+ dataGridFr
+);
+
+const lightTheme = createTheme(
+ {
+ palette: {
+ mode: "light",
+ },
+ },
+ dataGridFr
+);
+
+interface DarkThemeContext {
+ enabled: boolean;
+ setEnabled: (enabled: boolean) => void;
+}
+
+const DarkThemeContextK = React.createContext(null);
+
+export function DarkThemeProvider(p: PropsWithChildren): React.ReactElement {
+ const [enabled, setEnabled] = React.useState(
+ localStorage.getItem(localStorageKey) !== "false"
+ );
+
+ return (
+
+
+ {p.children}
+
+
+ );
+}
+
+export function useDarkTheme(): DarkThemeContext {
+ return React.useContext(DarkThemeContextK)!;
+}
diff --git a/moneymgr_web/src/hooks/context_providers/LoadingMessageProvider.tsx b/moneymgr_web/src/hooks/context_providers/LoadingMessageProvider.tsx
new file mode 100644
index 0000000..6c0c826
--- /dev/null
+++ b/moneymgr_web/src/hooks/context_providers/LoadingMessageProvider.tsx
@@ -0,0 +1,64 @@
+import {
+ CircularProgress,
+ Dialog,
+ DialogContent,
+ DialogContentText,
+} from "@mui/material";
+import React, { PropsWithChildren } from "react";
+
+type LoadingMessageContext = {
+ show: (message: string) => void;
+ hide: () => void;
+};
+
+const LoadingMessageContextK =
+ React.createContext(null);
+
+export function LoadingMessageProvider(
+ p: PropsWithChildren
+): React.ReactElement {
+ const [open, setOpen] = React.useState(false);
+
+ const [message, setMessage] = React.useState("");
+
+ const hook: LoadingMessageContext = {
+ show(message) {
+ setMessage(message);
+ setOpen(true);
+ },
+ hide() {
+ setMessage("");
+ setOpen(false);
+ },
+ };
+
+ return (
+ <>
+
+ {p.children}
+
+
+
+ >
+ );
+}
+
+export function useLoadingMessage(): LoadingMessageContext {
+ return React.useContext(LoadingMessageContextK)!;
+}
diff --git a/moneymgr_web/src/hooks/context_providers/SnackbarProvider.tsx b/moneymgr_web/src/hooks/context_providers/SnackbarProvider.tsx
new file mode 100644
index 0000000..203b3c9
--- /dev/null
+++ b/moneymgr_web/src/hooks/context_providers/SnackbarProvider.tsx
@@ -0,0 +1,43 @@
+import { Snackbar } from "@mui/material";
+
+import React, { PropsWithChildren } from "react";
+
+type SnackbarContext = (message: string, duration?: number) => void;
+
+const SnackbarContextK = React.createContext(null);
+
+export function SnackbarProvider(p: PropsWithChildren): React.ReactElement {
+ const [open, setOpen] = React.useState(false);
+
+ const [message, setMessage] = React.useState("");
+ const [duration, setDuration] = React.useState(0);
+
+ const handleClose = () => {
+ setOpen(false);
+ };
+
+ const hook: SnackbarContext = (message, duration) => {
+ setMessage(message);
+ setDuration(duration ?? 6000);
+ setOpen(true);
+ };
+
+ return (
+ <>
+
+ {p.children}
+
+
+
+ >
+ );
+}
+
+export function useSnackbar(): SnackbarContext {
+ return React.useContext(SnackbarContextK)!;
+}
diff --git a/moneymgr_web/src/index.css b/moneymgr_web/src/index.css
index 08a3ac9..b303f80 100644
--- a/moneymgr_web/src/index.css
+++ b/moneymgr_web/src/index.css
@@ -1,68 +1,9 @@
-:root {
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
body {
margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
}
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
+html,
+body,
+#root {
+ height: 100%;
}
diff --git a/moneymgr_web/src/main.tsx b/moneymgr_web/src/main.tsx
index bef5202..dacb543 100644
--- a/moneymgr_web/src/main.tsx
+++ b/moneymgr_web/src/main.tsx
@@ -1,10 +1,40 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './index.css'
-import App from './App.tsx'
+import "@fontsource/roboto/300.css";
+import "@fontsource/roboto/400.css";
+import "@fontsource/roboto/500.css";
+import "@fontsource/roboto/700.css";
+import { LocalizationProvider } from "@mui/x-date-pickers";
+import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { ServerApi } from "./api/ServerApi.ts";
+import { App } from "./App.tsx";
+import { AlertDialogProvider } from "./hooks/context_providers/AlertDialogProvider.tsx";
+import { ConfirmDialogProvider } from "./hooks/context_providers/ConfirmDialogProvider.tsx";
+import { DarkThemeProvider } from "./hooks/context_providers/DarkThemeProvider.tsx";
+import { LoadingMessageProvider } from "./hooks/context_providers/LoadingMessageProvider.tsx";
+import { SnackbarProvider } from "./hooks/context_providers/SnackbarProvider.tsx";
+import "./index.css";
+import { AsyncWidget } from "./widgets/AsyncWidget.tsx";
-createRoot(document.getElementById('root')!).render(
+createRoot(document.getElementById("root")!).render(
-
- ,
-)
+
+
+
+
+
+
+ await ServerApi.LoadConfig()}
+ errMsg="Failed to load static server configuration!"
+ build={() => }
+ />
+
+
+
+
+
+
+
+);
diff --git a/moneymgr_web/src/routes/HomeRoute.tsx b/moneymgr_web/src/routes/HomeRoute.tsx
new file mode 100644
index 0000000..6207f28
--- /dev/null
+++ b/moneymgr_web/src/routes/HomeRoute.tsx
@@ -0,0 +1,3 @@
+export function HomeRoute(): React.ReactElement {
+ return <>home authenticated todo>;
+}
diff --git a/moneymgr_web/src/routes/NotFound.tsx b/moneymgr_web/src/routes/NotFound.tsx
new file mode 100644
index 0000000..72812e6
--- /dev/null
+++ b/moneymgr_web/src/routes/NotFound.tsx
@@ -0,0 +1,23 @@
+import { Button } from "@mui/material";
+import { RouterLink } from "../widgets/RouterLink";
+
+export function NotFoundRoute(): React.ReactElement {
+ return (
+
+
404 Not found
+
The page you requested was not found!
+
+
+
+
+ );
+}
diff --git a/moneymgr_web/src/routes/auth/LoginRoute.tsx b/moneymgr_web/src/routes/auth/LoginRoute.tsx
new file mode 100644
index 0000000..f55b91b
--- /dev/null
+++ b/moneymgr_web/src/routes/auth/LoginRoute.tsx
@@ -0,0 +1,51 @@
+import { Alert, CircularProgress } from "@mui/material";
+import Button from "@mui/material/Button";
+import * as React from "react";
+import { AuthApi } from "../../api/AuthApi";
+import { ServerApi } from "../../api/ServerApi";
+
+/**
+ * Login form
+ */
+export function LoginRoute(): React.ReactElement {
+ const [loading, setLoading] = React.useState(false);
+ const [error, setError] = React.useState(null);
+
+ const authWithOpenID = async () => {
+ try {
+ setLoading(true);
+
+ const res = await AuthApi.StartOpenIDLogin();
+ window.location.href = res.url;
+ } catch (e) {
+ console.error(e);
+ setError("Failed to initialize OpenID login");
+ }
+ };
+
+ if (loading)
+ return (
+ <>
+
+ >
+ );
+
+ return (
+ <>
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+ >
+ );
+}
diff --git a/moneymgr_web/src/routes/auth/OIDCCbRoute.tsx b/moneymgr_web/src/routes/auth/OIDCCbRoute.tsx
new file mode 100644
index 0000000..60c17c7
--- /dev/null
+++ b/moneymgr_web/src/routes/auth/OIDCCbRoute.tsx
@@ -0,0 +1,53 @@
+import { CircularProgress } from "@mui/material";
+import { useEffect, useRef, useState } from "react";
+import { useNavigate, useSearchParams } from "react-router-dom";
+import { AuthApi } from "../../api/AuthApi";
+import { useAuth } from "../../App";
+import { AuthSingleMessage } from "../../widgets/AuthSingleMessage";
+
+/**
+ * OpenID login callback route
+ */
+export function OIDCCbRoute(): React.ReactElement {
+ const auth = useAuth();
+ const navigate = useNavigate();
+
+ const [error, setError] = useState(false);
+
+ const [searchParams] = useSearchParams();
+ const code = searchParams.get("code");
+ const state = searchParams.get("state");
+
+ const count = useRef("");
+
+ useEffect(() => {
+ const load = async () => {
+ try {
+ if (count.current === code) {
+ return;
+ }
+ count.current = code!;
+
+ await AuthApi.FinishOpenIDLogin(code!, state!);
+ navigate("/");
+ auth.setSignedIn(true);
+ } catch (e) {
+ console.error(e);
+ setError(true);
+ }
+ };
+
+ load();
+ });
+
+ if (error)
+ return (
+
+ );
+
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/moneymgr_web/src/widgets/AsyncWidget.tsx b/moneymgr_web/src/widgets/AsyncWidget.tsx
new file mode 100644
index 0000000..87c3ca2
--- /dev/null
+++ b/moneymgr_web/src/widgets/AsyncWidget.tsx
@@ -0,0 +1,92 @@
+import { Alert, Box, Button, CircularProgress } from "@mui/material";
+import { useEffect, useRef, useState } from "react";
+
+enum State {
+ Loading,
+ Ready,
+ Error,
+}
+
+export function AsyncWidget(p: {
+ loadKey: any;
+ load: () => Promise;
+ errMsg: string;
+ build: () => React.ReactElement;
+ ready?: boolean;
+ errAdditionalElement?: () => React.ReactElement;
+}): React.ReactElement {
+ const [state, setState] = useState(State.Loading);
+
+ const counter = useRef(null);
+
+ const load = async () => {
+ try {
+ setState(State.Loading);
+ await p.load();
+ setState(State.Ready);
+ } catch (e) {
+ console.error(e);
+ setState(State.Error);
+ }
+ };
+
+ useEffect(() => {
+ if (counter.current === p.loadKey) return;
+ counter.current = p.loadKey;
+
+ load();
+ });
+
+ if (state === State.Error)
+ return (
+
+ theme.palette.mode === "light"
+ ? theme.palette.grey[100]
+ : theme.palette.grey[900],
+ }}
+ >
+
+ {p.errMsg}
+
+
+
+
+ {p.errAdditionalElement && p.errAdditionalElement()}
+
+ );
+
+ if (state === State.Loading || p.ready === false)
+ return (
+
+ theme.palette.mode === "light"
+ ? theme.palette.grey[100]
+ : theme.palette.grey[900],
+ }}
+ >
+
+
+ );
+
+ return p.build();
+}
diff --git a/moneymgr_web/src/widgets/AuthSingleMessage.tsx b/moneymgr_web/src/widgets/AuthSingleMessage.tsx
new file mode 100644
index 0000000..fad73be
--- /dev/null
+++ b/moneymgr_web/src/widgets/AuthSingleMessage.tsx
@@ -0,0 +1,13 @@
+import { Button } from "@mui/material";
+import { Link } from "react-router-dom";
+
+export function AuthSingleMessage(p: { message: string }): React.ReactElement {
+ return (
+ <>
+ {p.message}
+
+
+
+ >
+ );
+}
diff --git a/moneymgr_web/src/widgets/BaseAuthenticatedPage.tsx b/moneymgr_web/src/widgets/BaseAuthenticatedPage.tsx
new file mode 100644
index 0000000..2e8256e
--- /dev/null
+++ b/moneymgr_web/src/widgets/BaseAuthenticatedPage.tsx
@@ -0,0 +1,88 @@
+import { Box, Button } from "@mui/material";
+import * as React from "react";
+import { Outlet, useNavigate } from "react-router-dom";
+import { useAuth } from "../App";
+import { AuthApi, AuthInfo } from "../api/AuthApi";
+import { AsyncWidget } from "./AsyncWidget";
+import { MoneyWebAppBar } from "./MoneyWebAppBar";
+import { MoneyNavList } from "./MoneyNavList";
+
+interface AuthInfoContext {
+ info: AuthInfo;
+ reloadAuthInfo: () => void;
+}
+
+const AuthInfoContextK = React.createContext(null);
+
+export function BaseAuthenticatedPage(): React.ReactElement {
+ const [authInfo, setAuthInfo] = React.useState(null);
+
+ const auth = useAuth();
+ const navigate = useNavigate();
+
+ const signOut = () => {
+ AuthApi.SignOut();
+ navigate("/");
+ auth.setSignedIn(false);
+ };
+
+ const load = async () => {
+ setAuthInfo(await AuthApi.GetAuthInfo());
+ };
+
+ return (
+ (
+ <>
+
+ >
+ )}
+ build={() => (
+
+
+ theme.palette.mode === "light"
+ ? theme.palette.grey[100]
+ : theme.palette.grey[900],
+ color: (theme) =>
+ theme.palette.mode === "light"
+ ? theme.palette.grey[900]
+ : theme.palette.grey[100],
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+ )}
+ />
+ );
+}
+
+export function useAuthInfo(): AuthInfoContext {
+ return React.useContext(AuthInfoContextK)!;
+}
diff --git a/moneymgr_web/src/widgets/BaseLoginPage.tsx b/moneymgr_web/src/widgets/BaseLoginPage.tsx
new file mode 100644
index 0000000..dda6431
--- /dev/null
+++ b/moneymgr_web/src/widgets/BaseLoginPage.tsx
@@ -0,0 +1,94 @@
+import { mdiServer } from "@mdi/js";
+import Icon from "@mdi/react";
+import Avatar from "@mui/material/Avatar";
+import Box from "@mui/material/Box";
+import CssBaseline from "@mui/material/CssBaseline";
+import Grid from "@mui/material/Grid2";
+import Paper from "@mui/material/Paper";
+import Typography from "@mui/material/Typography";
+import { Link, Outlet } from "react-router-dom";
+import loginSplashImage from "./mufid-majnun-LVcjYwuHQlg-unsplash.jpg";
+
+function Copyright(props: any): React.ReactElement {
+ return (
+
+ {"Copyright © "}
+
+ Pierre HUBERT
+ {" "}
+ {new Date().getFullYear()}
+ {"."}
+
+ );
+}
+
+export function BaseLoginPage() {
+ return (
+
+
+
+ t.palette.mode === "light"
+ ? t.palette.grey[50]
+ : t.palette.grey[900],
+ backgroundSize: "cover",
+ backgroundPosition: "center",
+ }}
+ />
+
+
+
+
+
+
+
+ Money manager
+
+
+
+ Open source money managment
+
+
+ {/* inner page */}
+
+
+
+
+
+
+ );
+}
diff --git a/moneymgr_web/src/widgets/DarkThemeButton.tsx b/moneymgr_web/src/widgets/DarkThemeButton.tsx
new file mode 100644
index 0000000..cbbcf70
--- /dev/null
+++ b/moneymgr_web/src/widgets/DarkThemeButton.tsx
@@ -0,0 +1,19 @@
+import Brightness7Icon from "@mui/icons-material/Brightness7";
+import DarkModeIcon from "@mui/icons-material/DarkMode";
+import { IconButton, Tooltip } from "@mui/material";
+import { useDarkTheme } from "../hooks/context_providers/DarkThemeProvider";
+
+export function DarkThemeButton(): React.ReactElement {
+ const darkTheme = useDarkTheme();
+
+ return (
+
+ darkTheme.setEnabled(!darkTheme.enabled)}
+ style={{ color: "inherit" }}
+ >
+ {!darkTheme.enabled ? : }
+
+
+ );
+}
diff --git a/moneymgr_web/src/widgets/MoneyNavList.tsx b/moneymgr_web/src/widgets/MoneyNavList.tsx
new file mode 100644
index 0000000..ca614d6
--- /dev/null
+++ b/moneymgr_web/src/widgets/MoneyNavList.tsx
@@ -0,0 +1,53 @@
+import { mdiApi, mdiHome } from "@mdi/js";
+import Icon from "@mdi/react";
+import {
+ List,
+ ListItemButton,
+ ListItemIcon,
+ ListItemText,
+} from "@mui/material";
+import { useLocation } from "react-router-dom";
+import { useAuthInfo } from "./BaseAuthenticatedPage";
+import { RouterLink } from "./RouterLink";
+
+export function MoneyNavList(): React.ReactElement {
+ const user = useAuthInfo().info;
+ return (
+
+ }
+ />
+
+ }
+ />
+
+ );
+}
+
+function NavLink(p: {
+ icon: React.ReactElement;
+ uri: string;
+ label: string;
+}): React.ReactElement {
+ const location = useLocation();
+ return (
+
+
+ {p.icon}
+
+
+
+ );
+}
diff --git a/moneymgr_web/src/widgets/MoneyWebAppBar.tsx b/moneymgr_web/src/widgets/MoneyWebAppBar.tsx
new file mode 100644
index 0000000..0aa5a31
--- /dev/null
+++ b/moneymgr_web/src/widgets/MoneyWebAppBar.tsx
@@ -0,0 +1,81 @@
+import { mdiCash } from "@mdi/js";
+import Icon from "@mdi/react";
+import SettingsIcon from "@mui/icons-material/Settings";
+import { Button } from "@mui/material";
+import AppBar from "@mui/material/AppBar";
+import Menu from "@mui/material/Menu";
+import MenuItem from "@mui/material/MenuItem";
+import Toolbar from "@mui/material/Toolbar";
+import Typography from "@mui/material/Typography";
+import * as React from "react";
+import { useAuthInfo } from "./BaseAuthenticatedPage";
+import { DarkThemeButton } from "./DarkThemeButton";
+import { RouterLink } from "./RouterLink";
+
+export function MoneyWebAppBar(p: {
+ onSignOut: () => void;
+}): React.ReactElement {
+ const authInfo = useAuthInfo();
+
+ const [anchorEl, setAnchorEl] = React.useState(null);
+ const handleMenu = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handleCloseMenu = () => {
+ setAnchorEl(null);
+ };
+
+ const signOut = () => {
+ handleCloseMenu();
+ p.onSignOut();
+ };
+
+ return (
+
+
+
+
+
+ Money Manager
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/moneymgr_web/src/widgets/RouterLink.tsx b/moneymgr_web/src/widgets/RouterLink.tsx
new file mode 100644
index 0000000..108d105
--- /dev/null
+++ b/moneymgr_web/src/widgets/RouterLink.tsx
@@ -0,0 +1,16 @@
+import { PropsWithChildren } from "react";
+import { Link } from "react-router-dom";
+
+export function RouterLink(
+ p: PropsWithChildren<{ to: string; target?: React.HTMLAttributeAnchorTarget }>
+): React.ReactElement {
+ return (
+
+ {p.children}
+
+ );
+}
diff --git a/moneymgr_web/src/widgets/mufid-majnun-LVcjYwuHQlg-unsplash.jpg b/moneymgr_web/src/widgets/mufid-majnun-LVcjYwuHQlg-unsplash.jpg
new file mode 100644
index 0000000..718e3d5
Binary files /dev/null and b/moneymgr_web/src/widgets/mufid-majnun-LVcjYwuHQlg-unsplash.jpg differ