254 lines
7.9 KiB
TypeScript

import clsx from "clsx";
import { FC, HTMLAttributes, useEffect, useState } from "react";
import CircleInfo from "@dndbeyond/fontawesome-cache/svgs/regular/circle-info.svg";
import { Accordion } from "~/components/Accordion";
import { HtmlContent } from "~/components/HtmlContent";
import { Link } from "~/components/Link";
import { Toggle } from "~/components/Toggle";
import { Tooltip } from "~/components/Tooltip";
import { orderBy } from "~/helpers/sortUtils";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import { useSource } from "~/hooks/useSource";
import { Select } from "~/tools/js/smartComponents/legacy";
import { RaceDefinitionContract } from "~/types";
import { Button } from "../../../../components/Button";
import { ConfirmSpeciesModal } from "../../components/ConfirmSpeciesModal";
import { Listing } from "../../components/Listing";
import { PortraitName } from "../../components/PortraitName";
import { Search } from "../../components/Search";
import { SpeciesDisplay } from "../../components/SpeciesDisplay";
import { Spinner } from "../../components/Spinner";
import { useSpeciesContext } from "../../contexts/Species";
import styles from "./styles.module.css";
//showHeader and OnQuickSelect are only be used from QuickBuild.tsx and RandomBuild.tsx
export interface SpeciesChooseProps extends HTMLAttributes<HTMLDivElement> {
showHeader?: boolean;
onQuickSelect?: (species: RaceDefinitionContract) => void;
}
export interface SourceGroupMapping {
name: string;
id: number;
items: RaceDefinitionContract[];
sortOrder: number | undefined;
isOpen: boolean;
}
export const SpeciesChoose: FC<SpeciesChooseProps> = ({
showHeader = true,
onQuickSelect,
className,
...props
}) => {
const { race: currentSpecies } = useCharacterEngine();
const {
allSpecies,
closeModal,
filteredSpecies,
isLegacyShowing,
isModalShowing,
setSource,
source,
sourceOptions,
toggleLegacyContent,
transformSpecies,
query,
setQuery,
isLoading,
getSpeciesInGroups,
} = useSpeciesContext();
const { getSourceCategoryGroups, getSourceCategoryDescription } = useSource();
const [mappedSourceCategoryGroups, setMappedSourceCategoryGroups] = useState<
SourceGroupMapping[]
>([]);
useEffect(() => {
const sourceCategoryGroups = getSourceCategoryGroups(filteredSpecies);
setMappedSourceCategoryGroups(
sourceCategoryGroups.map((group) => ({
...group,
isOpen: true,
}))
);
}, [filteredSpecies]);
const handleOverrideClick = (override): void => {
const updatedGroups = mappedSourceCategoryGroups.map((group) => ({
...group,
isOpen: override,
}));
setMappedSourceCategoryGroups(updatedGroups);
};
const handleSourceCategoryClick = (id: string, state: boolean): void => {
const parsedId = parseInt(id);
const updatedGroups = mappedSourceCategoryGroups.map((group) => ({
...group,
isOpen: group.id === parsedId ? state : group.isOpen,
}));
setMappedSourceCategoryGroups(updatedGroups);
};
return (
<div
className={clsx([styles.speciesChoose, className])}
{...props}
data-testid="chooseSpeciesSection"
>
{showHeader && (
<>
<PortraitName />
<hr className={styles.divider} />
<h2 className={styles.title}>
{currentSpecies ? "Change Origin: " : "Choose Origin: "}
{"Species"}
</h2>
{currentSpecies && (
<>
<SpeciesDisplay
headingText="Current Species"
actionText="Keep Species"
/>
<h3 className={styles.title}>Select New Species</h3>
</>
)}
</>
)}
<div className={styles.filters}>
<div>
<div className={styles.label}>Filter Species Source(s)</div>
<Select
placeholder={"-- All Sources --"}
options={sourceOptions}
value={source}
onChange={(value) => setSource(value)}
/>
</div>
<div className={styles.legacy}>
<Toggle
className={styles.toggle}
onClick={toggleLegacyContent}
checked={isLegacyShowing}
color="secondary"
aria-labelledby="legacy-content-label"
/>
<label id="legacy-content-label">
Show Legacy Content{" "}
<>
<CircleInfo
data-tooltip-id="legacy-info"
data-tooltip-place="bottom"
className={styles.infoIcon}
/>
<Tooltip id="legacy-info" clickable>
Legacy content doesn't reflect the latest rules and lore.{" "}
<Link
href="https://dndbeyond.com/legacy"
target="_blank"
rel="noopener noreferrer"
>
Learn More
</Link>
</Tooltip>
</>
</label>
</div>
</div>
<hr className={styles.divider} />
<div className={clsx([styles.text, styles.marketplace])}>
Check your source settings on the <strong>Home</strong> tab if you can't
find Species you've purchased.
<br />
Expand your library in the <Link href="/marketplace">
Marketplace
</Link>{" "}
for more Species options.
</div>
{isLoading ? (
<Spinner />
) : (
<>
<Search
className={styles.search}
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<div className={styles.collapseExpand}>
<Button
variant="text"
size="x-small"
className={styles.collapseExpandButton}
onClick={() => handleOverrideClick(false)}
forceThemeMode="light"
>
Collapse All
</Button>
<Button
variant="text"
size="x-small"
className={styles.collapseExpandButton}
onClick={() => handleOverrideClick(true)}
forceThemeMode="light"
>
Expand All
</Button>
</div>
{mappedSourceCategoryGroups.length > 0 ? (
mappedSourceCategoryGroups.map((category) => {
const name = category.name;
const id = category.id;
const description = getSourceCategoryDescription(category.id);
const currentSpeciesId = currentSpecies
? [
`${currentSpecies.entityRaceId}-${currentSpecies.entityRaceTypeId}`,
]
: [];
const groupedSpecies = getSpeciesInGroups(
transformSpecies(category.items)
);
const metaItems = description
? [<HtmlContent html={description} className={styles.text} />]
: [];
return (
<Accordion
id={category.id.toString()}
className={styles.accordion}
summary={<h3 className={styles.heading}>{name}</h3>}
summaryMetaItems={metaItems}
variant="text"
resetOpen={allSpecies.length !== filteredSpecies.length}
key={id}
forceShow
override={category.isOpen}
handleIsOpen={handleSourceCategoryClick}
>
<Listing
items={orderBy(groupedSpecies, "heading")}
disabledIds={currentSpeciesId}
onQuickSelect={onQuickSelect}
/>
</Accordion>
);
})
) : (
<p className={styles.notFound}>No Results Found</p>
)}
</>
)}
<ConfirmSpeciesModal open={isModalShowing} onClose={closeModal} />
</div>
);
};