New source found from dndbeyond.com

This commit is contained in:
2026-07-08 01:00:09 -07:00
parent 9a983a6d7b
commit dcefa0d2f2
2865 changed files with 222467 additions and 49053 deletions
@@ -1,11 +1,11 @@
import React, { useCallback, useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import ArrowDown from "@dndbeyond/fontawesome-cache/svgs/regular/arrow-down-to-line.svg";
import { Button } from "~/components/Button";
import { MaxCharactersDialog } from "~/components/MaxCharactersDialog";
import config from "~/config";
import { UserPreferenceProvider } from "~/tools/js/smartComponents/UserPreference";
import { logUnlockSubscribeClicked } from "../../../../helpers/analytics";
import { byProp, createSortValue } from "../../../../helpers/sortUtils";
@@ -27,6 +27,7 @@ import {
SortOrderEnum,
SortTypeEnum,
} from "../../../../types";
import { UserPreferencesProvider } from "../../contexts/UserPreferences";
import { ApiStatusIndicator } from "../ApiStatusIndicator";
import { CharacterCard } from "../CharacterCard";
import { SubscriptionBanner } from "../SubscriptionBanner";
@@ -76,6 +77,7 @@ export const CharacterGrid: React.FC<CharacterGridProps> = ({
}) => {
const userId = useUserId();
const subscriptionTier = useSubscriptionTier();
const { search: searchParams } = useLocation();
const [sortPreference, setSortPreference] = useLocalStorage<string>(
`MY_CHARACTERS_SORT_PREFERENCE:${userId}`,
@@ -190,15 +192,17 @@ export const CharacterGrid: React.FC<CharacterGridProps> = ({
maxCharacterSlotsAllowed !== null &&
characterCount >= maxCharacterSlotsAllowed;
const createCharacterHref = !hasMaxCharacters
? `${config.basePathname}/builder`
: undefined;
const createCharacterOnClick = hasMaxCharacters
? () => setIsMaxCharacterMessageOpen(true)
: undefined;
const createCharacterHref =
!hasMaxCharacters && !hasLockedCharacters
? `${config.basePathname}/builder${searchParams}`
: undefined;
const createCharacterOnClick =
hasMaxCharacters || hasLockedCharacters
? () => setIsMaxCharacterMessageOpen(true)
: undefined;
return (
<UserPreferenceProvider>
<UserPreferencesProvider>
<div
className={[className, "ddb-characters-listing"]
.filter(Boolean)
@@ -403,8 +407,9 @@ export const CharacterGrid: React.FC<CharacterGridProps> = ({
<MaxCharactersDialog
open={isMaxCharacterMessageOpen}
onClose={() => setIsMaxCharacterMessageOpen(false)}
hasLockedCharacters={hasLockedCharacters}
/>
</div>
</UserPreferenceProvider>
</UserPreferencesProvider>
);
};
@@ -14,11 +14,11 @@ import { Button } from "@dndbeyond/ttui/components/Button";
import { Select } from "@dndbeyond/ttui/components/Select";
import PreferenceUpdateLocation from "~/tools/js/Shared/constants/PreferenceUpdateLocation";
import CharacterSettingsModal from "~/tools/js/smartComponents/CharacterSettingsModal";
import { SortOrderEnum, SortTypeEnum } from "~/types";
import { logListingSortChanged } from "../../../../../helpers/analytics";
import { createSortValue } from "../../../../../helpers/sortUtils";
import { CharacterSettingsModal } from "../../CharacterSettingsModal";
import { SortState } from "../CharacterGrid";
import styles from "./styles.module.css";
@@ -150,8 +150,7 @@ export const SearchSort: FC<SearchSortProps> = ({
</Button>
<CharacterSettingsModal
open={showSettingsModal}
updateLocation={PreferenceUpdateLocation.CharacterListing}
handleClose={() => setShowSettingsModal(false)}
onClose={() => setShowSettingsModal(false)}
/>
</>
</div>
@@ -0,0 +1,292 @@
import clsx from "clsx";
import { FC } from "react";
import { RuleDataUtils } from "@dndbeyond/character-rules-engine";
import GearIcon from "@dndbeyond/fontawesome-cache/svgs/regular/gear.svg";
import CloseIcon from "@dndbeyond/fontawesome-cache/svgs/regular/xmark.svg";
import { Button } from "~/components/Button";
import { CheckboxGroup, CheckboxInfo } from "~/components/CheckboxGroup";
import { Dialog, DialogProps } from "~/components/Dialog";
import { PrivacyTypeRadio } from "~/components/PrivacyTypeRadio";
import { SourceCategoryDescription } from "~/constants";
import { PreferenceAbilityScoreDisplayTypeEnum } from "~/constants";
import { useFeatureFlags } from "~/contexts/FeatureFlag";
import { generateCharacterPreferences } from "~/helpers/generateCharacterPreferences";
import PreferenceUpdateLocation from "~/tools/js/Shared/constants/PreferenceUpdateLocation";
import AbilitySummary from "~/tools/js/smartComponents/AbilitySummary";
import { useUserPreferences } from "../../contexts/UserPreferences";
import styles from "./styles.module.css";
type AbilityScoreDisplayTypes = {
label: string;
value: number;
}[];
export interface CharacterSettingsModalProps extends DialogProps {}
export const CharacterSettingsModal: FC<CharacterSettingsModalProps> = ({
className,
open,
onClose,
...props
}) => {
const {
privacyType,
isDarkModeEnabled,
abilityScoreDisplayType,
updatePreferences,
ruleData,
defaultEnabledSourceCategories,
isHomebrewEnabled,
} = useUserPreferences();
const { featureFlags } = useFeatureFlags();
const updateLocation = PreferenceUpdateLocation.CharacterListing;
// Build an array of display types from enum
const abilityScoreDisplayTypes: AbilityScoreDisplayTypes = Object.entries(
PreferenceAbilityScoreDisplayTypeEnum
)
// Remove any duplicates which don't have numeric values
.filter((opt) => opt.some((val) => typeof val === "number"))
// Map to object with label and value formatting
.map(([label, value]) => ({
label: label.replace("_", " "),
value: Number(value),
}));
const preferences = generateCharacterPreferences(featureFlags);
const getTheme = (darkMode: boolean) => ({
name: "DDB Red",
backgroundColor: darkMode ? "#10161ADB" : "#FEFEFE",
isDefault: true,
themeColor: "#C53131",
themeColorId: null,
isDarkMode: darkMode || false,
});
const rollContext = {
entityId: "52962664",
entityType: "character",
name: "Stor Hornraven",
avatarUrl:
"https://stg.dndbeyond.com/avatars/18589/680/1581111423-52962664.jpeg?width=150&height=150&fit=crop&quality=95&auto=webp",
};
const handleSourceCategoryChange = (
sourceId: number,
isActive: boolean
): void => {
const newSourceCategories = {
...defaultEnabledSourceCategories,
[sourceId]: isActive,
};
updatePreferences({
privacyType,
isDarkModeEnabled,
abilityScoreDisplayType,
updateLocation,
defaultEnabledSourceCategories: newSourceCategories,
isHomebrewEnabled,
});
};
let sourceToggles: Array<CheckboxInfo> = [];
let partneredSourceCheckboxes: Array<CheckboxInfo> = [];
let allPartneredSources: Array<number> = [];
if (ruleData) {
const activeSourceCategories = Object.keys(defaultEnabledSourceCategories)
.filter((key) => defaultEnabledSourceCategories[key])
.map(Number);
RuleDataUtils.getSourceCategories(ruleData).forEach((sourceCategory) => {
if (!sourceCategory.isToggleable) {
return null;
}
const checkbox: CheckboxInfo = {
label: `${sourceCategory.name}`,
initiallyEnabled: activeSourceCategories.includes(sourceCategory.id),
onChange: (e) =>
handleSourceCategoryChange(sourceCategory.id, e.target.checked),
sortOrder: sourceCategory.sortOrder,
description: sourceCategory.description ?? "",
};
if (sourceCategory.isPartneredContent) {
delete checkbox.description;
partneredSourceCheckboxes.push(checkbox);
allPartneredSources.push(sourceCategory.id);
} else {
sourceToggles.push(checkbox);
}
});
//Add Homebrew to sources for display
sourceToggles.push({
label: "Homebrew",
initiallyEnabled: isHomebrewEnabled,
description: SourceCategoryDescription.homebrew,
onChange: (e) => {
updatePreferences({
privacyType,
isDarkModeEnabled,
abilityScoreDisplayType,
updateLocation,
defaultEnabledSourceCategories,
isHomebrewEnabled: e.target.checked,
});
},
sortOrder: 0,
});
}
const handleChangeUnderdarkMode = () => {
const isChecked = !isDarkModeEnabled;
updatePreferences({
privacyType,
abilityScoreDisplayType,
updateLocation,
isDarkModeEnabled: isChecked,
defaultEnabledSourceCategories,
isHomebrewEnabled,
});
};
const handleChangeDisplayType = (value: number) => {
if (!isNaN(value))
updatePreferences({
privacyType,
isDarkModeEnabled,
updateLocation,
abilityScoreDisplayType: value,
defaultEnabledSourceCategories,
isHomebrewEnabled,
});
};
return (
<Dialog
className={clsx([styles.characterSettingsModal, className])}
open={open}
onClose={onClose}
modal
{...props}
>
<button
className={styles.closeButton}
onClick={onClose}
aria-label="Close the Character Settings Modal"
>
<CloseIcon />
</button>
<header className={styles.header}>
<GearIcon />
<h2>Default Character Settings</h2>
</header>
<section className={styles.body}>
<p className={styles.helpText}>
These settings apply defaults to all new Characters you create. Any
settings you apply to a specific Character will override the choices
you make here.
</p>
<CheckboxGroup
className={styles.checkboxGroup}
title="Sources"
description={SourceCategoryDescription.official}
checkboxes={sourceToggles}
name="sources"
showAccordion={true}
accordionDefaultOpen={true}
/>
<CheckboxGroup
className={styles.checkboxGroup}
title="Partnered Content"
description={SourceCategoryDescription.partnered}
checkboxes={partneredSourceCheckboxes}
name="partnered-content"
showAccordion={true}
accordionSummary="Choose Partners"
/>
<PrivacyTypeRadio
className={styles.privacyTypeRadio}
initialValue={privacyType}
onChange={(e) =>
updatePreferences({
isDarkModeEnabled,
abilityScoreDisplayType,
updateLocation,
privacyType: parseInt(e.target.value),
defaultEnabledSourceCategories,
isHomebrewEnabled,
})
}
/>
<div className={styles.displayGrid}>
<div className={styles.displayFields}>
<CheckboxGroup
className={styles.checkboxGroup}
title="Display"
name="underdark-mode"
data-testid="underdark-mode-toggle"
checkboxes={[
{
label: "Underdark Mode",
initiallyEnabled: isDarkModeEnabled,
onChange: handleChangeUnderdarkMode,
description: "Activate dark mode for Character Sheets.",
},
]}
/>
<div className={styles.abilityScores}>
<p className={styles.label}>Ability Scores / Modifiers</p>
<p className={styles.description}>
Reverse the display of Ability Scores and Modifiers.
</p>
<div className={styles.buttonGroup}>
{abilityScoreDisplayTypes
.reverse()
.map(({ label, value }, i) => (
<Button
className={clsx([
styles.buttonGroupButton,
abilityScoreDisplayType === value &&
styles.buttonGroupSelected,
])}
onClick={() => handleChangeDisplayType(Number(value))}
variant="outline"
size="small"
key={`ability-score-display-type-${value}`}
>
{label}
</Button>
))}
</div>
</div>
</div>
<div className={styles.displayPreview}>
<div className={styles.previewContainer}>
<p className={styles.previewText}>Preview</p>
<AbilitySummary
ability={{ name: "Strength", score: 14, modifier: 2 }}
preferences={{
...preferences,
abilityScoreDisplayType: abilityScoreDisplayType,
}}
theme={getTheme(isDarkModeEnabled || false)}
onClick={(e) => e.getBaseScore()}
rollContext={rollContext}
/>
</div>
</div>
</div>
</section>
</Dialog>
);
};
@@ -96,7 +96,6 @@ export const MyCharacters = ({ characterQuery }: Props) => {
const { isLoading, data, refetch, error } = characterQuery;
const { characters, hasLockedCharacters, maxCharacterSlotsAllowed } =
generateAppData(userName, data);
// Check for max active characters
const hasMaxCharacters =
characters?.filter(
(character) => getStatus(character) === CharacterStatusEnum.Active
@@ -0,0 +1,76 @@
import { createContext, useContext, useEffect, useState } from "react";
import { RuleData } from "@dndbeyond/character-rules-engine/es";
import {
getUserPreferences,
updateUserPreferences,
getRulesData,
} from "~/helpers/characterServiceApi";
import PreferenceUpdateLocation from "~/tools/js/Shared/constants/PreferenceUpdateLocation";
import { UserPreferences } from "~/types";
interface UserPreferencesContextProps extends UserPreferences {
updatePreferences: (updated: UserPreferences) => void;
ruleData: RuleData | null;
}
const initialValue = {
abilityScoreDisplayType: 2,
privacyType: 2,
isDarkModeEnabled: false,
isDiceRollingEnabled: true,
updateLocation: PreferenceUpdateLocation.CharacterBuilder,
updatePreferences: () => {},
ruleData: null,
defaultEnabledSourceCategories: {},
isHomebrewEnabled: true,
};
export const UserPreferencesContext =
createContext<UserPreferencesContextProps>(initialValue);
export const UserPreferencesProvider = ({ children }) => {
const [preferences, setPreferences] = useState(initialValue);
const [isLoaded, setIsLoaded] = useState(false);
const [ruleData, setRuleData] = useState(null);
const updatePreferences = (updated: UserPreferences) => {
setPreferences((prev) => {
return { ...prev, ...updated };
});
};
const getPreferences = async () => {
const userPreferences = await getUserPreferences();
const userPreferencesJSON = await userPreferences.json();
const rulesData = await getRulesData();
const rulesDataJSON = await rulesData.json();
setRuleData(rulesDataJSON.data);
updatePreferences(userPreferencesJSON.data);
setIsLoaded(true);
};
useEffect(() => {
getPreferences();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (isLoaded) {
updateUserPreferences(preferences);
}
}, [preferences]);
return (
<UserPreferencesContext.Provider
value={{ ...preferences, updatePreferences, ruleData }}
>
{children}
</UserPreferencesContext.Provider>
);
};
export const useUserPreferences = () => useContext(UserPreferencesContext);