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.
247 lines
8.8 KiB
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]
|
|
}
|
|
|
|
|