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,5 +1,5 @@
import clsx from "clsx";
import { FC, HTMLAttributes, useMemo, useState } from "react";
import { FC, FocusEvent, useMemo, useState } from "react";
import { useDispatch } from "react-redux";
import {
@@ -19,8 +19,8 @@ import {
HP_BONUS_VALUE,
HP_OVERRIDE_MAX_VALUE,
} from "~/subApps/sheet/constants";
import { FormInputField } from "~/tools/js/Shared/components/common/FormInputField";
import { InputField } from "../InputField";
import styles from "./styles.module.css";
export interface HpManageModalProps
@@ -65,52 +65,55 @@ export const HpManageModal: FC<HpManageModalProps> = ({
reset();
};
const transformBaseHp = (value: string): number => {
const handleBaseHp = (e: FocusEvent<HTMLInputElement>) => {
let parsedNumber = HelperUtils.parseInputInt(
value,
e.target.value,
RuleDataUtils.getMinimumHpTotal(ruleData)
);
return HelperUtils.clampInt(
const clampedValue = HelperUtils.clampInt(
parsedNumber,
RuleDataUtils.getMinimumHpTotal(ruleData),
HP_BASE_MAX_VALUE
);
setBaseHp(clampedValue);
};
const transformBonusHp = (value: string): number | null => {
let parsedNumber = HelperUtils.parseInputInt(value);
if (parsedNumber === null) {
return parsedNumber;
}
return HelperUtils.clampInt(
const handleBonusHp = (e: FocusEvent<HTMLInputElement>) => {
let parsedNumber = HelperUtils.parseInputInt(e.target.value);
if (parsedNumber === null) return parsedNumber;
const clampedValue = HelperUtils.clampInt(
parsedNumber,
HP_BONUS_VALUE.MIN,
HP_BONUS_VALUE.MAX
);
setBonusHp(clampedValue);
};
const transformOverrideHp = (value: string): number | null => {
let parsedNumber = HelperUtils.parseInputInt(value, null);
const handleOverrideHp = (e: FocusEvent<HTMLInputElement>) => {
let parsedNumber = HelperUtils.parseInputInt(e.target.value, null);
if (parsedNumber === null) {
setOverrideHp(null);
return parsedNumber;
}
return HelperUtils.clampInt(
const clampedValue = HelperUtils.clampInt(
parsedNumber,
RuleDataUtils.getMinimumHpTotal(ruleData)
);
};
const handleOverrideHpUpdate = (value: number | null): void => {
const newOverride =
value === null
? null
: HelperUtils.clampInt(
value,
RuleDataUtils.getMinimumHpTotal(ruleData),
HP_OVERRIDE_MAX_VALUE
);
const clampedOverride = HelperUtils.clampInt(
clampedValue,
RuleDataUtils.getMinimumHpTotal(ruleData),
HP_OVERRIDE_MAX_VALUE
);
setOverrideHp(newOverride);
setOverrideHp(clampedOverride);
};
const totalHp = useMemo(() => {
@@ -148,35 +151,33 @@ export const HpManageModal: FC<HpManageModalProps> = ({
<span className={styles.baseHpValue}>{baseHp}</span>
</p>
) : (
<FormInputField
<InputField
className={styles.inputField}
label="Rolled HP"
initialValue={baseHp}
type="number"
onBlur={(value) => setBaseHp(value as number)}
transformValueOnBlur={transformBaseHp}
onBlur={handleBaseHp}
/>
)}
<FormInputField
<InputField
className={styles.inputField}
label="HP Modifier"
initialValue={bonusHp}
type="number"
placeholder={"--"}
onBlur={(value) => setBonusHp(value as number)}
transformValueOnBlur={transformBonusHp}
placeholder="--"
onBlur={handleBonusHp}
/>
<FormInputField
inputAttributes={
{
min: RuleDataUtils.getMinimumHpTotal(ruleData),
max: HP_OVERRIDE_MAX_VALUE,
} as HTMLAttributes<HTMLInputElement>
}
<InputField
className={styles.inputField}
label="Override HP"
initialValue={overrideHp}
type="number"
placeholder={"--"}
onBlur={handleOverrideHpUpdate}
transformValueOnBlur={transformOverrideHp}
inputProps={{
min: RuleDataUtils.getMinimumHpTotal(ruleData),
max: HP_OVERRIDE_MAX_VALUE,
}}
onBlur={handleOverrideHp}
/>
</div>
@@ -0,0 +1,132 @@
import clsx from "clsx";
import {
ChangeEvent,
FC,
FocusEvent,
HTMLAttributes,
HTMLInputTypeAttribute,
useEffect,
useState,
} from "react";
import { v4 as uuidv4 } from "uuid";
import styles from "./styles.module.css";
interface InputProps extends HTMLAttributes<HTMLInputElement> {
min?: number;
max?: number;
autoComplete?: string;
}
export interface InputFieldProps extends HTMLAttributes<HTMLInputElement> {
errorMessage?: string;
initialValue?: string | number | null;
label: string;
maxLength?: number;
placeholder?: string;
type?: HTMLInputTypeAttribute;
inputProps?: InputProps;
}
export const InputField: FC<InputFieldProps> = ({
className,
errorMessage,
initialValue = "",
inputProps,
label,
maxLength,
onBlur,
onChange,
onFocus,
placeholder,
type = "text",
...props
}) => {
// Set default error message if not provided
errorMessage ||= `The maximum length is ${maxLength} characters.`;
// Set up component state and a shared id
const [value, setValue] = useState(initialValue);
const [showError, setShowError] = useState(false);
const id = props.id ?? `input-field-${uuidv4()}`;
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const valueLength = e.target.value.length;
if (maxLength) {
// If the value length exceeds maxLength, show error
if (valueLength > maxLength) {
setShowError(true);
//return; Prevent further processing if maxLength exceeded
return;
}
// If the value length is equal to maxLength, show error
if (valueLength === maxLength) {
setShowError(true);
}
// If the value length is less than maxLength, hide error
if (valueLength < maxLength) {
setShowError(false);
}
}
// Update component state
setValue(e.target.value);
// Call the provided onChange handler if it exists
if (onChange) onChange(e);
return;
};
const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
const intValue = parseInt(e.target.value);
// If entered value is below the minimum, reset to minimum
if (!isNaN(intValue) && inputProps?.min && intValue < inputProps?.min) {
setValue(inputProps.min);
}
// If entered value is above the maximum and no initialValue exists, reset to maximum
if (!isNaN(intValue) && inputProps?.max && intValue > inputProps?.max) {
setValue(inputProps.max);
}
// Call the provided onBlur handler if it exists
if (onBlur) onBlur(e);
// Reset error state on blur
setShowError(false);
};
const handleFocus = (e: FocusEvent<HTMLInputElement>) => {
// Call the provided onFocus handler if it exists
if (onFocus) onFocus(e);
// Check for maxLength error
const valueLength = e.target.value.length;
if (maxLength && valueLength > maxLength - 1) setShowError(true);
};
useEffect(() => {
// Update the value when initialValue changes
setValue(initialValue);
// Reset error state when initial value changes
setShowError(false);
}, [initialValue]);
const inputAttrs = { id, type, placeholder };
return (
<div className={clsx([styles.inputField, className])} {...props}>
<label className={styles.label} htmlFor={id}>
{label}
</label>
<input
className={styles.input}
value={value ?? ""}
onBlur={handleBlur}
onChange={handleChange}
onFocus={handleFocus}
{...inputAttrs}
{...inputProps}
/>
{showError && <span className={styles.error}>{errorMessage}</span>}
</div>
);
};
@@ -16,11 +16,11 @@ import {
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import { builderActions } from "~/tools/js/CharacterBuilder/actions";
import { builderSelectors } from "~/tools/js/CharacterBuilder/selectors";
import { FormInputField } from "~/tools/js/Shared/components/common/FormInputField";
import { PortraitManager } from "~/tools/js/Shared/containers/panes/DecoratePane";
import Tooltip from "~/tools/js/commonComponents/Tooltip";
import { CharacterAvatarPortrait } from "~/tools/js/smartComponents/CharacterAvatar";
import { InputField } from "../InputField";
import styles from "./styles.module.css";
interface Props extends HTMLAttributes<HTMLDivElement> {
@@ -56,7 +56,7 @@ export const PortraitName: FC<Props> = ({
setCurrentSuggestedNames(suggestedNames);
}, [suggestedNames]);
const handleNameFieldSet = (value: string | null): void => {
const handleNameFieldSet = (value: string) => {
let updatedName = value ?? "";
// If the flag useDefaultCharacterName is true and value is falsy, set the name to the DefaultCharacterName
@@ -137,18 +137,16 @@ export const PortraitName: FC<Props> = ({
</>
)}
<div className={styles.inputContainer}>
<FormInputField
<InputField
label="Character Name"
onBlur={handleNameFieldSet}
initialValue={localValue}
inputAttributes={
{
spellCheck: false,
autoComplete: "off",
} as HTMLAttributes<HTMLInputElement>
}
maxLength={InputLimits.characterNameMaxLength}
maxLengthErrorMsg={CharacterNameLimitMsg}
errorMessage={CharacterNameLimitMsg}
onBlur={(e) => handleNameFieldSet(e.target.value)}
inputProps={{
spellCheck: false,
autoComplete: "off",
}}
/>
<Button
className={styles.suggestionButton}
@@ -11,7 +11,6 @@ import Collapsible, {
CollapsibleHeaderContent,
} from "~/tools/js/smartComponents/Collapsible";
import { TodoIcon } from "~/tools/js/smartComponents/Icons";
import PrerequisiteFailureSummary from "~/tools/js/smartComponents/PrerequisiteFailureSummary";
import { CharacterTheme } from "~/types";
import {
@@ -110,10 +109,9 @@ export const Feat: FC<Props> = ({
<Reference name={feat.getPrimarySourceName()} />
</span>
{showFailures && (
<PrerequisiteFailureSummary
failures={feat.getPrerequisiteFailures()}
className={styles.failures}
/>
<div className={styles.failures}>
{feat.getPrerequisiteFailuresText()}
</div>
)}
</div>
}