diff --git a/package-lock.json b/package-lock.json
index 8291946..8ca393d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35,7 +35,8 @@
"react-feather": "^2.0.10",
"react-responsive": "^10.0.0",
"react-router-dom": "^6.26.2",
- "vite-express": "0.16.0"
+ "vite-express": "0.16.0",
+ "zustand": "^5.0.2"
},
"devDependencies": {
"@types/react": "^18.3.2",
@@ -5728,6 +5729,34 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zustand": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.2.tgz",
+ "integrity": "sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index 9030925..1a88de4 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,8 @@
"react-feather": "^2.0.10",
"react-responsive": "^10.0.0",
"react-router-dom": "^6.26.2",
- "vite-express": "0.16.0"
+ "vite-express": "0.16.0",
+ "zustand": "^5.0.2"
},
"devDependencies": {
"@types/react": "^18.3.2",
diff --git a/src/App.tsx b/src/App.tsx
index a65eef4..68fff38 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -17,12 +17,16 @@ const App = () => {
}>
} />
- } />
- } />
- } />
- } />
- } />} />
- } />} />
+ }>
+ } />
+
+ }>
+ } />
+ } />
+
+ }>
+ } />
+
} />
diff --git a/src/components/ProtectedAuthorizedUserRoute.tsx b/src/components/ProtectedAuthorizedUserRoute.tsx
index 7ff419f..108b0d2 100644
--- a/src/components/ProtectedAuthorizedUserRoute.tsx
+++ b/src/components/ProtectedAuthorizedUserRoute.tsx
@@ -1,29 +1,43 @@
-import React, { useEffect, useState } from 'react'
+import { useEffect, useState } from 'react'
import NotFound from '../pages/NotFound/NotFound.tsx'
import { User } from '../services/userProfile'
import { isAuthorizedToCreateTeam } from '../services/createTeam'
-import { Effect } from 'effect'
+import { Effect, Option as O } from 'effect'
import { isDaplaAdmin } from '../utils/services'
+import { option } from '../utils/utils'
+import { Skeleton } from '@mui/material'
+import PageLayout from './PageLayout/PageLayout.tsx'
+import { Outlet } from 'react-router-dom'
+import { useUserProfileStore } from '../services/store.ts'
-export interface Props {
- component: React.ReactElement
-}
+const ProtectedAuthorizedUserRoute = () => {
+ // O.none() here represents the loading state
+ const [oIsAuthorized, setIsAuthorized] = useState>(O.none())
+ const maybeUser: O.Option = useUserProfileStore((state) => state.loggedInUser)
-const ProtectedAuthorizedUserRoute = ({ component }: Props) => {
- const [isAuthorized, setIsAuthorized] = useState(false)
useEffect(() => {
- const userProfileItem = localStorage.getItem('userProfile')
- if (!userProfileItem) return
+ Effect.gen(function* () {
+ const user: User = yield* O.match(maybeUser, {
+ onNone: () => Effect.fail(new Error('User not logged in!')),
+ onSome: (user) => Effect.succeed(user),
+ })
- const user = JSON.parse(userProfileItem) as User
- if (!user) return
+ const daplaAdmin: boolean = yield* Effect.promise(() => isDaplaAdmin(user.principal_name))
- Effect.promise(() => isDaplaAdmin(user.principal_name))
- .pipe(Effect.runPromise)
- .then((isDaplaAdmin: boolean) => setIsAuthorized(isAuthorizedToCreateTeam(isDaplaAdmin, user.job_title)))
+ yield* Effect.sync(() => setIsAuthorized(O.some(isAuthorizedToCreateTeam(daplaAdmin, user.job_title))))
+ }).pipe(Effect.runPromise)
}, [])
- return isAuthorized ? component :
+ return option(
+ oIsAuthorized,
+ () => (
+ }
+ />
+ ),
+ (isAuthorized) => (isAuthorized ? : )
+ )
}
export default ProtectedAuthorizedUserRoute
diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx
index 104a90c..757792e 100644
--- a/src/components/ProtectedRoute.tsx
+++ b/src/components/ProtectedRoute.tsx
@@ -5,11 +5,13 @@ import { fetchUserInformationFromAuthToken } from '../utils/services'
import { Cause, Effect, Option as O } from 'effect'
import { customLogger } from '../utils/logger.ts'
import { ApiError } from '../utils/services.ts'
+import { useUserProfileStore } from '../services/store.ts'
import { User } from '../services/userProfile.ts'
const ProtectedRoute = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false)
+ const setUser = useUserProfileStore((state) => state.setUser)
const navigate = useNavigate()
const from = location.pathname
@@ -20,8 +22,8 @@ const ProtectedRoute = () => {
Effect.flatMap((x) => (x instanceof ApiError ? Effect.fail(x) : Effect.succeed(x))),
Effect.map(JSON.stringify)
)
- yield* Effect.logInfo(`UserProfile set in localStorage: ${userProfile}`)
yield* Effect.sync(() => localStorage.setItem('userProfile', userProfile))
+ yield* Effect.sync(() => setUser(userProfile))
yield* Effect.sync(() => setIsAuthenticated(true))
}).pipe(Effect.provide(customLogger))
@@ -34,7 +36,10 @@ const ProtectedRoute = () => {
onSome: (userProfile) =>
// invalidate cached user profile if 'job_title' field is missing
userProfile.job_title
- ? Effect.sync(() => setIsAuthenticated(true))
+ ? Effect.zip(
+ Effect.sync(() => setIsAuthenticated(true)),
+ Effect.sync(() => setUser(userProfile))
+ )
: Effect.zipRight(
Effect.logInfo("'job_title' field missing, invalidating UserProfile cache"),
fetchUserProfile()