Grabbed dndbeyond's source code

```
~/go/bin/sourcemapper -output ddb -jsurl https://media.dndbeyond.com/character-app/static/js/main.90aa78c5.js
```
This commit is contained in:
2025-05-28 11:50:03 -07:00
commit 8df9031d27
3592 changed files with 319051 additions and 0 deletions
@@ -0,0 +1,247 @@
import { ComponentType, FC, HTMLAttributes, ReactNode } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import {
LightLinkOutSvg,
ThemedBackdropSvg,
ThemedBuilderSvg,
ThemedExportSvg,
ThemedFrameSvg,
ThemedLongRestSvg,
ThemedManageLevelSvg,
ThemedManageXpSvg,
ThemedPortraitSvg,
ThemedPreferencesSvg,
ThemedShareSvg,
ThemedShortRestSvg,
ThemedThemeIconSvg,
ThemedChatBubbleSvg,
} from "@dndbeyond/character-components/es";
import D6 from "@dndbeyond/fontawesome-cache/svgs/regular/dice-d6.svg";
import { Button } from "~/components/Button";
import config from "~/config";
import {
PreferencePrivacyTypeEnum,
PreferenceProgressionTypeEnum,
} from "~/constants";
import { useSidebar } from "~/contexts/Sidebar";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import { sheetAppSelectors } from "~/tools/js/CharacterSheet/selectors";
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
import { CharacterStatusSlug } from "~/types";
import {
PaneMenu,
PaneMenuGroup,
PaneMenuItem,
} from "../../components/PaneMenu";
import { PaneComponentEnum } from "../../types";
import { Overview } from "./Overview";
import styles from "./styles.module.css";
const BASE_PATHNAME = config.basePathname;
interface CharacterManagePaneProps extends HTMLAttributes<HTMLDivElement> {}
export const CharacterManagePane: FC<CharacterManagePaneProps> = ({
...props
}) => {
const navigate = useNavigate();
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
const builderUrl = useSelector(sheetAppSelectors.getBuilderUrl);
const {
preferences,
decorationInfo,
helperUtils,
decorationUtils,
characterStatusSlug: characterStatus,
characterTheme,
} = useCharacterEngine();
const {
pane: { paneHistoryPush },
} = useSidebar();
const handleXpMenuClick = (): void => {
paneHistoryPush(PaneComponentEnum.XP);
};
const handleGameLogMenuClick = (): void => {
paneHistoryPush(PaneComponentEnum.GAME_LOG);
};
const handleShortRestMenuClick = (): void => {
paneHistoryPush(PaneComponentEnum.SHORT_REST);
};
const handleLongRestMenuClick = (): void => {
paneHistoryPush(PaneComponentEnum.LONG_REST);
};
const handlePreferencesMenuClick = (): void => {
paneHistoryPush(PaneComponentEnum.PREFERENCES);
};
const handleShareMenuClick = (): void => {
paneHistoryPush(PaneComponentEnum.SHARE_URL);
};
const handlePdfClick = (): void => {
paneHistoryPush(PaneComponentEnum.EXPORT_PDF);
};
const handleBuilderClick = (): void => {
navigate(builderUrl);
};
//USE A LOOKUP TO GET EACH MENU ICON
const getMenuIconNode = (menuKey: string): ReactNode => {
const menuIconLookup: Record<string, ComponentType> = {
builder: ThemedBuilderSvg,
shortrest: ThemedShortRestSvg,
longrest: ThemedLongRestSvg,
share: ThemedShareSvg,
managelevel: ThemedManageLevelSvg,
managexp: ThemedManageXpSvg,
gamelog: ThemedChatBubbleSvg,
frame: ThemedFrameSvg,
backdrop: ThemedBackdropSvg,
theme: ThemedThemeIconSvg,
portrait: ThemedPortraitSvg,
preferences: ThemedPreferencesSvg,
downloadpdf: ThemedExportSvg,
};
const IconNode: ComponentType<any> | null =
menuIconLookup[menuKey] &&
helperUtils.lookupDataOrFallback(menuIconLookup, menuKey);
if (IconNode === null) {
return null;
}
if (menuKey === "gamelog") {
return (
<D6
className="ddbc-svg ddbc-svg--themed"
style={{ fill: characterTheme.themeColor }}
/>
);
}
return (
<IconNode theme={decorationUtils.getCharacterTheme(decorationInfo)} />
);
};
return (
<div
className={styles.characterManagePane}
data-testid="character-manage-pane"
{...props}
>
<Overview />
{/* Display Readonly CTA or MENU */}
{isReadonly ? (
<div className={styles.readonlyBackground}>
<div className={styles.readonlyContent}>
<div>
You are viewing a{" "}
{characterStatus === CharacterStatusSlug.PREMADE
? "premade"
: "read-only"}{" "}
Character Sheet.
</div>
<Button
className={styles.createCTA}
size="medium"
variant="solid"
target="_blank"
themed
href={`${BASE_PATHNAME}/builder`}
forceThemeMode="dark"
>
<span>Create a Character</span>
<LightLinkOutSvg />
</Button>
</div>
</div>
) : (
<PaneMenu>
<PaneMenuGroup label="My Character">
<PaneMenuItem
menukey="builder"
prefixIcon={getMenuIconNode("builder")}
suffixIcon={<LightLinkOutSvg />}
onClick={handleBuilderClick}
>
Manage Character & Levels
</PaneMenuItem>
{preferences.progressionType ===
PreferenceProgressionTypeEnum.XP && (
<PaneMenuItem
menukey="managexp"
prefixIcon={getMenuIconNode("managexp")}
onClick={handleXpMenuClick}
>
Manage Experience
</PaneMenuItem>
)}
<PaneMenuItem
menukey="preferences"
prefixIcon={getMenuIconNode("preferences")}
onClick={handlePreferencesMenuClick}
>
Character Settings
</PaneMenuItem>
</PaneMenuGroup>
<PaneMenuGroup label="Play">
<PaneMenuItem
menukey="gamelog"
prefixIcon={getMenuIconNode("gamelog")}
onClick={handleGameLogMenuClick}
>
Game Log
</PaneMenuItem>
<PaneMenuItem
menukey="shortrest"
prefixIcon={getMenuIconNode("shortrest")}
onClick={handleShortRestMenuClick}
>
Short Rest
</PaneMenuItem>
<PaneMenuItem
menukey="longrest"
prefixIcon={getMenuIconNode("longrest")}
onClick={handleLongRestMenuClick}
>
Long Rest
</PaneMenuItem>
</PaneMenuGroup>
<PaneMenuGroup label="Share">
{preferences.privacyType === PreferencePrivacyTypeEnum.PUBLIC && (
<PaneMenuItem
menukey="share"
prefixIcon={getMenuIconNode("share")}
onClick={handleShareMenuClick}
>
Shareable Link
</PaneMenuItem>
)}
<PaneMenuItem
menukey="downloadpdf"
prefixIcon={getMenuIconNode("downloadpdf")}
onClick={handlePdfClick}
>
Export to PDF
</PaneMenuItem>
</PaneMenuGroup>
</PaneMenu>
)}
</div>
);
};
@@ -0,0 +1,280 @@
import clsx from "clsx";
import {
ChangeEvent,
FC,
HTMLAttributes,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useDispatch, useSelector } from "react-redux";
import {
CharacterAvatarPortrait,
CharacterName,
CharacterProgressionSummary,
CharacterSummary,
LightPencilSvg,
ThemedPaintBrushSvg,
} from "@dndbeyond/character-components/es";
import { Button } from "~/components/Button/Button";
import {
CharacterNameLimitMsg,
DeathCauseEnum,
InputLimits,
} from "~/constants";
import { useCharacterTheme } from "~/contexts/CharacterTheme";
import { useSidebar } from "~/contexts/Sidebar";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import { useMaxLengthErrorHandling } from "~/hooks/useErrorHandling/useMaxLengthErrorHandling";
import useUserId from "~/hooks/useUserId";
import { appEnvActions } from "~/tools/js/Shared/actions";
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
import { CharacterStatusSlug } from "~/types";
import { Popover } from "../../../../../../../components/Popover";
import { PopoverContent } from "../../../../../../../components/PopoverContent";
import { PaneComponentEnum } from "../../../types";
import styles from "./styles.module.css";
export interface OverviewProps extends HTMLAttributes<HTMLDivElement> {}
export const Overview: FC<OverviewProps> = ({ className, ...props }) => {
const {
ruleData,
characterName,
characterGender: gender,
race: species,
experienceInfo: xpInfo,
deathCause,
preferences,
decorationInfo,
playerName,
playerId,
characterActions,
decorationUtils,
classUtils,
classes,
characterStatusSlug: characterStatus,
} = useCharacterEngine();
const dispatch = useDispatch();
const { isDarkMode } = useCharacterTheme();
const userId = useUserId();
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
const canOverrideReadOnly = useSelector(
appEnvSelectors.getCanOverrideReadOnly
);
const {
pane: { paneHistoryPush },
} = useSidebar();
const { MaxLengthErrorMessage, hideError, handleMaxLengthErrorMsg } =
useMaxLengthErrorHandling(
(characterName?.length ?? 0) >= InputLimits.characterNameMaxLength,
InputLimits.characterNameMaxLength,
CharacterNameLimitMsg
);
const isUserCharacter = playerId === Number(userId);
const isPremade = characterStatus === CharacterStatusSlug.PREMADE;
const [isNameEditorVisible, setIsNameEditorVisible] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isNameEditorVisible && inputRef.current) {
inputRef.current.focus();
}
}, [isNameEditorVisible]);
const handleNameClick = (): void => {
setIsNameEditorVisible(true);
};
const handleNameChange = (evt: ChangeEvent<HTMLInputElement>): void => {
const inputValue = evt.target.value;
if (inputValue !== characterName && inputValue !== "") {
dispatch(characterActions.nameSet(inputValue));
}
setIsNameEditorVisible(false);
hideError();
};
const handleToggleEdit = (status: boolean): void => {
dispatch(appEnvActions.dataSet({ isReadonly: status }));
};
const handleNameUpdate = (evt: ChangeEvent<HTMLInputElement>): void => {
handleMaxLengthErrorMsg(evt.target.value);
};
const handleDecorateMenuClick = (): void => {
paneHistoryPush(PaneComponentEnum.DECORATE);
};
const isEditingCharacterName = useMemo(
() => isNameEditorVisible && !isReadonly,
[isNameEditorVisible, isReadonly]
);
return (
<div
className={clsx([styles.intro, isDarkMode && styles.dark, className])}
{...props}
>
<div className={styles.summary}>
<CharacterAvatarPortrait
className={styles.avatar}
avatarUrl={decorationUtils.getAvatarInfo(decorationInfo).avatarUrl}
/>
{
<div
className={clsx([
styles.characterName,
isEditingCharacterName && styles.editName,
])}
onClick={isReadonly ? undefined : handleNameClick}
>
{isEditingCharacterName ? (
<>
<input
ref={inputRef}
type="text"
defaultValue={characterName ?? ""}
maxLength={InputLimits.characterNameMaxLength}
onBlur={handleNameChange}
onChange={handleNameUpdate}
onFocus={handleNameUpdate}
/>
<MaxLengthErrorMessage />
</>
) : (
<>
<CharacterName
name={characterName ?? ""}
isDead={deathCause !== DeathCauseEnum.NONE}
isFaceMenu={true}
/>
{!isReadonly && <LightPencilSvg />}
</>
)}
</div>
}
{/* DISPLAY PLAYER NAME IF ITS NOT A PREMADE OR YOUR CHARACTER */}
{!isUserCharacter && !isPremade && (
<div>
<span className={styles.playerNameLabel}>Player: </span>
{playerName}
</div>
)}
{/* SHOW SPECIES, GENDER AND XP/LEVEL INFO */}
<div className={styles.description}>
<CharacterSummary
className={styles.summaryText}
classes={null}
species={species}
gender={gender}
/>
<CharacterProgressionSummary
ruleData={ruleData}
progressionType={preferences.progressionType}
xpInfo={xpInfo}
/>
</div>
</div>
{/* DISPLAY CLASS LIST */}
<div className={styles.classList}>
{classes.map((charClass) => (
<div className={styles.classItem} key={classUtils.getId(charClass)}>
<div
className={styles.classImage}
style={{
backgroundImage: `url(${classUtils.getPortraitUrl(charClass)})`,
}}
>
<div className={styles.classLevel}>
{classUtils.getLevel(charClass)}
</div>
</div>
<div className={styles.classSummary}>
<div>{classUtils.getName(charClass)}</div>
<div className={styles.classMeta}>
{classUtils.getSubclassName(charClass) ?? "No Subclass"}
</div>
</div>
</div>
))}
</div>
<div className={styles.buttonGroup}>
{/* This is only for PREMADE characters */}
{canOverrideReadOnly && (
<div className={styles.featureCallout}>
{isReadonly ? (
<Popover
trigger={
<Button
size="x-small"
variant="solid"
className={clsx([styles.toggleButton])}
forceThemeMode="dark"
>
<LightPencilSvg />
<span>Enable Edit Premade</span>
</Button>
}
maxWidth={284}
className={styles.popoverToggle}
>
<PopoverContent
title="PROCEED WITH CAUTION!!!"
content="⚠️ If you enable editing you will be modifying a published character ⚠️"
confirmText="Enable"
onConfirm={() => handleToggleEdit(false)}
withCancel
/>
</Popover>
) : (
<Button
size="x-small"
variant="solid"
className={clsx([styles.toggleButton])}
forceThemeMode="dark"
onClick={() => handleToggleEdit(true)}
>
<LightPencilSvg />
<span>DISABLE Edit Premade</span>
</Button>
)}
</div>
)}
{/* DISPLAY DECORATE BUTTON AS A "FEATURE CALLOUT" */}
{!isReadonly && (
<div className={styles.featureCallout}>
<Button
size="x-small"
variant="outline"
className={styles.decorateButton}
forceThemeMode="dark"
themed
onClick={handleDecorateMenuClick}
>
<ThemedPaintBrushSvg
theme={decorationUtils.getCharacterTheme(decorationInfo)}
/>
<span>Change Sheet Appearance</span>
</Button>
</div>
)}
</div>
</div>
);
};