From 163f5641e564c1235c8612d618487db959b9dfd4 Mon Sep 17 00:00:00 2001 From: siujamo Date: Tue, 7 Jul 2026 15:17:49 +0800 Subject: [PATCH] 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 --- web/.env.example | 4 + web/package.json | 4 + web/pnpm-lock.yaml | 141 +++++++++++++++++++++++++++++ web/src/App.tsx | 35 ------- web/src/components/EmptyLayout.tsx | 13 +++ web/src/components/ErrorPage.tsx | 41 +++++++++ web/src/components/Layout.tsx | 4 + web/src/main.tsx | 26 ++++-- web/src/router/index.tsx | 72 +++++++++++++++ web/src/store/auth-slice.ts | 24 +++++ web/src/store/hooks.ts | 5 + web/src/store/index.ts | 46 ++++++++++ web/src/vite-env.d.ts | 3 +- 13 files changed, 375 insertions(+), 43 deletions(-) delete mode 100644 web/src/App.tsx create mode 100644 web/src/components/EmptyLayout.tsx create mode 100644 web/src/components/ErrorPage.tsx create mode 100644 web/src/router/index.tsx create mode 100644 web/src/store/auth-slice.ts create mode 100644 web/src/store/hooks.ts create mode 100644 web/src/store/index.ts diff --git a/web/.env.example b/web/.env.example index 27994cb..d268230 100644 --- a/web/.env.example +++ b/web/.env.example @@ -11,3 +11,7 @@ # Default: https://pipely.onixbyte.com/ # Development: http://localhost:8080 VITE_API_BASE_URL=https://pipely.onixbyte.com/ + +# Redux Persistent storage location +# Options: `local` (default), `session` +VITE_REDUX_STORAGE=local diff --git a/web/package.json b/web/package.json index 67b7c87..a72281c 100644 --- a/web/package.json +++ b/web/package.json @@ -14,12 +14,16 @@ "@emotion/styled": "^11.14.1", "@mui/icons-material": "^9.2.0", "@mui/material": "^9.2.0", + "@reduxjs/toolkit": "^2.12.0", "@tailwindcss/vite": "^4.3.2", "axios": "^1.18.1", "dayjs": "^1.11.21", "react": "^19.2.7", "react-dom": "^19.2.7", + "react-redux": "^9.3.0", + "react-router": "^8.1.0", "react-router-dom": "^7.18.1", + "redux-persist": "^6.0.0", "tailwindcss": "^4.3.2" }, "devDependencies": { diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index de47a1a..7b3e3dd 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@mui/material': 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) + '@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': specifier: ^4.3.2 version: 4.3.2(vite@8.1.3(@types/node@24.13.2)(jiti@2.7.0)) @@ -35,9 +38,18 @@ importers: react-dom: specifier: ^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: specifier: ^7.18.1 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: specifier: ^4.3.2 version: 4.3.2 @@ -499,6 +511,17 @@ packages: '@popperjs/core@2.11.8': 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': resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -604,6 +627,12 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} 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': resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==} @@ -735,6 +764,9 @@ packages: '@types/react@19.2.17': 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': resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} @@ -985,6 +1017,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -1445,6 +1480,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immer@11.1.11: + resolution: {integrity: sha512-qzXuyXAkPySAGYkfsAwodDPWT8Zm7/Uo5BNt4BjhMhG5WlWyZZ4wQqnWwdS8kjlQ1Cwu6gjw3A6+0gTQwlyYtw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -2000,6 +2038,18 @@ packages: react-is@19.2.7: 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: resolution: {integrity: sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==} engines: {node: '>=20.0.0'} @@ -2017,6 +2067,16 @@ packages: react-dom: 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: resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -2031,10 +2091,30 @@ packages: resolution: {integrity: sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==} 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: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + reselect@5.2.0: + resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2286,6 +2366,11 @@ packages: uri-js@4.4.1: 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: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -2917,6 +3002,18 @@ snapshots: '@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': optional: true @@ -2972,6 +3069,10 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@standard-schema/spec@1.1.0': {} + + '@standard-schema/utils@0.3.0': {} + '@tailwindcss/node@4.3.2': dependencies: '@jridgewell/remapping': 2.3.5 @@ -3077,6 +3178,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/use-sync-external-store@0.0.6': {} + '@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)': @@ -3343,6 +3446,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie-es@3.1.1: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} @@ -3822,6 +3927,8 @@ snapshots: ignore@7.0.5: {} + immer@11.1.11: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -4258,6 +4365,15 @@ snapshots: 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): dependencies: react: 19.2.7 @@ -4272,6 +4388,13 @@ snapshots: optionalDependencies: 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): dependencies: '@babel/runtime': 7.29.7 @@ -4291,8 +4414,22 @@ snapshots: tiny-invariant: 1.3.3 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: {} + reselect@5.2.0: {} + resolve-from@4.0.0: {} resolve@1.22.12: @@ -4584,6 +4721,10 @@ snapshots: dependencies: 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: {} validate-npm-package-name@7.0.2: {} diff --git a/web/src/App.tsx b/web/src/App.tsx deleted file mode 100644 index f82b494..0000000 --- a/web/src/App.tsx +++ /dev/null @@ -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 - return <>{children} -} - -export default function App() { - return ( - - - } /> - - - - }> - } /> - } /> - } /> - } /> - - } /> - - - ) -} diff --git a/web/src/components/EmptyLayout.tsx b/web/src/components/EmptyLayout.tsx new file mode 100644 index 0000000..250a383 --- /dev/null +++ b/web/src/components/EmptyLayout.tsx @@ -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 ( +
+ +
+ ) +} diff --git a/web/src/components/ErrorPage.tsx b/web/src/components/ErrorPage.tsx new file mode 100644 index 0000000..6729dff --- /dev/null +++ b/web/src/components/ErrorPage.tsx @@ -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 ( + + + + {status} + + + {message} + + + + + ) +} \ No newline at end of file diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx index a0c9b1f..fc86925 100644 --- a/web/src/components/Layout.tsx +++ b/web/src/components/Layout.tsx @@ -1,4 +1,6 @@ import { NavLink, useNavigate, Outlet } from "react-router-dom" +import { useDispatch } from "react-redux" +import { setToken } from "@/store/auth-slice" import { Drawer, List, @@ -24,8 +26,10 @@ const drawerWidth = 240 export default function Layout() { const navigate = useNavigate() + const dispatch = useDispatch() function handleLogout() { + dispatch(setToken("")) navigate("/") } diff --git a/web/src/main.tsx b/web/src/main.tsx index aa4c02a..876c007 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,20 +1,32 @@ import { StrictMode } from "react" 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 router from "@/router" +import store, { persistor } from "@/store" import "./index.css" -import App from "./App.tsx" const theme = createTheme({ palette: { - mode: 'light', + mode: "light", }, }) +/** + * Main application entry point. + * Sets up the React app with React Router. + */ createRoot(document.getElementById("root")!).render( - - - - + + + + + + + + -) \ No newline at end of file +) diff --git a/web/src/router/index.tsx b/web/src/router/index.tsx new file mode 100644 index 0000000..a320376 --- /dev/null +++ b/web/src/router/index.tsx @@ -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 }>(importer: () => Promise) { + return async () => { + const module = await importer() + return { Component: module.default } + } +} + +const hydrateFallbackElement =
Loading...
+ +function RequireAuth({ children }: { children: ReactNode }) { + const authenticated = useAppSelector(isAuthenticated) + if (!authenticated) return + return <>{children} +} + +const router = createBrowserRouter( + [ + { + path: "/", + element: , + }, + { + element: ( + + + + ), + hydrateFallbackElement, + errorElement: , + 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: , + hydrateFallbackElement, + errorElement: , + children: [ + { + path: "login", + lazy: lazy(() => import("@/pages/Login")), + }, + ], + }, + ], + { basename: "/" } +) + +export default router diff --git a/web/src/store/auth-slice.ts b/web/src/store/auth-slice.ts new file mode 100644 index 0000000..bc97d16 --- /dev/null +++ b/web/src/store/auth-slice.ts @@ -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) { + state.token = action.payload + }, + }, +}) + +export const { setToken } = authSlice.actions +export const authReducer = authSlice.reducer + +export const isAuthenticated = (state: { auth: AuthState }) => state.auth.token !== "" diff --git a/web/src/store/hooks.ts b/web/src/store/hooks.ts new file mode 100644 index 0000000..f9a3047 --- /dev/null +++ b/web/src/store/hooks.ts @@ -0,0 +1,5 @@ +import { useDispatch, useSelector } from "react-redux" +import type { AppDispatch, RootState } from "@/store" + +export const useAppDispatch = useDispatch.withTypes() +export const useAppSelector = useSelector.withTypes() diff --git a/web/src/store/index.ts b/web/src/store/index.ts new file mode 100644 index 0000000..fc9f4e0 --- /dev/null +++ b/web/src/store/index.ts @@ -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 +export type AppDispatch = typeof store.dispatch +export type AppStore = typeof store + +export { useAppSelector, useAppDispatch } from "./hooks" diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts index d7013dd..f20e2c6 100644 --- a/web/src/vite-env.d.ts +++ b/web/src/vite-env.d.ts @@ -2,8 +2,9 @@ interface ImportMetaEnv { readonly VITE_API_BASE_URL?: string + readonly VITE_REDUX_STORAGE?: "local" | "session" } interface ImportMeta { readonly env: ImportMetaEnv -} \ No newline at end of file +}