mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 07:55:25 +02:00
added dashboard buttons; added use user hook
This commit is contained in:
parent
0a0b9b55f8
commit
e37aa919cc
15
src/App.tsx
15
src/App.tsx
@ -8,9 +8,10 @@ import {queryClient} from "~/constants/react-query"
|
||||
import {lightTheme} from "~/constants/themes"
|
||||
import {getServerSettings} from "~/apis"
|
||||
import AuthContextProvider from "~/AuthContext/AuthContextProvider"
|
||||
import AuthenticateRoute from "~/routes/AuthenticateRoute"
|
||||
import AuthenticatedRoute from "~/routes/AuthenticatedRoute"
|
||||
import RootRoute from "~/routes/Root"
|
||||
import SignupRoute from "~/routes/SignupRoute"
|
||||
import SingleElementRoute from "~/routes/SingleElementRoute"
|
||||
import VerifyEmailRoute from "~/routes/VerifyEmailRoute"
|
||||
|
||||
const router = createBrowserRouter([
|
||||
@ -20,21 +21,25 @@ const router = createBrowserRouter([
|
||||
errorElement: <div></div>,
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
element: <SingleElementRoute />,
|
||||
path: "/auth",
|
||||
element: <AuthenticateRoute />,
|
||||
children: [
|
||||
{
|
||||
loader: getServerSettings,
|
||||
path: "/verify-email",
|
||||
path: "/auth/verify-email",
|
||||
element: <VerifyEmailRoute />,
|
||||
},
|
||||
{
|
||||
loader: getServerSettings,
|
||||
path: "/signup",
|
||||
path: "/auth/signup",
|
||||
element: <SignupRoute />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
element: <AuthenticatedRoute />,
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
@ -1,11 +1,16 @@
|
||||
import {ReactElement, ReactNode, useCallback, useMemo} from "react"
|
||||
import {AxiosError} from "axios"
|
||||
import {ReactElement, ReactNode, useCallback, useEffect, useMemo} from "react"
|
||||
import {useLocalStorage} from "react-use"
|
||||
import axios, {AxiosError} from "axios"
|
||||
|
||||
import {useMutation} from "@tanstack/react-query"
|
||||
|
||||
import {User} from "~/server-types"
|
||||
import {RefreshTokenResult, logout as logoutUser, refreshToken} from "~/apis"
|
||||
import {
|
||||
REFRESH_TOKEN_URL,
|
||||
RefreshTokenResult,
|
||||
logout as logoutUser,
|
||||
refreshToken,
|
||||
} from "~/apis"
|
||||
|
||||
import AuthContext, {AuthContextType} from "./AuthContext"
|
||||
|
||||
@ -51,5 +56,30 @@ export default function AuthContextProvider({
|
||||
[refresh, login, logout],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const interceptor = axios.interceptors.response.use(
|
||||
response => response,
|
||||
async (error: AxiosError) => {
|
||||
if (error.isAxiosError) {
|
||||
if (error.response?.status === 401) {
|
||||
// Check if error comes from refreshing the token.
|
||||
// If yes, the user has been logged out completely.
|
||||
const request: XMLHttpRequest = error.request
|
||||
|
||||
if (request.responseURL === REFRESH_TOKEN_URL) {
|
||||
await logout(false)
|
||||
} else {
|
||||
await refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw error
|
||||
},
|
||||
)
|
||||
|
||||
return () => axios.interceptors.response.eject(interceptor)
|
||||
}, [logout, refresh])
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
||||
}
|
||||
|
@ -7,10 +7,12 @@ export interface RefreshTokenResult {
|
||||
detail: string
|
||||
}
|
||||
|
||||
export const REFRESH_TOKEN_URL = `${
|
||||
import.meta.env.VITE_SERVER_BASE_URL
|
||||
}/api/refresh-token`
|
||||
|
||||
export default async function refreshToken(): Promise<RefreshTokenResult> {
|
||||
const {data} = await axios.post(
|
||||
`${import.meta.env.VITE_SERVER_BASE_URL}/api/refresh-token`,
|
||||
)
|
||||
const {data} = await axios.post(REFRESH_TOKEN_URL)
|
||||
|
||||
return data
|
||||
}
|
||||
|
@ -2,3 +2,5 @@ export * from "./use-interval-update"
|
||||
export {default as useIntervalUpdate} from "./use-interval-update"
|
||||
export * from "./use-query-params"
|
||||
export {default as useQueryParams} from "./use-query-params"
|
||||
export * from "./use-user"
|
||||
export {default as useUser} from "./use-user"
|
||||
|
20
src/hooks/use-user.ts
Normal file
20
src/hooks/use-user.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import {useNavigate} from "react-router-dom"
|
||||
import {useContext, useLayoutEffect} from "react"
|
||||
|
||||
import {User} from "~/server-types"
|
||||
import AuthContext from "~/AuthContext/AuthContext"
|
||||
|
||||
/// Returns the currently authenticated user.
|
||||
// If the user is not authenticated, it will automatically redirect to the login page.
|
||||
export default function useUser(): User {
|
||||
const navigate = useNavigate()
|
||||
const {user, isAuthenticated} = useContext(AuthContext)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
navigate("/login")
|
||||
}
|
||||
}, [isAuthenticated, navigate])
|
||||
|
||||
return user as User
|
||||
}
|
66
src/route-widgets/AuthenticateRoute/NavigationButton.tsx
Normal file
66
src/route-widgets/AuthenticateRoute/NavigationButton.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import {ReactElement} from "react"
|
||||
import {BiStats} from "react-icons/bi"
|
||||
import {MdMail, MdSettings} from "react-icons/md"
|
||||
import {IoMdDocument} from "react-icons/io"
|
||||
import {Link as RouterLink, useLocation} from "react-router-dom"
|
||||
|
||||
import {Button} from "@mui/material"
|
||||
|
||||
export enum NavigationSection {
|
||||
Overview,
|
||||
Aliases,
|
||||
Reports,
|
||||
Settings,
|
||||
}
|
||||
|
||||
export interface NavigationButtonProps {
|
||||
section: NavigationSection
|
||||
}
|
||||
|
||||
const SECTION_ICON_MAP: Record<NavigationSection, ReactElement> = {
|
||||
[NavigationSection.Overview]: <BiStats />,
|
||||
[NavigationSection.Aliases]: <MdMail />,
|
||||
[NavigationSection.Reports]: <IoMdDocument />,
|
||||
[NavigationSection.Settings]: <MdSettings />,
|
||||
}
|
||||
|
||||
const SECTION_TEXT_MAP: Record<NavigationSection, string> = {
|
||||
[NavigationSection.Overview]: "Overview",
|
||||
[NavigationSection.Aliases]: "Aliases",
|
||||
[NavigationSection.Reports]: "Reports",
|
||||
[NavigationSection.Settings]: "Settings",
|
||||
}
|
||||
|
||||
const PATH_SECTION_MAP: Record<string, NavigationSection> = {
|
||||
"/": NavigationSection.Overview,
|
||||
"/aliases": NavigationSection.Aliases,
|
||||
"/reports": NavigationSection.Reports,
|
||||
"/settings": NavigationSection.Settings,
|
||||
}
|
||||
|
||||
export default function NavigationButton({
|
||||
section,
|
||||
}: NavigationButtonProps): ReactElement {
|
||||
const location = useLocation()
|
||||
|
||||
const currentSection = PATH_SECTION_MAP[location.pathname]
|
||||
const Icon = SECTION_ICON_MAP[section]
|
||||
const text = SECTION_TEXT_MAP[section]
|
||||
|
||||
return (
|
||||
<Button
|
||||
fullWidth
|
||||
color="inherit"
|
||||
variant={section === currentSection ? "outlined" : "text"}
|
||||
startIcon={Icon}
|
||||
component={RouterLink}
|
||||
to={
|
||||
Object.keys(PATH_SECTION_MAP).find(
|
||||
path => PATH_SECTION_MAP[path] === section,
|
||||
) ?? "/"
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
)
|
||||
}
|
@ -4,7 +4,7 @@ import {MdAdd, MdLogin} from "react-icons/md"
|
||||
|
||||
import {Box, Button, Grid} from "@mui/material"
|
||||
|
||||
export default function SingleElementRoute(): ReactElement {
|
||||
export default function AuthenticateRoute(): ReactElement {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
85
src/routes/AuthenticatedRoute.tsx
Normal file
85
src/routes/AuthenticatedRoute.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import {ReactElement} from "react"
|
||||
import {Outlet, useLocation} from "react-router-dom"
|
||||
|
||||
import {Box, Container, List, ListItem, Paper, useTheme} from "@mui/material"
|
||||
|
||||
import {useUser} from "~/hooks"
|
||||
import NavigationButton, {
|
||||
NavigationSection,
|
||||
} from "~/route-widgets/AuthenticateRoute/NavigationButton"
|
||||
|
||||
enum Section {
|
||||
Overview,
|
||||
Aliases,
|
||||
Reports,
|
||||
Settings,
|
||||
}
|
||||
|
||||
export default function AuthenticatedRoute(): ReactElement {
|
||||
const theme = useTheme()
|
||||
const route = useLocation()
|
||||
|
||||
const section = (() => {
|
||||
switch (route.pathname) {
|
||||
case "/":
|
||||
return Section.Overview
|
||||
case "/aliases":
|
||||
return Section.Aliases
|
||||
case "/reports":
|
||||
return Section.Reports
|
||||
case "/settings":
|
||||
return Section.Settings
|
||||
}
|
||||
})()
|
||||
|
||||
useUser()
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
height="100vh"
|
||||
>
|
||||
<Box width="90vw" justifyContent="center" alignItems="center">
|
||||
<Container
|
||||
maxWidth="md"
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Box
|
||||
bgcolor={theme.palette.background.paper}
|
||||
component="nav"
|
||||
>
|
||||
<List>
|
||||
{(
|
||||
Object.keys(NavigationSection) as Array<
|
||||
keyof typeof NavigationSection
|
||||
>
|
||||
).map(key => (
|
||||
<ListItem key={key}>
|
||||
<NavigationButton
|
||||
section={NavigationSection[key]}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
<Paper>
|
||||
<Box padding={4}>
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user