Grabbed dndbeyond's source code

```
~/go/bin/sourcemapper -output ddb -jsurl https://media.dndbeyond.com/character-app/static/js/main.90aa78c5.js
```
This commit is contained in:
2025-05-28 11:50:03 -07:00
commit 8df9031d27
3592 changed files with 319051 additions and 0 deletions
@@ -0,0 +1,66 @@
import { createContext, FC, useContext, useState } from "react";
import { BuilderMethod } from "../../types";
export interface BuilderContextType {
builderMethod: BuilderMethod | null;
isCharacterLoading: boolean;
isCharacterLoaded: boolean;
pdfUrl: string | null;
setBuilderMethod: (method: BuilderMethod) => void;
suggestedNames: string[];
}
export const BuilderContext = createContext<BuilderContextType>({
builderMethod: null,
isCharacterLoading: true,
isCharacterLoaded: false,
pdfUrl: null,
setBuilderMethod: () => {},
suggestedNames: [],
});
export const BuilderProvider: FC = ({ children }) => {
const [builderMethod, setBuilderMethodState] = useState<BuilderMethod | null>(
null
);
const [isCharacterLoading, setIsCharacterLoading] = useState(true);
const [isCharacterLoaded, setIsCharacterLoaded] = useState(false);
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [suggestedNames, setSuggestedNames] = useState<string[]>([]);
const setBuilderMethod = async (method: BuilderMethod) => {
// Handle setting the builder method via fetch
};
// const response = yield call(ApiRequests.getCharacterRuleData, {
// params: { v: appConfig.version },
// });
// const data: UnpackMakeApiResponseData<
// typeof ApiRequests.getCharacterRuleData
// > | null = ApiAdapterUtils.getResponseData(response);
// if (data !== null) {
// yield put(ruleDataActions.dataSet(data));
// }
// yield put(builderActions.builderMethodSetCommit(action.payload));
return (
<BuilderContext.Provider
value={{
builderMethod,
isCharacterLoading,
isCharacterLoaded,
pdfUrl,
setBuilderMethod,
suggestedNames,
}}
>
{children}
</BuilderContext.Provider>
);
};
export const useBuilderContext = () => {
return useContext(BuilderContext);
};
@@ -0,0 +1,90 @@
import { createContext, FC, useContext, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { ApiAdapterUtils } from "@dndbeyond/character-rules-engine";
import { builderActions } from "~/tools/js/CharacterBuilder/actions";
import { apiCreatorSelectors } from "~/tools/js/Shared/selectors";
import { ClassDefinitionContract as ClassDef } from "~/types";
/**
* A context to provide data to the application without causing a re-render or
* unnecessary fetches and transforms. This context is used in places where
* character-specific data is not needed.
*/
export interface ClassContextType {
query: string;
setQuery: (query: string) => void;
filteredClasses: Array<ClassDef>;
allClasses: Array<ClassDef>;
handleSelectClass: (id: number) => void;
isLoading: boolean;
isModalShowing: boolean;
closeModal: () => void;
}
export const ClassContext = createContext<ClassContextType>(null!);
export const ClassProvider: FC = ({ children }) => {
const dispatch = useDispatch();
const [query, setQuery] = useState("");
const [allClasses, setAllClasses] = useState<Array<ClassDef>>([]);
const [isLoading, setIsLoading] = useState(true);
const [isModalShowing, setIsModalShowing] = useState(false);
const closeModal = () => setIsModalShowing(false);
const loadClasses = useSelector(apiCreatorSelectors.makeLoadAvailableClasses);
const handleSelectClass = (id: number) => {
const entity = allClasses.find((c) => c.id === id);
if (entity) {
dispatch(builderActions.confirmClassSet(entity));
setIsModalShowing(true);
window.scrollTo(0, 0);
}
};
// Handle searching through items
const filteredClasses = query
? allClasses.filter((c) =>
c.name?.toLowerCase().includes(query.toLowerCase())
)
: allClasses;
useEffect(() => {
const getAllData = async () => {
// Load all classes
const response = await loadClasses();
const classes = ApiAdapterUtils.getResponseData(response) || [];
setAllClasses(classes);
if (classes.length > 0) {
setIsLoading(false);
}
};
getAllData();
}, [loadClasses]);
return (
<ClassContext.Provider
value={{
query,
setQuery,
filteredClasses,
allClasses,
handleSelectClass,
isLoading,
isModalShowing,
closeModal,
}}
>
{children}
</ClassContext.Provider>
);
};
export const useClassContext = () => {
return useContext(ClassContext);
};
@@ -0,0 +1,59 @@
import { createContext, FC, useContext, useState } from "react";
import { ConfirmModal, ConfirmModalProps } from "~/components/ConfirmModal";
export interface ModalData {
content: React.ReactNode;
props: Omit<ConfirmModalProps, "open">;
}
export interface ModalManagerContextType {
createModal: (modalData: ModalData) => void;
}
export const ModalManagerContext = createContext<ModalManagerContextType>(
null!
);
export const ModalManagerProvider: FC = ({ children }) => {
const [modalData, setModalData] = useState<ModalData | null>(null);
const createModal = (modalData: ModalData) => {
setModalData(modalData);
};
const removeModal = () => {
setModalData(null);
};
const handleClose = () => {
removeModal();
if (modalData) {
modalData.props.onClose();
}
};
const handleConfirm = () => {
removeModal();
if (modalData) {
modalData.props.onConfirm();
}
};
return (
<ModalManagerContext.Provider value={{ createModal }}>
{children}
<ConfirmModal
{...modalData?.props}
onConfirm={handleConfirm}
onClose={handleClose}
open={!!modalData}
>
{modalData?.content}
</ConfirmModal>
</ModalManagerContext.Provider>
);
};
export const useModalManager = () => {
return useContext(ModalManagerContext);
};
@@ -0,0 +1,302 @@
import { groupBy, keyBy, uniqBy } from "lodash";
import { createContext, FC, useContext, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import { useSource } from "~/hooks/useSource";
import { apiCreatorSelectors } from "~/tools/js/Shared/selectors";
import { TypeScriptUtils } from "~/tools/js/Shared/utils";
import {
HtmlSelectOption,
HtmlSelectOptionGroup,
RaceDefinitionContract as RaceDef,
RaceDefinitionContract,
SimpleSourcedDefinitionContract,
} from "~/types";
import { GroupedListingItem, ListingItem } from "../../types";
export interface SpeciesContextType {
closeModal: () => void;
defaultSpeciesImageUrl: string;
isLegacyShowing: boolean;
isModalShowing: boolean;
query: string;
selectedSpecies?: RaceDef;
setQuery: (query: string) => void;
source: string;
sourceOptions: (HtmlSelectOptionGroup | HtmlSelectOption)[];
filteredSpecies: RaceDef[];
allSpecies: RaceDef[];
toggleLegacyContent: () => void;
transformSpecies: (data: RaceDef[]) => ListingItem[];
setSource: (source: string) => void;
isLoading: boolean;
getSpeciesInGroups: (
species: ListingItem[]
) => (ListingItem | GroupedListingItem)[];
}
export const SpeciesContext = createContext<SpeciesContextType>(null!);
export const SpeciesProvider: FC = ({ children }) => {
const {
apiAdapterUtils: { getResponseData },
raceUtils: { getSubRaceShortName, getBaseName, getIsHomebrew, getSources },
ruleData,
ruleDataUtils: { getDefaultRaceImageUrl, getRaceGroups },
characterUtils: { isPrimarySource },
} = useCharacterEngine();
const { getSourceDescription, getGroupedOptionsBySourceCategory } =
useSource();
const [allSpecies, setAllSpecies] = useState<Array<RaceDef>>([]);
const [isLegacyShowing, setIsLegacyShowing] = useState(true);
const [isModalShowing, setIsModalShowing] = useState(false);
const [query, setQuery] = useState("");
const [selectedSpecies, setSelectedSpecies] = useState<RaceDef>();
const [source, setSource] = useState("");
const [sourceOptions, setSourceOptions] = useState<
(HtmlSelectOptionGroup | HtmlSelectOption)[]
>([]);
const [isLoading, setIsLoading] = useState(true);
const loadSpecies = useSelector(apiCreatorSelectors.makeLoadAvailableRaces);
const defaultSpeciesImageUrl = getDefaultRaceImageUrl(ruleData) ?? "";
const handleFilteredSpecies = () => {
let filtered = allSpecies;
//Check legacy toggle
if (!isLegacyShowing) {
filtered = filtered.filter((s) => !s.isLegacy);
}
// Check sources
if (source !== "") {
if (source === "HOMEBREW") {
filtered = filtered.filter((s) => s.isHomebrew);
} else {
filtered = filtered.filter((s) =>
s.sources?.some(({ sourceId }) => source === sourceId.toString())
);
}
}
// Check querys
if (query !== "") {
filtered = filtered.filter((s) => {
return s.fullName?.toLowerCase().includes(query.toLowerCase());
});
}
return filtered;
};
// Handle searching through items
const filteredSpecies = handleFilteredSpecies();
const onSelectSpecies = (id: string) => {
const entity = allSpecies.find(
(s) => `${s.entityRaceId}-${s.entityRaceTypeId}` === id
);
if (entity) {
setSelectedSpecies(entity);
setIsModalShowing(true);
window.scrollTo(0, 0);
}
};
const closeModal = () => setIsModalShowing(false);
const toggleLegacyContent = () => {
setIsLegacyShowing(!isLegacyShowing);
};
//Transform Species data into ListingItems
const transformSpecies = (data: RaceDef[]): ListingItem[] => {
return data.map((species) => {
const shortName = getSubRaceShortName(species);
const baseName = getBaseName(species);
const heading = shortName ? `${shortName} ${baseName}` : baseName || "";
const text = getIsHomebrew(species)
? "Homebrew"
: getSourceDescription(species?.sources?.[0]?.sourceId || 0) || "";
return {
type: "species",
id: `${species.entityRaceId}-${species.entityRaceTypeId}`,
entityTypeId: species.entityRaceTypeId,
heading,
text,
image: species.portraitAvatarUrl || defaultSpeciesImageUrl,
entity: species,
isLegacy: species.isLegacy,
onClick: onSelectSpecies,
};
});
};
// get the ListingItems for species, both grouped and individual
const getSpeciesInGroups = (
species: ListingItem[]
): (ListingItem | GroupedListingItem)[] => {
let groupedSpecies: ListingItem[] = [];
let unGroupedSpecies: ListingItem[] = [];
//species that have a groupId go into the groupedSpecies array and species that don't have a groupId go into the unGroupedSpecies array
species.forEach((s) => {
const groupIds = (s.entity as RaceDefinitionContract)?.groupIds;
if (groupIds && groupIds.length > 0) {
groupIds.forEach((groupId) => {
groupedSpecies.push({
...s,
groupId,
});
});
} else {
unGroupedSpecies.push(s);
}
});
//Create a lookup of grouped species by groupId
const groupedSpeciesLookup = groupBy(groupedSpecies, "groupId");
//Create a lookup from all Species Groups from rules data
const groupDataLookup = keyBy(getRaceGroups(ruleData), "id");
//This array will hold the final list of grouped species
const groups: GroupedListingItem[] = [];
//Iterate through the grouped species lookup to create final groupings
Object.keys(groupedSpeciesLookup).forEach((groupId) => {
const groupItems = groupedSpeciesLookup[groupId];
//If the group only has one item, it is not a group - put it in the unGroupedSpecies array, otherwise create the Grouped Listing Item for the group
if (groupItems.length === 1) {
unGroupedSpecies.push(groupItems[0]);
} else {
const groupInfo = groupDataLookup[parseInt(groupId)];
groups.push({
id: groupId,
type: "group",
image: groupInfo.avatarUrl ?? "",
heading: groupInfo.name ?? "",
groupId: parseInt(groupId),
listItems: groupItems,
onClick: undefined,
});
}
});
return [...groups, ...unGroupedSpecies];
};
const getSpecies = async () => {
const species = getResponseData(await loadSpecies()) || [];
setAllSpecies(species);
if (species.length > 0) {
setIsLoading(false);
}
};
// Set the source options when the species are loaded
useEffect(() => {
let shouldContaineHomebrewCategory = false;
let sourceShouldReset = true;
const sourcesData: Array<SimpleSourcedDefinitionContract> = allSpecies
.map((species) => {
//Flag homebrew category
if (species.isHomebrew) {
if (!shouldContaineHomebrewCategory) {
shouldContaineHomebrewCategory = true;
}
//don't reset source state if we have a homebrew category and the current source is homebrew
if (shouldContaineHomebrewCategory && source === "HOMEBREW") {
sourceShouldReset = false;
}
return null;
}
const primarySourceId =
getSources(species).filter(isPrimarySource)[0]?.sourceId;
//If there is no primary source and the above filter hasn't already returned null for isHombrew, return null
if (!primarySourceId) {
return null;
}
//don't reset source state if the current source is the sourceId of a species
if (primarySourceId.toString() === source) {
sourceShouldReset = false;
}
return {
sources: getSources(species),
name: getSourceDescription(primarySourceId) || "Unknown",
id: primarySourceId,
};
})
.filter(TypeScriptUtils.isNotNullOrUndefined);
const uniqueSources = uniqBy(sourcesData, "id");
const groupedOptions = getGroupedOptionsBySourceCategory(uniqueSources);
const homebrewOptions: HtmlSelectOption[] = [
{
label: "Homebrew",
value: "HOMEBREW",
},
{
label: "---------",
disabled: true,
value: "-----",
},
];
const sourceOptions: (HtmlSelectOptionGroup | HtmlSelectOption)[] = [];
if (shouldContaineHomebrewCategory) {
sourceOptions.push(...homebrewOptions);
}
sourceOptions.push(...groupedOptions);
setSourceOptions(sourceOptions);
//reset source state if the source isn't valid for the list of current species so that the list is updated
if (sourceShouldReset) {
setSource("");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allSpecies, source]);
useEffect(() => {
getSpecies();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loadSpecies]);
return (
<SpeciesContext.Provider
value={{
closeModal,
defaultSpeciesImageUrl,
isLegacyShowing,
isModalShowing,
query,
selectedSpecies,
setQuery,
sourceOptions,
filteredSpecies,
allSpecies,
toggleLegacyContent,
transformSpecies,
source,
setSource,
isLoading,
getSpeciesInGroups,
}}
>
{children}
</SpeciesContext.Provider>
);
};
export const useSpeciesContext = () => {
return useContext(SpeciesContext);
};