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
+2 -5
View File
@@ -1,9 +1,6 @@
import { useContext, useEffect, useState } from "react";
import {
AbilityManager,
FeaturesManager,
} from "@dndbeyond/character-rules-engine/es";
import { AbilityManager } from "@dndbeyond/character-rules-engine/es";
import { AttributesManagerContext } from "~/tools/js/Shared/managers/AttributesManagerContext";
@@ -18,7 +15,7 @@ export function useAbilities() {
setAbilities(abilities);
}
return FeaturesManager.subscribeToUpdates({ onUpdate });
return AbilityManager.subscribeToUpdates({ onUpdate });
}, [attributesManager, setAbilities]);
return abilities;
+7 -5
View File
@@ -8,7 +8,6 @@ import {
ApiAdapterUtils as apiAdapterUtils,
BackgroundUtils as backgroundUtils,
CampaignUtils as campaignUtils,
CampaignSettingUtils as campaignSettingUtils,
CharacterUtils as characterUtils,
ChoiceUtils as choiceUtils,
ClassFeatureUtils as classFeatureUtils,
@@ -124,7 +123,6 @@ export const useCharacterEngine = () => ({
baseFeats: useSelector(s.getBaseFeats),
bonusSavingThrowModifiers: useSelector(s.getBonusSavingThrowModifiers),
campaign: useSelector(s.getCampaign),
campaignSettings: useSelector(sds.getCampaignSettings),
carryingCapacity: useSelector(s.getCarryCapacity),
castableSpellSlotLevels: useSelector(s.getCastableSpellSlotLevels),
castablePactMagicSlotLevels: useSelector(s.getCastablePactMagicSlotLevels),
@@ -151,7 +149,7 @@ export const useCharacterEngine = () => ({
classSpells: useSelector(s.getClassSpells),
classSpellInfo: useSelector(s.getClassSpellInfoLookup),
classSpellLists: useSelector(s.getClassSpellLists),
classSpellListSpells: useSelector(s.getClassSpellListSpellsLookup),
classSpellListSpellsLookup: useSelector(s.getClassSpellListSpellsLookup),
classes: useSelector(s.getClasses),
classesModifiers: useSelector(s.getClassesModifiers),
combinedMaxSpellSlotLevel: useSelector(s.getCombinedMaxSpellSlotLevel),
@@ -170,6 +168,7 @@ export const useCharacterEngine = () => ({
creatureOwnerData: useSelector(s.getCreatureOwnerData),
creatureRules: useSelector(s.getCreatureRules),
creatures: useSelector(s.getCreatures),
currencies: useSelector(s.getCurrencies),
currentCarriedWeightSpeed: useSelector(s.getCurrentCarriedWeightSpeed),
currentCarriedWeightType: useSelector(s.getCurrentCarriedWeightType),
currentLevel: useSelector(s.getCurrentLevel),
@@ -235,6 +234,7 @@ export const useCharacterEngine = () => ({
inventoryContainers: useSelector(s.getInventoryContainers),
inventoryLookup: useSelector(s.getInventoryLookup),
inventoryInfusion: useSelector(s.getInventoryInfusionLookup),
itemPlans: useSelector(s.getAvailableItemPlans),
isSheetReady: useSelector(s.isCharacterSheetReady),
isDead: useSelector(s.isDead),
isMulticlassCharacter: useSelector(s.isMulticlassCharacter),
@@ -252,7 +252,9 @@ export const useCharacterEngine = () => ({
levelSpells: useSelector(s.getLevelSpells),
lifestyle: useSelector(s.getLifestyle),
loadSpecies: useSelector(api.makeLoadAvailableRaces),
longRestText: useSelector(sds.getLongRestText),
maxPactMagicSlotLevel: useSelector(s.getMaxPactMagicSlotLevel),
maxReplicatedItemsCount: useSelector(s.getMaxReplicatedItemsCount),
maxSpellSlotLevel: useSelector(s.getMaxSpellSlotLevel),
miscModifiers: useSelector(s.getMiscModifiers),
modifierData: useSelector(s.getModifierData),
@@ -279,7 +281,7 @@ export const useCharacterEngine = () => ({
partyInfo: useSelector(sds.getPartyInfo),
partyInventory: useSelector(s.getPartyInventory),
partyInventoryContainers: useSelector(s.getPartyInventoryContainers),
partyInventoryItem: useSelector(s.getPartyInventoryLookup),
partyInventoryLookup: useSelector(s.getPartyInventoryLookup),
passiveInsight: useSelector(s.getPassiveInsight),
passiveInvestigation: useSelector(s.getPassiveInvestigation),
passivePerception: useSelector(s.getPassivePerception),
@@ -294,6 +296,7 @@ export const useCharacterEngine = () => ({
playerName: useSelector(s.getUsername),
preferences: useSelector(s.getCharacterPreferences),
prerequisiteData: useSelector(s.getPrerequisiteData),
premadeInfo: useSelector(char.getPremadeInfo),
processedInitiative: useSelector(s.getProcessedInitiative),
proficiency: useSelector(s.getProficiencyLookup),
proficiencyBonus: useSelector(s.getProficiencyBonus),
@@ -370,7 +373,6 @@ export const useCharacterEngine = () => ({
apiAdapterUtils,
backgroundUtils,
campaignUtils,
campaignSettingUtils,
characterUtils,
choiceUtils,
classFeatureUtils,
+2 -1
View File
@@ -1,8 +1,9 @@
import { useCallback, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useSelector } from "react-redux";
import { characterSelectors } from "@dndbeyond/character-rules-engine/es";
import { useDispatch } from "~/hooks/useDispatch";
import { toastMessageActions } from "~/tools/js/Shared/actions";
import {
+59
View File
@@ -0,0 +1,59 @@
import { useCallback } from "react";
import { KETCH_RETRY_CONFIG } from "~/constants";
import { KetchConsentResponse } from "~/types";
export const useConsent = () => {
const setupAnalyticsConsentListener = useCallback(
(onConsentChange: (hasConsent: boolean) => void) => {
let retries = 0;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
if (process.env.NODE_ENV === "development") {
onConsentChange(true);
return () => {
// noop cleanup for dev
};
}
const trySetup = () => {
if ("window" in globalThis && window.ketch) {
try {
window.ketch(
"on",
"consent",
(consentData: KetchConsentResponse) => {
onConsentChange(Boolean(consentData.purposes?.analytics));
}
);
} catch (error) {
console.error("Failed to setup Ketch consent listener:", error);
onConsentChange(false);
}
} else if (retries < KETCH_RETRY_CONFIG.maxRetries) {
retries++;
timeoutId = setTimeout(trySetup, KETCH_RETRY_CONFIG.retryInterval);
} else {
console.warn(
`Failed to initialize consent management after ${KETCH_RETRY_CONFIG.maxRetries} retries`
);
onConsentChange(false);
}
};
trySetup();
// Return cleanup function to cancel pending retries
return () => {
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
};
},
[]
);
return {
setupAnalyticsConsentListener,
};
};
+91
View File
@@ -0,0 +1,91 @@
import { datadogRum } from "@datadog/browser-rum";
import { useEffect, useState } from "react";
import type { User } from "@dndbeyond/authentication-lib-js";
import config from "~/config";
import { useConsent } from "~/hooks/useConsent";
const isDndbeyondUrl = (url: string): boolean => {
try {
const { protocol, hostname } = new URL(url);
return (
protocol === "https:" &&
(hostname === "dndbeyond.com" || hostname.endsWith(".dndbeyond.com"))
);
} catch {
return false;
}
};
export const useDatadogRum = (user: User | null) => {
const [hasConsent, setHasConsent] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
const environment = config.environment;
const version = config.version.replace(/@dndbeyond\/character-app@/, ""); // example: 1.70.116
const { setupAnalyticsConsentListener } = useConsent();
useEffect(() => {
const unsubscribe = setupAnalyticsConsentListener(setHasConsent);
return () => {
unsubscribe();
};
}, [setupAnalyticsConsentListener]);
useEffect(() => {
if (datadogRum.getInternalContext()) {
return;
}
try {
datadogRum.init({
applicationId: "2cfa3248-a632-4f5b-bf9e-aafa18dc69ae", // Not a secret, safe to include in client code
clientToken: "pub940bbbc614a4ae3ab0c872fdad597554", // Not a secret, safe to include in client code
site: "datadoghq.com",
service: "characters-app",
env: environment === "production" ? "live" : environment,
version: `${version}`,
sessionSampleRate: environment === "production" ? 10 : 100,
sessionReplaySampleRate: environment === "production" ? 1 : 10,
traceContextInjection: "sampled",
traceSampleRate: 10,
trackingConsent: "not-granted", // Start with no consent, will update when consent status is known
trackUserInteractions: true,
trackResources: true,
trackLongTasks: true,
defaultPrivacyLevel: "mask-user-input",
allowedTracingUrls: [isDndbeyondUrl],
});
setIsInitialized(true);
} catch (error) {
console.warn("Datadog RUM failed to initialize:", error);
}
}, [environment]);
useEffect(() => {
if (!isInitialized) {
return;
}
if (!user) {
datadogRum.clearUser();
return;
}
datadogRum.setUser({
id: `${user.id ?? ""}`,
name: `${user.name ?? ""}`,
sub_tier: `${user.subscriptionTier ?? ""}`,
});
}, [user, isInitialized]);
useEffect(() => {
if (!isInitialized) {
return;
}
datadogRum.setTrackingConsent(hasConsent ? "granted" : "not-granted");
}, [hasConsent, isInitialized]);
};
+7
View File
@@ -0,0 +1,7 @@
import { useDispatch as useReactReduxDispatch } from "react-redux";
import { AnyAction, Dispatch } from "redux";
// TODO: We are typing useDispatch this way to work around issues with UnknownAction, the type that was introduced in Redux 5.
// If we decide to modernize to Redux 5, we will need to refactor much of the Rules Engine redux usage.
export const useDispatch =
useReactReduxDispatch.withTypes<Dispatch<AnyAction>>();
@@ -1,33 +1,34 @@
import { HTMLAttributes, useState } from "react";
import styles from '../../styles/errors.module.css';
import { HTMLAttributes, JSX, useState } from "react";
import styles from "../../styles/errors.module.css";
export interface ErrorHandlerOptions {
initialState: boolean;
errMsg: string;
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>;
};
initialState: boolean;
errMsg: string;
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>;
}
export interface ErrorHandler {
showError: boolean;
setShowError: (value: boolean) => void;
ErrorMessage: () => JSX.Element;
};
showError: boolean;
setShowError: (value: boolean) => void;
ErrorMessage: () => JSX.Element;
}
export const useErrorHandling = (
initialState: boolean,
errMsg: string,
errorMessageAttributes: HTMLAttributes<HTMLDivElement> | null = null
initialState: boolean,
errMsg: string,
errorMessageAttributes: HTMLAttributes<HTMLDivElement> | null = null
): ErrorHandler => {
const [showError, setShowError] = useState(initialState);
const ErrorMessage = (): JSX.Element => (
<div className={styles.inputError} {...errorMessageAttributes}>
{errMsg}
</div>
);
const [showError, setShowError] = useState(initialState);
const ErrorMessage = (): JSX.Element => (
<div className={styles.inputError} {...errorMessageAttributes}>
{errMsg}
</div>
);
return {
showError,
setShowError,
ErrorMessage,
};
return {
showError,
setShowError,
ErrorMessage,
};
};
@@ -1,49 +1,49 @@
import { HTMLAttributes } from "react";
import { HTMLAttributes, JSX } from "react";
import { useErrorHandling } from "./useErrorHandling";
export interface MaxLengthErrorHandler {
handleMaxLengthErrorMsg: (value: string) => void;
hideError: () => void;
MaxLengthErrorMessage: () => JSX.Element;
};
handleMaxLengthErrorMsg: (value: string) => void;
hideError: () => void;
MaxLengthErrorMessage: () => JSX.Element;
}
export const useMaxLengthErrorHandling = (
initialState: boolean,
maxLength: number | null,
errMsg: string = "",
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>
initialState: boolean,
maxLength: number | null,
errMsg: string = "",
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>
): MaxLengthErrorHandler => {
errMsg ||= `The max length is ${maxLength} characters.`;
errMsg ||= `The max length is ${maxLength} characters.`;
const {
showError,
setShowError,
ErrorMessage,
} = useErrorHandling(initialState, errMsg, errorMessageAttributes);
const { showError, setShowError, ErrorMessage } = useErrorHandling(
initialState,
errMsg,
errorMessageAttributes
);
const handleMaxLengthErrorMsg = (value: string): void => {
if (!maxLength) {
// Skip if maxLength is 0 or not set.
return;
}
const isTooLong = (value?.length ?? 0) >= (maxLength ?? 0);
if (isTooLong !== showError) {
setShowError(isTooLong);
}
};
const hideError = () => {
setShowError(false);
const handleMaxLengthErrorMsg = (value: string): void => {
if (!maxLength) {
// Skip if maxLength is 0 or not set.
return;
}
const MaxLengthErrorMessage = (): JSX.Element => showError && errMsg
? (<ErrorMessage />)
: <></>;
const isTooLong = (value?.length ?? 0) >= (maxLength ?? 0);
if (isTooLong !== showError) {
setShowError(isTooLong);
}
};
return {
handleMaxLengthErrorMsg,
MaxLengthErrorMessage,
hideError
};
}
const hideError = () => {
setShowError(false);
};
const MaxLengthErrorMessage = (): JSX.Element =>
showError && errMsg ? <ErrorMessage /> : <></>;
return {
handleMaxLengthErrorMsg,
MaxLengthErrorMessage,
hideError,
};
};
+2 -5
View File
@@ -1,9 +1,6 @@
import { useContext, useEffect, useState } from "react";
import {
ExtraManager,
FeaturesManager,
} from "@dndbeyond/character-rules-engine/es";
import { ExtraManager } from "@dndbeyond/character-rules-engine/es";
import { ExtrasManagerContext } from "~/tools/js/Shared/managers/ExtrasManagerContext";
@@ -19,7 +16,7 @@ export function useExtras() {
const onUpdate = () => {
setExtras(extrasManager.getCharacterExtraManagers());
};
return FeaturesManager.subscribeToUpdates({ onUpdate });
return ExtraManager.subscribeToUpdates({ onUpdate });
}, [extrasManager]);
return extras;
+2 -1
View File
@@ -1,5 +1,5 @@
import { useCallback, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useSelector } from "react-redux";
import {
ApiAdapterUtils,
@@ -9,6 +9,7 @@ import {
syncTransactionActions,
} from "@dndbeyond/character-rules-engine";
import { useDispatch } from "~/hooks/useDispatch";
import { toastMessageActions } from "~/tools/js/Shared/actions";
export interface ExportPdfData {
+1
View File
@@ -11,6 +11,7 @@ export interface AppUser extends User {
roles: Array<string>;
subscription: string;
subscriptionTier: string;
avatarUrl?: string;
}
/**
* AppUserState has three states: