Files
dndbeyond_src/ddb_main/tools/js/index.tsx
T

237 lines
6.7 KiB
TypeScript

import React, { useEffect } from "react";
import Modal from "react-modal";
import { useStore } from "react-redux";
import {
useLocation,
useMatch,
useNavigate,
useSearchParams,
} from "react-router-dom";
import {
characterEnvActions,
ConfigUtils,
Constants,
generateApiAdapter,
} from "@dndbeyond/character-rules-engine/es";
import config from "~/config";
import { useAuth } from "~/contexts/Authentication";
import { CharacterThemeProvider } from "~/contexts/CharacterTheme";
import { SidebarProvider } from "~/contexts/Sidebar";
import useUserName from "~/hooks/useUserName";
import useUserRoles from "~/hooks/useUserRoles";
import "../styles/styles.scss";
import CharacterBuilderContainer from "./CharacterBuilder/containers/CharacterBuilderContainer";
import CharacterSheetContainer from "./CharacterSheet/containers/CharacterSheetContainer";
import { appEnvActions } from "./Shared/actions";
import { Managers } from "./Shared/managers";
import {
DiceFeatureConfigurationState,
GameLogState,
} from "./Shared/stores/typings";
import {
AppLoggerUtils,
AppNotificationUtils,
ErrorUtils,
} from "./Shared/utils";
import appConfig from "./config";
const BASE_PATHNAME = config.basePathname;
interface Props {
readOnly: boolean;
redirect?: string;
characterId: number | null;
}
export const SheetBuilderApp: React.FC<Props> = ({
readOnly,
characterId,
redirect,
}) => {
const username = useUserName();
const user = useAuth();
const userRoles = useUserRoles();
const navigate = useNavigate();
const matchPremadePage = useMatch(`${BASE_PATHNAME}/premade/*`);
const matchBuilderWithOutCharacter = useMatch(`${BASE_PATHNAME}/builder/*`);
const matchBuilderWithCharacter = useMatch(
`${BASE_PATHNAME}/:characterId/builder/*`
);
const matchSheet = useMatch(`${BASE_PATHNAME}/:characterId/`);
const matchSheetWithShareId = useMatch(
`${BASE_PATHNAME}/:characterId/:shareId`
);
const currentPath = useLocation().pathname;
const [searchParams] = useSearchParams();
const isVttView = searchParams.get("view") === "vtt";
if (config === undefined) {
throw Error("Missing Config");
}
let appContext: Constants.AppContextTypeEnum | null = null;
switch (true) {
case !!(
matchBuilderWithOutCharacter ||
matchBuilderWithCharacter ||
matchPremadePage
):
appContext = Constants.AppContextTypeEnum.BUILDER;
break;
case !!(matchSheet || matchSheetWithShareId):
appContext = Constants.AppContextTypeEnum.SHEET;
break;
default:
throw new Error("Invalid app context");
}
const diceFeatureConfiguration: DiceFeatureConfigurationState = {
enabled: true,
menu: true,
assetBaseLocation: config.diceAssetEndpoint,
apiEndpoint: config.diceApiEndpoint,
trackingId: config.analyticTrackingId,
};
ErrorUtils.initReporting({
debug: appConfig.debug,
});
const store = useStore();
const initializedRef = React.useRef(false);
let parsedDiceEnabled: boolean = true;
if (!diceFeatureConfiguration.enabled) {
parsedDiceEnabled = false;
} else {
// if local storage is disabled it will just crash when trying to access, this prevents crashing.
try {
const diceEnabled = localStorage.getItem("dice-enabled");
// once dice are enabled set the default to on;
parsedDiceEnabled = diceEnabled === "true" || diceEnabled === null;
} catch (e) {}
}
let lastMessageTime = 0;
try {
const lsLastMessageTime = localStorage.getItem(
`gameLog-lastMessageTime-${characterId}`
);
if (lsLastMessageTime) {
lastMessageTime = parseInt(lsLastMessageTime);
}
} catch (e) {}
const gameLogSettings: GameLogState = {
isOpen: false,
lastMessageTime: lastMessageTime,
apiEndpoint: config.gameLogApiEndpoint,
ddbApiEndpoint: config.ddbApiEndpoint,
};
ConfigUtils.configureRulesEngine({
apiAdapter: generateApiAdapter(config.authEndpoint, readOnly),
apiInfo: config.apiInfo,
store,
onLogMessage: AppLoggerUtils.logMessage,
onLogError: AppLoggerUtils.logError,
onNotification: AppNotificationUtils.dispatchNotification,
includeCustomItems: true,
canUseCurrencyContainers: true,
});
// CharacterSheet.componentDidMount fires before parent useEffects, so characterId and
// appContext must be in the store synchronously before children mount. Without this,
// the character load saga reads a null characterId and the character fails to load.
// The useEffect below handles re-dispatching once username/user resolve asynchronously.
if (!initializedRef.current) {
initializedRef.current = true;
store.dispatch(
characterEnvActions.dataSet({
context: appContext,
})
);
store.dispatch(
appEnvActions.dataSet({
username,
userId: user?.id ? Number(user.id) : -1,
userRoles,
characterId,
authEndpoint: config.authEndpoint,
redirect,
diceEnabled: parsedDiceEnabled,
diceFeatureConfiguration: diceFeatureConfiguration,
gameLog: gameLogSettings,
})
);
}
useEffect(() => {
store.dispatch({
type: "SET_ROUTE_CONTROLS",
payload: { navigate, currentPath },
});
}, [navigate, currentPath]);
useEffect(() => {
store.dispatch(
appEnvActions.dataSet({
username,
userId: user?.id ? Number(user.id) : -1,
userRoles,
characterId,
authEndpoint: config.authEndpoint,
redirect,
diceEnabled: parsedDiceEnabled,
diceFeatureConfiguration: diceFeatureConfiguration,
gameLog: gameLogSettings,
})
);
}, [username, user]);
// Setup axe-core in development, not in production
if (process.env.NODE_ENV !== "production") {
// axe(React, ReactDOM, 10000);
}
Modal.setAppElement("#character-tools-target");
// If the view is the VTT view, set --top-navigation-height to 0 because the top nav will not be present.
useEffect(() => {
if (isVttView) {
document.body.style.setProperty("--top-navigation-height", "0px");
}
}, []);
return (
<Managers>
<CharacterThemeProvider>
<Content />
</CharacterThemeProvider>
</Managers>
);
};
const Content = () => {
const matchBuilderWithOutCharacter = useMatch(`${BASE_PATHNAME}/builder/*`);
const matchBuilderWithCharacter = useMatch(
`${BASE_PATHNAME}/:characterId/builder/*`
);
const matchPremadePage = useMatch(`${BASE_PATHNAME}/premade/*`);
return (
<SidebarProvider>
{matchBuilderWithOutCharacter ||
matchBuilderWithCharacter ||
matchPremadePage ? (
<CharacterBuilderContainer isPremadeListing={!!matchPremadePage} />
) : (
<CharacterSheetContainer authEndpoint={config.authEndpoint} />
)}
</SidebarProvider>
);
};