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.
277 lines
9.3 KiB
277 lines
9.3 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"
|
|
import { VisualizerPage } from "./pages/VisualizerPage.tsx"
|
|
|
|
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 fetcher = useMemo(() => new Fetcher(getStoredAuthentication()), [])
|
|
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")
|
|
}, 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(<NewTacticPage />)}
|
|
/>
|
|
<Route
|
|
path={"/tactic/:tacticId/edit"}
|
|
element={suspense(
|
|
<LoggedInPage>
|
|
<Editor guestMode={false} />
|
|
</LoggedInPage>,
|
|
)}
|
|
/>
|
|
<Route
|
|
path={"/tactic/:tacticId/view"}
|
|
element={suspense(
|
|
<LoggedInPage>
|
|
<VisualizerPage guestMode={false} />
|
|
,
|
|
</LoggedInPage>,
|
|
)}
|
|
/>
|
|
<Route
|
|
path={"/tactic/view-guest"}
|
|
element={suspense(
|
|
<VisualizerPage guestMode={true} />,
|
|
)}
|
|
/>
|
|
<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 | null) => 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 | null) => void] {
|
|
const { user, setUser } = useContext(SignedInUserContext)!
|
|
return [user, setUser]
|
|
}
|