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