You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Application-Web/src/App.tsx

247 lines
8.8 KiB

import { BrowserRouter, Navigate, Outlet, Route, Routes, useLocation } from "react-router-dom"
import { Header } from "./pages/template/Header.tsx"
import "./style/app.css"
import { createContext, lazy, ReactNode, Suspense, useCallback, useContext, useEffect, useMemo, useState } from "react"
import { BASE } from "./Constants.ts"
import { Authentication, Fetcher } from "./app/Fetcher.ts"
import { User } from "./model/User.ts"
const HomePage = lazy(() => import("./pages/HomePage.tsx"))
const LoginPage = lazy(() => import("./pages/LoginPage.tsx"))
const RegisterPage = lazy(() => import("./pages/RegisterPage.tsx"))
const NotFoundPage = lazy(() => import("./pages/404.tsx"))
const CreateTeamPage = lazy(() => import("./pages/CreateTeamPage.tsx"))
const TeamPanelPage = lazy(() => import("./pages/TeamPanel.tsx"))
const NewTacticPage = lazy(() => import("./pages/NewTacticPage.tsx"))
const Editor = lazy(() => import("./pages/Editor.tsx"))
const Settings = lazy(() => import("./pages/Settings.tsx"))
const TOKEN_REFRESH_INTERVAL_MS = 60 * 1000
export default function App() {
function suspense(node: ReactNode) {
return (
<Suspense fallback={<p>Loading, please wait...</p>}>
{node}
</Suspense>
)
}
const storedAuth = useMemo(() => getStoredAuthentication(), [])
const fetcher = useMemo(() => new Fetcher(storedAuth), [storedAuth])
const [user, setUser] = useState<User | null>(null)
const handleAuthSuccess = useCallback(async (auth: Authentication) => {
fetcher.updateAuthentication(auth)
const user = await fetchUser(fetcher)
setUser(user)
storeAuthentication(auth)
}, [fetcher])
useEffect(() => {
const interval = setInterval(() => {
fetcher.fetchAPIGet("auth/keep-alive")
console.log("KEPT ALIVE !")
}, TOKEN_REFRESH_INTERVAL_MS)
return () => clearInterval(interval)
}, [fetcher])
return (
<div id="app">
<FetcherContext.Provider value={fetcher}>
<SignedInUserContext.Provider value={{
user,
setUser,
}}>
<BrowserRouter basename={BASE}>
<Outlet />
<Routes>
<Route
path={"/login"}
element={suspense(<LoginPage onSuccess={handleAuthSuccess} />)}
/>
<Route
path={"/register"}
element={suspense(<RegisterPage onSuccess={handleAuthSuccess} />)}
/>
<Route path={"/"} element={suspense(<AppLayout />)}>
<Route path={"/"} element={
suspense(
<LoggedInPage>
<HomePage />
</LoggedInPage>,
)
}
/>
<Route
path={"/home"}
element={
suspense(
<LoggedInPage>
<HomePage />
</LoggedInPage>,
)
}
/>
<Route
path={"/settings"}
element={
suspense(
<LoggedInPage>
<Settings />
</LoggedInPage>,
)
}
/>
<Route
path={"/team/new"}
element={
suspense(<CreateTeamPage />)
}
/>
<Route
path={"/team/:teamId"}
element={
suspense(
<LoggedInPage>
<TeamPanelPage />
</LoggedInPage>,
)
}
/>
<Route
path={"/tactic/new"}
element={
suspense(
<LoggedInPage>
<NewTacticPage />
</LoggedInPage>,
)
}
/>
<Route
path={"/tactic/:tacticId/edit"}
element={
suspense(
<LoggedInPage>
<Editor guestMode={false} />
</LoggedInPage>,
)
}
/>
<Route
path={"/tactic/edit-guest"}
element={suspense(<Editor guestMode={true} />)}
/>
<Route
path={"*"}
element={suspense(<NotFoundPage />)}
/>
</Route>
</Routes>
</BrowserRouter>
</SignedInUserContext.Provider>
</FetcherContext.Provider>
</div>
)
}
async function fetchUser(fetcher: Fetcher): Promise<User> {
const response = await fetcher.fetchAPIGet("user")
if (!response.ok) {
throw Error("Could not retrieve user information : " + await response.text())
}
return await response.json()
}
const STORAGE_AUTH_KEY = "token"
function getStoredAuthentication(): Authentication {
const storedUser = localStorage.getItem(STORAGE_AUTH_KEY)
return storedUser == null ? null : JSON.parse(storedUser)
}
function storeAuthentication(auth: Authentication) {
localStorage.setItem(STORAGE_AUTH_KEY, JSON.stringify(auth))
}
interface LoggedInPageProps {
children: ReactNode
}
enum UserFetchingState {
FETCHING,
FETCHED,
ERROR
}
function LoggedInPage({ children }: LoggedInPageProps) {
const [user, setUser] = useUser()
const fetcher = useAppFetcher()
const [userFetchingState, setUserFetchingState] = useState(user === null ? UserFetchingState.FETCHING : UserFetchingState.FETCHED)
const location = useLocation()
useEffect(() => {
async function initUser() {
try {
const user = await fetchUser(fetcher)
setUser(user)
setUserFetchingState(UserFetchingState.FETCHED)
} catch (e) {
setUserFetchingState(UserFetchingState.ERROR)
}
}
if (userFetchingState === UserFetchingState.FETCHING)
initUser()
}, [fetcher, setUser, userFetchingState])
switch (userFetchingState) {
case UserFetchingState.ERROR:
return <Navigate to={"/login"} replace state={{ from: location.pathname }} />
case UserFetchingState.FETCHED:
return children
case UserFetchingState.FETCHING:
return <p>Fetching user...</p>
}
}
function AppLayout() {
return (
<>
<Header />
<Outlet />
</>
)
}
interface UserContext {
user: User | null
setUser: (user: User) => void
}
const SignedInUserContext = createContext<UserContext | null>(null)
const FetcherContext = createContext(new Fetcher())
export function useAppFetcher() {
return useContext(FetcherContext)
}
export function useUser(): [User | null, (user: User) => void] {
const { user, setUser } = useContext(SignedInUserContext)!
return [user, setUser]
}