New source found from dndbeyond.com
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import clsx from "clsx";
|
||||
import { FC, HTMLAttributes } from "react";
|
||||
|
||||
import { DeathCauseEnum, DefaultCharacterName } from "~/constants";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
export interface CharacterNameProps extends HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export const CharacterName: FC<CharacterNameProps> = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const { deathCause, characterName } = useCharacterEngine();
|
||||
|
||||
return (
|
||||
<h1 className={clsx([styles.characterName, className])} {...props}>
|
||||
{deathCause !== DeathCauseEnum.NONE && "(Dead) "}
|
||||
{characterName || DefaultCharacterName}
|
||||
</h1>
|
||||
);
|
||||
};
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { rulesEngineSelectors } from "@dndbeyond/character-rules-engine";
|
||||
|
||||
import { GuidedTour } from "~/components/GuidedTour";
|
||||
import { useFeatureFlags } from "~/contexts/FeatureFlag";
|
||||
|
||||
import { getCharacterSheetSteps } from "./getCharacterSheetSteps";
|
||||
|
||||
export const CharacterSheetGuidedTour = ({ children, ...props }) => {
|
||||
const [step, setStep] = useState(0);
|
||||
const { characterSheetTourFlag } = useFeatureFlags();
|
||||
const isMobile = window.matchMedia("(max-width: 767px)").matches;
|
||||
const isTablet = window.matchMedia(
|
||||
"(min-width: 768px and max-width: 1023px)"
|
||||
).matches;
|
||||
const isDesktop = window.matchMedia("(min-width: 1024px)").matches;
|
||||
const ready = useSelector(rulesEngineSelectors.isCharacterSheetReady);
|
||||
// const hasSpells = useSelector(rulesEngineSelectors.hasSpells);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const setCurrentStep = (currentStep) => {
|
||||
switch (currentStep) {
|
||||
case 11: {
|
||||
if (isMobile) {
|
||||
const toggle = document.querySelector(
|
||||
"[class^='styles_navToggle']"
|
||||
) as HTMLButtonElement;
|
||||
toggle?.click();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 13: {
|
||||
if (isTablet) {
|
||||
const toggle = document.querySelector(
|
||||
"[class^='styles_navToggle']"
|
||||
) as HTMLButtonElement;
|
||||
toggle?.click();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
setStep(currentStep);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// If character sheet has loaded, enable tour
|
||||
if (ready) setLoading(false);
|
||||
}, [ready]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{characterSheetTourFlag && !loading ? (
|
||||
<GuidedTour
|
||||
steps={getCharacterSheetSteps(false, isTablet, isDesktop)}
|
||||
step={step}
|
||||
setStep={setCurrentStep}
|
||||
showOnFirstLoad={true}
|
||||
cookieName="characterSheetGuidedTour"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</GuidedTour>
|
||||
) : (
|
||||
<>{children}</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
import { StepType } from "@reactour/tour";
|
||||
|
||||
import { GuidedTourStep } from "~/components/GuidedTourStep";
|
||||
|
||||
/**
|
||||
* CHARACTER SHEET TOUR STEPS
|
||||
**/
|
||||
export const getCharacterSheetSteps = (
|
||||
hasSpells: boolean,
|
||||
isTablet: boolean,
|
||||
isDesktop: boolean
|
||||
): StepType[] => {
|
||||
const getSelector = (
|
||||
mobileSelector: string,
|
||||
tabletSelector?: string,
|
||||
desktopSelector?: string
|
||||
) =>
|
||||
isDesktop && desktopSelector
|
||||
? desktopSelector
|
||||
: isTablet && tabletSelector
|
||||
? tabletSelector
|
||||
: mobileSelector;
|
||||
|
||||
const stepList = [
|
||||
{
|
||||
selector: getSelector("html"),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Welcome!"
|
||||
content={
|
||||
<>
|
||||
Welcome to your character sheet! Here, you can find information
|
||||
about your character, roll dice, and find more information about
|
||||
other characters in your game. If you ever have any questions
|
||||
about rules or what something is for, you can click on almost
|
||||
anything to learn more!
|
||||
</>
|
||||
}
|
||||
showClose
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(".dice-toolbar"),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Digital Dice"
|
||||
content={
|
||||
<>
|
||||
Click here if you need to make a custom dice roll. Your dice
|
||||
collection can be found{" "}
|
||||
<a
|
||||
href="https://www.dndbeyond.com/my-dice"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(
|
||||
".ct-character-header-mobile__group-tidbits",
|
||||
".ct-character-header-tablet__group-tidbits",
|
||||
".ct-character-header-desktop__group-tidbits"
|
||||
),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Character Overview"
|
||||
content={
|
||||
<>
|
||||
Basic information about your character such as Name, Species,
|
||||
Class, and Level can be found here. Click this area to change your
|
||||
character's name, sheet styles, and modify other settings for your
|
||||
character.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: ".ct-character-header-desktop__group--builder",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Character Builder"
|
||||
content={
|
||||
<>
|
||||
Click here to visit the Character Builder. Each time you level up,
|
||||
check here to see how to develop your character.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(
|
||||
".ct-main-mobile__abilities",
|
||||
".ct-main-tablet__abilities",
|
||||
".ct-quick-info__abilities"
|
||||
),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Ability Scores"
|
||||
content={
|
||||
<>
|
||||
Much of what your character does in the game depends on his or her
|
||||
six abilities: Strength, Dexterity, Constitution, Intelligence,
|
||||
Wisdom, and Charisma. Learn more about an ability by clicking on
|
||||
it.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(
|
||||
".ct-combat-mobile__extra--proficiency",
|
||||
".ct-combat-tablet__extra--proficiency",
|
||||
".ct-quick-info__box--proficiency"
|
||||
),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Proficiency Bonus"
|
||||
content={
|
||||
<>
|
||||
Proficiency is added to rolls made to accomplish tasks with which
|
||||
your character is proficient. The bonus is automatically added to
|
||||
rolls when it is needed. Click here to learn more about how
|
||||
Proficiency Bonuses are used.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(
|
||||
".ct-combat-mobile__extra--speed",
|
||||
".ct-combat-tablet__extra--speed",
|
||||
".ct-quick-info__box--speed"
|
||||
),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Speed"
|
||||
content={
|
||||
<>
|
||||
In each round of combat, your character can move up to the total
|
||||
distance indicated by your speed. Click here to learn more or
|
||||
customize your speed.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(
|
||||
".ct-status-summary-mobile__health",
|
||||
".ct-status-summary-mobile__health",
|
||||
".ct-health-summary"
|
||||
),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Hit Points"
|
||||
content={
|
||||
<>
|
||||
In this area, you can manage your character's hit points. When
|
||||
your character reaches zero hit points, they are on the brink of
|
||||
death and begin making death saving throws.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(
|
||||
".ct-combat-mobile__extra--initiative",
|
||||
".ct-combat-tablet__extra--initiative",
|
||||
".ct-combat__summary-group--initiative"
|
||||
),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Initiative"
|
||||
content={
|
||||
<>
|
||||
Initiative rolls determine the order in which you go in combat.
|
||||
The higher the number, the sooner you get to fight. Click here to
|
||||
roll initiative!
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(
|
||||
".ct-combat-mobile__extra--ac",
|
||||
".ct-combat-tablet__extra--ac",
|
||||
".ct-combat__summary-group--ac"
|
||||
),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Armor Class"
|
||||
content={
|
||||
<>
|
||||
Your Armor Class (AC) represents how well your character avoids
|
||||
being wounded in battle.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(
|
||||
".ct-main-mobile__saving-throws",
|
||||
".ct-saving-throws-box",
|
||||
".ct-saving-throws-box"
|
||||
),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Saving Throws"
|
||||
content={
|
||||
<>
|
||||
Your DM may ask you to roll a saving throw (or make a save) to
|
||||
resist an incoming effect. Click on a save to automatically roll.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(isDesktop || isTablet
|
||||
? [
|
||||
{
|
||||
selector: ".ct-proficiency-groups-box",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Proficiencies"
|
||||
content={
|
||||
<>
|
||||
Proficiencies tell you what tools, equipment, and languages
|
||||
your character is skilled at using.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: ".ct-skills-box",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Skills"
|
||||
content={
|
||||
<>
|
||||
If you wish to perform an action, your DM will determine
|
||||
which of these skills you will use. Click on a skill to roll
|
||||
or learn more.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
selector: ".ct-quick-nav__menu-item--skills",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Skills"
|
||||
content={
|
||||
<>
|
||||
If you wish to perform an action, your DM will determine
|
||||
which of these skills you will use. Click on a skill to roll
|
||||
or learn more.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]),
|
||||
...(!isDesktop
|
||||
? [
|
||||
{
|
||||
selector: ".ct-quick-nav__menu-item--actions",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Actions"
|
||||
content={
|
||||
<>
|
||||
When you take your action on your turn, you can take one of
|
||||
the actions presented here. You can track actions or make
|
||||
rolls to perform them by clicking in this panel.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: ".ct-quick-nav__menu-item--equipment",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Inventory"
|
||||
content={
|
||||
<>
|
||||
View and manage your character's items and coin from this
|
||||
panel.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: ".ct-quick-nav__menu-item--spells",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Spells"
|
||||
content={
|
||||
<>
|
||||
If your character has the ability to cast spells, look here
|
||||
for a list of spells and to track your spellcasting.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: ".ct-quick-nav__menu-item--features",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Features & Traits"
|
||||
content={
|
||||
<>
|
||||
This section describes the source of your character's
|
||||
abilities. How this manifests in your character's
|
||||
personality is up to you.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(!isTablet
|
||||
? [
|
||||
{
|
||||
selector: ".ct-quick-nav__menu-item--proficiencies",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Proficiencies"
|
||||
content={
|
||||
<>
|
||||
Proficiencies tell you what tools, equipment, and
|
||||
languages your character is skilled at using.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
selector: ".ct-quick-nav__menu-item--description",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Description"
|
||||
content={
|
||||
<>
|
||||
Use the description panel to tell your character's story.
|
||||
Your character's background features are also found here.
|
||||
</>
|
||||
}
|
||||
showClose
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
selector: ".ct-primary-box__tab--actions",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Actions"
|
||||
content={
|
||||
<>
|
||||
When you take your action on your turn, you can take one of
|
||||
the actions presented here. You can track actions or make
|
||||
rolls to perform them by clicking in this panel.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(hasSpells
|
||||
? [
|
||||
{
|
||||
selector: ".ct-primary-box__tab--spells",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Spells"
|
||||
content={
|
||||
<>
|
||||
If your character has the ability to cast spells, look
|
||||
here for a list of spells and to track your
|
||||
spellcasting.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
selector: ".ct-primary-box__tab--equipment",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Inventory"
|
||||
content={
|
||||
<>
|
||||
View and manage your character's items and coin from this
|
||||
panel.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: ".ct-primary-box__tab--features",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Features & Traits"
|
||||
content={
|
||||
<>
|
||||
This section describes the source of your character's
|
||||
abilities. How this manifests in your character's
|
||||
personality is up to you.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: ".ct-primary-box__tab--description",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Description"
|
||||
content={
|
||||
<>
|
||||
Use the description panel to tell your character's story.
|
||||
Your character's background features are also found here.
|
||||
</>
|
||||
}
|
||||
showClose
|
||||
/>
|
||||
),
|
||||
},
|
||||
]),
|
||||
];
|
||||
return stepList.filter((s) => s);
|
||||
};
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import clsx from "clsx";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
|
||||
import ChevronIcon from "@dndbeyond/fontawesome-cache/svgs/solid/chevron-right.svg";
|
||||
import CloseIcon from "@dndbeyond/fontawesome-cache/svgs/solid/x.svg";
|
||||
|
||||
import { Button } from "~/components/Button";
|
||||
import { Dialog, DialogProps } from "~/components/Dialog";
|
||||
import characterGalleryImg from "~/images/character-gallery.png";
|
||||
import characterSheetImg from "~/images/character-sheet.png";
|
||||
import tadaEmoji from "~/images/tada-emoji.svg?url";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
export interface ClaimConfirmationDialogProps
|
||||
extends Omit<DialogProps, "onClose" | "open"> {
|
||||
characterId: number;
|
||||
campaignId?: number | null;
|
||||
}
|
||||
|
||||
export const ClaimConfirmationDialog: FC<ClaimConfirmationDialogProps> = ({
|
||||
className,
|
||||
characterId,
|
||||
campaignId,
|
||||
...props
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const handleClose = () => {
|
||||
window.location.href = `/characters/${characterId}`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (characterId) setIsOpen(true);
|
||||
}, [characterId]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
className={clsx([styles.dialog, className])}
|
||||
open={isOpen}
|
||||
onClose={handleClose}
|
||||
modal
|
||||
{...props}
|
||||
>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>
|
||||
<img src={tadaEmoji} alt="Ta-da Emoji" />
|
||||
Congratulations!
|
||||
</h2>
|
||||
<Button
|
||||
className={styles.closeButton}
|
||||
variant="tool"
|
||||
onClick={handleClose}
|
||||
aria-label="Close Modal"
|
||||
>
|
||||
<CloseIcon />
|
||||
</Button>
|
||||
</header>
|
||||
<div className={styles.content}>
|
||||
<p className={styles.text}>
|
||||
Your character has been claimed and is now available:
|
||||
</p>
|
||||
<div className={styles.card}>
|
||||
<img
|
||||
className={styles.cardImage}
|
||||
src={characterGalleryImg}
|
||||
alt="Premade character gallery"
|
||||
/>
|
||||
<Button
|
||||
className={styles.cardButton}
|
||||
href="/characters"
|
||||
color="primary"
|
||||
size="x-small"
|
||||
>
|
||||
Go to My Characters <ChevronIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.card}>
|
||||
<img
|
||||
className={styles.cardImage}
|
||||
src={characterSheetImg}
|
||||
alt="Sample character sheet"
|
||||
/>
|
||||
<Button
|
||||
className={styles.cardButton}
|
||||
href={`/characters/${characterId}`}
|
||||
color="info"
|
||||
size="x-small"
|
||||
>
|
||||
View Your Character <ChevronIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<footer className={styles.footer}>
|
||||
{campaignId && (
|
||||
<Button
|
||||
href={`/campaigns/${campaignId}`}
|
||||
variant="text"
|
||||
color="info"
|
||||
size="small"
|
||||
>
|
||||
or return to your campaign
|
||||
</Button>
|
||||
)}
|
||||
</footer>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import clsx from "clsx";
|
||||
import { FC, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import ChevronRight from "@dndbeyond/fontawesome-cache/svgs/solid/chevron-right.svg";
|
||||
|
||||
import { Button, ButtonProps } from "~/components/Button";
|
||||
import { MaxCharactersDialog } from "~/components/MaxCharactersDialog";
|
||||
import { useAuth } from "~/contexts/Authentication";
|
||||
import { useClaimCharacter } from "~/hooks/useClaimCharacter";
|
||||
import bgImage from "~/images/claim.png";
|
||||
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||
|
||||
import { ClaimConfirmationDialog } from "../ClaimConfirmationDialog";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
/**
|
||||
* This component is used when previewing a premade character. It is displayed on the character
|
||||
* sheet and renders with an onclick when signed in or a link to create an account when not signed
|
||||
* in.
|
||||
*/
|
||||
|
||||
export const ClaimPremadeButton: FC<ButtonProps> = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const createAccountLink = `/create-account?returnUrl=${window.location.pathname}`;
|
||||
const params = new URLSearchParams(globalThis.location.search);
|
||||
const campaignJoinCode = params.get("campaignJoinCode");
|
||||
const isAssigned = params.get("isAssigned") === "true";
|
||||
const [isMaxCharacterOpen, setIsMaxCharacterOpen] = useState(false);
|
||||
const user = useAuth();
|
||||
const { characterSlotLimit, activeCharacterCount } = useSelector(
|
||||
appEnvSelectors.getCharacterSlots
|
||||
);
|
||||
|
||||
const [
|
||||
claimCharacter,
|
||||
isClaimingCharacter,
|
||||
isFinishedClaimingCharacter,
|
||||
newCharacterId,
|
||||
campaignId,
|
||||
] = useClaimCharacter({
|
||||
campaignJoinCode,
|
||||
isAssigned,
|
||||
});
|
||||
const isClaimed = isFinishedClaimingCharacter && !!newCharacterId;
|
||||
// Character slot limit is null for admin accounts
|
||||
const hasOpenSlot =
|
||||
characterSlotLimit === null || activeCharacterCount < characterSlotLimit;
|
||||
|
||||
const handleClick = () => {
|
||||
hasOpenSlot ? claimCharacter() : setIsMaxCharacterOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isClaimed && (
|
||||
<Button
|
||||
className={clsx([styles.claimPremadeButton, className])}
|
||||
href={!user ? createAccountLink : undefined}
|
||||
onClick={!!user ? handleClick : undefined}
|
||||
disabled={isClaimingCharacter}
|
||||
tabIndex={0}
|
||||
{...props}
|
||||
>
|
||||
<img className={styles.bgImage} src={bgImage} alt="" />
|
||||
<div className={styles.content}>
|
||||
{!user && "Create Account to "}
|
||||
Claim Character <ChevronRight />
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
<MaxCharactersDialog
|
||||
open={isMaxCharacterOpen}
|
||||
onClose={() => setIsMaxCharacterOpen(false)}
|
||||
useMyCharactersLink
|
||||
/>
|
||||
{isClaimed && (
|
||||
<ClaimConfirmationDialog
|
||||
characterId={newCharacterId}
|
||||
campaignId={campaignId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
import clsx from "clsx";
|
||||
import { FC, HTMLAttributes, useContext } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
import {
|
||||
CampaignUtils,
|
||||
CharacterCurrencyContract,
|
||||
RuleDataUtils,
|
||||
} from "@dndbeyond/character-rules-engine";
|
||||
|
||||
import { Button } from "~/components/Button";
|
||||
import { NumberDisplay } from "~/components/NumberDisplay";
|
||||
import { useCharacterTheme } from "~/contexts/CharacterTheme";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import CurrencyButton from "~/tools/js/CharacterSheet/components/CurrencyButton";
|
||||
import { CoinManagerContext } from "~/tools/js/Shared/managers/CoinManagerContext";
|
||||
import { InventoryManagerContext } from "~/tools/js/Shared/managers/InventoryManagerContext";
|
||||
import { PaneIdentifierUtils } from "~/tools/js/Shared/utils";
|
||||
|
||||
import { PaneComponentEnum } from "../../Sidebar/types";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
export interface EquipmentOverviewProps extends HTMLAttributes<HTMLDivElement> {
|
||||
activeEquipmentTab: "character" | "party";
|
||||
}
|
||||
|
||||
export const EquipmentOverview: FC<EquipmentOverviewProps> = ({
|
||||
className,
|
||||
activeEquipmentTab,
|
||||
...props
|
||||
}) => {
|
||||
const {
|
||||
partyInfo,
|
||||
currencies,
|
||||
totalCarriedWeight,
|
||||
currentCarriedWeightType,
|
||||
} = useCharacterEngine();
|
||||
const { isDarkMode } = useCharacterTheme();
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
const { coinManager } = useContext(CoinManagerContext);
|
||||
const { inventoryManager } = useContext(InventoryManagerContext);
|
||||
const [searchParams] = useSearchParams();
|
||||
const isVttView = searchParams.get("view") === "vtt";
|
||||
|
||||
const isPartyTab = activeEquipmentTab === "party";
|
||||
const campaignName = partyInfo ? CampaignUtils.getName(partyInfo) : null;
|
||||
|
||||
const weightSpeedLabel = RuleDataUtils.getWeightSpeedTypeLabel(
|
||||
currentCarriedWeightType
|
||||
);
|
||||
|
||||
let coin: CharacterCurrencyContract | null = currencies;
|
||||
//if "coins in containers" is enabled, get the total coin for either the player or party
|
||||
if (isPartyTab && partyInfo) {
|
||||
coin = CampaignUtils.getCoin(partyInfo);
|
||||
}
|
||||
if (coinManager?.canUseCointainers()) {
|
||||
coin =
|
||||
isPartyTab && partyInfo
|
||||
? coinManager.getAllPartyCoin()
|
||||
: coinManager.getAllCharacterCoin();
|
||||
}
|
||||
|
||||
const handlePrimaryButtonClick = (
|
||||
evt: React.MouseEvent | React.KeyboardEvent
|
||||
): void => {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
if (isPartyTab) {
|
||||
paneHistoryStart(PaneComponentEnum.CAMPAIGN);
|
||||
} else {
|
||||
paneHistoryStart(PaneComponentEnum.ENCUMBRANCE);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCurrencyClick = (
|
||||
evt: React.MouseEvent | React.KeyboardEvent
|
||||
): void => {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
|
||||
const partyContainerKey =
|
||||
inventoryManager.getPartyEquipmentContainerDefinitionKey();
|
||||
const characterContainerKey =
|
||||
inventoryManager.getCharacterContainerDefinitionKey();
|
||||
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.CURRENCY,
|
||||
PaneIdentifierUtils.generateCurrencyContext(
|
||||
isPartyTab && partyContainerKey
|
||||
? partyContainerKey
|
||||
: characterContainerKey
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx([styles.equipmentOverview, className])} {...props}>
|
||||
{isPartyTab ? (
|
||||
!isVttView ? (
|
||||
<Button
|
||||
variant="text"
|
||||
themed
|
||||
className={styles.overviewPrimaryButton}
|
||||
onClick={handlePrimaryButtonClick}
|
||||
size="x-small"
|
||||
>
|
||||
{`Campaign: ${campaignName}`}
|
||||
</Button>
|
||||
) : (
|
||||
<p className={styles.overviewText}>{`Campaign: ${campaignName}`}</p>
|
||||
)
|
||||
) : (
|
||||
<Button
|
||||
variant="text"
|
||||
themed
|
||||
className={styles.overviewPrimaryButton}
|
||||
onClick={handlePrimaryButtonClick}
|
||||
size="x-small"
|
||||
>
|
||||
<div className={styles.weightButton}>
|
||||
<span className={styles.weightButtonText}>
|
||||
Weight Carried:{" "}
|
||||
<NumberDisplay
|
||||
className={styles.weight}
|
||||
data-testid="weight-carried-number"
|
||||
type="weightInLb"
|
||||
number={totalCarriedWeight}
|
||||
/>
|
||||
</span>
|
||||
<span className={styles.weightSecondaryText}>
|
||||
{weightSpeedLabel}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
<CurrencyButton
|
||||
handleCurrencyClick={handleCurrencyClick}
|
||||
coin={coin}
|
||||
isDarkMode={isDarkMode}
|
||||
shouldShowPartyInventory={isPartyTab}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { keyBy } from "lodash";
|
||||
import React from "react";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
|
||||
import { Snippet } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
@@ -23,16 +23,18 @@ import {
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { Reference } from "~/components/Reference";
|
||||
import FeatureSnippetActions from "~/tools/js/CharacterSheet/components/FeatureSnippet/FeatureSnippetActions";
|
||||
import FeatureSnippetChoices from "~/tools/js/CharacterSheet/components/FeatureSnippet/FeatureSnippetChoices";
|
||||
import FeatureSnippetInfusionChoices from "~/tools/js/CharacterSheet/components/FeatureSnippet/FeatureSnippetInfusionChoices";
|
||||
import FeatureSnippetOption from "~/tools/js/CharacterSheet/components/FeatureSnippet/FeatureSnippetOption";
|
||||
import FeatureSnippetSpells from "~/tools/js/CharacterSheet/components/FeatureSnippet/FeatureSnippetSpells";
|
||||
import {
|
||||
FeatureSnippetActions,
|
||||
FeatureSnippetChoices,
|
||||
FeatureSnippetInfusionChoices,
|
||||
FeatureSnippetOption,
|
||||
FeatureSnippetSpells,
|
||||
} from "~/tools/js/CharacterSheet/components/FeatureSnippet";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
//TODO: tools/js/CharacterSheet/components/FeatureSnipper components still need to be gradually migrated to FC
|
||||
interface Props {
|
||||
interface Props extends PropsWithChildren {
|
||||
heading: React.ReactNode;
|
||||
dataOriginExtra?: string;
|
||||
extraMeta: Array<string>;
|
||||
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
import clsx from "clsx";
|
||||
import { FC, HTMLAttributes, useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { Button } from "~/components/Button";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { useUnpropagatedClick } from "~/hooks/useUnpropagatedClick";
|
||||
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||
|
||||
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
import clsx from "clsx";
|
||||
import { FC, HTMLAttributes, useEffect, useState, useRef } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import accessibility from "~/styles/accessibility.module.css";
|
||||
import { HP_TEMP_VALUE } from "~/subApps/sheet/constants";
|
||||
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import clsx from "clsx";
|
||||
import { FC, HTMLAttributes, useContext } from "react";
|
||||
import { FC, HTMLAttributes } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
AdvantageIcon,
|
||||
BoxBackground,
|
||||
DigitalDiceWrapper,
|
||||
InitiativeBoxSvg,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
import { DiceTools, RollKind, RollType } from "@dndbeyond/dice";
|
||||
import { GameLogContext } from "@dndbeyond/game-log-components";
|
||||
import {
|
||||
RollKinds,
|
||||
RollTypes,
|
||||
} from "@dndbeyond/pocket-dimension-dice/constants";
|
||||
|
||||
import { NumberDisplay } from "~/components/NumberDisplay";
|
||||
import { useCharacterTheme } from "~/contexts/CharacterTheme";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import a11yStyles from "~/styles/accessibility.module.css";
|
||||
import {
|
||||
appEnvSelectors,
|
||||
characterRollContextSelectors,
|
||||
} from "~/tools/js/Shared/selectors";
|
||||
import { isNotNullOrUndefined } from "~/tools/js/Shared/utils/TypeScript/utils";
|
||||
|
||||
import { PaneComponentEnum } from "../Sidebar/types";
|
||||
import styles from "./styles.module.css";
|
||||
import { NumberDisplay } from "~/components/NumberDisplay";
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
isMobile?: boolean;
|
||||
isTablet?: boolean;
|
||||
}
|
||||
export const InitiativeBox: FC<Props> = ({ isMobile, isTablet, ...props }) => {
|
||||
const [{ messageTargetOptions, defaultMessageTargetOption, userId }] =
|
||||
useContext(GameLogContext);
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
@@ -40,9 +42,10 @@ export const InitiativeBox: FC<Props> = ({ isMobile, isTablet, ...props }) => {
|
||||
|
||||
const {
|
||||
hasInitiativeAdvantage,
|
||||
processedInitiative: initiative,
|
||||
processedInitiative,
|
||||
characterTheme: theme,
|
||||
} = useCharacterEngine();
|
||||
const initiative = Number(processedInitiative);
|
||||
const { isDarkMode } = useCharacterTheme();
|
||||
|
||||
const diceEnabled = useSelector(appEnvSelectors.getDiceEnabled);
|
||||
@@ -71,35 +74,32 @@ export const InitiativeBox: FC<Props> = ({ isMobile, isTablet, ...props }) => {
|
||||
{!isMobile && (
|
||||
<>
|
||||
<BoxBackground StyleComponent={InitiativeBoxSvg} theme={theme} />
|
||||
<h2 style={visuallyHidden}>Initiative</h2>
|
||||
<h2 className={a11yStyles.screenreaderOnly}>Initiative</h2>
|
||||
</>
|
||||
)}
|
||||
|
||||
<DigitalDiceWrapper
|
||||
diceNotation={DiceTools.CustomD20(initiative)}
|
||||
rollType={RollType.Roll}
|
||||
rollAction={"Initiative"}
|
||||
rollKind={hasInitiativeAdvantage ? RollKind.Advantage : RollKind.None}
|
||||
diceEnabled={diceEnabled}
|
||||
themeColor={theme.themeColor}
|
||||
rollContext={rollContext}
|
||||
rollTargetOptions={
|
||||
messageTargetOptions?.entities
|
||||
? Object.values(messageTargetOptions?.entities).filter(
|
||||
isNotNullOrUndefined
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
rollTargetDefault={defaultMessageTargetOption}
|
||||
userId={Number(userId)}
|
||||
diceNotation={`1d20${
|
||||
initiative > 0
|
||||
? `+${initiative}`
|
||||
: initiative !== 0
|
||||
? initiative
|
||||
: ""
|
||||
}`}
|
||||
rollType={RollTypes.Roll}
|
||||
action="Initiative"
|
||||
rollKind={hasInitiativeAdvantage ? RollKinds.Advantage : undefined}
|
||||
isDiceEnabled={diceEnabled}
|
||||
entityId={String(rollContext.entityId)}
|
||||
entityType={String(rollContext.entityType)}
|
||||
name={String(rollContext.name)}
|
||||
>
|
||||
<NumberDisplay
|
||||
number={initiative}
|
||||
type="signed"
|
||||
size="large"
|
||||
className={clsx([
|
||||
isMobile && !theme.isDarkMode && styles.numberColorOverride,
|
||||
])}
|
||||
number={initiative}
|
||||
type="signed"
|
||||
size="large"
|
||||
className={clsx([
|
||||
isMobile && !theme.isDarkMode && styles.numberColorOverride,
|
||||
])}
|
||||
/>
|
||||
</DigitalDiceWrapper>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import clsx from "clsx";
|
||||
import { FC, HTMLAttributes, MouseEvent } from "react";
|
||||
|
||||
import {
|
||||
characterActions,
|
||||
Item,
|
||||
ItemPlan,
|
||||
ItemPlanUtils,
|
||||
ItemUtils,
|
||||
} from "@dndbeyond/character-rules-engine";
|
||||
|
||||
import { Button } from "~/components/Button";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { PaneIdentifierUtils } from "~/tools/js/Shared/utils";
|
||||
import ItemPreview from "~/tools/js/smartComponents/ItemPreview";
|
||||
|
||||
import { PaneComponentEnum } from "../Sidebar/types";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
export interface MagicItemPlansProps extends HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export const MagicItemPlans: FC<MagicItemPlansProps> = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const { characterTheme, itemPlans, maxReplicatedItemsCount } =
|
||||
useCharacterEngine();
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const itemPlanWithMappingsCount = itemPlans.filter((plan) =>
|
||||
ItemPlanUtils.getItem(plan)
|
||||
).length;
|
||||
|
||||
const atMax =
|
||||
maxReplicatedItemsCount !== null &&
|
||||
itemPlanWithMappingsCount >= maxReplicatedItemsCount;
|
||||
|
||||
const handleItemPlansManagePaneShow = (
|
||||
e: MouseEvent<HTMLButtonElement>
|
||||
): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
paneHistoryStart(PaneComponentEnum.ITEM_PLANS_MANAGE);
|
||||
};
|
||||
|
||||
const handleItemClick = (item: Item): void => {
|
||||
const inventoryMappingId = ItemUtils.getMappingId(item);
|
||||
|
||||
if (inventoryMappingId) {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.ITEM_DETAIL,
|
||||
PaneIdentifierUtils.generateItem(inventoryMappingId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReplicateItem = (itemPlan: ItemPlan): void => {
|
||||
const itemDefinitionKey = ItemPlanUtils.getItemDefinitionKey(itemPlan);
|
||||
const itemPlanOriginDefinitionKey =
|
||||
ItemPlanUtils.getOriginDefinitionKey(itemPlan);
|
||||
|
||||
if (itemDefinitionKey) {
|
||||
dispatch(
|
||||
characterActions.itemPlanInventoryMappingCreate(
|
||||
itemDefinitionKey,
|
||||
1,
|
||||
itemPlanOriginDefinitionKey
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx([styles.magicItemPlans, className])} {...props}>
|
||||
{itemPlans.length > 0 ? (
|
||||
itemPlans.map((plan) => {
|
||||
const planOriginDefinitionKey =
|
||||
ItemPlanUtils.getOriginDefinitionKey(plan);
|
||||
const planName = ItemPlanUtils.getItemName(plan);
|
||||
const itemDefinitionKey = ItemPlanUtils.getItemDefinitionKey(plan);
|
||||
const item = ItemPlanUtils.getItem(plan);
|
||||
const planDescription = ItemPlanUtils.getDescription(plan);
|
||||
|
||||
return itemDefinitionKey ? (
|
||||
<div className={styles.itemPlan} key={planOriginDefinitionKey}>
|
||||
<div className={styles.planHeader}>
|
||||
<p className={styles.planName}>{planName}</p>
|
||||
<p className={styles.description}>{planDescription}</p>
|
||||
</div>
|
||||
<div className={styles.itemContent}>
|
||||
{item ? (
|
||||
<div className={styles.itemPreview}>
|
||||
<ItemPreview
|
||||
theme={characterTheme}
|
||||
item={item}
|
||||
onClick={() => handleItemClick(item)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
className={styles.replicateButton}
|
||||
themed
|
||||
size="xx-small"
|
||||
onClick={() => handleReplicateItem(plan)}
|
||||
disabled={atMax}
|
||||
>
|
||||
{atMax ? "At Max Replicated Items" : "Replicate Item"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
})
|
||||
) : (
|
||||
<div className={styles.empty}>
|
||||
No Magic Item plans have been selected for this character
|
||||
<Button
|
||||
className={styles.emptyButton}
|
||||
themed
|
||||
size="x-small"
|
||||
onClick={handleItemPlansManagePaneShow}
|
||||
>
|
||||
Add Magic Item Plans
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,8 +4,8 @@ import { createPortal } from "react-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { rulesEngineSelectors } from "@dndbeyond/character-rules-engine";
|
||||
import { Dialog, DialogProps } from "@dndbeyond/ttui/components/Dialog";
|
||||
|
||||
import { Dialog, DialogProps } from "~/components/Dialog";
|
||||
import { useSheetContext } from "~/subApps/sheet/contexts/Sheet";
|
||||
import {
|
||||
ActionsIcon,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import clsx from "clsx";
|
||||
import { FC } from "react";
|
||||
|
||||
import GearIcon from "@dndbeyond/fontawesome-cache/svgs/solid/gear.svg";
|
||||
|
||||
import { Button, ButtonProps } from "~/components/Button";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
|
||||
import { PaneComponentEnum } from "../Sidebar/types";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
export interface SettingsButtonProps extends ButtonProps {
|
||||
context: string;
|
||||
}
|
||||
|
||||
export const SettingsButton: FC<SettingsButtonProps> = ({
|
||||
className,
|
||||
context,
|
||||
...props
|
||||
}) => {
|
||||
const { pane } = useSidebar();
|
||||
const { SETTINGS } = PaneComponentEnum;
|
||||
|
||||
const handleClick = () => pane?.paneHistoryPush(SETTINGS, { context });
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={clsx([styles.settingsButton, className])}
|
||||
onClick={handleClick}
|
||||
variant="text"
|
||||
size="small"
|
||||
themed
|
||||
{...props}
|
||||
>
|
||||
<GearIcon />
|
||||
Settings
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
+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",
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import clsx from "clsx";
|
||||
import { FC, HTMLAttributes, useEffect, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import VideoIcon from "@dndbeyond/fontawesome-cache/svgs/solid/video.svg";
|
||||
import CloseIcon from "@dndbeyond/fontawesome-cache/svgs/solid/x.svg";
|
||||
|
||||
import { Button } from "~/components/Button";
|
||||
import { Dialog } from "~/components/Dialog";
|
||||
import { MaxCharactersDialog } from "~/components/MaxCharactersDialog";
|
||||
import { useAuth } from "~/contexts/Authentication";
|
||||
import { useClaimCharacter } from "~/hooks/useClaimCharacter";
|
||||
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||
|
||||
import { ClaimConfirmationDialog } from "../ClaimConfirmationDialog";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
export interface WatchTourDialogProps extends HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export const WatchTourDialog: FC<WatchTourDialogProps> = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isMaxCharacterMessageOpen, setIsMaxCharacterMessageOpen] =
|
||||
useState(false);
|
||||
const user = useAuth();
|
||||
const { characterSlotLimit, activeCharacterCount } = useSelector(
|
||||
appEnvSelectors.getCharacterSlots
|
||||
);
|
||||
|
||||
// Character slot limit is null for admin accounts
|
||||
const hasOpenSlot =
|
||||
characterSlotLimit === null || activeCharacterCount < characterSlotLimit;
|
||||
const signedIn = !!user;
|
||||
const signupLink = `/create-account?returnUrl=${window.location.pathname}`;
|
||||
const params = new URLSearchParams(globalThis.location.search);
|
||||
const campaignJoinCode = params.get("campaignJoinCode");
|
||||
const isAssigned = params.get("isAssigned") !== "false";
|
||||
|
||||
const [
|
||||
claimCharacter,
|
||||
isClaimingCharacter,
|
||||
isFinishedClaimingCharacter,
|
||||
newCharacterId,
|
||||
campaignId,
|
||||
] = useClaimCharacter({
|
||||
campaignJoinCode,
|
||||
isAssigned,
|
||||
});
|
||||
|
||||
const handleOpen = () => setIsOpen(true);
|
||||
|
||||
const handleClose = () => setIsOpen(false);
|
||||
|
||||
const handleClaim = () => {
|
||||
if (hasOpenSlot) {
|
||||
claimCharacter();
|
||||
} else {
|
||||
setIsMaxCharacterMessageOpen(true);
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isFinishedClaimingCharacter) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [isFinishedClaimingCharacter]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={clsx([styles.watchTourDialog, className])} {...props}>
|
||||
<Button
|
||||
className={styles.trigger}
|
||||
onClick={handleOpen}
|
||||
color="secondary"
|
||||
size="x-small"
|
||||
>
|
||||
<VideoIcon />
|
||||
Watch Tour
|
||||
</Button>
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
open={isOpen}
|
||||
onClose={handleClose}
|
||||
modal
|
||||
>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>D&D Beyond Character Sheet Tour</h2>
|
||||
<Button
|
||||
className={styles.closeButton}
|
||||
variant="tool"
|
||||
onClick={handleClose}
|
||||
aria-label="Close Modal"
|
||||
>
|
||||
<CloseIcon />
|
||||
</Button>
|
||||
</header>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.videoWrapper}>
|
||||
<iframe
|
||||
className={styles.video}
|
||||
src="https://www.youtube-nocookie.com/embed/ChYPIdCrBdE"
|
||||
title="D&D Beyond Sheet Tour"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
For a complete guide to getting your game started, see{" "}
|
||||
<a href="https://www.dndbeyond.com/posts/754-how-to-play-dungeons-dragons-using-d-d-beyond">
|
||||
How to Play Dungeons & Dragons Using D&D Beyond
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
<footer className={styles.footer}>
|
||||
<Button
|
||||
className={styles.claimButton}
|
||||
{...(!signedIn && { href: signupLink })}
|
||||
{...(signedIn && { onClick: handleClaim })}
|
||||
disabled={isClaimingCharacter}
|
||||
color="info"
|
||||
size="x-small"
|
||||
>
|
||||
Claim Character
|
||||
</Button>
|
||||
</footer>
|
||||
</Dialog>
|
||||
</div>
|
||||
<MaxCharactersDialog
|
||||
open={isMaxCharacterMessageOpen}
|
||||
onClose={() => setIsMaxCharacterMessageOpen(false)}
|
||||
useMyCharactersLink
|
||||
/>
|
||||
{isFinishedClaimingCharacter && !!newCharacterId && (
|
||||
<ClaimConfirmationDialog
|
||||
characterId={newCharacterId ?? 0}
|
||||
campaignId={campaignId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,10 @@
|
||||
import { createContext, FC, useContext, useState } from "react";
|
||||
import {
|
||||
createContext,
|
||||
FC,
|
||||
PropsWithChildren,
|
||||
useContext,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { MobileSections } from "../../types";
|
||||
|
||||
@@ -11,7 +17,7 @@ export interface SheetContextType {
|
||||
|
||||
export const SheetContext = createContext<SheetContextType>(null!);
|
||||
|
||||
export const SheetProvider: FC = ({ children }) => {
|
||||
export const SheetProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const [mobileActiveSectionId, setMobileActiveSectionId] =
|
||||
useState<MobileSections>("main");
|
||||
const [swipedAmount, setSwipedAmount] = useState(0);
|
||||
|
||||
Reference in New Issue
Block a user