New source found from dndbeyond.com
This commit is contained in:
+157
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
autoUpdate,
|
||||
flip,
|
||||
FloatingFocusManager,
|
||||
offset,
|
||||
Placement,
|
||||
shift,
|
||||
useClick,
|
||||
useDismiss,
|
||||
useFloating,
|
||||
useInteractions,
|
||||
useRole,
|
||||
} from "@floating-ui/react";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
FC,
|
||||
Fragment,
|
||||
HTMLAttributes,
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { GroupedMenuOption } from "@dndbeyond/character-rules-engine";
|
||||
import ChevronDown from "@dndbeyond/fontawesome-cache/svgs/solid/chevron-down.svg";
|
||||
|
||||
import { Button, ButtonProps } from "~/components/Button";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
/**
|
||||
* A button that displays a menu of options when clicked. If there is only one option, it will
|
||||
* render as a simple button that triggers the onSelect callback when clicked unless the
|
||||
* `showSingleOption` prop is set to true, in which case it will still show the dropdown with only
|
||||
* one option.
|
||||
*/
|
||||
|
||||
export interface ButtonWithMenuProps
|
||||
extends PropsWithChildren,
|
||||
Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
|
||||
placement?: Placement;
|
||||
groupedOptions: Array<GroupedMenuOption>;
|
||||
showSingleOption?: boolean;
|
||||
onSelect: (containerDefinitionKey: string) => void;
|
||||
variant?: ButtonProps["variant"];
|
||||
}
|
||||
|
||||
export const ButtonWithMenu: FC<ButtonWithMenuProps> = ({
|
||||
className,
|
||||
children,
|
||||
groupedOptions,
|
||||
showSingleOption,
|
||||
placement,
|
||||
onSelect,
|
||||
variant,
|
||||
...props
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// Set up floating element and trigger
|
||||
const { refs, floatingStyles, context } = useFloating({
|
||||
placement,
|
||||
strategy: "fixed",
|
||||
open: isOpen,
|
||||
onOpenChange: setIsOpen,
|
||||
middleware: [offset(), flip(), shift()],
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
// Handle interactions for the floating element
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([
|
||||
useClick(context),
|
||||
useDismiss(context),
|
||||
useRole(context),
|
||||
]);
|
||||
// Determine whether we should show the dropdown
|
||||
const hasSingleOption =
|
||||
groupedOptions.length === 1 &&
|
||||
groupedOptions[0].options?.length === 1 &&
|
||||
!showSingleOption;
|
||||
|
||||
const handleClickTrigger = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (hasSingleOption) {
|
||||
onSelect(groupedOptions[0].options[0]?.value);
|
||||
} else {
|
||||
setIsOpen((prev) => !prev);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickOption = (e, value: string) => {
|
||||
e.stopPropagation();
|
||||
onSelect(value);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const getTestId = (value: ReactNode) => {
|
||||
const content = value?.toString() ?? "Move";
|
||||
return `theme-button-with-menu-${content}`
|
||||
.toLowerCase()
|
||||
.replace(/\s/g, "-");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx([styles.buttonWithMenu, className])} {...props}>
|
||||
<span
|
||||
className={styles.trigger}
|
||||
ref={refs.setReference}
|
||||
{...getReferenceProps()}
|
||||
>
|
||||
<Button
|
||||
size="xx-small"
|
||||
variant={variant}
|
||||
themed
|
||||
data-testid={getTestId(children)}
|
||||
onClick={handleClickTrigger}
|
||||
>
|
||||
{children}
|
||||
{!hasSingleOption && <ChevronDown className={styles.buttonIcon} />}
|
||||
</Button>
|
||||
</span>
|
||||
{!hasSingleOption && isOpen && (
|
||||
<FloatingFocusManager context={context} modal={false}>
|
||||
<menu
|
||||
className={styles.menu}
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
{groupedOptions.map((option) => (
|
||||
<Fragment key={option.label}>
|
||||
<li
|
||||
className={styles.menuHeading}
|
||||
data-testid={getTestId(`subheader-${option.label}`)}
|
||||
>
|
||||
{option.label}
|
||||
</li>
|
||||
<ul>
|
||||
{option.options?.map((item) => (
|
||||
<li key={item.value}>
|
||||
<button
|
||||
className={styles.menuButton}
|
||||
onClick={(e) => handleClickOption(e, item.value)}
|
||||
data-testid={getTestId(`item-${item.label}`)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Fragment>
|
||||
))}
|
||||
</menu>
|
||||
</FloatingFocusManager>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -41,6 +41,7 @@ export const Pane: FC<PaneProps> = ({
|
||||
forceDarkMode && styles.dark,
|
||||
isFullWidth && styles.fullWidth,
|
||||
])}
|
||||
data-scrollable-container="true"
|
||||
>
|
||||
<PaneControls handlePrevious={handlePrevious} handleNext={handleNext} />
|
||||
<PaneContent activePane={activePane} />
|
||||
|
||||
@@ -28,7 +28,7 @@ export const PaneContent: FC<PaneContentProps> = ({ activePane, ...props }) => {
|
||||
|
||||
// Return correct component
|
||||
return (
|
||||
<div className={styles.maxHeight} {...props}>
|
||||
<div className={styles.wrapper} {...props}>
|
||||
<Component
|
||||
theme={isDarkMode ? "DARK" : "LIGHT"}
|
||||
identifiers={activePane.identifiers}
|
||||
|
||||
+15
-5
@@ -1,28 +1,38 @@
|
||||
import { FC, useEffect } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { ToastMessageMeta } from "~/tools/js/Shared/stores/typings";
|
||||
|
||||
import { toastMessageActions } from "../../../../../../tools/js/Shared/actions";
|
||||
|
||||
interface Props {
|
||||
errorTitle?: string;
|
||||
errorMessage?: string;
|
||||
toastMeta?: ToastMessageMeta;
|
||||
}
|
||||
export const PaneInitFailureContent: FC<Props> = ({ errorMessage }) => {
|
||||
export const PaneInitFailureContent: FC<Props> = ({
|
||||
errorTitle,
|
||||
errorMessage,
|
||||
toastMeta,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
sidebar: { setIsVisible },
|
||||
pane: { resetActivePane },
|
||||
} = useSidebar();
|
||||
|
||||
useEffect(() => {
|
||||
resetActivePane();
|
||||
setIsVisible(false);
|
||||
dispatch(
|
||||
toastMessageActions.toastError(
|
||||
"Content Error",
|
||||
errorMessage ?? "We couldn't find that content! Please try again."
|
||||
errorTitle ?? "Content Error",
|
||||
errorMessage ?? "We couldn't find that content! Please try again.",
|
||||
toastMeta
|
||||
)
|
||||
);
|
||||
}, [errorMessage, dispatch]);
|
||||
}, [errorMessage, errorTitle, toastMeta]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { CreaturePane } from "~/subApps/sheet/components/Sidebar/panes/CreaturePane";
|
||||
import { GameLogPane } from "~/subApps/sheet/components/Sidebar/panes/GameLogPane";
|
||||
import { ItemPane } from "~/subApps/sheet/components/Sidebar/panes/ItemPane";
|
||||
import { ItemPlansManagePane } from "~/subApps/sheet/components/Sidebar/panes/ItemPlansManagePane";
|
||||
import { LongRestPane } from "~/subApps/sheet/components/Sidebar/panes/LongRestPane";
|
||||
import { SpeciesTraitPane } from "~/subApps/sheet/components/Sidebar/panes/SpeciesTraitPane";
|
||||
import BlessingPane from "~/tools/js/Shared/containers/panes/BlessingPane";
|
||||
import CharacterSpellPane from "~/tools/js/Shared/containers/panes/CharacterSpellPane";
|
||||
import ClassSpellPane from "~/tools/js/Shared/containers/panes/ClassSpellPane";
|
||||
import ConditionManagePane from "~/tools/js/Shared/containers/panes/ConditionManagePane";
|
||||
@@ -18,8 +20,6 @@ import EquipmentManagePane from "~/tools/js/Shared/containers/panes/EquipmentMan
|
||||
import { ExportPdfPane } from "~/tools/js/Shared/containers/panes/ExportPdfPane";
|
||||
import ExtraManagePane from "~/tools/js/Shared/containers/panes/ExtraManagePane";
|
||||
import InfusionChoicePane from "~/tools/js/Shared/containers/panes/InfusionChoicePane";
|
||||
import ItemPane from "~/tools/js/Shared/containers/panes/ItemPane";
|
||||
import LongRestPane from "~/tools/js/Shared/containers/panes/LongRestPane";
|
||||
import NoteManagePane from "~/tools/js/Shared/containers/panes/NoteManagePane";
|
||||
import { PreferencesHitPointConfirmPane } from "~/tools/js/Shared/containers/panes/PreferencesHitPointConfirmPane";
|
||||
import PreferencesOptionalClassFeaturesConfirmPane from "~/tools/js/Shared/containers/panes/PreferencesOptionalClassFeaturesConfirmPane/PreferencesOptionalClassFeaturesConfirmPane";
|
||||
@@ -66,6 +66,9 @@ export const getActiveEntryComponent = (
|
||||
if (!componentType) return null;
|
||||
|
||||
switch (componentType) {
|
||||
case PaneComponentEnum.ITEM_PLANS_MANAGE:
|
||||
return ItemPlansManagePane;
|
||||
|
||||
case PaneComponentEnum.HEALTH_MANAGE:
|
||||
return HitPointsManagePane;
|
||||
|
||||
@@ -237,8 +240,7 @@ export const getActiveEntryComponent = (
|
||||
case PaneComponentEnum.SETTINGS:
|
||||
return SettingsPane;
|
||||
|
||||
case PaneComponentEnum.BLESSING_DETAIL:
|
||||
default:
|
||||
return BlessingPane;
|
||||
return CharacterManagePane;
|
||||
}
|
||||
};
|
||||
|
||||
+5
-8
@@ -1,9 +1,8 @@
|
||||
import { ComponentType, FC, HTMLAttributes, ReactNode } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
AbilityIcon,
|
||||
Collapsible,
|
||||
DarkModeNegativeBonusNegativeSvg,
|
||||
DarkModePositiveBonusPositiveSvg,
|
||||
NegativeBonusNegativeSvg,
|
||||
@@ -18,12 +17,14 @@ import {
|
||||
ValueUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { Accordion } from "~/components/Accordion";
|
||||
import { HtmlContent } from "~/components/HtmlContent";
|
||||
import { NumberDisplay } from "~/components/NumberDisplay";
|
||||
import { RuleKeyEnum } from "~/constants";
|
||||
import { useCharacterTheme } from "~/contexts/CharacterTheme";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { useRuleData } from "~/hooks/useRuleData";
|
||||
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
||||
import { Preview } from "~/subApps/sheet/components/Sidebar/components/Preview";
|
||||
@@ -126,11 +127,7 @@ export const AbilitySavingThrowsPane: FC<Props> = ({
|
||||
</span>
|
||||
</Header>
|
||||
{!isReadonly && (
|
||||
<Collapsible
|
||||
layoutType={"minimal"}
|
||||
header="Customize"
|
||||
className={styles.customize}
|
||||
>
|
||||
<Accordion variant="minimal" size="x-small" summary="Customize" themed>
|
||||
<EditorBox className={styles.editorBox}>
|
||||
<ValueEditor
|
||||
dataLookup={ValueUtils.getEntityData(
|
||||
@@ -148,7 +145,7 @@ export const AbilitySavingThrowsPane: FC<Props> = ({
|
||||
ruleData={ruleData}
|
||||
/>
|
||||
</EditorBox>
|
||||
</Collapsible>
|
||||
</Accordion>
|
||||
)}
|
||||
{situationalBonusSavingThrowsInfo && (
|
||||
<div className={styles.situational}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC, HTMLAttributes, ReactNode } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { ActionName } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
@@ -16,6 +16,7 @@ import { ItemName } from "~/components/ItemName";
|
||||
import { DataOriginTypeEnum } from "~/constants";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
||||
import { BaseInventoryContract } from "~/types";
|
||||
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ export const ArmorClassDetail: FC<ArmorClassDetailProps> = ({
|
||||
amountDisplay = <NumberDisplay type="signed" number={amount} />;
|
||||
}
|
||||
|
||||
let extraDisplay: ReactNode;
|
||||
let extraDisplay;
|
||||
switch (type) {
|
||||
case ArmorClassTypeEnum.DEX_BONUS:
|
||||
extraDisplay = extra === null ? "" : `Max ${extra}`;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { FC, HTMLAttributes } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { Collapsible } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
characterActions,
|
||||
ValueUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { Accordion } from "~/components/Accordion";
|
||||
import { HtmlContent } from "~/components/HtmlContent";
|
||||
import { AdjustmentTypeEnum, RuleKeyEnum } from "~/constants";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { useRuleData } from "~/hooks/useRuleData";
|
||||
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
||||
import EditorBox from "~/tools/js/Shared/components/EditorBox";
|
||||
@@ -75,10 +76,12 @@ export const ArmorManagePane: FC<ArmorManagePaneProps> = ({ ...props }) => {
|
||||
<ArmorClassDetail />
|
||||
{/* AC CUSTOMIZATION */}
|
||||
{!isReadonly && (
|
||||
<Collapsible
|
||||
layoutType={"minimal"}
|
||||
header="Customize"
|
||||
<Accordion
|
||||
className={styles.customize}
|
||||
summary="Customize"
|
||||
variant="minimal"
|
||||
size="x-small"
|
||||
themed
|
||||
>
|
||||
<EditorBox className={styles.editorBox}>
|
||||
<ValueEditor
|
||||
@@ -91,7 +94,7 @@ export const ArmorManagePane: FC<ArmorManagePaneProps> = ({ ...props }) => {
|
||||
ruleData={ruleData}
|
||||
/>
|
||||
</EditorBox>
|
||||
</Collapsible>
|
||||
</Accordion>
|
||||
)}
|
||||
{/* AC Description */}
|
||||
{acRule && (
|
||||
|
||||
+81
-70
@@ -1,12 +1,10 @@
|
||||
import { ComponentType, FC, HTMLAttributes, ReactNode } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import {
|
||||
LightLinkOutSvg,
|
||||
ThemedBackdropSvg,
|
||||
ThemedBuilderSvg,
|
||||
ThemedExportSvg,
|
||||
ThemedFrameSvg,
|
||||
ThemedLongRestSvg,
|
||||
ThemedManageLevelSvg,
|
||||
@@ -16,9 +14,10 @@ import {
|
||||
ThemedShareSvg,
|
||||
ThemedShortRestSvg,
|
||||
ThemedThemeIconSvg,
|
||||
ThemedChatBubbleSvg,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
import D6 from "@dndbeyond/fontawesome-cache/svgs/regular/dice-d6.svg";
|
||||
import PdfSvg from "@dndbeyond/fontawesome-cache/svgs/light/file-pdf.svg";
|
||||
import ListTimeline from "@dndbeyond/fontawesome-cache/svgs/light/list-timeline.svg";
|
||||
import LinkIcon from "@dndbeyond/fontawesome-cache/svgs/solid/arrow-up-right-from-square.svg";
|
||||
|
||||
import { Button } from "~/components/Button";
|
||||
import config from "~/config";
|
||||
@@ -49,6 +48,8 @@ export const CharacterManagePane: FC<CharacterManagePaneProps> = ({
|
||||
...props
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { search } = useLocation();
|
||||
const isVttView = new URLSearchParams(search).get("view") === "vtt";
|
||||
|
||||
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
|
||||
const builderUrl = useSelector(sheetAppSelectors.getBuilderUrl);
|
||||
@@ -107,15 +108,23 @@ export const CharacterManagePane: FC<CharacterManagePaneProps> = ({
|
||||
share: ThemedShareSvg,
|
||||
managelevel: ThemedManageLevelSvg,
|
||||
managexp: ThemedManageXpSvg,
|
||||
gamelog: ThemedChatBubbleSvg,
|
||||
frame: ThemedFrameSvg,
|
||||
backdrop: ThemedBackdropSvg,
|
||||
theme: ThemedThemeIconSvg,
|
||||
portrait: ThemedPortraitSvg,
|
||||
preferences: ThemedPreferencesSvg,
|
||||
downloadpdf: ThemedExportSvg,
|
||||
downloadpdf: PdfSvg,
|
||||
};
|
||||
|
||||
if (menuKey === "gamelog") {
|
||||
return (
|
||||
<ListTimeline
|
||||
className="ddbc-svg ddbc-svg--themed"
|
||||
style={{ fill: characterTheme.themeColor }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const IconNode: ComponentType<any> | null =
|
||||
menuIconLookup[menuKey] &&
|
||||
helperUtils.lookupDataOrFallback(menuIconLookup, menuKey);
|
||||
@@ -124,17 +133,12 @@ export const CharacterManagePane: FC<CharacterManagePaneProps> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
if (menuKey === "gamelog") {
|
||||
return (
|
||||
<D6
|
||||
className="ddbc-svg ddbc-svg--themed"
|
||||
style={{ fill: characterTheme.themeColor }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<IconNode theme={decorationUtils.getCharacterTheme(decorationInfo)} />
|
||||
<IconNode
|
||||
className="ddbc-svg ddbc-svg--themed"
|
||||
style={{ fill: characterTheme.themeColor }}
|
||||
theme={decorationUtils.getCharacterTheme(decorationInfo)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -144,7 +148,7 @@ export const CharacterManagePane: FC<CharacterManagePaneProps> = ({
|
||||
data-testid="character-manage-pane"
|
||||
{...props}
|
||||
>
|
||||
<Overview />
|
||||
<Overview isVttView={isVttView} />
|
||||
{/* Display Readonly CTA or MENU */}
|
||||
{isReadonly ? (
|
||||
<div className={styles.readonlyBackground}>
|
||||
@@ -162,25 +166,27 @@ export const CharacterManagePane: FC<CharacterManagePaneProps> = ({
|
||||
variant="solid"
|
||||
target="_blank"
|
||||
themed
|
||||
href={`${BASE_PATHNAME}/builder`}
|
||||
href={`${BASE_PATHNAME}/builder${search}`}
|
||||
forceThemeMode="dark"
|
||||
>
|
||||
<span>Create a Character</span>
|
||||
<LightLinkOutSvg />
|
||||
<LinkIcon className={styles.linkIcon} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PaneMenu>
|
||||
<PaneMenuGroup label="My Character">
|
||||
<PaneMenuItem
|
||||
menukey="builder"
|
||||
prefixIcon={getMenuIconNode("builder")}
|
||||
suffixIcon={<LightLinkOutSvg />}
|
||||
onClick={handleBuilderClick}
|
||||
>
|
||||
Manage Character & Levels
|
||||
</PaneMenuItem>
|
||||
{!isVttView && (
|
||||
<PaneMenuItem
|
||||
menukey="builder"
|
||||
prefixIcon={getMenuIconNode("builder")}
|
||||
suffixIcon={<LinkIcon className={styles.linkIcon} />}
|
||||
onClick={handleBuilderClick}
|
||||
>
|
||||
Manage Character & Levels
|
||||
</PaneMenuItem>
|
||||
)}
|
||||
{preferences.progressionType ===
|
||||
PreferenceProgressionTypeEnum.XP && (
|
||||
<PaneMenuItem
|
||||
@@ -199,47 +205,52 @@ export const CharacterManagePane: FC<CharacterManagePaneProps> = ({
|
||||
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>
|
||||
{!isVttView && (
|
||||
<>
|
||||
<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>
|
||||
|
||||
+18
-18
@@ -8,11 +8,10 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
CharacterAvatarPortrait,
|
||||
CharacterName,
|
||||
CharacterProgressionSummary,
|
||||
CharacterSummary,
|
||||
LightPencilSvg,
|
||||
@@ -20,16 +19,14 @@ import {
|
||||
} from "@dndbeyond/character-components/es";
|
||||
|
||||
import { Button } from "~/components/Button/Button";
|
||||
import {
|
||||
CharacterNameLimitMsg,
|
||||
DeathCauseEnum,
|
||||
InputLimits,
|
||||
} from "~/constants";
|
||||
import { CharacterNameLimitMsg, InputLimits } from "~/constants";
|
||||
import { useCharacterTheme } from "~/contexts/CharacterTheme";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { useMaxLengthErrorHandling } from "~/hooks/useErrorHandling/useMaxLengthErrorHandling";
|
||||
import useUserId from "~/hooks/useUserId";
|
||||
import { CharacterName } from "~/subApps/sheet/components/CharacterName";
|
||||
import { appEnvActions } from "~/tools/js/Shared/actions";
|
||||
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||
import { CharacterStatusSlug } from "~/types";
|
||||
@@ -39,16 +36,21 @@ import { PopoverContent } from "../../../../../../../components/PopoverContent";
|
||||
import { PaneComponentEnum } from "../../../types";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
export interface OverviewProps extends HTMLAttributes<HTMLDivElement> {}
|
||||
export interface OverviewProps extends HTMLAttributes<HTMLDivElement> {
|
||||
isVttView?: boolean;
|
||||
}
|
||||
|
||||
export const Overview: FC<OverviewProps> = ({ className, ...props }) => {
|
||||
export const Overview: FC<OverviewProps> = ({
|
||||
isVttView = false,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const {
|
||||
ruleData,
|
||||
characterName,
|
||||
characterGender: gender,
|
||||
race: species,
|
||||
experienceInfo: xpInfo,
|
||||
deathCause,
|
||||
preferences,
|
||||
decorationInfo,
|
||||
playerName,
|
||||
@@ -138,14 +140,16 @@ export const Overview: FC<OverviewProps> = ({ className, ...props }) => {
|
||||
{
|
||||
<div
|
||||
className={clsx([
|
||||
styles.characterName,
|
||||
styles.characterNameContainer,
|
||||
!isReadonly && !isVttView && styles.editableName,
|
||||
isEditingCharacterName && styles.editName,
|
||||
])}
|
||||
onClick={isReadonly ? undefined : handleNameClick}
|
||||
onClick={isReadonly || isVttView ? undefined : handleNameClick}
|
||||
>
|
||||
{isEditingCharacterName ? (
|
||||
<>
|
||||
<input
|
||||
className={styles.nameInput}
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
defaultValue={characterName ?? ""}
|
||||
@@ -158,12 +162,8 @@ export const Overview: FC<OverviewProps> = ({ className, ...props }) => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CharacterName
|
||||
name={characterName ?? ""}
|
||||
isDead={deathCause !== DeathCauseEnum.NONE}
|
||||
isFaceMenu={true}
|
||||
/>
|
||||
{!isReadonly && <LightPencilSvg />}
|
||||
<CharacterName />
|
||||
{!isReadonly && !isVttView && <LightPencilSvg />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+10
-2
@@ -1,15 +1,17 @@
|
||||
import { FC, HTMLAttributes, useContext } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { FeatManager } from "@dndbeyond/character-rules-engine";
|
||||
|
||||
import { FeatureChoices } from "~/components/FeatureChoices";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
||||
import { Preview } from "~/subApps/sheet/components/Sidebar/components/Preview";
|
||||
import { ClassFeatureSnippet } from "~/tools/js/CharacterSheet/components/FeatureSnippet";
|
||||
import { CharacterFeaturesManagerContext } from "~/tools/js/Shared/managers/CharacterFeaturesManagerContext";
|
||||
import { InventoryManagerContext } from "~/tools/js/Shared/managers/InventoryManagerContext";
|
||||
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "~/tools/js/Shared/utils";
|
||||
import {
|
||||
@@ -49,6 +51,7 @@ export const ClassFeaturePane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
const { characterFeaturesManager } = useContext(
|
||||
CharacterFeaturesManagerContext
|
||||
);
|
||||
const { inventoryManager } = useContext(InventoryManagerContext);
|
||||
|
||||
const {
|
||||
pane: { paneHistoryPush },
|
||||
@@ -87,6 +90,7 @@ export const ClassFeaturePane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
if (!charClass || !classFeature) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
characterActions.classFeatureChoiceSetRequest(
|
||||
classUtils.getActiveId(charClass),
|
||||
@@ -94,7 +98,10 @@ export const ClassFeaturePane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
type,
|
||||
id,
|
||||
helperUtils.parseInputInt(value),
|
||||
parentChoiceId
|
||||
parentChoiceId,
|
||||
classFeatureUtils.getHasItemMappings(classFeature)
|
||||
? inventoryManager.handleAcceptOnSuccess(true)
|
||||
: undefined
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -215,6 +222,7 @@ export const ClassFeaturePane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
<FeatureChoices
|
||||
choices={classFeatureUtils.getChoices(classFeature)}
|
||||
charClass={charClass}
|
||||
classFeature={classFeature}
|
||||
onChoiceChange={handleChoiceChange}
|
||||
collapseDescription={true}
|
||||
showHeading={true}
|
||||
|
||||
@@ -7,14 +7,13 @@ import {
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleHeaderCallout,
|
||||
CollapsibleHeaderContent,
|
||||
CreatureName,
|
||||
InfusionPreview,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
characterActions,
|
||||
@@ -30,10 +29,12 @@ import {
|
||||
import { Button } from "~/components/Button";
|
||||
import { EditableName } from "~/components/EditableName";
|
||||
import { HtmlContent } from "~/components/HtmlContent";
|
||||
import { InfusionPreview } from "~/components/InfusionPreview";
|
||||
import { Reference } from "~/components/Reference";
|
||||
import { TagGroup } from "~/components/TagGroup";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { CreatureBlock } from "~/subApps/sheet/components/CreatureBlock";
|
||||
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
||||
import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading";
|
||||
@@ -67,9 +68,6 @@ export const CreaturePane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
ruleData,
|
||||
creatures,
|
||||
entityValueLookup,
|
||||
snippetData,
|
||||
infusionChoiceLookup,
|
||||
proficiencyBonus,
|
||||
characterTheme: theme,
|
||||
hpInfo,
|
||||
deathCause,
|
||||
@@ -498,14 +496,7 @@ export const CreaturePane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
if (infusion) {
|
||||
return (
|
||||
<div className={styles.hasSeparator}>
|
||||
<InfusionPreview
|
||||
infusion={infusion}
|
||||
snippetData={snippetData}
|
||||
ruleData={ruleData}
|
||||
infusionChoiceLookup={infusionChoiceLookup}
|
||||
onClick={handleInfusionClick}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
/>
|
||||
<InfusionPreview infusion={infusion} onClick={handleInfusionClick} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC, HTMLAttributes, useContext } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { ChoiceUtils } from "@dndbeyond/character-rules-engine";
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button } from "~/components/Button";
|
||||
import { HelperTextAccordion } from "~/components/HelperTextAccordion";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { FeatFeatureSnippet } from "~/tools/js/CharacterSheet/components/FeatureSnippet";
|
||||
import { DetailChoice } from "~/tools/js/Shared/containers/DetailChoice";
|
||||
import { CharacterFeaturesManagerContext } from "~/tools/js/Shared/managers/CharacterFeaturesManagerContext";
|
||||
@@ -145,7 +146,7 @@ export const FeatPane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
<HelperTextAccordion
|
||||
builderHelperText={feat.getHelperText()}
|
||||
size="small"
|
||||
useTheme
|
||||
themed
|
||||
/>
|
||||
{prerequisiteDescription && (
|
||||
<div className={styles.prereq}>
|
||||
@@ -198,6 +199,7 @@ export const FeatPane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
choiceInfo={choiceInfo}
|
||||
showBackgroundProficiencyOptions={true}
|
||||
description={description}
|
||||
collapseDescription={true}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
import { FC, useContext, useEffect, useState } from "react";
|
||||
|
||||
import { FeatureManager } from "@dndbeyond/character-rules-engine";
|
||||
|
||||
import { ThemeButton } from "~/tools/js/Shared/components/common/Button";
|
||||
import DataLoadingStatusEnum from "~/tools/js/Shared/constants/DataLoadingStatusEnum";
|
||||
import { CharacterFeaturesManagerContext } from "~/tools/js/Shared/managers/CharacterFeaturesManagerContext";
|
||||
import { AppLoggerUtils, AppNotificationUtils } from "~/tools/js/Shared/utils";
|
||||
import Collapsible, {
|
||||
CollapsibleHeaderContent,
|
||||
} from "~/tools/js/smartComponents/Collapsible";
|
||||
|
||||
export const BlessingShoppe: FC<{}> = () => {
|
||||
const { characterFeaturesManager } = useContext(
|
||||
CharacterFeaturesManagerContext
|
||||
);
|
||||
|
||||
const [loadingStatus, setLoadingStatus] = useState<DataLoadingStatusEnum>(
|
||||
DataLoadingStatusEnum.NOT_INITIALIZED
|
||||
);
|
||||
const [blessingShoppe, setBlessingShoppe] = useState<Array<FeatureManager>>(
|
||||
[]
|
||||
);
|
||||
const [blessings, setBlessings] = useState<Array<FeatureManager>>([]);
|
||||
|
||||
async function fetchBlessings() {
|
||||
const blessings = await characterFeaturesManager.getBlessings();
|
||||
setBlessings(blessings);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingStatus !== DataLoadingStatusEnum.LOADED) {
|
||||
characterFeaturesManager
|
||||
.getBlessingShoppe()
|
||||
.then((blessingShoppe) => {
|
||||
setLoadingStatus(DataLoadingStatusEnum.LOADED);
|
||||
setBlessingShoppe(blessingShoppe);
|
||||
})
|
||||
.catch(AppLoggerUtils.handleAdhocApiError);
|
||||
}
|
||||
fetchBlessings();
|
||||
}, [loadingStatus, characterFeaturesManager]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
A blessing is usually bestowed by a god or a godlike being. A character
|
||||
might receive a blessing from a deity for doing something truly
|
||||
momentous — an accomplishment that catches the attention of both gods
|
||||
and mortals.
|
||||
</p>
|
||||
<div>
|
||||
{blessingShoppe.map((blessing) => {
|
||||
return (
|
||||
<Collapsible
|
||||
key={blessing.getId()}
|
||||
layoutType={"minimal"}
|
||||
header={
|
||||
<CollapsibleHeaderContent
|
||||
heading={blessing.getName()}
|
||||
callout={
|
||||
characterFeaturesManager.hasBlessing(blessing) ? (
|
||||
<ThemeButton
|
||||
onClick={() => {
|
||||
blessing.handleRemove(() => {
|
||||
fetchBlessings();
|
||||
// AppNotificationUtils.dispatchSuccess(
|
||||
// 'Removed Blessing',
|
||||
// 'You removed a blessing',
|
||||
// );
|
||||
});
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
Remove
|
||||
</ThemeButton>
|
||||
) : (
|
||||
<ThemeButton
|
||||
onClick={() => {
|
||||
blessing.handleAdd(() => {
|
||||
fetchBlessings();
|
||||
AppNotificationUtils.dispatchSuccess(
|
||||
"Added Blessing",
|
||||
"You added a blessing"
|
||||
);
|
||||
});
|
||||
}}
|
||||
size="small"
|
||||
style="outline"
|
||||
>
|
||||
Add
|
||||
</ThemeButton>
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{blessing.getDescription()}
|
||||
</Collapsible>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -1,5 +1,4 @@
|
||||
import { FC, HTMLAttributes } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import {
|
||||
characterActions,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
|
||||
import { HtmlContent } from "~/components/HtmlContent";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { DetailChoice } from "~/tools/js/Shared/containers/DetailChoice";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
+6
-41
@@ -1,23 +1,14 @@
|
||||
import { FC } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { useFeatureFlags } from "~/contexts/FeatureFlag";
|
||||
import { SettingsButton } from "~/subApps/sheet/components/SettingsButton";
|
||||
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
||||
import SettingsButton from "~/tools/js/CharacterSheet/components/SettingsButton";
|
||||
import { SettingsContextsEnum } from "~/tools/js/Shared/containers/panes/SettingsPane/typings";
|
||||
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||
import Collapsible, {
|
||||
CollapsibleHeaderContent,
|
||||
CollapsibleHeading,
|
||||
} from "~/tools/js/smartComponents/Collapsible";
|
||||
|
||||
import { BlessingShoppe } from "./BlessingShoppe";
|
||||
import { FeatShoppe } from "./FeatShoppe";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
export const FeatsManagePane: FC<{}> = () => {
|
||||
const { gfsBlessingsUiFlag } = useFeatureFlags();
|
||||
|
||||
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
|
||||
if (isReadonly) {
|
||||
return null;
|
||||
@@ -27,40 +18,14 @@ export const FeatsManagePane: FC<{}> = () => {
|
||||
<div>
|
||||
<Header
|
||||
callout={
|
||||
<SettingsButton
|
||||
context={SettingsContextsEnum.FEATURES}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
!isReadonly && (
|
||||
<SettingsButton context={SettingsContextsEnum.FEATURES} />
|
||||
)
|
||||
}
|
||||
>
|
||||
Manage {gfsBlessingsUiFlag ? "Features" : "Feats"}
|
||||
Manage Feats
|
||||
</Header>
|
||||
{gfsBlessingsUiFlag ? (
|
||||
<Collapsible
|
||||
header={
|
||||
<CollapsibleHeaderContent
|
||||
heading={<CollapsibleHeading>Add Feats</CollapsibleHeading>}
|
||||
/>
|
||||
}
|
||||
className={styles.featList}
|
||||
>
|
||||
<FeatShoppe />
|
||||
</Collapsible>
|
||||
) : (
|
||||
<FeatShoppe />
|
||||
)}
|
||||
{gfsBlessingsUiFlag && (
|
||||
<Collapsible
|
||||
header={
|
||||
<CollapsibleHeaderContent
|
||||
heading={<CollapsibleHeading>Add Blessings</CollapsibleHeading>}
|
||||
/>
|
||||
}
|
||||
className={styles.featList}
|
||||
>
|
||||
<BlessingShoppe />
|
||||
</Collapsible>
|
||||
)}
|
||||
<FeatShoppe />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import clsx from "clsx";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { Dice } from "@dndbeyond/dice";
|
||||
import { GameLog } from "@dndbeyond/game-log-components";
|
||||
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { appEnvActions } from "~/tools/js/Shared/actions/appEnv";
|
||||
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||
|
||||
@@ -36,9 +36,6 @@ export const GameLogPane = () => {
|
||||
lastMessageTime.toString()
|
||||
);
|
||||
} catch (e) {}
|
||||
|
||||
// turn the dice notifications off if the panel is open
|
||||
Dice.setDiceNotificationEnabled(!isOpen);
|
||||
};
|
||||
|
||||
updateOpenState(true);
|
||||
@@ -52,8 +49,8 @@ export const GameLogPane = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.gameLogPane)} data-testid="gamelog-pane">
|
||||
{characterId && <GameLog />}
|
||||
<div className={styles.gameLogPane} data-testid="gamelog-pane">
|
||||
<GameLog isVisible={Boolean(characterId)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+21
-24
@@ -1,17 +1,23 @@
|
||||
import clsx from "clsx";
|
||||
import { FC, HTMLAttributes, useContext } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { FC, HTMLAttributes } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
DigitalDiceWrapper,
|
||||
LightDiceSvg,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
import { RollKind, RollRequest, RollType } from "@dndbeyond/dice";
|
||||
import { GameLogContext } from "@dndbeyond/game-log-components";
|
||||
import {
|
||||
RollKinds,
|
||||
RollTypes,
|
||||
} from "@dndbeyond/pocket-dimension-dice/constants";
|
||||
import {
|
||||
RollDicePayload,
|
||||
RollKind,
|
||||
} from "@dndbeyond/pocket-dimension-dice/types";
|
||||
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { isNotNullOrUndefined } from "~/helpers/validation";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { getDataOriginComponentInfo } from "~/subApps/sheet/components/Sidebar/helpers/paneUtils";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
import DiceAdjustmentSummary from "~/tools/js/Shared/components/DiceAdjustmentSummary";
|
||||
@@ -46,8 +52,6 @@ export const DeathSavesManager: FC<Props> = ({
|
||||
const characterRollContext = useSelector(
|
||||
characterRollContextSelectors.getCharacterRollContext
|
||||
);
|
||||
const [{ messageTargetOptions, defaultMessageTargetOption, userId }] =
|
||||
useContext(GameLogContext);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
@@ -60,12 +64,12 @@ export const DeathSavesManager: FC<Props> = ({
|
||||
|
||||
const hasAdvantage = deathSaveInfo.advantageAdjustments.length > 0;
|
||||
const hasDisadvantage = deathSaveInfo.disadvantageAdjustments.length > 0;
|
||||
let rollKind: RollKind | undefined;
|
||||
|
||||
let rollKind = RollKind.None;
|
||||
if (hasAdvantage && !hasDisadvantage) {
|
||||
rollKind = RollKind.Advantage;
|
||||
rollKind = RollKinds.Advantage;
|
||||
} else if (hasDisadvantage && !hasAdvantage) {
|
||||
rollKind = RollKind.Disadvantage;
|
||||
rollKind = RollKinds.Disadvantage;
|
||||
}
|
||||
|
||||
/* --- On Click Functions --- */
|
||||
@@ -134,7 +138,7 @@ export const DeathSavesManager: FC<Props> = ({
|
||||
{isDiceEnabled && (
|
||||
<DigitalDiceWrapper
|
||||
diceNotation={"1d20"}
|
||||
onRollResults={(rollRequest: RollRequest) => {
|
||||
rollCallback={(rollRequest: RollDicePayload["data"]) => {
|
||||
const roll = rollRequest.rolls[0].result?.total;
|
||||
|
||||
if (roll) {
|
||||
@@ -149,20 +153,13 @@ export const DeathSavesManager: FC<Props> = ({
|
||||
}
|
||||
}
|
||||
}}
|
||||
rollType={RollType.Save}
|
||||
rollType={RollTypes.Save}
|
||||
rollKind={rollKind}
|
||||
rollAction={"Death"}
|
||||
diceEnabled={isDiceEnabled}
|
||||
rollContext={characterRollContext}
|
||||
rollTargetOptions={
|
||||
messageTargetOptions?.entities
|
||||
? Object.values(messageTargetOptions.entities).filter(
|
||||
isNotNullOrUndefined
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
rollTargetDefault={defaultMessageTargetOption}
|
||||
userId={Number(userId)}
|
||||
action="Death"
|
||||
isDiceEnabled={isDiceEnabled}
|
||||
entityId={String(characterRollContext.entityId)}
|
||||
entityType={String(characterRollContext.entityType)}
|
||||
name={String(characterRollContext.name)}
|
||||
>
|
||||
<LightDiceSvg />
|
||||
<span>Roll</span>
|
||||
|
||||
+2
-2
@@ -1,6 +1,5 @@
|
||||
import clsx from "clsx";
|
||||
import { createRef, FC, HTMLAttributes, useEffect, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import { DataOriginName } from "@dndbeyond/character-components/es";
|
||||
import { Constants, ItemManager } from "@dndbeyond/character-rules-engine";
|
||||
@@ -9,6 +8,7 @@ import { Button } from "~/components/Button";
|
||||
import { Checkbox } from "~/components/Checkbox";
|
||||
import { ItemName } from "~/components/ItemName";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { HP_DAMAGE_TAKEN_VALUE } from "~/subApps/sheet/constants";
|
||||
import { Item, HitPointInfo, Creature } from "~/types";
|
||||
|
||||
@@ -378,7 +378,7 @@ export const HitPointsAdjuster: FC<Props> = ({
|
||||
themed
|
||||
darkMode={characterTheme.isDarkMode}
|
||||
id={`${protectionSupplier.key}-checkbox`}
|
||||
onClick={() => {
|
||||
onChange={() => {
|
||||
isEnabled = !isEnabled;
|
||||
setActiveProtectionSupplierKey(
|
||||
isEnabled ? protectionSupplier.key : null
|
||||
|
||||
+1
-1
@@ -1,11 +1,11 @@
|
||||
import { FC, HTMLAttributes, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import { Constants } from "@dndbeyond/character-rules-engine";
|
||||
|
||||
import { HtmlContent } from "~/components/HtmlContent";
|
||||
import { RuleKeyEnum } from "~/constants";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
||||
import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading";
|
||||
import { toastMessageActions } from "~/tools/js/Shared/actions";
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import clsx from "clsx";
|
||||
import { createRef, FC, HTMLAttributes, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import {
|
||||
HP_BONUS_VALUE,
|
||||
HP_DAMAGE_TAKEN_VALUE,
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import { FC, HTMLAttributes, useContext, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
Action,
|
||||
ActionUtils,
|
||||
Constants,
|
||||
DataOrigin,
|
||||
EntityUtils,
|
||||
HelperUtils,
|
||||
Infusion,
|
||||
InfusionUtils,
|
||||
ItemPlanUtils,
|
||||
ItemUtils,
|
||||
Spell,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { EditableName } from "~/components/EditableName";
|
||||
import { ItemName } from "~/components/ItemName";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
||||
import { Preview } from "~/subApps/sheet/components/Sidebar/components/Preview";
|
||||
import {
|
||||
getDataOriginComponentInfo,
|
||||
getSpellComponentInfo,
|
||||
} from "~/subApps/sheet/components/Sidebar/helpers/paneUtils";
|
||||
import {
|
||||
PaneComponentEnum,
|
||||
PaneIdentifiersItem,
|
||||
} from "~/subApps/sheet/components/Sidebar/types";
|
||||
import ItemDetail from "~/tools/js/Shared/components/ItemDetail";
|
||||
import ItemListInformationCollapsible from "~/tools/js/Shared/components/ItemListInformationCollapsible";
|
||||
import { InventoryManagerContext } from "~/tools/js/Shared/managers/InventoryManagerContext";
|
||||
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "~/tools/js/Shared/utils";
|
||||
|
||||
import { PaneInitFailureContent } from "../../components/PaneInitFailureContent";
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
identifiers: PaneIdentifiersItem | null;
|
||||
}
|
||||
export const ItemPane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
const { inventoryManager } = useContext(InventoryManagerContext);
|
||||
const {
|
||||
pane: { paneHistoryPush },
|
||||
} = useSidebar();
|
||||
const {
|
||||
ruleData,
|
||||
allInventoryItems: items,
|
||||
weaponSpellDamageGroups,
|
||||
entityValueLookup,
|
||||
snippetData,
|
||||
infusionChoiceLookup,
|
||||
characterTheme: theme,
|
||||
containerLookup,
|
||||
partyInfo,
|
||||
itemPlans,
|
||||
} = useCharacterEngine();
|
||||
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
|
||||
|
||||
const [isCustomizeClosed, setIsCustomizeClosed] = useState(true);
|
||||
const [shouldShowPaneInitFailure, setShouldShowPaneInitFailure] =
|
||||
useState(true);
|
||||
|
||||
const item =
|
||||
items.find((item) => identifiers?.id === ItemUtils.getMappingId(item)) ??
|
||||
null;
|
||||
|
||||
const handleToggleCustomize = () => {
|
||||
setIsCustomizeClosed(!isCustomizeClosed);
|
||||
};
|
||||
|
||||
const handleCustomItemEdit = (adjustments: Record<string, any>): void => {
|
||||
if (item === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (adjustments.notes === "") {
|
||||
adjustments.notes = null;
|
||||
}
|
||||
|
||||
inventoryManager.handleCustomEdit({
|
||||
item,
|
||||
adjustments,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCustomDataUpdate = (
|
||||
adjustmentType: Constants.AdjustmentTypeEnum,
|
||||
value: any
|
||||
): void => {
|
||||
if (item) {
|
||||
inventoryManager.handleCustomizationSet({
|
||||
item,
|
||||
adjustmentType,
|
||||
value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveCustomizations = (): void => {
|
||||
if (item) {
|
||||
inventoryManager.handleCustomizationsRemove({ item });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDataOriginClick = (dataOrigin: DataOrigin) => {
|
||||
let component = getDataOriginComponentInfo(dataOrigin);
|
||||
if (component.type !== PaneComponentEnum.ERROR_404) {
|
||||
paneHistoryPush(component.type, component.identifiers);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSpellClick = (spell: Spell): void => {
|
||||
let component = getSpellComponentInfo(spell);
|
||||
if (component.type) {
|
||||
paneHistoryPush(component.type, component.identifiers);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInfusionClick = (infusion: Infusion): void => {
|
||||
const choiceKey = InfusionUtils.getChoiceKey(infusion);
|
||||
if (choiceKey !== null) {
|
||||
paneHistoryPush(
|
||||
PaneComponentEnum.INFUSION_CHOICE,
|
||||
PaneIdentifierUtils.generateInfusionChoice(choiceKey)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActionClick = (action: Action): void => {
|
||||
const dataOrigin = ActionUtils.getDataOrigin(action);
|
||||
let component = getDataOriginComponentInfo(dataOrigin);
|
||||
if (component.type !== PaneComponentEnum.ERROR_404) {
|
||||
paneHistoryPush(component.type, component.identifiers);
|
||||
}
|
||||
};
|
||||
|
||||
const handleParentClick = (): void => {
|
||||
if (item) {
|
||||
const itemPlan = ItemUtils.getItemPlan(item, itemPlans);
|
||||
if (itemPlan) {
|
||||
const dataOrigin = ItemPlanUtils.getDataOrigin(itemPlan);
|
||||
let component = getDataOriginComponentInfo(dataOrigin);
|
||||
if (component.type !== PaneComponentEnum.ERROR_404) {
|
||||
paneHistoryPush(component.type, component.identifiers);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (item === null) {
|
||||
return shouldShowPaneInitFailure ? (
|
||||
<PaneInitFailureContent
|
||||
errorTitle="Item Not Found"
|
||||
errorMessage="That item is no longer in your inventory! Please try again."
|
||||
toastMeta={{ level: "info" }}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
let parentName: string | null = null;
|
||||
const itemPlan = ItemUtils.getItemPlan(item, itemPlans);
|
||||
if (itemPlan) {
|
||||
const dataOrigin = ItemPlanUtils.getDataOrigin(itemPlan);
|
||||
parentName = EntityUtils.getDataOriginName(dataOrigin);
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
<Header
|
||||
parent={parentName}
|
||||
onClick={handleParentClick}
|
||||
preview={<Preview imageUrl={ItemUtils.getAvatarUrl(item)} />}
|
||||
>
|
||||
<EditableName onClick={handleToggleCustomize}>
|
||||
<ItemName item={item} />
|
||||
</EditableName>
|
||||
</Header>
|
||||
<ItemListInformationCollapsible item={item} ruleData={ruleData} />
|
||||
<ItemDetail
|
||||
theme={theme}
|
||||
key={ItemUtils.getUniqueKey(item)}
|
||||
item={item}
|
||||
weaponSpellDamageGroups={weaponSpellDamageGroups}
|
||||
ruleData={ruleData}
|
||||
snippetData={snippetData}
|
||||
onCustomDataUpdate={handleCustomDataUpdate}
|
||||
onCustomizationsRemove={handleRemoveCustomizations}
|
||||
onDataOriginClick={handleDataOriginClick}
|
||||
onSpellClick={handleSpellClick}
|
||||
onInfusionClick={handleInfusionClick}
|
||||
onMasteryActionClick={handleActionClick}
|
||||
entityValueLookup={entityValueLookup}
|
||||
infusionChoiceLookup={infusionChoiceLookup}
|
||||
isReadonly={isReadonly}
|
||||
container={HelperUtils.lookupDataOrFallback(
|
||||
containerLookup,
|
||||
ItemUtils.getContainerDefinitionKey(item)
|
||||
)}
|
||||
showCustomize={!isReadonly}
|
||||
onPostRemoveNavigation={PaneComponentEnum.EQUIPMENT_MANAGE}
|
||||
partyInfo={partyInfo}
|
||||
onCustomItemEdit={handleCustomItemEdit}
|
||||
isCustomizeClosed={isCustomizeClosed}
|
||||
onCustomizeClick={handleToggleCustomize}
|
||||
onRemoveItem={() => setShouldShowPaneInitFailure(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
import clsx from "clsx";
|
||||
import { sortBy } from "lodash";
|
||||
import { FC, HTMLAttributes, useContext, useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
characterActions,
|
||||
CharClass,
|
||||
ClassFeature,
|
||||
ClassFeatureUtils,
|
||||
ClassUtils,
|
||||
ContainerManager,
|
||||
HelperUtils,
|
||||
} from "@dndbeyond/character-rules-engine";
|
||||
import ArrowRightIcon from "@dndbeyond/fontawesome-cache/svgs/solid/arrow-right.svg";
|
||||
|
||||
import { Button } from "~/components/Button";
|
||||
import { FeatureChoices } from "~/components/FeatureChoices";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import DataLoadingStatusEnum from "~/tools/js/Shared/constants/DataLoadingStatusEnum";
|
||||
import { InventoryManagerContext } from "~/tools/js/Shared/managers/InventoryManagerContext";
|
||||
import { AppLoggerUtils, PaneIdentifierUtils } from "~/tools/js/Shared/utils";
|
||||
import Collapsible from "~/tools/js/smartComponents/Collapsible";
|
||||
import LoadingPlaceholder from "~/tools/js/smartComponents/LoadingPlaceholder";
|
||||
|
||||
import { Header } from "../../components/Header";
|
||||
import { Heading } from "../../components/Heading";
|
||||
import { PaneComponentEnum } from "../../types";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
export interface ItemPlansManagePaneProps
|
||||
extends HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export const ItemPlansManagePane: FC<ItemPlansManagePaneProps> = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { classes } = useCharacterEngine();
|
||||
const {
|
||||
pane: { paneHistoryPush },
|
||||
} = useSidebar();
|
||||
const { inventoryManager } = useContext(InventoryManagerContext);
|
||||
|
||||
const [itemShoppe, setItemShoppe] = useState<ContainerManager | null>(null);
|
||||
const [itemsLoadingStatus, setItemsLoadingStatus] =
|
||||
useState<DataLoadingStatusEnum>(DataLoadingStatusEnum.NOT_INITIALIZED);
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
if (
|
||||
itemShoppe === null &&
|
||||
itemsLoadingStatus !== DataLoadingStatusEnum.LOADED
|
||||
) {
|
||||
if (inventoryManager) {
|
||||
setItemsLoadingStatus(DataLoadingStatusEnum.LOADING);
|
||||
|
||||
inventoryManager
|
||||
.getInventoryShoppe({
|
||||
onSuccess: (shoppeContainer: ContainerManager) => {
|
||||
setItemShoppe(shoppeContainer);
|
||||
setItemsLoadingStatus(DataLoadingStatusEnum.LOADED);
|
||||
},
|
||||
additionalApiConfig: {
|
||||
signal: abortController.signal,
|
||||
},
|
||||
})
|
||||
.catch((e) => {
|
||||
if (abortController.signal.aborted) return;
|
||||
AppLoggerUtils.handleAdhocApiError(e);
|
||||
});
|
||||
} else {
|
||||
setItemsLoadingStatus(DataLoadingStatusEnum.LOADED);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [itemsLoadingStatus, inventoryManager, itemShoppe]);
|
||||
|
||||
const handleClassFeatureClick = (
|
||||
charClass: CharClass,
|
||||
classFeature: ClassFeature
|
||||
): void => {
|
||||
paneHistoryPush(
|
||||
PaneComponentEnum.CLASS_FEATURE_DETAIL,
|
||||
PaneIdentifierUtils.generateClassFeature(
|
||||
ClassFeatureUtils.getId(classFeature),
|
||||
ClassUtils.getMappingId(charClass)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleChoiceChange = (
|
||||
charClass: CharClass,
|
||||
classFeature: ClassFeature,
|
||||
id: string,
|
||||
type: number,
|
||||
value: any,
|
||||
parentChoiceId: string | null
|
||||
): void => {
|
||||
if (!charClass || !classFeature) {
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
characterActions.classFeatureChoiceSetRequest(
|
||||
ClassUtils.getActiveId(charClass),
|
||||
ClassFeatureUtils.getId(classFeature),
|
||||
type,
|
||||
id,
|
||||
HelperUtils.parseInputInt(value),
|
||||
parentChoiceId,
|
||||
ClassFeatureUtils.getHasItemMappings(classFeature)
|
||||
? inventoryManager.handleAcceptOnSuccess(true)
|
||||
: undefined
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx([styles.itemPlansManagePane, className])} {...props}>
|
||||
<Header>Manage Magic Item Plans</Header>
|
||||
{classes.map((charClass) => {
|
||||
const subclass = ClassUtils.getSubclass(charClass);
|
||||
const subclassId = subclass?.id;
|
||||
const subclassName = subclass?.name;
|
||||
const baseClassId = ClassUtils.getId(charClass);
|
||||
|
||||
const activeFeaturesWithItemMappings =
|
||||
ClassUtils.getActiveClassFeatures(charClass).filter(
|
||||
ClassFeatureUtils.getHasItemMappings
|
||||
);
|
||||
|
||||
const baseClassFeatures: ClassFeature[] = [];
|
||||
const subClassFeatures: ClassFeature[] = [];
|
||||
activeFeaturesWithItemMappings.forEach((feature) => {
|
||||
const featureClassId = ClassFeatureUtils.getClassId(feature);
|
||||
if (featureClassId === baseClassId) {
|
||||
baseClassFeatures.push(feature);
|
||||
} else if (featureClassId === subclassId) {
|
||||
subClassFeatures.push(feature);
|
||||
}
|
||||
});
|
||||
|
||||
const sortedBaseFeatures = sortBy(baseClassFeatures, [
|
||||
(feature) => ClassFeatureUtils.getName(feature),
|
||||
]);
|
||||
|
||||
const sortedSubclassFeatures = sortBy(subClassFeatures, [
|
||||
(feature) => ClassFeatureUtils.getName(feature),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div key={ClassUtils.getMappingId(charClass)}>
|
||||
<Heading>{ClassUtils.getName(charClass)}</Heading>
|
||||
{itemsLoadingStatus !== DataLoadingStatusEnum.LOADED && (
|
||||
<LoadingPlaceholder />
|
||||
)}
|
||||
{itemsLoadingStatus === DataLoadingStatusEnum.LOADED &&
|
||||
sortedBaseFeatures.map((feature) => {
|
||||
return (
|
||||
<Collapsible
|
||||
className={styles.classFeature}
|
||||
key={ClassFeatureUtils.getUniqueKey(feature)}
|
||||
header={`${ClassFeatureUtils.getName(feature)} (${
|
||||
ClassFeatureUtils.getChoices(feature).length
|
||||
})`}
|
||||
initiallyCollapsed={false}
|
||||
>
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button
|
||||
themed
|
||||
size="xx-small"
|
||||
className={styles.viewDetailsButton}
|
||||
onClick={() =>
|
||||
handleClassFeatureClick(charClass, feature)
|
||||
}
|
||||
>
|
||||
View {ClassFeatureUtils.getName(feature)} Details
|
||||
<ArrowRightIcon className={styles.arrowIcon} />
|
||||
</Button>
|
||||
</div>
|
||||
<FeatureChoices
|
||||
className={styles.featureChoices}
|
||||
choices={ClassFeatureUtils.getChoices(feature)}
|
||||
charClass={charClass}
|
||||
classFeature={feature}
|
||||
onChoiceChange={(id, type, value, parentChoiceId) =>
|
||||
handleChoiceChange(
|
||||
charClass,
|
||||
feature,
|
||||
id,
|
||||
type,
|
||||
value,
|
||||
parentChoiceId
|
||||
)
|
||||
}
|
||||
collapseDescription={true}
|
||||
shouldFetch={false}
|
||||
itemShoppe={itemShoppe}
|
||||
/>
|
||||
</Collapsible>
|
||||
);
|
||||
})}
|
||||
{itemsLoadingStatus === DataLoadingStatusEnum.LOADED &&
|
||||
sortedSubclassFeatures.length > 0 && (
|
||||
<>
|
||||
<Heading>{subclassName}</Heading>
|
||||
{sortedSubclassFeatures.map((feature) => {
|
||||
return (
|
||||
<Collapsible
|
||||
className={styles.classFeature}
|
||||
key={ClassFeatureUtils.getUniqueKey(feature)}
|
||||
header={`${ClassFeatureUtils.getName(feature)} (${
|
||||
ClassFeatureUtils.getChoices(feature).length
|
||||
})`}
|
||||
initiallyCollapsed={false}
|
||||
>
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button
|
||||
themed
|
||||
size="xx-small"
|
||||
className={styles.viewDetailsButton}
|
||||
onClick={() =>
|
||||
handleClassFeatureClick(charClass, feature)
|
||||
}
|
||||
>
|
||||
View {ClassFeatureUtils.getName(feature)} Details
|
||||
<ArrowRightIcon className={styles.arrowIcon} />
|
||||
</Button>
|
||||
</div>
|
||||
<FeatureChoices
|
||||
className={styles.featureChoices}
|
||||
choices={ClassFeatureUtils.getChoices(feature)}
|
||||
charClass={charClass}
|
||||
classFeature={feature}
|
||||
onChoiceChange={(id, type, value, parentChoiceId) =>
|
||||
handleChoiceChange(
|
||||
charClass,
|
||||
feature,
|
||||
id,
|
||||
type,
|
||||
value,
|
||||
parentChoiceId
|
||||
)
|
||||
}
|
||||
collapseDescription={true}
|
||||
shouldFetch={false}
|
||||
itemShoppe={itemShoppe}
|
||||
/>
|
||||
</Collapsible>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,267 @@
|
||||
import { FC, HTMLAttributes, useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
ShortModelInfoContract,
|
||||
ApiRequests,
|
||||
serviceDataActions,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { Checkbox } from "~/components/Checkbox";
|
||||
import { RadioGroup } from "~/components/RadioGroup";
|
||||
import { PreferenceLongRestTypeEnum } from "~/constants";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
||||
import { toastMessageActions } from "~/tools/js/Shared/actions";
|
||||
import ThemeButton from "~/tools/js/Shared/components/common/Button/ThemeButton/ThemeButton";
|
||||
import DataLoadingStatusEnum from "~/tools/js/Shared/constants/DataLoadingStatusEnum";
|
||||
import { AppLoggerUtils } from "~/tools/js/Shared/utils";
|
||||
|
||||
import { RestoreLifeManager } from "../HitPointsManagePane/RestoreLifeManager";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface LongRestPaneProps extends HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export const LongRestPane: FC<LongRestPaneProps> = ({ ...props }) => {
|
||||
const [resetMaxHpModifier, setResetMaxHpModifier] = useState(true);
|
||||
const [adjustConditionLevel, setAdjustConditionLevel] = useState(false);
|
||||
const [restMessageLoadingStatus, setRestMessageLoadingStatus] =
|
||||
useState<DataLoadingStatusEnum>(DataLoadingStatusEnum.NOT_INITIALIZED);
|
||||
|
||||
const {
|
||||
activeConditions,
|
||||
isDead,
|
||||
preferences,
|
||||
characterUtils,
|
||||
helperUtils,
|
||||
apiAdapterUtils,
|
||||
characterActions,
|
||||
longRestText,
|
||||
} = useCharacterEngine();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(
|
||||
characterActions.longRest(resetMaxHpModifier, adjustConditionLevel)
|
||||
);
|
||||
dispatch(
|
||||
toastMessageActions.toastSuccess(
|
||||
"Long Rest Taken",
|
||||
"You have completed a long rest. Relevant abilities have been reset."
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleRestoreToLife = (restoreType: ShortModelInfoContract) => {
|
||||
const restoreChoice = restoreType.name === "Full" ? "full" : "1";
|
||||
|
||||
dispatch(characterActions.restoreLife(restoreType.id));
|
||||
dispatch(
|
||||
toastMessageActions.toastSuccess(
|
||||
"Character Restored to Life",
|
||||
`You have been restored to life with ${restoreChoice} HP.`
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleChangeLongRestType = (value: number | null) => {
|
||||
const typedPrefKey = characterUtils.getPreferenceKey("longRestType");
|
||||
|
||||
if (typedPrefKey !== null) {
|
||||
dispatch(characterActions.preferenceChoose(typedPrefKey, value));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
if (restMessageLoadingStatus !== DataLoadingStatusEnum.LOADED) {
|
||||
setRestMessageLoadingStatus(DataLoadingStatusEnum.LOADING);
|
||||
|
||||
ApiRequests.getCharacterRestLong({
|
||||
signal: abortController.signal,
|
||||
})
|
||||
.then((response) => {
|
||||
let message = apiAdapterUtils.getResponseData(response);
|
||||
if (message !== null) {
|
||||
dispatch(serviceDataActions.longRestTextSet(message));
|
||||
}
|
||||
setRestMessageLoadingStatus(DataLoadingStatusEnum.LOADED);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (abortController.signal.aborted) return;
|
||||
AppLoggerUtils.handleAdhocApiError(e);
|
||||
});
|
||||
}
|
||||
|
||||
// If the pane is quickly opened and closed multiple times, we cancel any in-flight requests.
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [restMessageLoadingStatus]);
|
||||
|
||||
const renderRecover = () => {
|
||||
if (isDead) {
|
||||
return <p>Your character is dead</p>;
|
||||
}
|
||||
const exhaustionIsActive = activeConditions.find(
|
||||
(condition) => condition.level !== null
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
{longRestText === null ||
|
||||
restMessageLoadingStatus === DataLoadingStatusEnum.LOADING
|
||||
? "Asking the server what will be reset..."
|
||||
: longRestText}
|
||||
</p>
|
||||
<div className={styles.recoverMaxHp}>
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
id="reset-max-hp"
|
||||
label="Reset Maximum HP changes during this rest"
|
||||
checked={resetMaxHpModifier}
|
||||
onChange={() => setResetMaxHpModifier(!resetMaxHpModifier)}
|
||||
themed
|
||||
/>
|
||||
</div>
|
||||
{exhaustionIsActive && (
|
||||
<div className={styles.recoverExhaustion}>
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
id="recover-exhaustion"
|
||||
label="Recover 1 Level of Exhaustion during this rest (requires food and drink)"
|
||||
checked={adjustConditionLevel}
|
||||
onChange={() => setAdjustConditionLevel(!adjustConditionLevel)}
|
||||
themed
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderActions = () => {
|
||||
if (isDead) {
|
||||
return (
|
||||
<>
|
||||
<RestoreLifeManager onSave={handleRestoreToLife} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.recoverActions}>
|
||||
<ThemeButton onClick={handleSave} enableConfirm={true}>
|
||||
Take Long Rest
|
||||
</ThemeButton>
|
||||
<ThemeButton
|
||||
onClick={() => {
|
||||
setResetMaxHpModifier(true);
|
||||
setAdjustConditionLevel(false);
|
||||
}}
|
||||
style="outline"
|
||||
>
|
||||
Reset
|
||||
</ThemeButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header>Long Rest</Header>
|
||||
<div>
|
||||
<p>
|
||||
A Long Rest is a period of extended downtime—at least 8
|
||||
hours—available to any creature. During a Long Rest, you sleep for at
|
||||
least 6 hours and perform no more than 2 hours of light activity, such
|
||||
as reading, talking, eating, or standing watch.
|
||||
</p>
|
||||
<p>
|
||||
While asleep, you have the Unconscious condition. After you finish a
|
||||
Long Rest, you must wait at least 16 hours before starting another
|
||||
one.
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.longRestType}>
|
||||
<RadioGroup
|
||||
className={styles.radioGroup}
|
||||
themed
|
||||
title="Long Rest Rules"
|
||||
name="long-rest-type"
|
||||
initialValue={preferences.longRestType}
|
||||
onChange={(e) =>
|
||||
handleChangeLongRestType(
|
||||
helperUtils.parseInputInt(e?.target?.value)
|
||||
)
|
||||
}
|
||||
options={[
|
||||
{
|
||||
label: "Recover 1/2 Hit Dice",
|
||||
description: "Use 5e Rules",
|
||||
value: PreferenceLongRestTypeEnum.HALF,
|
||||
},
|
||||
{
|
||||
label: "Recover all Hit Dice",
|
||||
description: "Use 5.5e Rules",
|
||||
value: PreferenceLongRestTypeEnum.FULL,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.recover}>
|
||||
<h2 className={styles.recoverHeading}>Recover</h2>
|
||||
{renderRecover()}
|
||||
</div>
|
||||
{renderActions()}
|
||||
<div className={styles.longRestInfo}>
|
||||
<p>
|
||||
<strong>
|
||||
<em>Benefits of the Rest.</em>
|
||||
</strong>{" "}
|
||||
To start a Long Rest, you must have at least 1 HP. When you finish the
|
||||
rest, you gain the following benefits:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
You regain all lost HP and all spent Hit Point Dice. If your HP
|
||||
maximum was reduced, it returns to normal.
|
||||
</li>
|
||||
<li>
|
||||
If any of your ability scores were reduced, they return to normal.
|
||||
</li>
|
||||
<li>
|
||||
If you have the Exhaustion condition, its level decreases by 1.
|
||||
</li>
|
||||
<li>
|
||||
If you have a feature that is recharged by a Long Rest, it recharges
|
||||
in the way specified in its description.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong>
|
||||
<em>Interrupting the Rest.</em>
|
||||
</strong>{" "}
|
||||
A Long Rest is stopped by the following interruptions:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Rolling Initiative</li>
|
||||
<li>Casting a spell other than a cantrip</li>
|
||||
<li>Taking any damage</li>
|
||||
<li>1 hour of walking or other physical exertion</li>
|
||||
</ul>
|
||||
<p>
|
||||
If you rested at least 1 hour before the interruption, you gain the
|
||||
benefits of a Short Rest.
|
||||
</p>
|
||||
<p>
|
||||
You can resume a Long Rest immediately after an interruption. If you
|
||||
do so, the rest requires 1 additional hour per interruption to finish.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+2
-1
@@ -1,5 +1,5 @@
|
||||
import { FC, HTMLAttributes, useContext } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
Action,
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { FeatureChoices } from "~/components/FeatureChoices";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
||||
import { Preview } from "~/subApps/sheet/components/Sidebar/components/Preview";
|
||||
import {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FC, HTMLAttributes, useEffect, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import { characterActions } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { Button } from "~/components/Button";
|
||||
import { XpManager } from "~/components/XpManager";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
||||
import { toastMessageActions } from "~/tools/js/Shared/actions";
|
||||
|
||||
|
||||
@@ -3,14 +3,7 @@ import { FC, HTMLAttributes } from "react";
|
||||
type SvgProps = HTMLAttributes<SVGElement>;
|
||||
|
||||
export const ArrowsLeftIcon: FC<SvgProps> = (props) => (
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="1em"
|
||||
height="1em"
|
||||
aria-labelledby="leftArrowsTitle"
|
||||
{...props}
|
||||
>
|
||||
<title id="leftArrowsTitle">Left Arrows Icon</title>
|
||||
<svg viewBox="0 0 16 16" width="1em" height="1em" {...props}>
|
||||
<path d="M11,2.48,5,8l6,5.52a1.3,1.3,0,0,1-.21,2.12h0a2.25,2.25,0,0,1-2.68-.17L0,8,8.11.53A2.25,2.25,0,0,1,10.79.36h0A1.3,1.3,0,0,1,11,2.48Z" />
|
||||
<polygon points="6.92 8 16 0 16 16 6.92 8" />
|
||||
</svg>
|
||||
|
||||
@@ -3,14 +3,7 @@ import { FC, HTMLAttributes } from "react";
|
||||
type SvgProps = HTMLAttributes<SVGElement>;
|
||||
|
||||
export const ArrowsRightIcon: FC<SvgProps> = (props) => (
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="1em"
|
||||
height="1em"
|
||||
aria-labelledby="rightArrowsTitle"
|
||||
{...props}
|
||||
>
|
||||
<title id="rightArrowsTitle">Right Arrows Icon</title>
|
||||
<svg viewBox="0 0 16 16" width="1em" height="1em" {...props}>
|
||||
<path d="M5.21.36h0A2.25,2.25,0,0,1,7.89.53L16,8,7.89,15.47a2.25,2.25,0,0,1-2.68.17h0A1.3,1.3,0,0,1,5,13.52L11,8,5,2.48A1.3,1.3,0,0,1,5.21.36Z" />
|
||||
<polygon points="9.09 8 0 0 0 16 9.09 8" />
|
||||
</svg>
|
||||
|
||||
@@ -90,10 +90,6 @@ export interface PaneIdentifiersSettingsContext {
|
||||
context: string;
|
||||
}
|
||||
|
||||
export interface PaneIdentifiersBlessing {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface PaneIdentifiersTrait {
|
||||
type: Constants.TraitTypeEnum;
|
||||
}
|
||||
@@ -125,7 +121,6 @@ export type PaneIdentifiers =
|
||||
| PaneIdentifiersContainer
|
||||
| PaneIdentifiersCurrencyContext
|
||||
| PaneIdentifiersSettingsContext
|
||||
| PaneIdentifiersBlessing
|
||||
| PaneIdentifiersTrait;
|
||||
|
||||
export interface PaneComponentProperties {
|
||||
@@ -148,7 +143,6 @@ export enum PaneComponentEnum {
|
||||
ARMOR_MANAGE = "ARMOR_MANAGE",
|
||||
BASIC_ACTION = "BASIC_ACTION",
|
||||
BACKGROUND = "BACKGROUND",
|
||||
BLESSING_DETAIL = "BLESSING_DETAIL",
|
||||
CAMPAIGN = "CAMPAIGN",
|
||||
CHARACTER_MANAGE = "CHARACTER_MANAGE",
|
||||
CHARACTER_SPELL_DETAIL = "CHARACTER_SPELL_DETAIL",
|
||||
@@ -176,6 +170,7 @@ export enum PaneComponentEnum {
|
||||
INITIATIVE = "INITIATIVE",
|
||||
INSPIRATION = "INSPIRATION",
|
||||
ITEM_DETAIL = "ITEM_DETAIL",
|
||||
ITEM_PLANS_MANAGE = "ITEM_PLANS_MANAGE",
|
||||
LONG_REST = "LONG_REST",
|
||||
NOTE_MANAGE = "NOTE_MANAGE",
|
||||
POSSESSIONS_MANAGE = "POSSESSIONS_MANAGE",
|
||||
|
||||
Reference in New Issue
Block a user