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:
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user