refactor(web): add Redux state management and centralised routing
- Replace inline BrowserRouter with createBrowserRouter and lazy-loaded pages
- Add Redux store with auth-slice (token management) and redux-persist
- Create RequireAuth guard using Redux state via isAuthenticated selector
- Wire Layout logout to dispatch setToken("") clearing persisted auth
- Add ErrorPage component for route error handling
- Add EmptyLayout for unauthenticated pages
- Add VITE_REDUX_STORAGE env var for storage selection
This commit is contained in:
@@ -11,3 +11,7 @@
|
|||||||
# Default: https://pipely.onixbyte.com/
|
# Default: https://pipely.onixbyte.com/
|
||||||
# Development: http://localhost:8080
|
# Development: http://localhost:8080
|
||||||
VITE_API_BASE_URL=https://pipely.onixbyte.com/
|
VITE_API_BASE_URL=https://pipely.onixbyte.com/
|
||||||
|
|
||||||
|
# Redux Persistent storage location
|
||||||
|
# Options: `local` (default), `session`
|
||||||
|
VITE_REDUX_STORAGE=local
|
||||||
|
|||||||
@@ -14,12 +14,16 @@
|
|||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@mui/icons-material": "^9.2.0",
|
"@mui/icons-material": "^9.2.0",
|
||||||
"@mui/material": "^9.2.0",
|
"@mui/material": "^9.2.0",
|
||||||
|
"@reduxjs/toolkit": "^2.12.0",
|
||||||
"@tailwindcss/vite": "^4.3.2",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
"axios": "^1.18.1",
|
"axios": "^1.18.1",
|
||||||
"dayjs": "^1.11.21",
|
"dayjs": "^1.11.21",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
|
"react-redux": "^9.3.0",
|
||||||
|
"react-router": "^8.1.0",
|
||||||
"react-router-dom": "^7.18.1",
|
"react-router-dom": "^7.18.1",
|
||||||
|
"redux-persist": "^6.0.0",
|
||||||
"tailwindcss": "^4.3.2"
|
"tailwindcss": "^4.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Generated
+141
@@ -20,6 +20,9 @@ importers:
|
|||||||
'@mui/material':
|
'@mui/material':
|
||||||
specifier: ^9.2.0
|
specifier: ^9.2.0
|
||||||
version: 9.2.0(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 9.2.0(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
|
'@reduxjs/toolkit':
|
||||||
|
specifier: ^2.12.0
|
||||||
|
version: 2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.3.2
|
specifier: ^4.3.2
|
||||||
version: 4.3.2(vite@8.1.3(@types/node@24.13.2)(jiti@2.7.0))
|
version: 4.3.2(vite@8.1.3(@types/node@24.13.2)(jiti@2.7.0))
|
||||||
@@ -35,9 +38,18 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.2.7
|
specifier: ^19.2.7
|
||||||
version: 19.2.7(react@19.2.7)
|
version: 19.2.7(react@19.2.7)
|
||||||
|
react-redux:
|
||||||
|
specifier: ^9.3.0
|
||||||
|
version: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1)
|
||||||
|
react-router:
|
||||||
|
specifier: ^8.1.0
|
||||||
|
version: 8.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
react-router-dom:
|
react-router-dom:
|
||||||
specifier: ^7.18.1
|
specifier: ^7.18.1
|
||||||
version: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
|
redux-persist:
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0(react@19.2.7)(redux@5.0.1)
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.3.2
|
specifier: ^4.3.2
|
||||||
version: 4.3.2
|
version: 4.3.2
|
||||||
@@ -499,6 +511,17 @@ packages:
|
|||||||
'@popperjs/core@2.11.8':
|
'@popperjs/core@2.11.8':
|
||||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||||
|
|
||||||
|
'@reduxjs/toolkit@2.12.0':
|
||||||
|
resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
|
||||||
|
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-redux:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.1.4':
|
'@rolldown/binding-android-arm64@1.1.4':
|
||||||
resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==}
|
resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -604,6 +627,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
|
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0':
|
||||||
|
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||||
|
|
||||||
|
'@standard-schema/utils@0.3.0':
|
||||||
|
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||||
|
|
||||||
'@tailwindcss/node@4.3.2':
|
'@tailwindcss/node@4.3.2':
|
||||||
resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==}
|
resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==}
|
||||||
|
|
||||||
@@ -735,6 +764,9 @@ packages:
|
|||||||
'@types/react@19.2.17':
|
'@types/react@19.2.17':
|
||||||
resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==}
|
resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==}
|
||||||
|
|
||||||
|
'@types/use-sync-external-store@0.0.6':
|
||||||
|
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
||||||
|
|
||||||
'@types/validate-npm-package-name@4.0.2':
|
'@types/validate-npm-package-name@4.0.2':
|
||||||
resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==}
|
resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==}
|
||||||
|
|
||||||
@@ -985,6 +1017,9 @@ packages:
|
|||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
|
cookie-es@3.1.1:
|
||||||
|
resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==}
|
||||||
|
|
||||||
cookie-signature@1.2.2:
|
cookie-signature@1.2.2:
|
||||||
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
||||||
engines: {node: '>=6.6.0'}
|
engines: {node: '>=6.6.0'}
|
||||||
@@ -1445,6 +1480,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
immer@11.1.11:
|
||||||
|
resolution: {integrity: sha512-qzXuyXAkPySAGYkfsAwodDPWT8Zm7/Uo5BNt4BjhMhG5WlWyZZ4wQqnWwdS8kjlQ1Cwu6gjw3A6+0gTQwlyYtw==}
|
||||||
|
|
||||||
import-fresh@3.3.1:
|
import-fresh@3.3.1:
|
||||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -2000,6 +2038,18 @@ packages:
|
|||||||
react-is@19.2.7:
|
react-is@19.2.7:
|
||||||
resolution: {integrity: sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==}
|
resolution: {integrity: sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==}
|
||||||
|
|
||||||
|
react-redux@9.3.0:
|
||||||
|
resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': ^18.2.25 || ^19
|
||||||
|
react: ^18.0 || ^19
|
||||||
|
redux: ^5.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
redux:
|
||||||
|
optional: true
|
||||||
|
|
||||||
react-router-dom@7.18.1:
|
react-router-dom@7.18.1:
|
||||||
resolution: {integrity: sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==}
|
resolution: {integrity: sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -2017,6 +2067,16 @@ packages:
|
|||||||
react-dom:
|
react-dom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
react-router@8.1.0:
|
||||||
|
resolution: {integrity: sha512-Mdfi61uObuvWNN9OhChOC0HV6YWOIfKRzEWOvCHRSuQg8IM+Nv10edaM/2HE8ZixBpUTdQbruyWqC3sDkkh9vw==}
|
||||||
|
engines: {node: '>=22.22.0'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=19.2.7'
|
||||||
|
react-dom: '>=19.2.7'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
react-transition-group@4.4.5:
|
react-transition-group@4.4.5:
|
||||||
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2031,10 +2091,30 @@ packages:
|
|||||||
resolution: {integrity: sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==}
|
resolution: {integrity: sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
redux-persist@6.0.0:
|
||||||
|
resolution: {integrity: sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16'
|
||||||
|
redux: '>4.0.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
redux-thunk@3.1.0:
|
||||||
|
resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
|
||||||
|
peerDependencies:
|
||||||
|
redux: ^5.0.0
|
||||||
|
|
||||||
|
redux@5.0.1:
|
||||||
|
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
|
||||||
|
|
||||||
require-from-string@2.0.2:
|
require-from-string@2.0.2:
|
||||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
reselect@5.2.0:
|
||||||
|
resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==}
|
||||||
|
|
||||||
resolve-from@4.0.0:
|
resolve-from@4.0.0:
|
||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -2286,6 +2366,11 @@ packages:
|
|||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
|
use-sync-external-store@1.6.0:
|
||||||
|
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
@@ -2917,6 +3002,18 @@ snapshots:
|
|||||||
|
|
||||||
'@popperjs/core@2.11.8': {}
|
'@popperjs/core@2.11.8': {}
|
||||||
|
|
||||||
|
'@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)':
|
||||||
|
dependencies:
|
||||||
|
'@standard-schema/spec': 1.1.0
|
||||||
|
'@standard-schema/utils': 0.3.0
|
||||||
|
immer: 11.1.11
|
||||||
|
redux: 5.0.1
|
||||||
|
redux-thunk: 3.1.0(redux@5.0.1)
|
||||||
|
reselect: 5.2.0
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.7
|
||||||
|
react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1)
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.1.4':
|
'@rolldown/binding-android-arm64@1.1.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -2972,6 +3069,10 @@ snapshots:
|
|||||||
|
|
||||||
'@sindresorhus/merge-streams@4.0.0': {}
|
'@sindresorhus/merge-streams@4.0.0': {}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0': {}
|
||||||
|
|
||||||
|
'@standard-schema/utils@0.3.0': {}
|
||||||
|
|
||||||
'@tailwindcss/node@4.3.2':
|
'@tailwindcss/node@4.3.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/remapping': 2.3.5
|
'@jridgewell/remapping': 2.3.5
|
||||||
@@ -3077,6 +3178,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.2.3
|
csstype: 3.2.3
|
||||||
|
|
||||||
|
'@types/use-sync-external-store@0.0.6': {}
|
||||||
|
|
||||||
'@types/validate-npm-package-name@4.0.2': {}
|
'@types/validate-npm-package-name@4.0.2': {}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)':
|
'@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)':
|
||||||
@@ -3343,6 +3446,8 @@ snapshots:
|
|||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
|
cookie-es@3.1.1: {}
|
||||||
|
|
||||||
cookie-signature@1.2.2: {}
|
cookie-signature@1.2.2: {}
|
||||||
|
|
||||||
cookie@0.7.2: {}
|
cookie@0.7.2: {}
|
||||||
@@ -3822,6 +3927,8 @@ snapshots:
|
|||||||
|
|
||||||
ignore@7.0.5: {}
|
ignore@7.0.5: {}
|
||||||
|
|
||||||
|
immer@11.1.11: {}
|
||||||
|
|
||||||
import-fresh@3.3.1:
|
import-fresh@3.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
parent-module: 1.0.1
|
parent-module: 1.0.1
|
||||||
@@ -4258,6 +4365,15 @@ snapshots:
|
|||||||
|
|
||||||
react-is@19.2.7: {}
|
react-is@19.2.7: {}
|
||||||
|
|
||||||
|
react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1):
|
||||||
|
dependencies:
|
||||||
|
'@types/use-sync-external-store': 0.0.6
|
||||||
|
react: 19.2.7
|
||||||
|
use-sync-external-store: 1.6.0(react@19.2.7)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.17
|
||||||
|
redux: 5.0.1
|
||||||
|
|
||||||
react-router-dom@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
react-router-dom@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
@@ -4272,6 +4388,13 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
|
|
||||||
|
react-router@8.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||||
|
dependencies:
|
||||||
|
cookie-es: 3.1.1
|
||||||
|
react: 19.2.7
|
||||||
|
optionalDependencies:
|
||||||
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
|
|
||||||
react-transition-group@4.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
react-transition-group@4.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.29.7
|
'@babel/runtime': 7.29.7
|
||||||
@@ -4291,8 +4414,22 @@ snapshots:
|
|||||||
tiny-invariant: 1.3.3
|
tiny-invariant: 1.3.3
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
redux-persist@6.0.0(react@19.2.7)(redux@5.0.1):
|
||||||
|
dependencies:
|
||||||
|
redux: 5.0.1
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.7
|
||||||
|
|
||||||
|
redux-thunk@3.1.0(redux@5.0.1):
|
||||||
|
dependencies:
|
||||||
|
redux: 5.0.1
|
||||||
|
|
||||||
|
redux@5.0.1: {}
|
||||||
|
|
||||||
require-from-string@2.0.2: {}
|
require-from-string@2.0.2: {}
|
||||||
|
|
||||||
|
reselect@5.2.0: {}
|
||||||
|
|
||||||
resolve-from@4.0.0: {}
|
resolve-from@4.0.0: {}
|
||||||
|
|
||||||
resolve@1.22.12:
|
resolve@1.22.12:
|
||||||
@@ -4584,6 +4721,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
use-sync-external-store@1.6.0(react@19.2.7):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.7
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
validate-npm-package-name@7.0.2: {}
|
validate-npm-package-name@7.0.2: {}
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"
|
|
||||||
import Layout from "@/components/Layout"
|
|
||||||
import Login from "@/pages/Login"
|
|
||||||
import Dashboard from "@/pages/Dashboard"
|
|
||||||
import Artefacts from "@/pages/Artefacts"
|
|
||||||
import Deployments from "@/pages/Deployments"
|
|
||||||
import Users from "@/pages/Users"
|
|
||||||
|
|
||||||
function RequireAuth({ children }: { children: React.ReactNode }) {
|
|
||||||
const token = localStorage.getItem("token")
|
|
||||||
if (!token) return <Navigate to="/" replace />
|
|
||||||
return <>{children}</>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return (
|
|
||||||
<BrowserRouter>
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<Login />} />
|
|
||||||
<Route
|
|
||||||
element={
|
|
||||||
<RequireAuth>
|
|
||||||
<Layout />
|
|
||||||
</RequireAuth>
|
|
||||||
}>
|
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
|
||||||
<Route path="/artefacts" element={<Artefacts />} />
|
|
||||||
<Route path="/deployments" element={<Deployments />} />
|
|
||||||
<Route path="/users" element={<Users />} />
|
|
||||||
</Route>
|
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
|
||||||
</Routes>
|
|
||||||
</BrowserRouter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Outlet } from "react-router-dom"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty layout component that provides minimal structure.
|
||||||
|
* Useful for pages that need full control over their layout.
|
||||||
|
*/
|
||||||
|
export default function EmptyLayout() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { useRouteError, isRouteErrorResponse } from "react-router-dom"
|
||||||
|
import { Container, Typography, Button, Box } from "@mui/material"
|
||||||
|
|
||||||
|
export default function ErrorPage() {
|
||||||
|
const error = useRouteError()
|
||||||
|
|
||||||
|
let status = 500
|
||||||
|
let message = "Something went wrong."
|
||||||
|
|
||||||
|
if (isRouteErrorResponse(error)) {
|
||||||
|
status = error.status
|
||||||
|
message = error.statusText || message
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
message = error.message
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="sm">
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
minHeight: "100vh",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h1" component="p" sx={{ mb: 2 }}>
|
||||||
|
{status}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h5" component="p" color="text.secondary" sx={{ mb: 4 }}>
|
||||||
|
{message}
|
||||||
|
</Typography>
|
||||||
|
<Button variant="contained" onClick={() => window.location.assign("/")}>
|
||||||
|
Back to Dashboard
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { NavLink, useNavigate, Outlet } from "react-router-dom"
|
import { NavLink, useNavigate, Outlet } from "react-router-dom"
|
||||||
|
import { useDispatch } from "react-redux"
|
||||||
|
import { setToken } from "@/store/auth-slice"
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
List,
|
List,
|
||||||
@@ -24,8 +26,10 @@ const drawerWidth = 240
|
|||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
|
dispatch(setToken(""))
|
||||||
navigate("/")
|
navigate("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-6
@@ -1,20 +1,32 @@
|
|||||||
import { StrictMode } from "react"
|
import { StrictMode } from "react"
|
||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client"
|
||||||
|
import { RouterProvider } from "react-router-dom"
|
||||||
|
import { Provider } from "react-redux"
|
||||||
|
import { PersistGate } from "redux-persist/integration/react"
|
||||||
import { CssBaseline, ThemeProvider, createTheme } from "@mui/material"
|
import { CssBaseline, ThemeProvider, createTheme } from "@mui/material"
|
||||||
|
import router from "@/router"
|
||||||
|
import store, { persistor } from "@/store"
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
import App from "./App.tsx"
|
|
||||||
|
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
mode: 'light',
|
mode: "light",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main application entry point.
|
||||||
|
* Sets up the React app with React Router.
|
||||||
|
*/
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ThemeProvider theme={theme}>
|
<Provider store={store}>
|
||||||
<CssBaseline />
|
<PersistGate loading={null} persistor={persistor}>
|
||||||
<App />
|
<ThemeProvider theme={theme}>
|
||||||
</ThemeProvider>
|
<CssBaseline />
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</ThemeProvider>
|
||||||
|
</PersistGate>
|
||||||
|
</Provider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import type { ComponentType, ReactNode } from "react"
|
||||||
|
import { createBrowserRouter, Navigate } from "react-router-dom"
|
||||||
|
import ErrorPage from "@/components/ErrorPage"
|
||||||
|
import Layout from "@/components/Layout"
|
||||||
|
import EmptyLayout from "@/components/EmptyLayout"
|
||||||
|
import { isAuthenticated } from "@/store/auth-slice"
|
||||||
|
import { useAppSelector } from "@/store"
|
||||||
|
|
||||||
|
function lazy<T extends { default: ComponentType<unknown> }>(importer: () => Promise<T>) {
|
||||||
|
return async () => {
|
||||||
|
const module = await importer()
|
||||||
|
return { Component: module.default }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hydrateFallbackElement = <div className="px-4 py-6 text-neutral-500">Loading...</div>
|
||||||
|
|
||||||
|
function RequireAuth({ children }: { children: ReactNode }) {
|
||||||
|
const authenticated = useAppSelector(isAuthenticated)
|
||||||
|
if (!authenticated) return <Navigate to="/login" replace />
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = createBrowserRouter(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <Navigate to="/dashboard" replace />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: (
|
||||||
|
<RequireAuth>
|
||||||
|
<Layout />
|
||||||
|
</RequireAuth>
|
||||||
|
),
|
||||||
|
hydrateFallbackElement,
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "dashboard",
|
||||||
|
lazy: lazy(() => import("@/pages/Dashboard")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "artefacts",
|
||||||
|
lazy: lazy(() => import("@/pages/Artefacts")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "deployments",
|
||||||
|
lazy: lazy(() => import("@/pages/Deployments")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "users",
|
||||||
|
lazy: lazy(() => import("@/pages/Users")),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: <EmptyLayout />,
|
||||||
|
hydrateFallbackElement,
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "login",
|
||||||
|
lazy: lazy(() => import("@/pages/Login")),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ basename: "/" }
|
||||||
|
)
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
||||||
|
|
||||||
|
export interface AuthState {
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: AuthState = {
|
||||||
|
token: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
const authSlice = createSlice({
|
||||||
|
name: "auth",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setToken(state, action: PayloadAction<string>) {
|
||||||
|
state.token = action.payload
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { setToken } = authSlice.actions
|
||||||
|
export const authReducer = authSlice.reducer
|
||||||
|
|
||||||
|
export const isAuthenticated = (state: { auth: AuthState }) => state.auth.token !== ""
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { useDispatch, useSelector } from "react-redux"
|
||||||
|
import type { AppDispatch, RootState } from "@/store"
|
||||||
|
|
||||||
|
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
||||||
|
export const useAppSelector = useSelector.withTypes<RootState>()
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { configureStore, combineReducers } from "@reduxjs/toolkit"
|
||||||
|
import {
|
||||||
|
persistStore,
|
||||||
|
persistReducer,
|
||||||
|
FLUSH,
|
||||||
|
REHYDRATE,
|
||||||
|
PAUSE,
|
||||||
|
PERSIST,
|
||||||
|
PURGE,
|
||||||
|
REGISTER,
|
||||||
|
} from "redux-persist"
|
||||||
|
import createWebStorage from "redux-persist/es/storage/createWebStorage"
|
||||||
|
import { authReducer } from "./auth-slice"
|
||||||
|
|
||||||
|
const storage = createWebStorage(import.meta.env.VITE_REDUX_STORAGE ?? "local")
|
||||||
|
|
||||||
|
const persistConfig = {
|
||||||
|
key: "root",
|
||||||
|
storage,
|
||||||
|
whitelist: ["auth"],
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
auth: authReducer,
|
||||||
|
})
|
||||||
|
|
||||||
|
const persistedReducer = persistReducer(persistConfig, rootReducer)
|
||||||
|
|
||||||
|
const store = configureStore({
|
||||||
|
reducer: persistedReducer,
|
||||||
|
middleware: (getDefaultMiddleware) =>
|
||||||
|
getDefaultMiddleware({
|
||||||
|
serializableCheck: {
|
||||||
|
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const persistor = persistStore(store)
|
||||||
|
|
||||||
|
export default store
|
||||||
|
export type RootState = ReturnType<typeof rootReducer>
|
||||||
|
export type AppDispatch = typeof store.dispatch
|
||||||
|
export type AppStore = typeof store
|
||||||
|
|
||||||
|
export { useAppSelector, useAppDispatch } from "./hooks"
|
||||||
Vendored
+1
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_API_BASE_URL?: string
|
readonly VITE_API_BASE_URL?: string
|
||||||
|
readonly VITE_REDUX_STORAGE?: "local" | "session"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|||||||
Reference in New Issue
Block a user