New source found from dndbeyond.com

This commit is contained in:
2025-06-18 01:00:16 -07:00
parent 451d940294
commit 0b403376c5
30 changed files with 496 additions and 418 deletions
@@ -1,9 +1,13 @@
import { FocusEvent } from "react";
import {
AbilityManager,
HelperUtils,
} from "@dndbeyond/character-rules-engine/es";
import { FormInputField } from "~/tools/js/Shared/components/common/FormInputField";
import { InputField } from "~/subApps/builder/components/InputField";
import styles from "./styles.module.css";
interface Props {
abilities: Array<AbilityManager>;
@@ -16,14 +20,19 @@ export default function AbilityScoreManagerManual({
minScore = 3,
maxScore = 18,
}: Props) {
const handleTransformValueOnBlur = (value: string): number | null => {
const parsedValue = HelperUtils.parseInputInt(value);
const handleBlur = (
e: FocusEvent<HTMLInputElement>,
abilityScore: AbilityManager
) => {
// Get the int value from the input
const parsedValue = HelperUtils.parseInputInt(e.target.value);
// Create a clamped variable to hold the clamped value
let clampedValue: number | null = null;
if (parsedValue !== null) {
// If the value is valid, clamp it to the min and max scores
if (parsedValue !== null)
clampedValue = HelperUtils.clampInt(parsedValue, minScore, maxScore);
}
return clampedValue;
// Set the score change from the clampped value
abilityScore.handleScoreChange(clampedValue);
};
return (
@@ -35,11 +44,17 @@ export default function AbilityScoreManagerManual({
key={abilityScore.getId()}
data-stat-id={abilityScore.getId()}
>
<FormInputField
transformValueOnBlur={handleTransformValueOnBlur}
onBlur={(value) => abilityScore.handleScoreChange(Number(value))}
<InputField
className={styles.input}
type="number"
label={abilityScore.getLabel() ?? ""}
initialValue={abilityScore.getBaseScore()}
inputProps={{
min: minScore,
max: maxScore,
autoComplete: "off",
}}
onBlur={(e) => handleBlur(e, abilityScore)}
/>
<div className="ability-score-manager-stat-total">
Total: {abilityScore.getTotalScore() ?? "--"}
@@ -1,6 +1,6 @@
import axios, { Canceler } from "axios";
import { orderBy } from "lodash";
import React, { HTMLAttributes, useContext } from "react";
import React, { FocusEvent, HTMLAttributes, useContext } from "react";
import { DispatchProp } from "react-redux";
import { LoadingPlaceholder, Select } from "@dndbeyond/character-components/es";
@@ -52,12 +52,12 @@ import { HtmlContent } from "~/components/HtmlContent";
import { Link } from "~/components/Link";
import { useSource } from "~/hooks/useSource";
import { EditorWithDialog } from "~/subApps/builder/components/EditorWithDialog";
import { InputField } from "~/subApps/builder/components/InputField";
import { RouteKey } from "~/subApps/builder/constants";
import {
ModalData,
useModalManager,
} from "~/subApps/builder/contexts/ModalManager";
import { FormInputField } from "~/tools/js/Shared/components/common/FormInputField";
import { DetailChoice } from "~/tools/js/Shared/containers/DetailChoice";
import { toastMessageActions } from "../../../../Shared/actions";
@@ -195,7 +195,7 @@ class CustomBackgroundManager extends React.PureComponent<
<div className="custom-background-property-value">
<input
type="text"
defaultValue={name ? name : ""}
defaultValue={name || ""}
onBlur={this.handleTextInputBlur.bind(this, "name")}
/>
</div>
@@ -204,7 +204,7 @@ class CustomBackgroundManager extends React.PureComponent<
<div className="custom-background-property custom-background-property-textarea">
<div className="custom-background-property-value">
<textarea
defaultValue={description ? description : ""}
defaultValue={description || ""}
onBlur={this.handleTextInputBlur.bind(this, "description")}
/>
</div>
@@ -471,35 +471,35 @@ class DescriptionManage extends React.PureComponent<Props, State> {
dispatch(characterActions.traitSet(traitType, content));
};
handleHairChange = (value: string): void => {
handleHairChange = (e: FocusEvent<HTMLInputElement>): void => {
const { dispatch } = this.props;
dispatch(characterActions.hairSet(value));
dispatch(characterActions.hairSet(e.target.value));
};
handleSkinChange = (value: string): void => {
handleSkinChange = (e: FocusEvent<HTMLInputElement>): void => {
const { dispatch } = this.props;
dispatch(characterActions.skinSet(value));
dispatch(characterActions.skinSet(e.target.value));
};
handleEyesChange = (value: string): void => {
handleEyesChange = (e: FocusEvent<HTMLInputElement>): void => {
const { dispatch } = this.props;
dispatch(characterActions.eyesSet(value));
dispatch(characterActions.eyesSet(e.target.value));
};
handleHeightChange = (value: string): void => {
handleHeightChange = (e: FocusEvent<HTMLInputElement>): void => {
const { dispatch } = this.props;
dispatch(characterActions.heightSet(value));
dispatch(characterActions.heightSet(e.target.value));
};
handleWeightChange = (value: string): void => {
handleWeightChange = (e: FocusEvent<HTMLInputElement>): void => {
const { dispatch } = this.props;
let parsedValue = HelperUtils.parseInputInt(value);
let parsedValue = this.handleTransformValueToNumberOnBlur(e.target.value);
dispatch(characterActions.weightSet(parsedValue));
};
handleAgeChange = (value: string): void => {
handleAgeChange = (e: FocusEvent<HTMLInputElement>): void => {
const { dispatch } = this.props;
let parsedValue = HelperUtils.parseInputInt(value);
let parsedValue = this.handleTransformValueToNumberOnBlur(e.target.value);
dispatch(characterActions.ageSet(parsedValue));
};
@@ -517,9 +517,9 @@ class DescriptionManage extends React.PureComponent<Props, State> {
return clampedValue;
};
handleGenderChange = (value: string): void => {
handleGenderChange = (e: FocusEvent<HTMLInputElement>): void => {
const { dispatch } = this.props;
dispatch(characterActions.genderSet(value));
dispatch(characterActions.genderSet(e.target.value));
};
handleBackgroundChangePromise = (
@@ -608,9 +608,9 @@ class DescriptionManage extends React.PureComponent<Props, State> {
dispatch(characterActions.lifestyleSet(HelperUtils.parseInputInt(value)));
};
handleFaithChange = (value: string): void => {
handleFaithChange = (e: FocusEvent<HTMLInputElement>): void => {
const { dispatch } = this.props;
dispatch(characterActions.faithSet(value));
dispatch(characterActions.faithSet(e.target.value));
};
handleCustomBackgroundSave = (properties: any): void => {
@@ -716,7 +716,7 @@ class DescriptionManage extends React.PureComponent<Props, State> {
<DetailChoice
{...choice}
choice={choice}
key={choiceId ? choiceId : ""}
key={choiceId || ""}
options={availableOptions}
onChange={this.handleChoiceChange}
choiceInfo={choiceInfo}
@@ -1035,7 +1035,7 @@ class DescriptionManage extends React.PureComponent<Props, State> {
) : (
<HtmlContent
className="description-manage-background-description"
html={shortDescription ? shortDescription : ""}
html={shortDescription || ""}
withoutTooltips
/>
)}
@@ -1325,17 +1325,17 @@ class DescriptionManage extends React.PureComponent<Props, State> {
{alignmentDescriptionNode}
</div>
<div className="description-manage-faith">
<FormInputField
<InputField
label="Faith"
initialValue={faith}
onBlur={this.handleFaithChange}
inputAttributes={
maxLength={512}
inputProps={
{
spellCheck: false,
autoComplete: "off",
} as HTMLAttributes<HTMLInputElement>
}
maxLength={512}
/>
</div>
<div className="description-manage-lifestyle">
@@ -1367,47 +1367,52 @@ class DescriptionManage extends React.PureComponent<Props, State> {
]}
variant="paper"
>
<FormInputField
<InputField
className={styles.inputField}
label="Hair"
initialValue={hair}
onBlur={this.handleHairChange}
maxLength={50}
/>
<FormInputField
<InputField
className={styles.inputField}
label="Skin"
initialValue={skin}
onBlur={this.handleSkinChange}
maxLength={50}
/>
<FormInputField
<InputField
className={styles.inputField}
label="Eyes"
initialValue={eyes}
onBlur={this.handleEyesChange}
maxLength={50}
/>
<FormInputField
<InputField
className={styles.inputField}
label="Height"
initialValue={height}
onBlur={this.handleHeightChange}
maxLength={50}
/>
<FormInputField
<InputField
className={styles.inputField}
label="Weight (lbs)"
type={"number"}
type="number"
initialValue={weight}
onBlur={this.handleWeightChange}
transformValueOnBlur={this.handleTransformValueToNumberOnBlur}
inputAttributes={numberInputAttributes}
inputProps={numberInputAttributes}
/>
<FormInputField
<InputField
className={styles.inputField}
label="Age (Years)"
type={"number"}
type="number"
initialValue={age}
onBlur={this.handleAgeChange}
transformValueOnBlur={this.handleTransformValueToNumberOnBlur}
inputAttributes={numberInputAttributes}
inputProps={numberInputAttributes}
/>
<FormInputField
<InputField
className={styles.inputField}
label="Gender"
initialValue={gender}
onBlur={this.handleGenderChange}
@@ -1,5 +1,10 @@
import { Typography } from "@mui/material";
import React, { ChangeEvent, HTMLAttributes, ReactNode } from "react";
import React, {
ChangeEvent,
FocusEvent,
HTMLAttributes,
ReactNode,
} from "react";
import { DispatchProp } from "react-redux";
import {
@@ -24,12 +29,12 @@ import {
import { Dice } from "@dndbeyond/dice";
import { SourceCategoryDescription } from "~/constants";
import { InputField } from "~/subApps/builder/components/InputField";
import { RouteKey } from "~/subApps/builder/constants";
import {
ModalData,
useModalManager,
} from "~/subApps/builder/contexts/ModalManager";
import { FormInputField } from "~/tools/js/Shared/components/common/FormInputField";
import UserRoles from "~/tools/js/Shared/constants/UserRoles";
import PrivacyTypeRadio from "~/tools/js/smartComponents/PrivacyTypeRadio";
@@ -723,66 +728,67 @@ class HomeBasicInfo extends React.PureComponent<Props> {
this.forceUpdate();
}}
/>
<FormInputField
<InputField
label="Long Description"
initialValue={premadeInfo.definition.longDescription}
inputAttributes={inputAttributes}
onBlur={(value: string) => {
premadeInfo.definition.longDescription = value;
inputProps={inputAttributes}
onBlur={(e: FocusEvent<HTMLInputElement>) => {
premadeInfo.definition.longDescription = e.target.value;
this.handlePremadeInfoChanged(premadeInfo);
}}
/>
<FormInputField
<InputField
label="Short Description"
initialValue={premadeInfo.definition.shortDescription}
inputAttributes={inputAttributes}
onBlur={(value: string) => {
premadeInfo.definition.shortDescription = value;
inputProps={inputAttributes}
onBlur={(e: FocusEvent<HTMLInputElement>) => {
premadeInfo.definition.shortDescription = e.target.value;
this.handlePremadeInfoChanged(premadeInfo);
}}
/>
<FormInputField
<InputField
label="Image Url"
initialValue={premadeInfo.definition.imageUrl}
inputAttributes={inputAttributes}
onBlur={(value: string) => {
premadeInfo.definition.imageUrl = value;
inputProps={inputAttributes}
onBlur={(e: FocusEvent<HTMLInputElement>) => {
premadeInfo.definition.imageUrl = e.target.value;
this.handlePremadeInfoChanged(premadeInfo);
}}
/>
<FormInputField
<InputField
label="Image Alt Text"
initialValue={premadeInfo.definition.imageAltText}
inputAttributes={inputAttributes}
onBlur={(value: string) => {
premadeInfo.definition.imageAltText = value;
inputProps={inputAttributes}
onBlur={(e: FocusEvent<HTMLInputElement>) => {
premadeInfo.definition.imageAltText = e.target.value;
this.handlePremadeInfoChanged(premadeInfo);
}}
/>
<FormInputField
<InputField
label="Mobile Image Url"
initialValue={premadeInfo.definition.mobileImageUrl}
inputAttributes={inputAttributes}
onBlur={(value: string) => {
premadeInfo.definition.mobileImageUrl = value;
inputProps={inputAttributes}
onBlur={(e: FocusEvent<HTMLInputElement>) => {
premadeInfo.definition.mobileImageUrl = e.target.value;
this.handlePremadeInfoChanged(premadeInfo);
}}
/>
<FormInputField
<InputField
label="Mobile Image Accessibility"
initialValue={premadeInfo.definition.mobileImageAccessibility}
inputAttributes={inputAttributes}
onBlur={(value: string) => {
premadeInfo.definition.mobileImageAccessibility = value;
inputProps={inputAttributes}
onBlur={(e: FocusEvent<HTMLInputElement>) => {
premadeInfo.definition.mobileImageAccessibility =
e.target.value;
this.handlePremadeInfoChanged(premadeInfo);
}}
/>
<FormInputField
<InputField
label="Theme Color Hex Code"
initialValue={premadeInfo.definition.themeColor}
inputAttributes={inputAttributes}
onBlur={(value: string) => {
premadeInfo.definition.themeColor = value;
inputProps={inputAttributes}
onBlur={(e: FocusEvent<HTMLInputElement>) => {
premadeInfo.definition.themeColor = e.target.value;
this.handlePremadeInfoChanged(premadeInfo);
}}
/>
@@ -62,8 +62,6 @@ interface Props {
theme?: CharacterTheme;
}
const infoItemProps = { role: "listItem", inline: true };
export const ActionDetail = ({
showCustomize = true,
largePoolMinAmount = 11,
@@ -79,6 +77,8 @@ export const ActionDetail = ({
theme,
inventoryLookup,
}: Props) => {
const infoItemProps = { role: "listItem", inline: true };
const handleRemoveCustomizations = () => {
if (onCustomizationsRemove) {
onCustomizationsRemove();
@@ -427,17 +427,17 @@ export const ActionDetail = ({
</InfoItem>
)}
{attackSubtypeId && (
<InfoItem label="Attack Type:">
<InfoItem label="Attack Type:" {...infoItemProps}>
{ActionUtils.getAttackSubtypeName(action)}
</InfoItem>
)}
{requiresAttackRoll && (
<InfoItem label="To Hit:">
<InfoItem label="To Hit:" {...infoItemProps}>
<NumberDisplay type="signed" number={toHit} />
</InfoItem>
)}
{requiresSavingThrow && (
<InfoItem label="Attack/Save:">
<InfoItem label="Attack/Save:" {...infoItemProps}>
{saveStateId !== null
? RuleDataUtils.getStatNameById(saveStateId, ruleData)
: ""}{" "}
@@ -446,14 +446,14 @@ export const ActionDetail = ({
)}
{renderDamageProperties()}
{requiresAttackRoll && (
<InfoItem label="Stat:">
<InfoItem label="Stat:" {...infoItemProps}>
{statId === null
? "--"
: RuleDataUtils.getStatNameById(statId, ruleData)}
</InfoItem>
)}
{rangeAreas.length > 0 && (
<InfoItem label="Range/Area:">
<InfoItem label="Range/Area:" {...infoItemProps}>
{rangeAreas.map((node, idx) => (
<React.Fragment key={idx}>
{node}
@@ -462,8 +462,16 @@ export const ActionDetail = ({
))}
</InfoItem>
)}
{isProficient && <InfoItem label="Proficient:">Yes</InfoItem>}
{notes && <InfoItem label="Notes:">{notes}</InfoItem>}
{isProficient && (
<InfoItem label="Proficient:" {...infoItemProps}>
Yes
</InfoItem>
)}
{notes && (
<InfoItem label="Notes:" {...infoItemProps}>
{notes}
</InfoItem>
)}
</div>
);
};
@@ -92,7 +92,7 @@ export default function SpellManagerContainer({
const getData = useCallback(
async function getData() {
let classSpellMap = await spellsManager.getSpellShoppe();
let classSpellMap = await spellsManager.getSpellShoppe(true);
if (!classSpellMap[charClassId] || shouldFetch) {
classSpellMap = await spellsManager.getSpellShoppe(true);
}
@@ -1,139 +0,0 @@
import clsx from "clsx";
import {
ChangeEvent,
FC,
FocusEvent,
HTMLAttributes,
useEffect,
useRef,
useState,
} from "react";
import { v4 as uuidv4 } from "uuid";
import { useMaxLengthErrorHandling } from "~/hooks/useErrorHandling/useMaxLengthErrorHandling";
export interface FormInputFieldProps
extends Omit<
HTMLAttributes<HTMLDivElement>,
"onChange" | "onBlur" | "onFocus"
> {
label: string;
type?: string;
placeholder?: string;
initialValue?: string | number | null;
inputAttributes?: HTMLAttributes<HTMLInputElement>;
maxLength?: number;
maxLengthErrorMsg?: string;
variant?: "default" | "small";
// Handlers
onChange?: (value: string | number | null) => void;
onBlur?: (value: string | number | null) => void;
onFocus?: (value: string | number | null) => void;
transformValueOnChange?: (value: string) => string | number | null;
transformValueOnBlur?: (value: string) => string | number | null;
transformValueOnFocus?: (value: string) => string | number | null;
}
export const FormInputField: FC<FormInputFieldProps> = ({
label, // req
type = "text",
placeholder = "",
initialValue = "",
inputAttributes = {},
maxLength,
maxLengthErrorMsg = "",
...handlers
}) => {
const prevInitialValue = useRef(initialValue);
const initialLengthErrorState = maxLength
? (initialValue?.toString().length ?? 0) >= maxLength
: false;
const { MaxLengthErrorMessage, hideError, handleMaxLengthErrorMsg } =
useMaxLengthErrorHandling(
initialLengthErrorState,
maxLength ?? null,
maxLengthErrorMsg
);
const guid = `FIF_${uuidv4()}`;
const [value, setValue] = useState(initialValue);
const handleOnChange = (evt: ChangeEvent<HTMLInputElement>): void => {
const { onChange = null, transformValueOnChange = null } = handlers;
const { value: targetElementValue } = evt.target;
const value = transformValueOnChange
? transformValueOnChange?.(targetElementValue)
: targetElementValue;
onChange?.(value);
setValue(value);
if (value !== null) {
handleMaxLengthErrorMsg(value.toString());
}
};
const handleOnBlur = (evt: FocusEvent<HTMLInputElement>) => {
const { onBlur = null, transformValueOnBlur = null } = handlers;
const { value: targetElementValue } = evt.target;
const value = transformValueOnBlur
? transformValueOnBlur?.(targetElementValue)
: targetElementValue;
onBlur?.(value);
if (value !== targetElementValue) {
setValue(value);
}
// Always hide any length warnings when leaving the field since you can't go over the limit anyways
hideError();
};
const handleOnFocus = (evt: FocusEvent<HTMLInputElement>) => {
const { onFocus = null, transformValueOnFocus = null } = handlers;
const { value: targetElementValue } = evt.target;
const value = transformValueOnFocus
? transformValueOnFocus?.(targetElementValue)
: targetElementValue;
onFocus?.(value);
if (value !== targetElementValue) {
setValue(value);
}
if (value !== null) {
handleMaxLengthErrorMsg(value.toString());
}
};
useEffect(() => {
if (initialValue !== prevInitialValue.current) {
setValue(initialValue ?? "");
prevInitialValue.current = initialValue;
}
}, [initialValue]);
return (
<div className={clsx(["builder-field", "form-input-field"])}>
<span className="builder-field-label">
<label
className="builder-field-heading form-input-field-label"
htmlFor={guid}
>
{label}
</label>
</span>
<span className="builder-field-input">
<input
className="builder-field-value"
id={guid}
type={type}
placeholder={placeholder}
onChange={handleOnChange}
onBlur={handleOnBlur}
onFocus={handleOnFocus}
value={value ?? ""}
maxLength={maxLength || undefined}
{...inputAttributes}
/>
</span>
<MaxLengthErrorMessage />
</div>
);
};
@@ -1,44 +0,0 @@
import clsx from "clsx";
import * as React from "react";
import { PrerequisiteFailure } from "@dndbeyond/character-rules-engine/es";
interface Props {
className: string;
failures: Array<Array<PrerequisiteFailure>>;
}
export default class PrerequisiteFailureSummary extends React.PureComponent<
Props,
{}
> {
static defaultProps = {
className: "",
};
render() {
const { failures, className } = this.props;
let strings: string = failures
.map((failure) =>
failure
.map((failureAnd) => failureAnd.data.requiredDescription)
.reduce((acc, desc, idx) => {
let connector: string = "";
if (idx < failure.length - 2) {
connector = ", ";
} else if (idx < failure.length - 1) {
connector = " and ";
}
acc += `${desc}${connector}`;
return acc;
}, "")
)
.join(" or ");
return (
<div className={clsx(["ddbc-prerequisite-failure-summary", className])}>
Missing: {strings.length > 0 ? strings : "Unknown"}
</div>
);
}
}
@@ -1,4 +0,0 @@
import PrerequisiteFailureSummary from "./PrerequisiteFailureSummary";
export default PrerequisiteFailureSummary;
export { PrerequisiteFailureSummary };