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 = ({ 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 ( ); }; const Content = () => { const matchBuilderWithOutCharacter = useMatch(`${BASE_PATHNAME}/builder/*`); const matchBuilderWithCharacter = useMatch( `${BASE_PATHNAME}/:characterId/builder/*` ); const matchPremadePage = useMatch(`${BASE_PATHNAME}/premade/*`); return ( {matchBuilderWithOutCharacter || matchBuilderWithCharacter || matchPremadePage ? ( ) : ( )} ); };