244 lines
7.5 KiB
TypeScript
244 lines
7.5 KiB
TypeScript
import { useQuery } from "@tanstack/react-query";
|
|
import { isEmpty } from "lodash";
|
|
import { JSX } from "react";
|
|
import { Provider, useSelector } from "react-redux";
|
|
import {
|
|
Navigate,
|
|
Outlet,
|
|
Route,
|
|
Routes as Router,
|
|
useMatch,
|
|
useParams,
|
|
} from "react-router-dom";
|
|
|
|
import { rulesEngineSelectors } from "@dndbeyond/character-rules-engine/es";
|
|
import { GameLogContextProvider } from "@dndbeyond/game-log-components";
|
|
import { PddContextProvider } from "@dndbeyond/pocket-dimension-dice/components/PddContextProvider";
|
|
import { Context as DiceWorkerContext } from "@dndbeyond/pocket-dimension-dice/context";
|
|
|
|
import configureSheetStore from "~/tools/js/CharacterSheet/store/configureStore";
|
|
|
|
import config, { authEndpoint } from "../config";
|
|
import { useAuth } from "../contexts/Authentication";
|
|
import { getCharacters } from "../helpers/characterServiceApi";
|
|
import { getStatus } from "../state/selectors/characterUtils";
|
|
import { MyCharacters } from "../subApps/listing";
|
|
import { getUrlWithParams } from "../tools/js/CharacterBuilder/utils/navigationUtils";
|
|
import { CharacterStatusEnum } from "../types";
|
|
import MaxCharactersMessage from "./max-characters-message";
|
|
import SheetBuilderApp from "./sheet-builder-app";
|
|
|
|
const BASE_PATHNAME = config.basePathname;
|
|
const store = configureSheetStore();
|
|
|
|
/**
|
|
*
|
|
* RequireAuth checks if you are logged in and redirects to the login page if you are not.
|
|
* To use just wrap your element prop in a Route component in it.
|
|
* For example a the Character Sheet has a AnonymousRoute view so it is not wrapped in a RequireAuth.
|
|
*/
|
|
function RequireAuth({ children }: { children: JSX.Element }) {
|
|
let user = useAuth();
|
|
const isCharactersListing = useMatch({ path: BASE_PATHNAME, end: true });
|
|
|
|
// loading state
|
|
if (user === undefined) {
|
|
return null;
|
|
}
|
|
// not authenticated
|
|
if (user === null) {
|
|
// Should only ever happen in local dev when not using app-shell locally
|
|
// if you remove this it will reveal a memory leak :)
|
|
if (process.env.NODE_ENV === "development" && isCharactersListing) {
|
|
return (
|
|
<div>{"YOU ARE NOT LOGGED IN. THIS VIEW IS HANDLED BY APP SHELL"}</div>
|
|
);
|
|
}
|
|
|
|
window.location.href = getUrlWithParams();
|
|
}
|
|
// authenticated
|
|
return children;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* GetCharacters uses the renderProp pattern to handle requiring getting all characters. when a user is logged in.
|
|
* This ensure that locked users are not able to use this part of the app until the resolved thier locked characters.
|
|
* This was setup this way to avoid having to make a new api call to get the characters when the user is Anonymous.
|
|
* Therefore, keeping our api calls to a minimum and minimizing errors from failed api calls.
|
|
* Note: this could be simpler but complexity was added to make typescript happy.
|
|
*/
|
|
function GetCallCharacters({
|
|
children,
|
|
}: {
|
|
children: (QueryResponseData) => void;
|
|
}): JSX.Element {
|
|
const isCharactersListing = useMatch({ path: BASE_PATHNAME, end: true });
|
|
|
|
const { isLoading, data, refetch, error } = useQuery({
|
|
queryKey: ["repoData"],
|
|
queryFn: () => getCharacters(),
|
|
refetchOnWindowFocus: false,
|
|
refetchInterval: 24 * 60 * 60 * 1000,
|
|
});
|
|
|
|
if (isLoading) {
|
|
return <>{null}</>;
|
|
}
|
|
if (!error && data?.data?.canUnlockCharacters && !isCharactersListing) {
|
|
return (
|
|
<>
|
|
<Navigate to={BASE_PATHNAME} replace />
|
|
</>
|
|
);
|
|
}
|
|
return <>{children({ isLoading, data, refetch, error })}</>;
|
|
}
|
|
|
|
function CharacterSheetLayout() {
|
|
return (
|
|
<Provider store={store}>
|
|
<CharacterSheetProviders />
|
|
</Provider>
|
|
);
|
|
}
|
|
|
|
function CharacterSheetProviders() {
|
|
const user = useAuth();
|
|
const isUserEmpty = isEmpty(user);
|
|
const campaignId = useSelector(rulesEngineSelectors.getCampaign)?.id;
|
|
const { characterId } = useParams();
|
|
|
|
return (
|
|
<PddContextProvider
|
|
logger={console.log}
|
|
errorCallback={() => {}}
|
|
nodeEnv={process.env.NODE_ENV}
|
|
env={config.environment}
|
|
user={isUserEmpty ? undefined : user}
|
|
gameId={campaignId ? String(campaignId) : undefined}
|
|
>
|
|
<GameLogContextProvider
|
|
baseUrl={config.gameLogApiEndpoint}
|
|
ddbApiUrl={config.ddbApiEndpoint}
|
|
diceServiceUrl={config.diceApiEndpoint}
|
|
authUrl={authEndpoint}
|
|
diceThumbnailsUrl={`${config.diceAssetEndpoint}/images/thumbnails`}
|
|
entityId={characterId ?? ""}
|
|
diceWorkerContext={DiceWorkerContext}
|
|
>
|
|
<Outlet />
|
|
</GameLogContextProvider>
|
|
</PddContextProvider>
|
|
);
|
|
}
|
|
|
|
export const Routes = () => {
|
|
const user = useAuth();
|
|
|
|
return (
|
|
<Router>
|
|
{/* Routes that need the Redux store + GameLog/Pdd providers */}
|
|
<Route element={<CharacterSheetLayout />}>
|
|
<Route
|
|
path={`${BASE_PATHNAME}/builder/*`}
|
|
element={
|
|
<RequireAuth>
|
|
<GetCallCharacters>
|
|
{({ data }) => {
|
|
const numberOfCharacters =
|
|
data?.data?.characters?.filter(
|
|
(character) =>
|
|
getStatus(character) === CharacterStatusEnum.Active
|
|
).length ?? 0;
|
|
const maxCharacterSlotsAllowed =
|
|
data?.data?.characterSlotLimit ?? Infinity;
|
|
const hasMaxCharacters =
|
|
numberOfCharacters >= maxCharacterSlotsAllowed;
|
|
if (hasMaxCharacters) {
|
|
return (
|
|
<Navigate
|
|
to={`${BASE_PATHNAME}/max-characters`}
|
|
replace
|
|
/>
|
|
);
|
|
}
|
|
return <SheetBuilderApp />;
|
|
}}
|
|
</GetCallCharacters>
|
|
</RequireAuth>
|
|
}
|
|
/>
|
|
<Route
|
|
path={`${BASE_PATHNAME}/premade/*`}
|
|
element={
|
|
<RequireAuth>
|
|
<GetCallCharacters>{() => <SheetBuilderApp />}</GetCallCharacters>
|
|
</RequireAuth>
|
|
}
|
|
/>
|
|
|
|
{/* Character routes */}
|
|
<Route path={`${BASE_PATHNAME}/:characterId`}>
|
|
<Route
|
|
path="builder/*"
|
|
element={
|
|
<RequireAuth>
|
|
<GetCallCharacters>
|
|
{() => <SheetBuilderApp />}
|
|
</GetCallCharacters>
|
|
</RequireAuth>
|
|
}
|
|
/>
|
|
<Route
|
|
path=":shareId"
|
|
element={
|
|
user ? (
|
|
<GetCallCharacters>
|
|
{() => <SheetBuilderApp />}
|
|
</GetCallCharacters>
|
|
) : (
|
|
<SheetBuilderApp />
|
|
)
|
|
}
|
|
/>
|
|
<Route
|
|
index
|
|
element={
|
|
user ? (
|
|
<GetCallCharacters>
|
|
{() => <SheetBuilderApp />}
|
|
</GetCallCharacters>
|
|
) : (
|
|
<SheetBuilderApp />
|
|
)
|
|
}
|
|
/>
|
|
</Route>
|
|
</Route>
|
|
|
|
{/* Routes that don't need the store */}
|
|
<Route
|
|
path={`${BASE_PATHNAME}/max-characters`}
|
|
element={<MaxCharactersMessage />}
|
|
/>
|
|
<Route
|
|
path={BASE_PATHNAME}
|
|
element={
|
|
<RequireAuth>
|
|
<GetCallCharacters>
|
|
{({ isLoading, data, refetch, error }) => (
|
|
<MyCharacters
|
|
characterQuery={{ isLoading, data, refetch, error }}
|
|
/>
|
|
)}
|
|
</GetCallCharacters>
|
|
</RequireAuth>
|
|
}
|
|
/>
|
|
<Route path="/" element={<Navigate to={BASE_PATHNAME} replace />} />
|
|
</Router>
|
|
);
|
|
};
|