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,614 @@
import axios, { Canceler } from "axios";
import sortBy from "lodash/sortBy";
import React from "react";
import {
Button,
LoadingPlaceholder,
TypeScriptUtils,
} from "@dndbeyond/character-components/es";
import {
ApiAdapterUtils,
BaseSpellContract,
CharacterTheme,
Constants,
DataOriginRefData,
EntityUtils,
FormatUtils,
HelperUtils,
RuleData,
RuleDataUtils,
SourceUtils,
Spell,
SpellCasterInfo,
SpellUtils,
} from "@dndbeyond/character-rules-engine/";
import { Accordion } from "~/components/Accordion";
import { Link } from "~/components/Link";
import { SpellFilter } from "~/components/SpellFilter";
import { SpellName } from "~/components/SpellName";
import DataLoadingStatusEnum from "../../../constants/DataLoadingStatusEnum";
import {
ApiSpellsPromise,
ApiSpellsRequest,
} from "../../../selectors/composite/apiCreator";
import { AppLoggerUtils, FilterUtils } from "../../../utils";
import SpellDetail from "../../SpellDetail";
import styles from "./styles.module.css";
export class SpellManagerGroupItem extends React.PureComponent<{
spell: Spell;
spellCasterInfo: any;
ruleData: RuleData;
dataOriginRefData: DataOriginRefData;
enableRemove: boolean;
enableSpellRemove: boolean;
enableAdd: boolean;
enablePrepare: boolean;
enableUnprepare: boolean;
isPrepareMaxed: boolean;
isCantripsKnownMaxed: boolean;
isSpellsKnownMaxed: boolean;
addButtonText: string;
removeButtonText: string;
proficiencyBonus: number;
theme: CharacterTheme;
onPrepare: (spell: Spell) => void;
onUnprepare: (spell: Spell) => void;
onAdd: (spell: Spell) => void;
onRemove: (spell: Spell) => void;
}> {
static defaultProps = {
enableRemove: true,
enableSpellRemove: true,
enableAdd: true,
enablePrepare: true,
enableUnprepare: true,
};
handlePrepareToggle = () => {
const { spell, onPrepare, onUnprepare } = this.props;
if (spell.prepared) {
onUnprepare(spell);
} else {
onPrepare(spell);
}
};
handleRemove = (evt) => {
const { spell, onRemove } = this.props;
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
onRemove(spell);
};
handleAdd = () => {
const { spell, onAdd } = this.props;
onAdd(spell);
};
renderButtons = (clsNames, includeRemove) => {
const {
spell,
isPrepareMaxed,
isCantripsKnownMaxed,
isSpellsKnownMaxed,
enableRemove,
enableAdd,
enablePrepare,
} = this.props;
let { addButtonText, removeButtonText } = this.props;
const { alwaysPrepared, canRemove, canPrepare, canAdd } = spell;
let isAddDisabled = false;
if (SpellUtils.getLevel(spell) === 0 && isCantripsKnownMaxed) {
isAddDisabled = true;
}
if (SpellUtils.getLevel(spell) > 0 && isSpellsKnownMaxed) {
isAddDisabled = true;
}
let showAlwaysPrepared =
(!includeRemove && alwaysPrepared) ||
(includeRemove && !canRemove && alwaysPrepared);
if (SpellUtils.isCantrip(spell)) {
addButtonText =
Constants.SpellCastingLearningStyleAddText[
Constants.SpellCastingLearningStyle.Learned
];
removeButtonText =
Constants.SpellCastingLearningStyleRemoveText[
Constants.SpellCastingLearningStyle.Learned
];
}
return (
<div className={clsNames.join(" ")}>
{showAlwaysPrepared && (
<div className="spell-manager-item-always-prepared">
Always Prepared
</div>
)}
{!alwaysPrepared && enablePrepare && canPrepare && (
<Button
size="small"
disabled={isPrepareMaxed && !spell.prepared}
style={spell.prepared ? "" : "outline"}
onClick={this.handlePrepareToggle}
>
{spell.prepared ? removeButtonText : addButtonText}
</Button>
)}
{includeRemove && enableRemove && canRemove && (
<div
className="spell-manager-spell-remove"
onClick={this.handleRemove}
>
<span className="spell-manager-spell-remove-icon" />{" "}
{removeButtonText}
</div>
)}
{enableAdd && canAdd && (
<Button
size="small"
onClick={this.handleAdd}
disabled={isAddDisabled}
style={"outline"}
>
{addButtonText}
</Button>
)}
</div>
);
};
getMetaItems = () => {
const { spell, dataOriginRefData } = this.props;
const level = SpellUtils.getLevel(spell);
const concentration = SpellUtils.getConcentration(spell);
const rangeArea = SpellUtils.getDefinitionRangeArea(spell);
const attackType = SpellUtils.getAttackType(spell);
let metaItems: Array<string> = [];
metaItems.push(FormatUtils.renderSpellLevelName(level));
let expandedDataOriginRef = SpellUtils.getExpandedDataOriginRef(spell);
if (expandedDataOriginRef !== null) {
metaItems.push(
EntityUtils.getDataOriginRefName(
expandedDataOriginRef,
dataOriginRefData
)
);
}
if (concentration) {
metaItems.push("Concentration");
}
if (rangeArea) {
metaItems.push(
attackType && attackType === Constants.AttackTypeRangeEnum.RANGED
? `Range ${rangeArea}`
: rangeArea
);
}
return metaItems;
};
render() {
const {
spell,
spellCasterInfo,
ruleData,
isPrepareMaxed,
proficiencyBonus,
theme,
enableSpellRemove,
} = this.props;
const school = SpellUtils.getSchool(spell);
const spellImage = `https://www.dndbeyond.com/Content/Skins/Waterdeep/images/spell-schools/35/${school?.toLowerCase()}.png`;
return (
<Accordion
className={styles.spell}
summary={
<SpellName
spell={spell}
showSpellLevel={false}
showLegacyBadge={true}
/>
}
summaryAction={this.renderButtons(
["spell-list-item-header-actions"],
enableSpellRemove
)}
summaryImage={spellImage}
summaryImageAlt={`Icon for the ${school} school of magic`}
summaryMetaItems={this.getMetaItems()}
variant="paper"
>
<SpellDetail
theme={theme}
spell={spell}
isPreparedMaxed={isPrepareMaxed}
enableCaster={false}
spellCasterInfo={spellCasterInfo}
ruleData={ruleData}
showActions={false}
showCustomize={false}
proficiencyBonus={proficiencyBonus}
/>
<div className="spell-list-item-actions">
{this.renderButtons(["spell-list-item-content-actions"], true)}
</div>
</Accordion>
);
}
}
enum LoadSpellType {
ADDITIONAL = "ADDITIONAL",
ALWAYS_KNOWN = "ALWAYS_KNOWN",
}
interface SpellManagerGroupProps {
spells: Array<Spell>;
spellCasterInfo: SpellCasterInfo;
overallSpellInfo: any;
knownSpellIds: Array<any>;
ruleData: RuleData;
dataOriginRefData: DataOriginRefData;
loadSpells?: ApiSpellsRequest | null;
loadAlwaysKnownSpells?: ApiSpellsRequest | null;
enableRemove: boolean;
enableSpellRemove: boolean;
enableAdd: boolean;
enablePrepare: boolean;
enableUnprepare: boolean;
isPrepareMaxed: boolean;
isCantripsKnownMaxed: boolean;
isSpellsKnownMaxed: boolean;
addButtonText: string;
removeButtonText: string;
proficiencyBonus: number;
theme: CharacterTheme;
onAlwaysKnownLoad?: (spells: Array<BaseSpellContract>) => void;
onPrepare: (spell: Spell) => void;
onUnprepare: (spell: Spell) => void;
onAdd: (spell: Spell) => void;
onRemove: (spell: Spell) => void;
activeSourceCategories: Array<number>;
}
interface SpellManagerGroupState {
query: string;
lazySpells: Array<Spell>;
staticSpells: Array<Spell>;
filteredSpells: Array<Spell>;
currentPage: number;
loadingStatus: DataLoadingStatusEnum;
filterLevels: Array<number>;
filterSourceCategories: Array<number>;
filterQuery: string;
}
export default class SpellManagerGroup extends React.PureComponent<
SpellManagerGroupProps,
SpellManagerGroupState
> {
static defaultProps = {
enablePrepare: true,
enableUnprepare: true,
enableRemove: true,
enableSpellRemove: true,
enableAdd: true,
};
loadRuleCancelers: Array<Canceler> = [];
constructor(props) {
super(props);
const { spells } = props;
this.state = {
query: "",
lazySpells: [],
staticSpells: spells,
filteredSpells: spells,
currentPage: 0,
loadingStatus: DataLoadingStatusEnum.NOT_INITIALIZED,
filterLevels: [],
filterSourceCategories: [],
filterQuery: "",
};
}
fetchSpells = () => {
const { loadSpells, loadAlwaysKnownSpells, onAlwaysKnownLoad } = this.props;
if (!loadSpells && !loadAlwaysKnownSpells) {
this.setState({
loadingStatus: DataLoadingStatusEnum.LOADED,
});
return;
}
let requests: Array<ApiSpellsPromise> = [];
let loadingRequestTypes: Array<LoadSpellType> = [];
if (loadSpells) {
requests.push(
loadSpells({
cancelToken: new axios.CancelToken((c) => {
this.loadRuleCancelers.push(c);
}),
})
);
loadingRequestTypes.push(LoadSpellType.ADDITIONAL);
}
if (loadAlwaysKnownSpells) {
requests.push(
loadAlwaysKnownSpells({
cancelToken: new axios.CancelToken((c) => {
this.loadRuleCancelers.push(c);
}),
})
);
loadingRequestTypes.push(LoadSpellType.ALWAYS_KNOWN);
}
this.setState({
loadingStatus: DataLoadingStatusEnum.LOADING,
});
axios
.all(requests)
.then((responses) => {
let lazySpells: Array<Spell> = [];
for (let i = 0; i < responses.length; i++) {
let response = responses[i];
let requestType = loadingRequestTypes[i];
let spells = ApiAdapterUtils.getResponseData(response);
switch (requestType) {
case LoadSpellType.ADDITIONAL:
lazySpells =
spells !== null ? this.transformLoadedSpells(spells) : [];
break;
case LoadSpellType.ALWAYS_KNOWN:
if (spells !== null && onAlwaysKnownLoad) {
onAlwaysKnownLoad(spells);
}
break;
default:
// not implemented
}
}
this.setState({
lazySpells,
loadingStatus: DataLoadingStatusEnum.LOADED,
});
this.loadRuleCancelers = [];
})
.catch(AppLoggerUtils.handleAdhocApiError);
};
componentDidMount() {
this.fetchSpells();
}
componentWillUnmount(): void {
if (this.loadRuleCancelers.length > 0) {
this.loadRuleCancelers.forEach((cancel) => {
cancel();
});
}
}
componentDidUpdate(
prevProps: Readonly<SpellManagerGroupProps>,
prevState: Readonly<SpellManagerGroupState>,
snapshot?: any
): void {
const { spells, spellCasterInfo } = this.props;
if (spells !== prevProps.spells) {
this.setState((prevState) => ({
staticSpells: spells,
}));
}
if (
spellCasterInfo.characterLevel !==
prevProps.spellCasterInfo.characterLevel
) {
this.fetchSpells();
}
}
getCombinedSpells = () => {
const { lazySpells, staticSpells } = this.state;
const { knownSpellIds } = this.props;
const remainingLazySpells = lazySpells.filter(
(spell) => !knownSpellIds.includes(SpellUtils.deriveKnownKey(spell))
);
return sortBy(
[...remainingLazySpells, ...staticSpells],
[
(spell) => SpellUtils.getLevel(spell),
(spell) => SpellUtils.getName(spell),
(spell) => SpellUtils.getExpandedDataOriginRef(spell) !== null,
(spell) => SpellUtils.getUniqueKey(spell),
]
);
};
getFilteredSpells = (combinedSpells) => {
const { filterLevels, filterQuery, filterSourceCategories } = this.state;
const { ruleData } = this.props;
const filteredSpells = combinedSpells.filter((spell) => {
if (filterSourceCategories.length !== 0) {
const spellSources = SpellUtils.getSources(spell);
const sources = spellSources
.map((source) =>
HelperUtils.lookupDataOrFallback(
RuleDataUtils.getSourceDataLookup(ruleData),
source.sourceId
)
)
.filter(TypeScriptUtils.isNotNullOrUndefined);
const sourceCategoryId = sources[0]?.sourceCategory?.id || 0;
if (!filterSourceCategories.includes(sourceCategoryId)) {
return false;
}
}
if (
filterLevels.length !== 0 &&
!filterLevels.includes(SpellUtils.getLevel(spell))
) {
return false;
}
if (
filterQuery !== "" &&
!FilterUtils.doesQueryMatchData(filterQuery, SpellUtils.getName(spell))
) {
return false;
}
return true;
});
return filteredSpells;
};
transformLoadedSpells = (data) => {
const { ruleData, overallSpellInfo } = this.props;
return data.map((spell) =>
SpellUtils.simulateSpell(spell, overallSpellInfo, ruleData, null, null)
);
};
handleFilterSpellLevel = (level: number) => {
this.setState((prevState) => ({
filterLevels: prevState.filterLevels.includes(level)
? prevState.filterLevels.filter((l) => l !== level)
: [...prevState.filterLevels, level],
}));
};
handleSourceCategoryClick = (categoryId: number) => {
this.setState((prevState) => ({
filterSourceCategories: prevState.filterSourceCategories.includes(
categoryId
)
? prevState.filterSourceCategories.filter((cat) => cat !== categoryId)
: [...prevState.filterSourceCategories, categoryId],
}));
};
render() {
const {
onPrepare,
onUnprepare,
onRemove,
onAdd,
isPrepareMaxed,
isCantripsKnownMaxed,
isSpellsKnownMaxed,
addButtonText,
removeButtonText,
enableRemove,
enableAdd,
enablePrepare,
enableUnprepare,
enableSpellRemove,
spellCasterInfo,
ruleData,
dataOriginRefData,
proficiencyBonus,
theme,
activeSourceCategories,
} = this.props;
const { filterQuery, filterLevels, loadingStatus, filterSourceCategories } =
this.state;
const combinedSpells = this.getCombinedSpells();
const filteredSpells = this.getFilteredSpells(combinedSpells);
const spellDefinitions = combinedSpells
.map((spell) => SpellUtils.getDefinition(spell))
.filter(TypeScriptUtils.isNotNullOrUndefined);
const sourceCategories = SourceUtils.getSimpleSourceCategoriesData(
spellDefinitions,
ruleData,
activeSourceCategories
);
return (
<div className="spell-manager-group">
<SpellFilter
spells={combinedSpells}
sourceCategories={sourceCategories}
filterQuery={filterQuery}
filterLevels={filterLevels}
onLevelFilterClick={this.handleFilterSpellLevel}
onQueryChange={(value) =>
this.setState({
filterQuery: value,
})
}
onSourceCategoryClick={this.handleSourceCategoryClick}
filterSourceCategories={filterSourceCategories}
buttonSize="x-small"
filterStyle="builder"
/>
<div className="ct-character-tools__marketplace-callout">
Looking for something not in the list below? Unlock all official
options in the <Link href="/marketplace">Marketplace</Link>.
</div>
{loadingStatus === DataLoadingStatusEnum.LOADING && (
<LoadingPlaceholder />
)}
{loadingStatus === DataLoadingStatusEnum.LOADED &&
(filteredSpells.length > 0 ? (
filteredSpells.map((spell, idx) => (
<SpellManagerGroupItem
theme={theme}
spell={spell}
key={`${spell.id}-${idx}`}
onPrepare={onPrepare}
onUnprepare={onUnprepare}
onRemove={onRemove}
onAdd={onAdd}
isPrepareMaxed={isPrepareMaxed}
isCantripsKnownMaxed={isCantripsKnownMaxed}
isSpellsKnownMaxed={isSpellsKnownMaxed}
addButtonText={addButtonText}
removeButtonText={removeButtonText}
enableRemove={enableRemove}
enableAdd={enableAdd}
enablePrepare={enablePrepare}
enableUnprepare={enableUnprepare}
enableSpellRemove={enableSpellRemove}
spellCasterInfo={spellCasterInfo}
ruleData={ruleData}
dataOriginRefData={dataOriginRefData}
proficiencyBonus={proficiencyBonus}
/>
))
) : (
<div>No Results Found</div>
))}
</div>
);
}
}
@@ -0,0 +1,4 @@
import SpellManagerGroup, { SpellManagerGroupItem } from "./SpellManagerGroup";
export default SpellManagerGroup;
export { SpellManagerGroup, SpellManagerGroupItem };