237 lines
6.7 KiB
TypeScript
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>
|
|
);
|
|
};
|