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:
+247
@@ -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>
|
||||
);
|
||||
};
|
||||
+280
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user