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:
commit
8df9031d27
8
README.md
Normal file
8
README.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
This is the main javascript logic from the dndbeyond.com character sheets
|
||||||
|
|
||||||
|
The data is downloaded using `sourcemapper` from https://github.com/denandz/sourcemapper
|
||||||
|
|
||||||
|
Given the main JS file URL, the source code can be downloaded and mapped using:
|
||||||
|
```
|
||||||
|
~/go/bin/sourcemapper -output ddb -jsurl https://media.dndbeyond.com/character-app/static/js/main.90aa78c5.js
|
||||||
|
```
|
270
ddb_main/components/AbilityScoreManager/AbilityScoreManager.tsx
Normal file
270
ddb_main/components/AbilityScoreManager/AbilityScoreManager.tsx
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { HTMLAttributes, ReactNode, useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AbilityManager,
|
||||||
|
AbilityScoreSupplierData,
|
||||||
|
DataOrigin,
|
||||||
|
DataOriginUtils,
|
||||||
|
HelperUtils,
|
||||||
|
} from "@dndbeyond/character-rules-engine/es";
|
||||||
|
|
||||||
|
import { NumberDisplay } from "~/components/NumberDisplay";
|
||||||
|
import { useSidebar } from "~/contexts/Sidebar";
|
||||||
|
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||||
|
import { getDataOriginComponentInfo } from "~/subApps/sheet/components/Sidebar/helpers/paneUtils";
|
||||||
|
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||||
|
import DataOriginName from "~/tools/js/smartComponents/DataOriginName";
|
||||||
|
import { AbilityIcon } from "~/tools/js/smartComponents/Icons";
|
||||||
|
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
ability: AbilityManager;
|
||||||
|
showHeader?: boolean;
|
||||||
|
isReadonly: boolean;
|
||||||
|
isBuilder?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AbilityScoreManager({
|
||||||
|
ability,
|
||||||
|
isReadonly,
|
||||||
|
showHeader = true,
|
||||||
|
className = "",
|
||||||
|
isBuilder = false,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
const { characterTheme: theme, originRef } = useCharacterEngine();
|
||||||
|
const {
|
||||||
|
pane: { paneHistoryPush },
|
||||||
|
} = useSidebar();
|
||||||
|
|
||||||
|
const [overrideScore, setOverrideScore] = useState(
|
||||||
|
ability.getOverrideScore()
|
||||||
|
);
|
||||||
|
const [otherBonus, setOtherBonus] = useState(ability.getOtherBonus());
|
||||||
|
|
||||||
|
const handleOtherBonusBlur = (
|
||||||
|
evt: React.FocusEvent<HTMLInputElement>
|
||||||
|
): void => {
|
||||||
|
let value: number | null = HelperUtils.parseInputInt(evt.target.value);
|
||||||
|
setOtherBonus(ability.handleOtherBonusChange(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOverrideScoreBlur = (
|
||||||
|
evt: React.FocusEvent<HTMLInputElement>
|
||||||
|
): void => {
|
||||||
|
const value: number | null = HelperUtils.parseInputInt(evt.target.value);
|
||||||
|
setOverrideScore(ability.handleOverrideScoreChange(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOtherBonusChange = (
|
||||||
|
evt: React.ChangeEvent<HTMLInputElement>
|
||||||
|
): void => {
|
||||||
|
setOtherBonus(HelperUtils.parseInputInt(evt.target.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOverrideScoreChange = (
|
||||||
|
evt: React.ChangeEvent<HTMLInputElement>
|
||||||
|
): void => {
|
||||||
|
setOverrideScore(HelperUtils.parseInputInt(evt.target.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDataOriginClick = (dataOrigin: DataOrigin) => {
|
||||||
|
let component = getDataOriginComponentInfo(dataOrigin);
|
||||||
|
if (component.type !== PaneComponentEnum.ERROR_404) {
|
||||||
|
paneHistoryPush(component.type, component.identifiers);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const label = ability.getLabel();
|
||||||
|
const modifier = ability.getModifier();
|
||||||
|
const baseScore = ability.getBaseScore();
|
||||||
|
const statId = ability.getId();
|
||||||
|
const totalScore = ability.getTotalScore();
|
||||||
|
const speciesBonus = ability.getRacialBonus();
|
||||||
|
const classBonuses = ability.getClassBonuses();
|
||||||
|
const miscBonus = ability.getMiscBonus();
|
||||||
|
const stackingBonus = ability.getStackingBonus();
|
||||||
|
const setScore = ability.getSetScore();
|
||||||
|
|
||||||
|
const allStatsBonusSuppliers = ability.getAllStatBonusSuppliers();
|
||||||
|
const statSetScoreSuppliers = ability.getStatSetScoreSuppliers();
|
||||||
|
const stackingBonusSuppliers = ability.getStackingBonusSuppliers();
|
||||||
|
|
||||||
|
const getSupplierData = (
|
||||||
|
suppliers: Array<AbilityScoreSupplierData>
|
||||||
|
): ReactNode => {
|
||||||
|
return (
|
||||||
|
<div className={styles.suppliers}>
|
||||||
|
{suppliers.map((supplier) => {
|
||||||
|
const origin = supplier.dataOrigin;
|
||||||
|
let expandedOrigin: DataOrigin | null = null;
|
||||||
|
|
||||||
|
if (supplier.expandedOriginRef) {
|
||||||
|
const primaryOrigin = DataOriginUtils.getRefPrimary(
|
||||||
|
supplier.expandedOriginRef,
|
||||||
|
originRef
|
||||||
|
);
|
||||||
|
|
||||||
|
if (primaryOrigin && primaryOrigin["componentId"] !== null) {
|
||||||
|
expandedOrigin = primaryOrigin["dataOrigin"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(styles.supplier, isBuilder && styles.builder)}
|
||||||
|
key={supplier.key}
|
||||||
|
>
|
||||||
|
<div className={styles.label}>
|
||||||
|
<DataOriginName
|
||||||
|
className={isBuilder ? styles.builder : ""}
|
||||||
|
dataOrigin={origin}
|
||||||
|
tryParent
|
||||||
|
theme={theme}
|
||||||
|
onClick={!isBuilder ? handleDataOriginClick : undefined}
|
||||||
|
/>
|
||||||
|
{expandedOrigin && (
|
||||||
|
<span>
|
||||||
|
{" "}
|
||||||
|
(
|
||||||
|
<DataOriginName
|
||||||
|
className={isBuilder ? styles.builder : ""}
|
||||||
|
dataOrigin={expandedOrigin}
|
||||||
|
tryParent
|
||||||
|
theme={theme}
|
||||||
|
onClick={!isBuilder ? handleDataOriginClick : undefined}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.value}>
|
||||||
|
(
|
||||||
|
{supplier.type === "set" ? (
|
||||||
|
supplier.value
|
||||||
|
) : (
|
||||||
|
<NumberDisplay
|
||||||
|
className={styles.number}
|
||||||
|
type="signed"
|
||||||
|
number={supplier.value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...props}>
|
||||||
|
{showHeader && (
|
||||||
|
<div className={styles.header}>
|
||||||
|
<AbilityIcon
|
||||||
|
statId={statId}
|
||||||
|
themeMode="light"
|
||||||
|
className={styles.icon}
|
||||||
|
/>
|
||||||
|
<div className={styles.name}>{label}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.table}>
|
||||||
|
<div className={clsx([styles.row, isBuilder && styles.highlight])}>
|
||||||
|
<div className={styles.label}>Total Score</div>
|
||||||
|
<div className={styles.value}>
|
||||||
|
{totalScore === null ? "--" : totalScore}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={clsx([styles.row, isBuilder && styles.highlight])}>
|
||||||
|
<div className={styles.label}>Modifier</div>
|
||||||
|
<div className={styles.value}>
|
||||||
|
<NumberDisplay type="signed" number={modifier} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<div className={styles.label}>Base Score</div>
|
||||||
|
<div className={styles.value}>
|
||||||
|
{baseScore === null ? "--" : baseScore}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
styles.row,
|
||||||
|
allStatsBonusSuppliers.length > 0 && styles.hasSuppliers
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.label}>
|
||||||
|
<div>Bonus</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.value}>
|
||||||
|
<NumberDisplay
|
||||||
|
type="signed"
|
||||||
|
number={classBonuses + speciesBonus + miscBonus}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{getSupplierData(allStatsBonusSuppliers)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
styles.row,
|
||||||
|
statSetScoreSuppliers.length > 0 && styles.hasSuppliers
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.label}>
|
||||||
|
<div>Set Score</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.value}>{setScore}</div>
|
||||||
|
</div>
|
||||||
|
{getSupplierData(statSetScoreSuppliers)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
styles.row,
|
||||||
|
stackingBonusSuppliers.length > 0 && styles.hasSuppliers
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.label}>
|
||||||
|
<div>Stacking Bonus</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.value}>
|
||||||
|
<NumberDisplay type="signed" number={stackingBonus} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{getSupplierData(stackingBonusSuppliers)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.overrides}>
|
||||||
|
<div className={styles.override}>
|
||||||
|
<div className={styles.label}>Other Modifier</div>
|
||||||
|
<div className={styles.value}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={otherBonus === null ? "" : otherBonus}
|
||||||
|
placeholder="--"
|
||||||
|
onChange={handleOtherBonusChange}
|
||||||
|
onBlur={handleOtherBonusBlur}
|
||||||
|
readOnly={isReadonly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.override}>
|
||||||
|
<div className={styles.label}>Override Score</div>
|
||||||
|
<div className={styles.value}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={overrideScore === null ? "" : overrideScore}
|
||||||
|
placeholder="--"
|
||||||
|
onChange={handleOverrideScoreChange}
|
||||||
|
onBlur={handleOverrideScoreBlur}
|
||||||
|
readOnly={isReadonly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
148
ddb_main/components/Accordion/Accordion.tsx
Normal file
148
ddb_main/components/Accordion/Accordion.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import {
|
||||||
|
FC,
|
||||||
|
HTMLAttributes,
|
||||||
|
MouseEvent,
|
||||||
|
ReactNode,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import ChevronDown from "@dndbeyond/fontawesome-cache/svgs/solid/chevron-down.svg";
|
||||||
|
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
export interface AccordionProps extends HTMLAttributes<HTMLElement> {
|
||||||
|
variant?: "default" | "text" | "paper";
|
||||||
|
summary: ReactNode;
|
||||||
|
description?: ReactNode;
|
||||||
|
size?: "small" | "medium";
|
||||||
|
forceShow?: boolean;
|
||||||
|
useTheme?: boolean;
|
||||||
|
resetOpen?: boolean;
|
||||||
|
override?: boolean | null;
|
||||||
|
handleIsOpen?: (id: string, isOpen: boolean) => void;
|
||||||
|
summaryAction?: ReactNode;
|
||||||
|
summaryImage?: string;
|
||||||
|
summaryImageAlt?: string;
|
||||||
|
summaryImageSize?: number;
|
||||||
|
summaryMetaItems?: Array<ReactNode>;
|
||||||
|
showAlert?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Accordion: FC<AccordionProps> = ({
|
||||||
|
className,
|
||||||
|
summary,
|
||||||
|
description,
|
||||||
|
size = "medium",
|
||||||
|
variant = "default",
|
||||||
|
children,
|
||||||
|
forceShow = false,
|
||||||
|
// style,
|
||||||
|
useTheme,
|
||||||
|
resetOpen,
|
||||||
|
handleIsOpen,
|
||||||
|
override = null,
|
||||||
|
id = "",
|
||||||
|
summaryAction,
|
||||||
|
summaryImage,
|
||||||
|
summaryImageAlt,
|
||||||
|
summaryImageSize = 32,
|
||||||
|
summaryMetaItems,
|
||||||
|
showAlert,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const summaryRef = useRef<HTMLElement>(null);
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const loaded = useRef(false);
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(forceShow);
|
||||||
|
const [height, setHeight] = useState<number | string>();
|
||||||
|
|
||||||
|
const handleToggle = (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsOpen((current) => {
|
||||||
|
if (handleIsOpen && id) handleIsOpen(id, !current);
|
||||||
|
return !current;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const summaryHeight = summaryRef.current?.clientHeight;
|
||||||
|
const contentHeight = contentRef.current?.getBoundingClientRect().height;
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
// If the dialog is open and it's not the first load, set the height to the summary + content
|
||||||
|
if (summaryHeight && contentHeight && loaded.current === true)
|
||||||
|
setHeight(summaryHeight + contentHeight);
|
||||||
|
} else {
|
||||||
|
// If the dialog is closed, set the height to the summary
|
||||||
|
if (summaryHeight) setHeight(summaryHeight);
|
||||||
|
}
|
||||||
|
loaded.current = true;
|
||||||
|
}, [isOpen, resetOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (resetOpen && !isOpen) {
|
||||||
|
setIsOpen(true);
|
||||||
|
}
|
||||||
|
}, [resetOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (override !== null) {
|
||||||
|
setIsOpen(override);
|
||||||
|
}
|
||||||
|
}, [override]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<details
|
||||||
|
className={clsx([
|
||||||
|
styles.details,
|
||||||
|
styles[size],
|
||||||
|
styles[variant],
|
||||||
|
useTheme && styles.theme,
|
||||||
|
showAlert && styles.alert,
|
||||||
|
className,
|
||||||
|
])}
|
||||||
|
open={isOpen}
|
||||||
|
// style={{ height, ...style }} INVESTIGATE THIS LATER
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<summary
|
||||||
|
className={styles.summary}
|
||||||
|
onClick={handleToggle}
|
||||||
|
ref={summaryRef}
|
||||||
|
>
|
||||||
|
{summaryImage && (
|
||||||
|
<img
|
||||||
|
className={styles.image}
|
||||||
|
src={summaryImage}
|
||||||
|
alt={summaryImageAlt}
|
||||||
|
width={summaryImageSize}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className={styles.heading}>
|
||||||
|
<div>
|
||||||
|
{summary}
|
||||||
|
{summaryMetaItems && (
|
||||||
|
<div className={styles.metaItems}>
|
||||||
|
{summaryMetaItems.map((metaItem, idx) => (
|
||||||
|
<div key={uuidv4()} className={styles.metaItem}>
|
||||||
|
{metaItem}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.actions}>{summaryAction}</div>
|
||||||
|
<ChevronDown className={styles.icon} />
|
||||||
|
</div>
|
||||||
|
{description && <div className={styles.description}>{description}</div>}
|
||||||
|
</summary>
|
||||||
|
<div className={styles.content} ref={contentRef}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
};
|
59
ddb_main/components/Button/Button.tsx
Normal file
59
ddb_main/components/Button/Button.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button as TtuiButton,
|
||||||
|
ButtonProps as TtuiButtonProps,
|
||||||
|
} from "@dndbeyond/ttui/components/Button";
|
||||||
|
|
||||||
|
import { useCharacterTheme } from "~/contexts/CharacterTheme";
|
||||||
|
import { ThemeMode } from "~/types";
|
||||||
|
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends Omit<TtuiButtonProps, "size" | "variant" | "color"> {
|
||||||
|
size?: "xx-small" | TtuiButtonProps["size"];
|
||||||
|
themed?: boolean;
|
||||||
|
forceThemeMode?: ThemeMode;
|
||||||
|
variant?: "builder" | "builder-text" | TtuiButtonProps["variant"];
|
||||||
|
color?: "builder-green" | TtuiButtonProps["color"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Button: FC<ButtonProps> = ({
|
||||||
|
className,
|
||||||
|
size = "medium",
|
||||||
|
themed,
|
||||||
|
variant = "solid",
|
||||||
|
forceThemeMode,
|
||||||
|
color,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
// Check if the button has a custom size
|
||||||
|
const isCustomSize = size === "xx-small";
|
||||||
|
const isCustomVariant = variant === "builder" || variant === "builder-text";
|
||||||
|
const isCustomColor = color === "builder-green";
|
||||||
|
const { isDarkMode } = useCharacterTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TtuiButton
|
||||||
|
// If the button has a custom size, use local styles
|
||||||
|
className={clsx([
|
||||||
|
styles[size],
|
||||||
|
themed && styles.themed,
|
||||||
|
forceThemeMode && styles[forceThemeMode],
|
||||||
|
styles[variant],
|
||||||
|
!forceThemeMode && isDarkMode && styles.dark,
|
||||||
|
isCustomColor && styles[color],
|
||||||
|
className,
|
||||||
|
])}
|
||||||
|
// If the button has a normal size, pass prop normally
|
||||||
|
size={!isCustomSize ? size : undefined}
|
||||||
|
// If the button is themed, use theme colors
|
||||||
|
variant={!isCustomVariant ? variant : undefined}
|
||||||
|
// Children are included in props
|
||||||
|
color={!isCustomColor ? color : undefined}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
82
ddb_main/components/Checkbox/Checkbox.tsx
Normal file
82
ddb_main/components/Checkbox/Checkbox.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { ChangeEvent, FC, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Checkbox as TtuiCheckbox,
|
||||||
|
CheckboxProps as TtuiCheckboxProps,
|
||||||
|
} from "@dndbeyond/ttui/components/Checkbox";
|
||||||
|
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
interface CheckboxProps extends TtuiCheckboxProps {
|
||||||
|
themed?: boolean;
|
||||||
|
darkMode?: boolean;
|
||||||
|
variant?: "default" | "builder" | "sidebar";
|
||||||
|
onClick?: (isEnabled: boolean) => void;
|
||||||
|
onChangePromise?: (
|
||||||
|
newIsEnabled: boolean,
|
||||||
|
accept: () => void,
|
||||||
|
reject: () => void
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Checkbox: FC<CheckboxProps> = ({
|
||||||
|
className,
|
||||||
|
themed,
|
||||||
|
variant = "default",
|
||||||
|
darkMode,
|
||||||
|
checked = false,
|
||||||
|
onChangePromise,
|
||||||
|
onClick,
|
||||||
|
disabled,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const [isChecked, setIsChecked] = useState(checked);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsChecked(checked);
|
||||||
|
}, [checked]);
|
||||||
|
|
||||||
|
const handleChange = (evt: ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
evt.stopPropagation();
|
||||||
|
evt.nativeEvent.stopImmediatePropagation();
|
||||||
|
|
||||||
|
const newCheckedState = !isChecked;
|
||||||
|
|
||||||
|
// promise-based logic
|
||||||
|
if (onChangePromise) {
|
||||||
|
onChangePromise(
|
||||||
|
newCheckedState,
|
||||||
|
() => {
|
||||||
|
// Promise accepted, update the state
|
||||||
|
setIsChecked(newCheckedState);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// Promise rejected, do nothing
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Update the state immediately if no promise handling - only IF onClick is provided
|
||||||
|
if (onClick) {
|
||||||
|
onClick(newCheckedState);
|
||||||
|
setIsChecked(newCheckedState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<TtuiCheckbox
|
||||||
|
className={clsx([
|
||||||
|
themed && styles.themed,
|
||||||
|
darkMode && styles[darkMode ? "darkMode" : "lightMode"],
|
||||||
|
className,
|
||||||
|
variant === "default" && styles.default,
|
||||||
|
variant === "builder" && styles.builder,
|
||||||
|
])}
|
||||||
|
onChange={handleChange}
|
||||||
|
aria-pressed={isChecked}
|
||||||
|
checked={isChecked}
|
||||||
|
disabled={disabled}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,62 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, HTMLAttributes, ReactNode, useState } from "react";
|
||||||
|
|
||||||
|
import { HtmlContent } from "../HtmlContent";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
forceShow?: boolean;
|
||||||
|
heading?: ReactNode;
|
||||||
|
maxLength?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component that will display a "Show More" button if the content is longer
|
||||||
|
* than the maxLength prop. It will also accept a heading prop to display text
|
||||||
|
* above the content.
|
||||||
|
*/
|
||||||
|
export const CollapsibleContent: FC<Props> = ({
|
||||||
|
forceShow,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
heading,
|
||||||
|
maxLength = 600,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(forceShow);
|
||||||
|
|
||||||
|
const handleToggleClick = (): void => {
|
||||||
|
setIsOpen((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof children === "string" && children?.length <= maxLength && !heading)
|
||||||
|
return (
|
||||||
|
<HtmlContent html={children as string} className={className} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx([
|
||||||
|
styles.collapsible,
|
||||||
|
heading && styles.withHeading,
|
||||||
|
isOpen && styles.open,
|
||||||
|
className,
|
||||||
|
])}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{heading && <div>{heading}</div>}
|
||||||
|
{typeof children === "string" ? (
|
||||||
|
<HtmlContent className={styles.content} html={children as string} />
|
||||||
|
) : (
|
||||||
|
<div className={styles.content}>{children}</div>
|
||||||
|
)}
|
||||||
|
{!forceShow && (
|
||||||
|
<button className={styles.button} onClick={handleToggleClick}>
|
||||||
|
{isOpen ? "Show Less" : "Show More"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
116
ddb_main/components/ConfirmModal/ConfirmModal.tsx
Normal file
116
ddb_main/components/ConfirmModal/ConfirmModal.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, useEffect } from "react";
|
||||||
|
|
||||||
|
import CloseIcon from "@dndbeyond/fontawesome-cache/svgs/solid/x.svg";
|
||||||
|
import { Dialog, DialogProps } from "@dndbeyond/ttui/components/Dialog";
|
||||||
|
|
||||||
|
import { Button, ButtonProps } from "../Button";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @heading Text for the title of the modal - defaults to "Confirm"
|
||||||
|
* @onClose Function to call when the Close button is clicked
|
||||||
|
* @onConfirm Function to call when the Confirm button is clicked
|
||||||
|
* @confirmButtonText Provide text to the Confirm button - defaults to "Confirm"
|
||||||
|
* @closeButtonText Provide text to the Close button - defaults to "Cancel"
|
||||||
|
* @variant "default" | "remove" | "confirm-only"
|
||||||
|
* --default: default modal with both Confirm and Close buttons using the "success" button color
|
||||||
|
* --remove: modal with both Confirm and Close buttons using the "secondary" color for the header and Confirm button
|
||||||
|
* --confirm-only: modal with only the Confirm button using the "success" button color
|
||||||
|
* @size "default" | "fit-content"
|
||||||
|
* --default: default modal size
|
||||||
|
* --fit-content: modal size adjusts to the content
|
||||||
|
* @color Passes a ButtonProps color to the confirm button
|
||||||
|
* @useMobileFullScreen Boolean to set the modal to full screen on mobile breakpoint
|
||||||
|
*/
|
||||||
|
export interface ConfirmModalProps extends DialogProps {
|
||||||
|
heading?: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
confirmButtonText?: string;
|
||||||
|
closeButtonText?: string;
|
||||||
|
variant?: "default" | "remove" | "confirm-only";
|
||||||
|
size?: "default" | "fit-content";
|
||||||
|
color?: ButtonProps["color"];
|
||||||
|
useMobileFullScreen?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConfirmModal: FC<ConfirmModalProps> = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
heading = "Confirm",
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
confirmButtonText = "Confirm",
|
||||||
|
closeButtonText = "Cancel",
|
||||||
|
open,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
color = "success",
|
||||||
|
useMobileFullScreen,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const isConfirmOnly = variant === "confirm-only";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const body = document.querySelector("body");
|
||||||
|
if (!body) return;
|
||||||
|
|
||||||
|
// Prevent scrolling when modal is visible
|
||||||
|
if (open) {
|
||||||
|
body.style.overflow = "hidden";
|
||||||
|
} else {
|
||||||
|
body.style.overflow = "";
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
className={clsx([
|
||||||
|
styles.confirmModal,
|
||||||
|
useMobileFullScreen && styles.fullScreen,
|
||||||
|
styles[variant],
|
||||||
|
styles[size],
|
||||||
|
className,
|
||||||
|
])}
|
||||||
|
onClose={onClose}
|
||||||
|
modal
|
||||||
|
open={open}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<h2>{heading}</h2>
|
||||||
|
<Button
|
||||||
|
className={styles.closeButton}
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label={`Close ${heading} Modal`}
|
||||||
|
forceThemeMode="light"
|
||||||
|
>
|
||||||
|
<CloseIcon className={styles.closeIcon} />
|
||||||
|
</Button>
|
||||||
|
</header>
|
||||||
|
<div className={styles.content}>{children}</div>
|
||||||
|
<footer className={styles.footer}>
|
||||||
|
{!isConfirmOnly && (
|
||||||
|
<Button className={styles.footerButton} onClick={onClose}>
|
||||||
|
{closeButtonText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
className={clsx([
|
||||||
|
styles.footerButton,
|
||||||
|
isConfirmOnly && styles.confirmOnly,
|
||||||
|
])}
|
||||||
|
color={variant === "remove" ? "secondary" : color}
|
||||||
|
onClick={onConfirm}
|
||||||
|
forceThemeMode="light"
|
||||||
|
size={isConfirmOnly ? "x-small" : undefined}
|
||||||
|
>
|
||||||
|
{confirmButtonText}
|
||||||
|
</Button>
|
||||||
|
</footer>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
40
ddb_main/components/EditableName/EditableName.tsx
Normal file
40
ddb_main/components/EditableName/EditableName.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { HTMLAttributes } from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
import Pen from "@dndbeyond/fontawesome-cache/svgs/light/pen.svg";
|
||||||
|
|
||||||
|
import { appEnvSelectors } from "../../tools/js/Shared/selectors";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
interface EditableNameProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component which displays children with an edit button to the right. It is
|
||||||
|
* used in panes to give custom names to items, etc.
|
||||||
|
*/
|
||||||
|
export const EditableName = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
|
...props
|
||||||
|
}: EditableNameProps) => {
|
||||||
|
const isReadOnly = useSelector(appEnvSelectors.getIsReadonly);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx([styles.editableName, className])} {...props}>
|
||||||
|
<div>{children}</div>
|
||||||
|
{!isReadOnly && (
|
||||||
|
<button
|
||||||
|
className={styles.button}
|
||||||
|
onClick={onClick}
|
||||||
|
aria-label="Edit name"
|
||||||
|
>
|
||||||
|
<Pen className={styles.icon} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
306
ddb_main/components/FeatureChoice/FeatureChoice.tsx
Normal file
306
ddb_main/components/FeatureChoice/FeatureChoice.tsx
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
import { FC, HTMLAttributes } from "react";
|
||||||
|
|
||||||
|
import { BuilderChoiceTypeEnum } from "~/constants";
|
||||||
|
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||||
|
import { useRuleData } from "~/hooks/useRuleData";
|
||||||
|
import { useSource } from "~/hooks/useSource";
|
||||||
|
import {
|
||||||
|
DetailChoice,
|
||||||
|
DetailChoiceFeat,
|
||||||
|
} from "~/tools/js/Shared/containers/DetailChoice";
|
||||||
|
import { TypeScriptUtils } from "~/tools/js/Shared/utils";
|
||||||
|
import {
|
||||||
|
CharClass,
|
||||||
|
Choice,
|
||||||
|
ClassDefinitionContract,
|
||||||
|
ClassFeature,
|
||||||
|
Feat,
|
||||||
|
FeatLookup,
|
||||||
|
FeatureChoiceOption,
|
||||||
|
HtmlSelectOptionGroup,
|
||||||
|
SourceData,
|
||||||
|
} from "~/types";
|
||||||
|
|
||||||
|
export interface FeatureChoiceProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
choice: Choice;
|
||||||
|
charClass?: CharClass;
|
||||||
|
feature?: ClassFeature;
|
||||||
|
featsData: Feat[];
|
||||||
|
subclassData?: ClassDefinitionContract[];
|
||||||
|
onChoiceChange: (
|
||||||
|
choiceId: string,
|
||||||
|
type: number,
|
||||||
|
value: any,
|
||||||
|
parentChoiceId: string | null
|
||||||
|
) => void;
|
||||||
|
collapseDescription?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for rendering a features choice(s) - for species traits and class features - using DetailChoiceFeat for a selected feat's choices and DetailChoice for all others.
|
||||||
|
It is used in both the builder and character sheet sidebar panes.
|
||||||
|
*/
|
||||||
|
export const FeatureChoice: FC<FeatureChoiceProps> = ({
|
||||||
|
charClass,
|
||||||
|
choice,
|
||||||
|
feature,
|
||||||
|
featsData,
|
||||||
|
subclassData,
|
||||||
|
onChoiceChange,
|
||||||
|
className,
|
||||||
|
collapseDescription,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
choiceUtils,
|
||||||
|
featUtils,
|
||||||
|
helperUtils,
|
||||||
|
prerequisiteUtils,
|
||||||
|
classUtils,
|
||||||
|
featLookup,
|
||||||
|
preferences,
|
||||||
|
prerequisiteData,
|
||||||
|
choiceInfo,
|
||||||
|
ruleData,
|
||||||
|
entityRestrictionData,
|
||||||
|
} = useCharacterEngine();
|
||||||
|
|
||||||
|
const { ruleDataUtils } = useRuleData();
|
||||||
|
|
||||||
|
const {
|
||||||
|
getGroupedOptionsBySourceCategory,
|
||||||
|
getSimpleSourcedDefinitionContracts,
|
||||||
|
} = useSource();
|
||||||
|
|
||||||
|
const optionValue = choiceUtils.getOptionValue(choice);
|
||||||
|
const options = choiceUtils.getOptions(choice);
|
||||||
|
const type = choiceUtils.getType(choice);
|
||||||
|
const tagConstraints = choiceUtils.getTagConstraints(choice);
|
||||||
|
|
||||||
|
let availableOptions: Array<FeatureChoiceOption> = [];
|
||||||
|
let availableGroupedOptions: HtmlSelectOptionGroup[] = [];
|
||||||
|
let detailChoiceDesc: string | null = null;
|
||||||
|
let subchoicesNode: React.ReactNode;
|
||||||
|
|
||||||
|
const handleChoiceChange = (
|
||||||
|
id: string,
|
||||||
|
type: number,
|
||||||
|
subType: number | null,
|
||||||
|
value: any,
|
||||||
|
parentChoiceId: string | null
|
||||||
|
): void => {
|
||||||
|
onChoiceChange(id, type, value, parentChoiceId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSubclassData = (): ClassDefinitionContract[] => {
|
||||||
|
if (!subclassData || !charClass) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: ClassDefinitionContract[] = [...subclassData];
|
||||||
|
|
||||||
|
let existingSubclass = classUtils.getSubclass(charClass);
|
||||||
|
if (
|
||||||
|
existingSubclass !== null &&
|
||||||
|
!data.some(
|
||||||
|
(classDefinition) =>
|
||||||
|
existingSubclass !== null &&
|
||||||
|
classDefinition.id === existingSubclass.id
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
data.push(existingSubclass);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAvailableFeatChoices = (
|
||||||
|
existingFeatId: number | null,
|
||||||
|
featData: Feat[],
|
||||||
|
featLookup: FeatLookup
|
||||||
|
): Feat[] => {
|
||||||
|
let data: Feat[] = [...featData];
|
||||||
|
|
||||||
|
if (existingFeatId !== null) {
|
||||||
|
let existingFeat = helperUtils.lookupDataOrFallback(
|
||||||
|
featLookup,
|
||||||
|
existingFeatId
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
existingFeat !== null &&
|
||||||
|
!data.some((feat) => featUtils.getId(feat) === existingFeatId)
|
||||||
|
) {
|
||||||
|
data.push(existingFeat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSubclassSources = (
|
||||||
|
subclass: ClassDefinitionContract
|
||||||
|
): SourceData[] => {
|
||||||
|
if (subclass.sources === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return subclass.sources
|
||||||
|
.map((sourceMapping) =>
|
||||||
|
helperUtils.lookupDataOrFallback(
|
||||||
|
ruleDataUtils.getSourceDataLookup(ruleData),
|
||||||
|
sourceMapping.sourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter(TypeScriptUtils.isNotNullOrUndefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION:
|
||||||
|
const availableFeats = getAvailableFeatChoices(
|
||||||
|
optionValue,
|
||||||
|
featsData,
|
||||||
|
featLookup
|
||||||
|
);
|
||||||
|
const repeatableFeatTracker = new Set();
|
||||||
|
|
||||||
|
// Add selected feat to repeatable tracker if repeatable
|
||||||
|
const selectedFeat = optionValue ? featLookup[optionValue] : null;
|
||||||
|
if (selectedFeat && featUtils.isRepeatable(selectedFeat)) {
|
||||||
|
const parentId = featUtils.getRepeatableGroupId(selectedFeat);
|
||||||
|
if (parentId) {
|
||||||
|
repeatableFeatTracker.add(parentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredFeats = availableFeats.filter((feat) => {
|
||||||
|
const featId = featUtils.getId(feat);
|
||||||
|
const isRepeatable = featUtils.isRepeatable(feat);
|
||||||
|
|
||||||
|
// If the feat is the currently selected, always include it
|
||||||
|
if (featId === optionValue) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always exclude all previous selected feats
|
||||||
|
if (featLookup[featId]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the Feat does not meet the tag constraints, should they exist, exclude it
|
||||||
|
const tagCategories = featUtils.getCategories(feat);
|
||||||
|
if (
|
||||||
|
tagConstraints &&
|
||||||
|
!featUtils.doesSatisfyTagConstraints(tagCategories, tagConstraints)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle prerequisites when enforcing feat rules
|
||||||
|
if (
|
||||||
|
preferences.enforceFeatRules &&
|
||||||
|
!prerequisiteUtils.validatePrerequisiteGrouping(
|
||||||
|
featUtils.getPrerequisites(feat),
|
||||||
|
prerequisiteData
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special handling for repeatable feats, there can be only one
|
||||||
|
if (isRepeatable) {
|
||||||
|
const parentId = featUtils.getRepeatableGroupId(feat);
|
||||||
|
|
||||||
|
// If a feat from this repeatable group exist exclude all others
|
||||||
|
if (repeatableFeatTracker.has(parentId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
repeatableFeatTracker.add(parentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If none of the exclusions above are met, include the feat
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
//Group available feats by source category
|
||||||
|
availableGroupedOptions = getGroupedOptionsBySourceCategory(
|
||||||
|
filteredFeats
|
||||||
|
.map((feat) => featUtils.getDefinition(feat))
|
||||||
|
.filter(TypeScriptUtils.isNotNullOrUndefined)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedFeat && optionValue !== null) {
|
||||||
|
detailChoiceDesc = featUtils.getDescription(selectedFeat);
|
||||||
|
subchoicesNode = <DetailChoiceFeat featId={optionValue} />;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BuilderChoiceTypeEnum.SUB_CLASS_OPTION:
|
||||||
|
const subclassData = getSubclassData();
|
||||||
|
|
||||||
|
//Group available subclasses by source category
|
||||||
|
availableGroupedOptions = getGroupedOptionsBySourceCategory(subclassData);
|
||||||
|
|
||||||
|
const chosenSubclass = subclassData.find(
|
||||||
|
(subclass) => subclass.id === optionValue
|
||||||
|
);
|
||||||
|
if (chosenSubclass) {
|
||||||
|
detailChoiceDesc = "";
|
||||||
|
let sources = getSubclassSources(chosenSubclass);
|
||||||
|
sources.forEach((source) => {
|
||||||
|
if (source.sourceCategory && source.sourceCategory.isToggleable) {
|
||||||
|
detailChoiceDesc += source.sourceCategory.description
|
||||||
|
? source.sourceCategory.description
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BuilderChoiceTypeEnum.ENTITY_SPELL_OPTION:
|
||||||
|
// Map over the options to mock parts of a SpellDefinitionContract.
|
||||||
|
const spellOptions = getSimpleSourcedDefinitionContracts(options);
|
||||||
|
availableGroupedOptions = getGroupedOptionsBySourceCategory(
|
||||||
|
spellOptions,
|
||||||
|
optionValue,
|
||||||
|
entityRestrictionData
|
||||||
|
);
|
||||||
|
|
||||||
|
// If there is a chosen spell, set detailChoiceDesc to its description.
|
||||||
|
const chosenSpell = spellOptions.find(
|
||||||
|
(spell) => spell.id === optionValue
|
||||||
|
);
|
||||||
|
|
||||||
|
if (chosenSpell) {
|
||||||
|
detailChoiceDesc = chosenSpell.description ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
availableOptions = options.map((option) => ({
|
||||||
|
...option,
|
||||||
|
value: option.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className} {...props}>
|
||||||
|
<DetailChoice
|
||||||
|
{...choice}
|
||||||
|
choice={choice}
|
||||||
|
options={
|
||||||
|
availableGroupedOptions.length > 0
|
||||||
|
? availableGroupedOptions
|
||||||
|
: availableOptions
|
||||||
|
}
|
||||||
|
onChange={handleChoiceChange}
|
||||||
|
description={detailChoiceDesc || ""}
|
||||||
|
choiceInfo={choiceInfo}
|
||||||
|
classId={charClass && classUtils.getId(charClass)}
|
||||||
|
showBackgroundProficiencyOptions={true}
|
||||||
|
collapseDescription={collapseDescription}
|
||||||
|
/>
|
||||||
|
{subchoicesNode}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
214
ddb_main/components/FilterGroup/FilterGroup.tsx
Normal file
214
ddb_main/components/FilterGroup/FilterGroup.tsx
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, HTMLAttributes, ReactNode } from "react";
|
||||||
|
|
||||||
|
import { useFeatureFlags } from "~/contexts/FeatureFlag";
|
||||||
|
import { Search } from "~/subApps/builder/components/Search";
|
||||||
|
import Checkbox from "~/tools/js/smartComponents/Checkbox";
|
||||||
|
|
||||||
|
import { Accordion } from "../Accordion";
|
||||||
|
import { Button } from "../Button";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
export interface FilterButtonData {
|
||||||
|
label: ReactNode;
|
||||||
|
type: number | string;
|
||||||
|
className?: string;
|
||||||
|
sortOrder?: number;
|
||||||
|
}
|
||||||
|
export interface FilterCheckboxData {
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
initiallyEnabled: boolean;
|
||||||
|
onChange: () => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
export interface FilterGroupProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
filterQuery: string;
|
||||||
|
onQueryChange: (value: string) => void;
|
||||||
|
searchPlaceholder?: string;
|
||||||
|
filterButtonData: Array<FilterButtonData>;
|
||||||
|
activeFilterButtonTypes: Array<number | string>;
|
||||||
|
onFilterButtonClick: (filterType: number | string) => void;
|
||||||
|
filterCheckboxData?: Array<FilterCheckboxData>;
|
||||||
|
sourceCategoryButtonData: Array<FilterButtonData>;
|
||||||
|
onSourceCategoryClick: (categoryId: number) => void;
|
||||||
|
activeFilterSourceCategories: Array<number>;
|
||||||
|
themed?: boolean;
|
||||||
|
buttonGroupLabel: string;
|
||||||
|
buttonSize?: "x-small" | "xx-small";
|
||||||
|
filterStyle?: "builder";
|
||||||
|
shouldOpenFilterButtons?: boolean;
|
||||||
|
shouldOpenSourceCategoryButtons?: boolean;
|
||||||
|
onSourceCategoriesCollapse?: () => void;
|
||||||
|
onFilterButtonsCollapse?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FilterGroup: FC<FilterGroupProps> = ({
|
||||||
|
filterQuery,
|
||||||
|
searchPlaceholder = "Search",
|
||||||
|
filterButtonData,
|
||||||
|
activeFilterButtonTypes,
|
||||||
|
onQueryChange,
|
||||||
|
onFilterButtonClick,
|
||||||
|
filterCheckboxData,
|
||||||
|
sourceCategoryButtonData,
|
||||||
|
onSourceCategoryClick,
|
||||||
|
activeFilterSourceCategories,
|
||||||
|
themed,
|
||||||
|
buttonGroupLabel,
|
||||||
|
buttonSize = "xx-small",
|
||||||
|
filterStyle,
|
||||||
|
shouldOpenFilterButtons,
|
||||||
|
shouldOpenSourceCategoryButtons,
|
||||||
|
onSourceCategoriesCollapse,
|
||||||
|
onFilterButtonsCollapse,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const handleQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onQueryChange(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSourceCategoryClick = (evt: React.MouseEvent, id: number) => {
|
||||||
|
evt.stopPropagation();
|
||||||
|
evt.nativeEvent.stopImmediatePropagation();
|
||||||
|
onSourceCategoryClick(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterButtonClick = (
|
||||||
|
evt: React.MouseEvent,
|
||||||
|
type: string | number
|
||||||
|
) => {
|
||||||
|
evt.stopPropagation();
|
||||||
|
evt.nativeEvent.stopImmediatePropagation();
|
||||||
|
onFilterButtonClick(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...props}>
|
||||||
|
<div className={styles.filterGroup}>
|
||||||
|
<label htmlFor="filter-input" className={styles.filterLabel}>
|
||||||
|
Filter
|
||||||
|
</label>
|
||||||
|
<Search
|
||||||
|
id="filter-input"
|
||||||
|
value={filterQuery}
|
||||||
|
onChange={handleQueryChange}
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FILTER BUTTONS */}
|
||||||
|
<div className={styles.filterGroup}>
|
||||||
|
<Accordion
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
summary={<div className={styles.filterLabel}>{buttonGroupLabel}</div>}
|
||||||
|
className={styles.accordion}
|
||||||
|
forceShow={shouldOpenFilterButtons}
|
||||||
|
onClick={onFilterButtonsCollapse}
|
||||||
|
>
|
||||||
|
<div className={styles.buttonGroup}>
|
||||||
|
{filterButtonData.map((button) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx([button.className, styles.button])}
|
||||||
|
key={button.type}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color={
|
||||||
|
filterStyle === "builder" ? "builder-green" : undefined
|
||||||
|
}
|
||||||
|
themed={themed}
|
||||||
|
variant={
|
||||||
|
activeFilterButtonTypes.includes(button.type)
|
||||||
|
? "solid"
|
||||||
|
: "outline"
|
||||||
|
}
|
||||||
|
size={buttonSize}
|
||||||
|
onClick={(evt) => handleFilterButtonClick(evt, button.type)}
|
||||||
|
>
|
||||||
|
{button.label}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{filterCheckboxData && filterCheckboxData.length > 0 && (
|
||||||
|
<div className={styles.checkboxGroup}>
|
||||||
|
{filterCheckboxData.map((checkbox) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx([
|
||||||
|
styles.checkbox,
|
||||||
|
filterStyle === "builder" && styles.builder,
|
||||||
|
])}
|
||||||
|
key={checkbox.type}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
stopPropagation={true}
|
||||||
|
initiallyEnabled={checkbox.initiallyEnabled}
|
||||||
|
onChange={checkbox.onChange}
|
||||||
|
label={checkbox.label}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SOURCE CATEGORY FILTERS */}
|
||||||
|
|
||||||
|
<div className={styles.filterGroup}>
|
||||||
|
<Accordion
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
summary={
|
||||||
|
<div className={styles.filterLabel}>Filter By Source Category</div>
|
||||||
|
}
|
||||||
|
className={styles.accordion}
|
||||||
|
forceShow={shouldOpenSourceCategoryButtons}
|
||||||
|
onClick={onSourceCategoriesCollapse}
|
||||||
|
>
|
||||||
|
<div className={styles.buttonGroup}>
|
||||||
|
{sourceCategoryButtonData.map((button) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx([
|
||||||
|
button.className,
|
||||||
|
styles.button,
|
||||||
|
styles.sourceCategoryButton,
|
||||||
|
filterStyle === "builder" && styles.builder,
|
||||||
|
])}
|
||||||
|
key={button.type}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color={
|
||||||
|
filterStyle === "builder" ? "builder-green" : undefined
|
||||||
|
}
|
||||||
|
themed={themed}
|
||||||
|
variant={
|
||||||
|
activeFilterSourceCategories.includes(
|
||||||
|
button.type as number
|
||||||
|
)
|
||||||
|
? "solid"
|
||||||
|
: "outline"
|
||||||
|
}
|
||||||
|
size={buttonSize}
|
||||||
|
onClick={(evt) =>
|
||||||
|
handleSourceCategoryClick(evt, button.type as number)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{button.label}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,38 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import { BuilderHelperTextInfoContract } from "@dndbeyond/character-rules-engine";
|
||||||
|
|
||||||
|
import { DdbBadgeSvg } from "~/tools/js/smartComponents/Svg";
|
||||||
|
|
||||||
|
import { Accordion, AccordionProps } from "../Accordion";
|
||||||
|
import { HtmlContent } from "../HtmlContent";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
export interface HelperTextAccordionProps
|
||||||
|
extends Omit<AccordionProps, "summary"> {
|
||||||
|
builderHelperText: BuilderHelperTextInfoContract[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HelperTextAccordion: FC<HelperTextAccordionProps> = ({
|
||||||
|
builderHelperText,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{builderHelperText.map((helperText, idx) => (
|
||||||
|
<Accordion
|
||||||
|
{...props}
|
||||||
|
key={uuidv4()}
|
||||||
|
summary={
|
||||||
|
<span className={styles.summary}>
|
||||||
|
<DdbBadgeSvg /> {helperText.label}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<HtmlContent html={helperText.description} />
|
||||||
|
</Accordion>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
25
ddb_main/components/HtmlContent/HtmlContent.tsx
Normal file
25
ddb_main/components/HtmlContent/HtmlContent.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, HTMLAttributes } from "react";
|
||||||
|
|
||||||
|
import { FormatUtils } from "@dndbeyond/character-rules-engine/es";
|
||||||
|
|
||||||
|
interface HtmlContentProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
html: string;
|
||||||
|
className?: string;
|
||||||
|
withoutTooltips?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HtmlContent: FC<HtmlContentProps> = ({
|
||||||
|
html,
|
||||||
|
className = "",
|
||||||
|
withoutTooltips,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className={clsx(["ddbc-html-content", className])}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: withoutTooltips ? FormatUtils.stripTooltipTags(html) : html,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
35
ddb_main/components/InfoItem/InfoItem.tsx
Normal file
35
ddb_main/components/InfoItem/InfoItem.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, HTMLAttributes } from "react";
|
||||||
|
|
||||||
|
import { InfoItem as TtuiInfoItem } from "@dndbeyond/ttui/components/InfoItem";
|
||||||
|
|
||||||
|
import { useUnpropagatedClick } from "~/hooks/useUnpropagatedClick";
|
||||||
|
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
interface InfoItemProps extends HTMLAttributes<HTMLElement> {
|
||||||
|
label: string;
|
||||||
|
color?: "primary"; // Not needed here, but fixes ts error
|
||||||
|
inline?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component is an attribute for a given item, spell, or other entity.
|
||||||
|
* The InfoItemList component is a customized version of the InfoItem component from
|
||||||
|
* the @dndbeyond/ttui library.
|
||||||
|
**/
|
||||||
|
export const InfoItem: FC<InfoItemProps> = ({
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const handleClick = useUnpropagatedClick(onClick);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TtuiInfoItem
|
||||||
|
className={clsx([styles.item, className])}
|
||||||
|
onClick={onClick ? handleClick : undefined}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
152
ddb_main/components/ItemFilter/ItemFilter.tsx
Normal file
152
ddb_main/components/ItemFilter/ItemFilter.tsx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, HTMLAttributes } from "react";
|
||||||
|
|
||||||
|
import { useFiltersContext } from "~/contexts/Filters";
|
||||||
|
import { SimpleSourceCategoryContract } from "~/types";
|
||||||
|
|
||||||
|
import { FilterGroup } from "../FilterGroup";
|
||||||
|
import {
|
||||||
|
FilterButtonData,
|
||||||
|
FilterCheckboxData,
|
||||||
|
} from "../FilterGroup/FilterGroup";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
export interface ItemFilterProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
filterQuery: string;
|
||||||
|
onQueryChange: (value: string) => void;
|
||||||
|
filterTypes: Array<string>;
|
||||||
|
onFilterButtonClick: (type: string) => void;
|
||||||
|
onCheckboxChange: (type: string) => void;
|
||||||
|
sourceCategories: Array<SimpleSourceCategoryContract>;
|
||||||
|
onSourceCategoryClick: (categoryId: number) => void;
|
||||||
|
filterSourceCategories: Array<number>;
|
||||||
|
filterProficient: boolean;
|
||||||
|
filterBasic: boolean;
|
||||||
|
filterMagic: boolean;
|
||||||
|
filterContainer: boolean;
|
||||||
|
themed?: boolean;
|
||||||
|
buttonSize?: "x-small" | "xx-small";
|
||||||
|
filterStyle?: "builder";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ItemFilter: FC<ItemFilterProps> = ({
|
||||||
|
filterQuery,
|
||||||
|
onQueryChange,
|
||||||
|
filterTypes,
|
||||||
|
onFilterButtonClick,
|
||||||
|
onCheckboxChange,
|
||||||
|
sourceCategories,
|
||||||
|
onSourceCategoryClick,
|
||||||
|
filterSourceCategories,
|
||||||
|
filterProficient,
|
||||||
|
filterBasic,
|
||||||
|
filterMagic,
|
||||||
|
filterContainer,
|
||||||
|
themed,
|
||||||
|
buttonSize = "xx-small",
|
||||||
|
filterStyle,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
showItemTypes,
|
||||||
|
showItemSourceCategories,
|
||||||
|
setShowItemSourceCategories,
|
||||||
|
setShowItemTypes,
|
||||||
|
} = useFiltersContext();
|
||||||
|
|
||||||
|
const itemTypes: Array<string> = [
|
||||||
|
"Armor",
|
||||||
|
"Potion",
|
||||||
|
"Ring",
|
||||||
|
"Rod",
|
||||||
|
"Scroll",
|
||||||
|
"Staff",
|
||||||
|
"Wand",
|
||||||
|
"Weapon",
|
||||||
|
"Wondrous item",
|
||||||
|
"Other Gear",
|
||||||
|
];
|
||||||
|
|
||||||
|
const toggleTypes: Array<string> = [
|
||||||
|
"Proficient",
|
||||||
|
"Common",
|
||||||
|
"Magical",
|
||||||
|
"Container",
|
||||||
|
];
|
||||||
|
|
||||||
|
const isChecked = (type: string): boolean => {
|
||||||
|
switch (type) {
|
||||||
|
case "Proficient":
|
||||||
|
return filterProficient;
|
||||||
|
case "Common":
|
||||||
|
return filterBasic;
|
||||||
|
case "Magical":
|
||||||
|
return filterMagic;
|
||||||
|
case "Container":
|
||||||
|
return filterContainer;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterCheckboxData: Array<FilterCheckboxData> = toggleTypes.map(
|
||||||
|
(type) => {
|
||||||
|
return {
|
||||||
|
label: type,
|
||||||
|
type: type,
|
||||||
|
onChange: () => onCheckboxChange(type),
|
||||||
|
initiallyEnabled: isChecked(type),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterButtons: Array<FilterButtonData> = itemTypes.map((itemType) => {
|
||||||
|
return {
|
||||||
|
type: itemType,
|
||||||
|
label: itemType === "Wondrous item" ? "Wondrous" : itemType,
|
||||||
|
className: clsx([
|
||||||
|
styles.filterButton,
|
||||||
|
buttonSize === "xx-small" && styles.filterButtonSmall,
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourcesData: Array<FilterButtonData> = sourceCategories.map(
|
||||||
|
(sourceCategory) => {
|
||||||
|
return {
|
||||||
|
label: sourceCategory.name,
|
||||||
|
type: sourceCategory.id,
|
||||||
|
className: styles.sourceCategoryButton,
|
||||||
|
sortOrder: sourceCategory.sortOrder,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterGroup
|
||||||
|
searchPlaceholder={"Weapon, Longsword, Vorpal Longsword, etc."}
|
||||||
|
filterQuery={filterQuery}
|
||||||
|
filterButtonData={filterButtons}
|
||||||
|
activeFilterButtonTypes={filterTypes}
|
||||||
|
onQueryChange={onQueryChange}
|
||||||
|
themed={themed}
|
||||||
|
onFilterButtonClick={onFilterButtonClick}
|
||||||
|
onSourceCategoryClick={onSourceCategoryClick}
|
||||||
|
activeFilterSourceCategories={filterSourceCategories}
|
||||||
|
sourceCategoryButtonData={sourcesData}
|
||||||
|
buttonGroupLabel={"Filter By Type"}
|
||||||
|
buttonSize={buttonSize}
|
||||||
|
filterStyle={filterStyle}
|
||||||
|
filterCheckboxData={filterCheckboxData}
|
||||||
|
shouldOpenSourceCategoryButtons={showItemSourceCategories}
|
||||||
|
shouldOpenFilterButtons={showItemTypes}
|
||||||
|
onFilterButtonsCollapse={() => setShowItemTypes(!showItemTypes)}
|
||||||
|
onSourceCategoriesCollapse={() =>
|
||||||
|
setShowItemSourceCategories(!showItemSourceCategories)
|
||||||
|
}
|
||||||
|
className={className}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
109
ddb_main/components/ItemName/ItemName.tsx
Normal file
109
ddb_main/components/ItemName/ItemName.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, HTMLAttributes } from "react";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Item,
|
||||||
|
Constants,
|
||||||
|
ItemUtils,
|
||||||
|
} from "@dndbeyond/character-rules-engine/es";
|
||||||
|
|
||||||
|
import { AttunementIcon } from "~/tools/js/smartComponents/Icons";
|
||||||
|
|
||||||
|
import { LegacyBadge } from "../LegacyBadge";
|
||||||
|
import { Tooltip } from "../Tooltip";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to display the name of an item with an attunement icon and a color
|
||||||
|
* to identify the rarity of the object. It is used in the equipment tab on the
|
||||||
|
* character sheet as well as the manage inventory pane.
|
||||||
|
*/
|
||||||
|
interface ItemNameProps extends HTMLAttributes<HTMLSpanElement> {
|
||||||
|
item: Item;
|
||||||
|
className?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
showAttunement?: boolean;
|
||||||
|
showLegacy?: boolean;
|
||||||
|
showLegacyBadge?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ItemName: FC<ItemNameProps> = ({
|
||||||
|
showAttunement = true,
|
||||||
|
showLegacy = false,
|
||||||
|
showLegacyBadge,
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
|
item,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const handleClick = (e: React.MouseEvent): void => {
|
||||||
|
if (onClick) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDisplayName = () => {
|
||||||
|
const name = ItemUtils.getName(item);
|
||||||
|
if (!name) return ItemUtils.getDefinitionName(item);
|
||||||
|
if (!name) return "Item";
|
||||||
|
return name;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getItemRarity = () => {
|
||||||
|
switch (ItemUtils.getRarity(item)) {
|
||||||
|
case Constants.ItemRarityNameEnum.ARTIFACT:
|
||||||
|
return "artifact";
|
||||||
|
case Constants.ItemRarityNameEnum.LEGENDARY:
|
||||||
|
return "legendary";
|
||||||
|
case Constants.ItemRarityNameEnum.VERY_RARE:
|
||||||
|
return "veryrare";
|
||||||
|
case Constants.ItemRarityNameEnum.RARE:
|
||||||
|
return "rare";
|
||||||
|
case Constants.ItemRarityNameEnum.UNCOMMON:
|
||||||
|
return "uncommon";
|
||||||
|
case Constants.ItemRarityNameEnum.COMMON:
|
||||||
|
default:
|
||||||
|
return "common";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tooltipId = `itemName-${uuidv4()}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={clsx([styles.itemName, styles[getItemRarity()], className])}
|
||||||
|
onClick={handleClick}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{getDisplayName()}
|
||||||
|
{item.isCustomized && (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={styles.asterisk}
|
||||||
|
data-tooltip-id={tooltipId}
|
||||||
|
data-tooltip-content="Item is Customized"
|
||||||
|
>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
<Tooltip id={tooltipId} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{showAttunement && item.isAttuned && (
|
||||||
|
<span className={styles.icon}>
|
||||||
|
<AttunementIcon />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{showLegacy && ItemUtils.isLegacy(item) && (
|
||||||
|
<span className={styles.legacy}>(Legacy)</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{showLegacyBadge && ItemUtils.isLegacy(item) && (
|
||||||
|
<LegacyBadge variant="margin-left" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
42
ddb_main/components/Layout/Layout.tsx
Normal file
42
ddb_main/components/Layout/Layout.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, HTMLAttributes } from "react";
|
||||||
|
import { useMatch } from "react-router-dom";
|
||||||
|
|
||||||
|
import { Footer } from "@dndbeyond/ttui/components/Footer";
|
||||||
|
import { MegaMenu } from "@dndbeyond/ttui/components/MegaMenu";
|
||||||
|
import { Sitebar } from "@dndbeyond/ttui/components/Sitebar";
|
||||||
|
|
||||||
|
import config from "~/config";
|
||||||
|
import useUser from "~/hooks/useUser";
|
||||||
|
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
const BASE_PATHNAME = config.basePathname;
|
||||||
|
|
||||||
|
interface LayoutProps extends HTMLAttributes<HTMLDivElement> {}
|
||||||
|
|
||||||
|
export const Layout: FC<LayoutProps> = ({ children, ...props }) => {
|
||||||
|
const isDev = process.env.NODE_ENV === "development";
|
||||||
|
const user = useUser();
|
||||||
|
const matchSheet = useMatch(`${BASE_PATHNAME}/:characterId/`);
|
||||||
|
|
||||||
|
// Don't show the navigation in production
|
||||||
|
if (!isDev) return <>{children}</>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...props}>
|
||||||
|
{/* TODO: fetch navItems */}
|
||||||
|
<div className={styles.siteStyles}>
|
||||||
|
<Sitebar user={user as any} navItems={[]} />
|
||||||
|
{/* TODO: fetch sources */}
|
||||||
|
<MegaMenu sources={[]} />
|
||||||
|
</div>
|
||||||
|
<div className={clsx(["container", styles.devContainer])}>{children}</div>
|
||||||
|
{!matchSheet && (
|
||||||
|
<div className={styles.siteStyles}>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
41
ddb_main/components/LegacyBadge/LegacyBadge.tsx
Normal file
41
ddb_main/components/LegacyBadge/LegacyBadge.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, HTMLAttributes } from "react";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import { LabelChip } from "@dndbeyond/ttui/components/LabelChip";
|
||||||
|
|
||||||
|
import { Link } from "../Link";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
interface LegacyBadgeProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
variant?: "margin-left";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LegacyBadge: FC<LegacyBadgeProps> = ({
|
||||||
|
variant,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<LabelChip
|
||||||
|
className={clsx([styles.chip, variant && styles[variant], className])}
|
||||||
|
data-tooltip-id={`legacybadge_${uuidv4()}`}
|
||||||
|
tooltipContent={
|
||||||
|
<span>
|
||||||
|
This doesn't reflect the latest rules and lore.{" "}
|
||||||
|
<Link
|
||||||
|
className={styles.tooltipLink}
|
||||||
|
href="https://dndbeyond.com/legacy"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
onClick={() => {}} //TODO - can fix this after we refactor Link to not stop propgation inside the component
|
||||||
|
>
|
||||||
|
Learn More
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
tooltipClickable
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
Legacy
|
||||||
|
</LabelChip>
|
||||||
|
);
|
52
ddb_main/components/Link/Link.tsx
Normal file
52
ddb_main/components/Link/Link.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { AnchorHTMLAttributes, FC } from "react";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
|
interface LinkProps
|
||||||
|
extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "onClick"> {
|
||||||
|
onClick?: Function;
|
||||||
|
useTheme?: boolean;
|
||||||
|
useRouter?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Link: FC<LinkProps> = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
href,
|
||||||
|
onClick,
|
||||||
|
useTheme,
|
||||||
|
useRouter,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
//TODO - refactor to handle stop propagation on the oustide of this component (about 7 or so files to change)
|
||||||
|
const handleClick = (e: React.MouseEvent): void => {
|
||||||
|
if (onClick) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (useRouter)
|
||||||
|
return (
|
||||||
|
<RouterLink
|
||||||
|
className={className}
|
||||||
|
onClick={handleClick}
|
||||||
|
to={href || ""}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</RouterLink>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className={clsx(["ddbc-link", useTheme && "ddbc-theme-link", className])}
|
||||||
|
onClick={handleClick}
|
||||||
|
href={href}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,65 @@
|
|||||||
|
import { HTMLAttributes, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@dndbeyond/ttui/components/Button";
|
||||||
|
|
||||||
|
import { MaxCharactersMessageText } from "../MaxCharactersMessageText";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
interface MaxCharactersDialogProps extends HTMLAttributes<HTMLDialogElement> {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
useMyCharactersLink?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MaxCharactersDialog: React.FC<MaxCharactersDialogProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
useMyCharactersLink,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||||
|
|
||||||
|
const handleClickBackdrop = (e) => {
|
||||||
|
const rect = dialogRef.current?.getBoundingClientRect();
|
||||||
|
const isClickInDialog =
|
||||||
|
rect &&
|
||||||
|
rect.top <= e.clientY &&
|
||||||
|
e.clientY <= rect.top + rect.height &&
|
||||||
|
rect.left <= e.clientX &&
|
||||||
|
e.clientX <= rect.left + rect.width;
|
||||||
|
|
||||||
|
if (!isClickInDialog) (dialogRef.current as any)?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
// Hacky workaround since showModal() isn't
|
||||||
|
// supported by TypeScript <4.8.3
|
||||||
|
(dialogRef.current as any)?.showModal();
|
||||||
|
} else {
|
||||||
|
// Hacky workaround since close() isn't
|
||||||
|
// supported by TypeScript <4.8.3
|
||||||
|
(dialogRef.current as any)?.close();
|
||||||
|
}
|
||||||
|
}, [onClose, open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dialog
|
||||||
|
className={styles.maxCharactersDialog}
|
||||||
|
ref={dialogRef}
|
||||||
|
onClick={handleClickBackdrop}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<h2 className={styles.dialogTitle}>Your character slots are full</h2>
|
||||||
|
</header>
|
||||||
|
<MaxCharactersMessageText useMyCharactersLink={useMyCharactersLink} />
|
||||||
|
<footer className={styles.dialogFooter}>
|
||||||
|
<Button href="/store/subscribe" variant="outline">
|
||||||
|
Subscribe
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onClose}>Close</Button>
|
||||||
|
</footer>
|
||||||
|
</dialog>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,47 @@
|
|||||||
|
import { FC, HTMLAttributes } from "react";
|
||||||
|
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
interface MaxCharactersMessageTextProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
includeTitle?: boolean;
|
||||||
|
useLinks?: boolean;
|
||||||
|
useMyCharactersLink?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MaxCharactersMessageText: FC<MaxCharactersMessageTextProps> = ({
|
||||||
|
includeTitle,
|
||||||
|
useLinks,
|
||||||
|
useMyCharactersLink,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const subscribe = useLinks ? (
|
||||||
|
<a className={styles.link} href="/store/subscribe">
|
||||||
|
Subscribe
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
"Subscribe"
|
||||||
|
);
|
||||||
|
|
||||||
|
const myCharacters =
|
||||||
|
useMyCharactersLink || useLinks ? (
|
||||||
|
<a className={styles.link} href="/characters">
|
||||||
|
My Characters
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
"My Characters"
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...props}>
|
||||||
|
{includeTitle && (
|
||||||
|
<p className={styles.messageTitle}>
|
||||||
|
Oops! Your character slots are full.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className={styles.messageSubtext}>
|
||||||
|
You're quite the adventurer! {subscribe} to unlock additional
|
||||||
|
slots, or return to {myCharacters} and delete a character to make room.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
117
ddb_main/components/NotificationSystem/NotificationSystem.tsx
Normal file
117
ddb_main/components/NotificationSystem/NotificationSystem.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import {
|
||||||
|
forwardRef,
|
||||||
|
HTMLAttributes,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
|
import XMark from "@dndbeyond/fontawesome-cache/svgs/regular/xmark.svg";
|
||||||
|
import { Toast } from "@dndbeyond/ttui/components/Toast";
|
||||||
|
|
||||||
|
import { Notification } from "~/tools/js/Shared/stores/typings";
|
||||||
|
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
export interface NotificationSystemHandlers {
|
||||||
|
addNotification: (notification: Notification) => void;
|
||||||
|
removeNotification: (notification: Notification) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutDuration = 5000;
|
||||||
|
|
||||||
|
interface NotificationSystemProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLElement>, "open" | "contentEditable"> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NotificationSystem is a component that manages the display of notifications
|
||||||
|
* in toast messages which appear in the character sheet.
|
||||||
|
*/
|
||||||
|
export const NotificationSystem = forwardRef<
|
||||||
|
HTMLDialogElement,
|
||||||
|
NotificationSystemProps
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
const [notifications, setNotifications] = useState<Array<Notification>>([]);
|
||||||
|
const [notificationTimeoutHandle, setNotificationTimeoutHandle] = useState<
|
||||||
|
number | undefined
|
||||||
|
>();
|
||||||
|
const [portal, setPortal] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const addNotification = (notification) => {
|
||||||
|
if (notifications.filter((n) => n.uid === notification.uid).length === 0) {
|
||||||
|
setNotifications([...notifications, notification]);
|
||||||
|
}
|
||||||
|
clearTimeout(notificationTimeoutHandle);
|
||||||
|
(function openNotification() {
|
||||||
|
if (!isOpen) {
|
||||||
|
setIsOpen(true);
|
||||||
|
} else {
|
||||||
|
setNotificationTimeoutHandle(() => {
|
||||||
|
const handle = window.setTimeout(() => {
|
||||||
|
openNotification();
|
||||||
|
}, timeoutDuration + 100);
|
||||||
|
return handle;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeNotification = (notification) => {
|
||||||
|
notifications[0].onRemove?.();
|
||||||
|
setNotifications(notifications.filter((n) => n.uid !== notification.uid));
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle<any, NotificationSystemHandlers>(
|
||||||
|
ref,
|
||||||
|
(): NotificationSystemHandlers => ({
|
||||||
|
addNotification,
|
||||||
|
removeNotification,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
removeNotification(notifications[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const portalEl = document.createElement("div");
|
||||||
|
portalEl.classList.add("ct-notification__portal");
|
||||||
|
portalEl.style.zIndex = "100000";
|
||||||
|
portalEl.style.position = "fixed";
|
||||||
|
document.body.appendChild(portalEl);
|
||||||
|
setPortal(portalEl);
|
||||||
|
return () => {
|
||||||
|
document.body.removeChild(portalEl);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return notifications[0] && portal
|
||||||
|
? createPortal(
|
||||||
|
<Toast
|
||||||
|
className={clsx([
|
||||||
|
styles.toast,
|
||||||
|
notifications[0].level && styles[notifications[0].level],
|
||||||
|
])}
|
||||||
|
open={isOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
autoHideDuration={timeoutDuration}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={styles.closeButton}
|
||||||
|
onClick={handleClose}
|
||||||
|
aria-label="Dismiss notification"
|
||||||
|
>
|
||||||
|
<XMark className={styles.closeIcon} />
|
||||||
|
</button>
|
||||||
|
<p className={styles.title}>{notifications[0].title}</p>
|
||||||
|
<p className={styles.message}>{notifications[0].message}</p>
|
||||||
|
</Toast>,
|
||||||
|
portal
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
});
|
95
ddb_main/components/NumberDisplay/NumberDisplay.tsx
Normal file
95
ddb_main/components/NumberDisplay/NumberDisplay.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, HTMLAttributes, ReactNode } from "react";
|
||||||
|
|
||||||
|
import { Constants } from "@dndbeyond/character-rules-engine/es";
|
||||||
|
|
||||||
|
import { NumberDisplayType } from "~/types";
|
||||||
|
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
const { FEET_IN_MILES } = Constants;
|
||||||
|
|
||||||
|
export interface NumberDisplayProps extends HTMLAttributes<HTMLSpanElement> {
|
||||||
|
number: number | null;
|
||||||
|
type: NumberDisplayType;
|
||||||
|
isModified?: boolean;
|
||||||
|
size?: "large";
|
||||||
|
numberFallback?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NumberDisplay: FC<NumberDisplayProps> = ({
|
||||||
|
number,
|
||||||
|
type,
|
||||||
|
isModified = false,
|
||||||
|
size,
|
||||||
|
numberFallback = "--",
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
let label = "";
|
||||||
|
let sign = "";
|
||||||
|
switch (type) {
|
||||||
|
case "weightInLb":
|
||||||
|
label = "lb.";
|
||||||
|
number = number && Math.round((number + Number.EPSILON) * 100) / 100;
|
||||||
|
break;
|
||||||
|
case "distanceInFt":
|
||||||
|
if (number && number % FEET_IN_MILES === 0) {
|
||||||
|
number = number / FEET_IN_MILES;
|
||||||
|
label = `mile${Math.abs(number) === 1 ? "" : "s"}`;
|
||||||
|
} else {
|
||||||
|
label = "ft.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "signed":
|
||||||
|
if (number != null) {
|
||||||
|
sign = number >= 0 ? "+" : "-";
|
||||||
|
number = Math.abs(number);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={clsx([
|
||||||
|
styles.numberDisplay,
|
||||||
|
type === "signed" && styles.signed,
|
||||||
|
type === "distanceInFt" && size && styles[size + "Distance"],
|
||||||
|
isModified && styles.modified,
|
||||||
|
size && styles[size],
|
||||||
|
className,
|
||||||
|
])}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{type === "signed" && (
|
||||||
|
/* NOTE: aria-label needs to go on an interactable element, not a span.
|
||||||
|
This will need to be refactored onto the containing button.
|
||||||
|
*/
|
||||||
|
<span
|
||||||
|
className={clsx([
|
||||||
|
styles.sign,
|
||||||
|
size && styles[size + "Sign"],
|
||||||
|
styles.labelSignColor,
|
||||||
|
])}
|
||||||
|
aria-label={sign === "+" ? "plus" : "minus"}
|
||||||
|
>
|
||||||
|
{sign}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span>{number === null ? numberFallback : number}</span>
|
||||||
|
{type !== "signed" && (
|
||||||
|
<span
|
||||||
|
className={clsx([
|
||||||
|
styles.label,
|
||||||
|
size && styles[size + "Label"],
|
||||||
|
styles.labelSignColor,
|
||||||
|
])}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
121
ddb_main/components/Popover/Popover.tsx
Normal file
121
ddb_main/components/Popover/Popover.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import React, {
|
||||||
|
FC,
|
||||||
|
ReactElement,
|
||||||
|
ReactNode,
|
||||||
|
cloneElement,
|
||||||
|
useEffect,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
import { Dialog } from "@dndbeyond/ttui/components/Dialog";
|
||||||
|
import { useIsVisible } from "@dndbeyond/ttui/hooks/useIsVisible";
|
||||||
|
|
||||||
|
import { useCharacterTheme } from "~/contexts/CharacterTheme";
|
||||||
|
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a popover component to handle dialogs that are dependent on a location from a trigger element.
|
||||||
|
* It uses the useIsVisible hook to handle the visibility of the dialog. The position of the dialog
|
||||||
|
* is determined by the position prop, which can be set to topLeft, topRight, left, right, bottomLeft, or bottomRight.
|
||||||
|
* The maxWidth prop can be used to set the maximum width of the dialog. The trigger prop is the element that will trigger
|
||||||
|
* the dialog to open.The popover component also handles the positioning of the dialog based on the trigger element. The
|
||||||
|
* handleToggle function toggles the visibility of the dialog, and the handleClose function closes the dialog. The popover
|
||||||
|
* component also adds an event listener to the cancel button in the dialog to close the dialog when clicked.
|
||||||
|
*
|
||||||
|
* In order for a cancel button to work, it must have a `data-cancel` attribute.
|
||||||
|
**/
|
||||||
|
interface PopoverProps {
|
||||||
|
className?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
position?:
|
||||||
|
| "topLeft"
|
||||||
|
| "topRight"
|
||||||
|
| "left"
|
||||||
|
| "right"
|
||||||
|
| "bottomLeft"
|
||||||
|
| "bottomRight"
|
||||||
|
| "bottom";
|
||||||
|
trigger: ReactElement;
|
||||||
|
maxWidth?: number;
|
||||||
|
variant?: "default" | "menu";
|
||||||
|
"data-testid"?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Popover: FC<PopoverProps> = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
maxWidth = 250,
|
||||||
|
position = "bottomRight",
|
||||||
|
trigger,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const { ref: popoverRef, isVisible, setIsVisible } = useIsVisible(false);
|
||||||
|
const { isDarkMode } = useCharacterTheme();
|
||||||
|
|
||||||
|
const handleToggle = (e: MouseEvent) => {
|
||||||
|
e?.stopPropagation();
|
||||||
|
setIsVisible(!isVisible);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (e: MouseEvent) => {
|
||||||
|
e?.stopPropagation();
|
||||||
|
setIsVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make a copy of the trigger element with an added `onClick` event listener
|
||||||
|
const triggerWithHandlers = cloneElement(trigger, {
|
||||||
|
onClick: (e: MouseEvent) => handleToggle(e),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check to see if the popover has a cancel button based on `data-cancel` attribute
|
||||||
|
const cancelButton = popoverRef.current?.querySelector("[data-cancel]");
|
||||||
|
|
||||||
|
// If there is a cancel button, add an event listener to close the popover
|
||||||
|
if (cancelButton) {
|
||||||
|
cancelButton.addEventListener("click", handleClose);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [popoverRef.current]);
|
||||||
|
|
||||||
|
const mapChildrenWithProps = () => {
|
||||||
|
// Check if children is a ReactElement
|
||||||
|
if (React.isValidElement(children)) {
|
||||||
|
// Create a copy of the ReactElement and attach handleClose function to it
|
||||||
|
return cloneElement(children as ReactElement, {
|
||||||
|
handleClose,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// If not just return the children - means its a primitive type
|
||||||
|
else {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx([styles.popoverContainer, isDarkMode && styles.dark])}
|
||||||
|
ref={popoverRef}
|
||||||
|
>
|
||||||
|
{triggerWithHandlers}
|
||||||
|
<Dialog
|
||||||
|
className={clsx([
|
||||||
|
styles.popover,
|
||||||
|
styles[variant],
|
||||||
|
styles[position],
|
||||||
|
className,
|
||||||
|
])}
|
||||||
|
style={{ maxWidth }}
|
||||||
|
open={isVisible}
|
||||||
|
onClose={(e) => handleClose(e as any)} //have to do any because of type mismatch for events
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{mapChildrenWithProps()}
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
54
ddb_main/components/PopoverContent/PopoverContent.tsx
Normal file
54
ddb_main/components/PopoverContent/PopoverContent.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { FC, ReactNode } from "react";
|
||||||
|
|
||||||
|
import { Button } from "../Button";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
interface PopoverContentProps {
|
||||||
|
title?: string;
|
||||||
|
content: string;
|
||||||
|
confirmText?: ReactNode;
|
||||||
|
onConfirm?: () => void;
|
||||||
|
withCancel?: boolean;
|
||||||
|
handleClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PopoverContent: FC<PopoverContentProps> = ({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
confirmText = "Confirm",
|
||||||
|
onConfirm,
|
||||||
|
withCancel,
|
||||||
|
handleClose,
|
||||||
|
}) => (
|
||||||
|
<>
|
||||||
|
{title && <header className={styles.deletePopoverHeader}>{title}</header>}
|
||||||
|
<div className={styles.deletePopoverContent}>{content}</div>
|
||||||
|
<footer className={styles.deletePopoverFooter}>
|
||||||
|
{withCancel && (
|
||||||
|
<Button
|
||||||
|
size="x-small"
|
||||||
|
variant="outline"
|
||||||
|
color="secondary"
|
||||||
|
themed
|
||||||
|
data-cancel
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onConfirm && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (handleClose) handleClose();
|
||||||
|
if (onConfirm) onConfirm();
|
||||||
|
}}
|
||||||
|
size="x-small"
|
||||||
|
variant="outline"
|
||||||
|
color="secondary"
|
||||||
|
themed
|
||||||
|
>
|
||||||
|
{confirmText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</footer>
|
||||||
|
</>
|
||||||
|
);
|
@ -0,0 +1,32 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import { CharacterStatusSlug } from "@dndbeyond/character-rules-engine/es";
|
||||||
|
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
interface PremadeCharacterEditStatusProps {
|
||||||
|
characterStatus: CharacterStatusSlug | null;
|
||||||
|
isReadonly: boolean;
|
||||||
|
isBuilderView?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PremadeCharacterEditStatus = ({
|
||||||
|
characterStatus,
|
||||||
|
isReadonly,
|
||||||
|
isBuilderView,
|
||||||
|
}: PremadeCharacterEditStatusProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{characterStatus === CharacterStatusSlug.PREMADE && !isReadonly && (
|
||||||
|
<div
|
||||||
|
className={clsx([
|
||||||
|
styles.editStateIndicator,
|
||||||
|
isBuilderView && styles.editStateIndicatorBuilder,
|
||||||
|
])}
|
||||||
|
>
|
||||||
|
Premade <strong>EDIT</strong> mode <strong>ENABLED</strong>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
56
ddb_main/components/Reference/Reference.tsx
Normal file
56
ddb_main/components/Reference/Reference.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, HTMLAttributes } from "react";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import { Tooltip } from "../Tooltip";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
export interface ReferenceProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
name: string | null;
|
||||||
|
tooltip?: string | null;
|
||||||
|
page?: number | null;
|
||||||
|
chapter?: number | null;
|
||||||
|
isDarkMode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component acts as a reference to a specific page or chapter in a book
|
||||||
|
* where the information can be found.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const Reference: FC<ReferenceProps> = ({
|
||||||
|
chapter,
|
||||||
|
className,
|
||||||
|
isDarkMode,
|
||||||
|
name,
|
||||||
|
page,
|
||||||
|
tooltip,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const id = uuidv4();
|
||||||
|
const reference: string[] = [];
|
||||||
|
if (chapter) reference.push(`ch. ${chapter}`);
|
||||||
|
if (page) reference.push(`pg. ${page}`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p
|
||||||
|
className={clsx([styles.reference, className])}
|
||||||
|
data-tooltip-id={id}
|
||||||
|
data-tooltip-content={tooltip}
|
||||||
|
data-tooltip-delay-show={1500}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className={styles.name}>{name}</span>
|
||||||
|
{reference.length > 0 && (
|
||||||
|
<span className={styles.ref}>, {reference.join(", ")}</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<Tooltip
|
||||||
|
className={styles.tooltip}
|
||||||
|
id={id}
|
||||||
|
variant={isDarkMode ? "light" : "dark"}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
129
ddb_main/components/SpellFilter/SpellFilter.tsx
Normal file
129
ddb_main/components/SpellFilter/SpellFilter.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, HTMLAttributes, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useFiltersContext } from "~/contexts/Filters";
|
||||||
|
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||||
|
import { SimpleSourceCategoryContract, Spell } from "~/types";
|
||||||
|
|
||||||
|
import { FilterGroup } from "../FilterGroup";
|
||||||
|
import { FilterButtonData } from "../FilterGroup/FilterGroup";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
export interface SpellFilterProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
spells: Array<Spell>;
|
||||||
|
filterQuery: string;
|
||||||
|
filterLevels: Array<number>;
|
||||||
|
onQueryChange: (value: string) => void;
|
||||||
|
onLevelFilterClick: (level: number) => void;
|
||||||
|
sourceCategories: Array<SimpleSourceCategoryContract>;
|
||||||
|
onSourceCategoryClick: (categoryId: number) => void;
|
||||||
|
filterSourceCategories: Array<number>;
|
||||||
|
themed?: boolean;
|
||||||
|
buttonSize?: "x-small" | "xx-small";
|
||||||
|
filterStyle?: "builder";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SpellFilter: FC<SpellFilterProps> = ({
|
||||||
|
spells,
|
||||||
|
filterQuery,
|
||||||
|
filterLevels,
|
||||||
|
onQueryChange,
|
||||||
|
onLevelFilterClick,
|
||||||
|
sourceCategories,
|
||||||
|
onSourceCategoryClick,
|
||||||
|
filterSourceCategories,
|
||||||
|
themed,
|
||||||
|
buttonSize = "xx-small",
|
||||||
|
filterStyle,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const { ruleData, spellUtils, ruleDataUtils, formatUtils } =
|
||||||
|
useCharacterEngine();
|
||||||
|
const {
|
||||||
|
showSpellLevels,
|
||||||
|
showSpellSourceCategories,
|
||||||
|
setShowSpellLevels,
|
||||||
|
setShowSpellSourceCategories,
|
||||||
|
} = useFiltersContext();
|
||||||
|
|
||||||
|
const [spellLevels, setSpellLevels] = useState<Array<number>>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const activeSpellLevelCounts: Array<number> = [];
|
||||||
|
const spellsLevels: Array<number> = [];
|
||||||
|
const maxSpellLevel = ruleDataUtils.getMaxSpellLevel(ruleData);
|
||||||
|
|
||||||
|
for (let i = 0; i <= maxSpellLevel; i++) {
|
||||||
|
activeSpellLevelCounts.push(0);
|
||||||
|
spellsLevels.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
spells.forEach((spell) => {
|
||||||
|
activeSpellLevelCounts[spellUtils.getLevel(spell)] += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
setSpellLevels(
|
||||||
|
spellsLevels.filter((level) => activeSpellLevelCounts[level] !== 0)
|
||||||
|
);
|
||||||
|
}, [spells]);
|
||||||
|
|
||||||
|
const filterButtons: Array<FilterButtonData> = spellLevels.map((level) => {
|
||||||
|
let buttonLabel: React.ReactNode;
|
||||||
|
if (level === 0) {
|
||||||
|
buttonLabel = formatUtils.renderSpellLevelAbbreviation(0);
|
||||||
|
} else {
|
||||||
|
buttonLabel = (
|
||||||
|
<div className={styles.buttonText}>
|
||||||
|
<span>{level}</span>
|
||||||
|
<span
|
||||||
|
className={clsx([
|
||||||
|
styles.ordinal,
|
||||||
|
buttonSize === "xx-small" && styles.ordinalSmall,
|
||||||
|
])}
|
||||||
|
>
|
||||||
|
{formatUtils.getOrdinalSuffix(level)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { type: level, label: buttonLabel, className: styles.filterButton };
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourcesData: Array<FilterButtonData> = sourceCategories.map(
|
||||||
|
(sourceCategory) => {
|
||||||
|
return {
|
||||||
|
label: sourceCategory.name,
|
||||||
|
type: sourceCategory.id,
|
||||||
|
className: styles.sourceCategoryButton,
|
||||||
|
sortOrder: sourceCategory.sortOrder,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterGroup
|
||||||
|
searchPlaceholder={"Enter Spell Name"}
|
||||||
|
filterQuery={filterQuery}
|
||||||
|
filterButtonData={filterButtons}
|
||||||
|
activeFilterButtonTypes={filterLevels}
|
||||||
|
onQueryChange={onQueryChange}
|
||||||
|
onSourceCategoryClick={onSourceCategoryClick}
|
||||||
|
activeFilterSourceCategories={filterSourceCategories}
|
||||||
|
sourceCategoryButtonData={sourcesData}
|
||||||
|
themed={themed}
|
||||||
|
onFilterButtonClick={onLevelFilterClick}
|
||||||
|
buttonGroupLabel={"Filter By Spell Level"}
|
||||||
|
buttonSize={buttonSize}
|
||||||
|
filterStyle={filterStyle}
|
||||||
|
shouldOpenSourceCategoryButtons={showSpellSourceCategories}
|
||||||
|
shouldOpenFilterButtons={showSpellLevels}
|
||||||
|
onFilterButtonsCollapse={() => setShowSpellLevels(!showSpellLevels)}
|
||||||
|
onSourceCategoriesCollapse={() =>
|
||||||
|
setShowSpellSourceCategories(!showSpellSourceCategories)
|
||||||
|
}
|
||||||
|
className={className}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
126
ddb_main/components/SpellName/SpellName.tsx
Normal file
126
ddb_main/components/SpellName/SpellName.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, HTMLAttributes, useEffect, useState } from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||||
|
import {
|
||||||
|
SpellUtils,
|
||||||
|
EntityUtils,
|
||||||
|
FormatUtils,
|
||||||
|
BaseSpell,
|
||||||
|
DataOriginRefData,
|
||||||
|
characterEnvSelectors,
|
||||||
|
} from "@dndbeyond/character-rules-engine/es";
|
||||||
|
|
||||||
|
import { useCharacterTheme } from "~/contexts/CharacterTheme";
|
||||||
|
import { useUnpropagatedClick } from "~/hooks/useUnpropagatedClick";
|
||||||
|
|
||||||
|
import { ConcentrationIcon, RitualIcon } from "../../tools/js/smartComponents/";
|
||||||
|
import { LegacyBadge } from "../LegacyBadge";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
interface Props extends HTMLAttributes<HTMLSpanElement> {
|
||||||
|
onClick?: () => void;
|
||||||
|
spell: BaseSpell;
|
||||||
|
showSpellLevel?: boolean;
|
||||||
|
showIcons?: boolean;
|
||||||
|
showExpandedType?: boolean;
|
||||||
|
showLegacy?: boolean;
|
||||||
|
showLegacyBadge?: boolean;
|
||||||
|
dataOriginRefData?: DataOriginRefData | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SpellName: FC<Props> = ({
|
||||||
|
onClick,
|
||||||
|
spell,
|
||||||
|
showSpellLevel = true,
|
||||||
|
showExpandedType = false,
|
||||||
|
showIcons = true,
|
||||||
|
dataOriginRefData = null,
|
||||||
|
showLegacy = false,
|
||||||
|
showLegacyBadge = false,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const [expandedInfoText, setExpandedInfoText] = useState("");
|
||||||
|
const location = useSelector(characterEnvSelectors.getContext);
|
||||||
|
const { isDarkMode } = useCharacterTheme();
|
||||||
|
|
||||||
|
const getIconTheme = () => {
|
||||||
|
if (location === "BUILDER") return "dark";
|
||||||
|
return isDarkMode ? "gray" : "dark";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = useUnpropagatedClick(onClick);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dataOriginRefData) {
|
||||||
|
let expandedDataOriginRef = SpellUtils.getExpandedDataOriginRef(spell);
|
||||||
|
if (expandedDataOriginRef === null) {
|
||||||
|
setExpandedInfoText("");
|
||||||
|
} else {
|
||||||
|
setExpandedInfoText(
|
||||||
|
EntityUtils.getDataOriginRefName(
|
||||||
|
expandedDataOriginRef,
|
||||||
|
dataOriginRefData
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={styles.spellName}
|
||||||
|
onClick={onClick ? handleClick : undefined}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{showExpandedType && expandedInfoText !== "" && (
|
||||||
|
<Tooltip
|
||||||
|
title={expandedInfoText}
|
||||||
|
className={styles.expanded}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{SpellUtils.getName(spell)}
|
||||||
|
{SpellUtils.isCustomized(spell) && (
|
||||||
|
<Tooltip
|
||||||
|
title="Spell is Customized"
|
||||||
|
className={styles.customized}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
>
|
||||||
|
*
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{showIcons && SpellUtils.getConcentration(spell) && (
|
||||||
|
<ConcentrationIcon
|
||||||
|
className={styles.icon}
|
||||||
|
themeMode={getIconTheme()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showIcons && SpellUtils.isRitual(spell) && (
|
||||||
|
<RitualIcon
|
||||||
|
className={clsx([styles.icon])}
|
||||||
|
themeMode={getIconTheme()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showSpellLevel && (
|
||||||
|
<span className={styles.level}>
|
||||||
|
({FormatUtils.renderSpellLevelShortName(SpellUtils.getLevel(spell))}
|
||||||
|
)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{showLegacy && SpellUtils.isLegacy(spell) && (
|
||||||
|
<span className={styles.legacy}> • Legacy</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{showLegacyBadge && SpellUtils.isLegacy(spell) && (
|
||||||
|
<LegacyBadge variant="margin-left" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
26
ddb_main/components/SummaryList/SummaryList.tsx
Normal file
26
ddb_main/components/SummaryList/SummaryList.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { FC, HTMLAttributes } from "react";
|
||||||
|
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
export interface SummaryListProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
title: string;
|
||||||
|
list: Array<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SummaryList: FC<SummaryListProps> = ({
|
||||||
|
title,
|
||||||
|
list,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
if (!list.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...props}>
|
||||||
|
<p className={styles.title}>
|
||||||
|
{title}: <span className={styles.listItems}>{list.join(", ")}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
11
ddb_main/components/Swipeable/Swipeable.tsx
Normal file
11
ddb_main/components/Swipeable/Swipeable.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { useSwipeable } from "react-swipeable";
|
||||||
|
|
||||||
|
export const Swipeable = ({
|
||||||
|
handlerFunctions,
|
||||||
|
deltaOverride = 75,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const handlers = useSwipeable({ ...handlerFunctions, delta: deltaOverride });
|
||||||
|
|
||||||
|
return <div className="swipeable" {...handlers} {...props}></div>;
|
||||||
|
};
|
98
ddb_main/components/TabFilter/TabFilter.tsx
Normal file
98
ddb_main/components/TabFilter/TabFilter.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, Fragment, HTMLAttributes, ReactNode, useState } from "react";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import { useCharacterTheme } from "~/contexts/CharacterTheme";
|
||||||
|
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
export interface TabFilterProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
filters:
|
||||||
|
| {
|
||||||
|
label: ReactNode;
|
||||||
|
content: ReactNode;
|
||||||
|
badge?: ReactNode;
|
||||||
|
}[];
|
||||||
|
showAllTab?: boolean;
|
||||||
|
sharedChildren?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TabFilter: FC<TabFilterProps> = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
filters,
|
||||||
|
sharedChildren,
|
||||||
|
showAllTab = true,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const [activeFilter, setActiveFilter] = useState(0);
|
||||||
|
const { isDarkMode } = useCharacterTheme();
|
||||||
|
const activeIndex = showAllTab ? activeFilter - 1 : activeFilter;
|
||||||
|
const nonEmptyFilters = filters.filter((f) => f.label !== "");
|
||||||
|
|
||||||
|
const getDefaultLabel = (label: ReactNode) => {
|
||||||
|
if (typeof label === "string") return label;
|
||||||
|
return uuidv4();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAllContent = () => {
|
||||||
|
return (
|
||||||
|
nonEmptyFilters
|
||||||
|
// Remove optional tabs with duplicate content
|
||||||
|
.filter(
|
||||||
|
(f) =>
|
||||||
|
// Don't render tabs with icons such as ritual or concentration spells
|
||||||
|
typeof f.label === "string" &&
|
||||||
|
// Don't render tabs with the same content as the Attack or Limited Use tab
|
||||||
|
!getDefaultLabel(f.label).match(/(Attack|Limited Use)/)
|
||||||
|
)
|
||||||
|
// Return the content of the remaining tabs to be rendered
|
||||||
|
.map((f) => (
|
||||||
|
<Fragment key={getDefaultLabel(f.label)}>{f.content}</Fragment>
|
||||||
|
))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx([styles.tabFilter, isDarkMode && styles.dark, className])}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={styles.buttons} data-testid="tab-filters">
|
||||||
|
{showAllTab && (
|
||||||
|
<button
|
||||||
|
className={clsx([activeFilter === 0 && styles.active])}
|
||||||
|
onClick={() => setActiveFilter(0)}
|
||||||
|
data-testid="tab-filter-all"
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{nonEmptyFilters.map((filter, i) => {
|
||||||
|
const index = showAllTab ? i + 1 : i;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={clsx([activeFilter === index && styles.active])}
|
||||||
|
onClick={() => setActiveFilter(index)}
|
||||||
|
data-testid={`tab-filter-${getDefaultLabel(filter.label)
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s/g, "-")}`}
|
||||||
|
key={`${index} + ${filter.label as string}`}
|
||||||
|
>
|
||||||
|
{filter.badge && (
|
||||||
|
<span className={styles.badge}>{filter.badge}</span>
|
||||||
|
)}
|
||||||
|
{filter.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className={styles.content}>
|
||||||
|
{sharedChildren}
|
||||||
|
{!showAllTab || activeFilter !== 0
|
||||||
|
? nonEmptyFilters[activeIndex]?.content
|
||||||
|
: getAllContent()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
107
ddb_main/components/TabList/TabList.tsx
Normal file
107
ddb_main/components/TabList/TabList.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, HTMLAttributes, ReactNode, useState } from "react";
|
||||||
|
|
||||||
|
import ChevronDown from "@dndbeyond/fontawesome-cache/svgs/solid/chevron-down.svg";
|
||||||
|
|
||||||
|
import { Popover } from "../Popover";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
interface TabItem {
|
||||||
|
label: ReactNode;
|
||||||
|
content: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TabListProps extends HTMLAttributes<HTMLElement> {
|
||||||
|
tabs: Array<TabItem | null>;
|
||||||
|
variant?: "default" | "collapse" | "toggle";
|
||||||
|
defaultActiveId?: string;
|
||||||
|
maxNavItemsShow?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TabList: FC<TabListProps> = ({
|
||||||
|
tabs,
|
||||||
|
defaultActiveId,
|
||||||
|
maxNavItemsShow = 7,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const [activeTab, setActiveTab] = useState(
|
||||||
|
defaultActiveId ? defaultActiveId : tabs[0]?.id
|
||||||
|
);
|
||||||
|
const [isTabVisible, setIsTabVisible] = useState(activeTab !== "");
|
||||||
|
|
||||||
|
const handleTabClick = (id: string) => {
|
||||||
|
if (id === activeTab && variant === "collapse") {
|
||||||
|
setIsTabVisible(!isTabVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id === activeTab && variant === "toggle") {
|
||||||
|
setActiveTab("");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id !== activeTab) {
|
||||||
|
setActiveTab(id);
|
||||||
|
setIsTabVisible(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const tabsWithContent = tabs.filter((tab) => tab);
|
||||||
|
const visibleTabs = tabsWithContent.slice(0, maxNavItemsShow);
|
||||||
|
const hiddenTabs = tabsWithContent.slice(maxNavItemsShow);
|
||||||
|
const selectedTab = tabs.find((tab) => tab?.id === activeTab);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...props}>
|
||||||
|
<menu className={styles.tabs}>
|
||||||
|
{/* List of visible tabs */}
|
||||||
|
{visibleTabs.map((tab: TabItem) => (
|
||||||
|
<li key={tab.id}>
|
||||||
|
<button
|
||||||
|
className={styles.tabButton}
|
||||||
|
role="radio"
|
||||||
|
aria-checked={activeTab === tab.id}
|
||||||
|
onClick={() => handleTabClick(tab.id)}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
{variant !== "default" && (
|
||||||
|
<ChevronDown className={!isTabVisible ? styles.closed : ""} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Toggle and menu for hidden tabs */}
|
||||||
|
{hiddenTabs.length > 0 && (
|
||||||
|
<li>
|
||||||
|
<Popover
|
||||||
|
className={styles.morePopover}
|
||||||
|
variant="menu"
|
||||||
|
trigger={<button className={styles.tabButton}>More</button>}
|
||||||
|
>
|
||||||
|
<menu className={styles.moreMenu}>
|
||||||
|
{hiddenTabs.map((tab: TabItem) => (
|
||||||
|
<li key={tab.id}>
|
||||||
|
<button
|
||||||
|
className={styles.tabButton}
|
||||||
|
role="radio"
|
||||||
|
aria-checked={activeTab === tab.id}
|
||||||
|
onClick={() => handleTabClick(tab.id)}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</menu>
|
||||||
|
</Popover>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</menu>
|
||||||
|
|
||||||
|
{/* Content of the active tab */}
|
||||||
|
<div className={clsx([styles.tabContent, selectedTab?.className])}>
|
||||||
|
{isTabVisible && activeTab !== "" && selectedTab?.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
30
ddb_main/components/TagGroup/TagGroup.tsx
Normal file
30
ddb_main/components/TagGroup/TagGroup.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, HTMLAttributes } from "react";
|
||||||
|
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
export interface TagGroupProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
label: string;
|
||||||
|
tags: Array<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagGroup: FC<TagGroupProps> = ({
|
||||||
|
label,
|
||||||
|
tags,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
if (!tags.length) return null;
|
||||||
|
return (
|
||||||
|
<div className={clsx([styles.tagGroup, className])} {...props}>
|
||||||
|
<div className={styles.label}>{label}:</div>
|
||||||
|
<div className={styles.group}>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<div className={styles.tag} key={tag}>
|
||||||
|
{tag}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
75
ddb_main/components/Textarea/Textarea.tsx
Normal file
75
ddb_main/components/Textarea/Textarea.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import {
|
||||||
|
forwardRef,
|
||||||
|
HTMLAttributes,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
export interface TextareaProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
||||||
|
value?: string;
|
||||||
|
onInputBlur: (textValue: string) => void;
|
||||||
|
onInputKeyUp: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component to be used as a text area with auto-resizing capabilities. This
|
||||||
|
* is used in the panes for notes and traits and needs to support text entry as
|
||||||
|
* well as programmatic updates.
|
||||||
|
*/
|
||||||
|
export const Textarea = forwardRef<HTMLDivElement, TextareaProps>(
|
||||||
|
(
|
||||||
|
{ className, value, onInputBlur, onInputKeyUp, placeholder, ...props },
|
||||||
|
forwardedRef
|
||||||
|
) => {
|
||||||
|
const [currentValue, setCurrentValue] = useState(value);
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
// Combine the local ref with the forwarded ref
|
||||||
|
useImperativeHandle(forwardedRef, () => ref.current as HTMLDivElement);
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const dataSet = e.target.parentNode.dataset;
|
||||||
|
dataSet.value = e.target.value;
|
||||||
|
setCurrentValue(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = (e) => onInputBlur(e.target.value);
|
||||||
|
const handleKeyUp = () => onInputKeyUp();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
const wrapper = ref.current as HTMLDivElement;
|
||||||
|
const textarea = wrapper.children[0] as HTMLTextAreaElement;
|
||||||
|
// Set the data-value attribute to the value
|
||||||
|
wrapper.dataset.value = value;
|
||||||
|
// Set the textarea value to the value
|
||||||
|
textarea.value = value || "";
|
||||||
|
setCurrentValue(value);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx([styles.textarea, className])}
|
||||||
|
ref={ref}
|
||||||
|
data-value={currentValue}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
rows={1}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
value={currentValue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
65
ddb_main/components/Toggle/Toggle.tsx
Normal file
65
ddb_main/components/Toggle/Toggle.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { useState, type FC, type HTMLAttributes } from "react";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||||
|
|
||||||
|
export interface ToggleProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLButtonElement>, "onClick"> {
|
||||||
|
|
||||||
|
checked?: boolean;
|
||||||
|
color?: "primary" | "secondary" | "warning" | "error" | "info" | "success" | "themed";
|
||||||
|
onClick?: (isEnabled: boolean) => void;
|
||||||
|
onChangePromise?: (
|
||||||
|
newIsEnabled: boolean,
|
||||||
|
accept: () => void,
|
||||||
|
reject: () => void
|
||||||
|
) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Toggle: FC<ToggleProps> = ({
|
||||||
|
checked,
|
||||||
|
className,
|
||||||
|
color = "primary",
|
||||||
|
onClick,
|
||||||
|
onChangePromise,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
|
||||||
|
const [enabled, setEnabled] = useState(checked);
|
||||||
|
|
||||||
|
const handleClick = (evt: React.MouseEvent): void => {
|
||||||
|
if (!isReadonly) {
|
||||||
|
evt.stopPropagation();
|
||||||
|
evt.nativeEvent.stopImmediatePropagation();
|
||||||
|
|
||||||
|
if (onChangePromise) {
|
||||||
|
onChangePromise(
|
||||||
|
!enabled,
|
||||||
|
() => {
|
||||||
|
setEnabled(!enabled);
|
||||||
|
},
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
} else if (onClick) {
|
||||||
|
onClick(!enabled);
|
||||||
|
setEnabled(!enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={clsx([
|
||||||
|
styles.toggle,
|
||||||
|
styles[color],
|
||||||
|
checked && styles.checked,
|
||||||
|
className,
|
||||||
|
])}
|
||||||
|
onClick={handleClick}
|
||||||
|
aria-pressed={checked}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
23
ddb_main/components/Tooltip/Tooltip.tsx
Normal file
23
ddb_main/components/Tooltip/Tooltip.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tooltip as TTUITooltip,
|
||||||
|
TooltipProps as TTUITooltipProps,
|
||||||
|
} from "@dndbeyond/ttui/components/Tooltip";
|
||||||
|
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
interface TooltipProps extends TTUITooltipProps {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This tooltip component is a wrapper around the TTUI Tooltip component to provide styles which match the existing tooltip styles. It should be used in all new components so we can get rid of the tippy.js implementation. The TTUI component uses [React Tooltip](https://react-tooltip.com/) under the hood, so you can find all options [here](https://react-tooltip.com/docs/options).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const Tooltip: FC<TooltipProps> = ({ className, ...props }) => (
|
||||||
|
<TTUITooltip
|
||||||
|
className={clsx([styles.tooltip, className])}
|
||||||
|
delayShow={300}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
208
ddb_main/components/XpManager/XpManager.tsx
Normal file
208
ddb_main/components/XpManager/XpManager.tsx
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, HTMLAttributes, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CharacterUtils,
|
||||||
|
FormatUtils,
|
||||||
|
HelperUtils,
|
||||||
|
HtmlSelectOption,
|
||||||
|
RuleDataUtils,
|
||||||
|
} from "@dndbeyond/character-rules-engine";
|
||||||
|
|
||||||
|
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||||
|
import XpBar from "~/tools/js/smartComponents/XpBar";
|
||||||
|
import { Select } from "~/tools/js/smartComponents/legacy";
|
||||||
|
|
||||||
|
import { Button } from "../Button";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
const XP_CHANGE_TYPE = {
|
||||||
|
ADD: "ADD",
|
||||||
|
REMOVE: "REMOVE",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface XpManagerProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
handleXpUpdates: (newXpTotal: number) => void;
|
||||||
|
shouldReset: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const XpManager: FC<XpManagerProps> = ({
|
||||||
|
handleXpUpdates,
|
||||||
|
shouldReset,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const { experienceInfo: xpInfo, ruleData } = useCharacterEngine();
|
||||||
|
|
||||||
|
const [xpNew, setXpNew] = useState<number | null>(null);
|
||||||
|
const [xpChange, setXpChange] = useState<number | null>(null);
|
||||||
|
const [levelChosen, setLevelChosen] = useState<number | null>(null);
|
||||||
|
const [changeType, setChangeType] = useState<string>(XP_CHANGE_TYPE.ADD);
|
||||||
|
const [newXpTotal, setNewXpTotal] = useState<number>(xpInfo.currentLevelXp);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let xpDiff: number = xpChange ?? 0;
|
||||||
|
if (changeType === XP_CHANGE_TYPE.REMOVE) {
|
||||||
|
xpDiff *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newXpTotal: number = xpInfo.currentLevelXp;
|
||||||
|
if (xpDiff) {
|
||||||
|
newXpTotal += xpDiff;
|
||||||
|
} else if (levelChosen !== null) {
|
||||||
|
newXpTotal = CharacterUtils.deriveCurrentLevelXp(levelChosen, ruleData);
|
||||||
|
} else if (xpNew !== null) {
|
||||||
|
newXpTotal = xpNew;
|
||||||
|
}
|
||||||
|
|
||||||
|
newXpTotal = Math.min(
|
||||||
|
Math.max(0, newXpTotal),
|
||||||
|
CharacterUtils.deriveMaxXp(ruleData)
|
||||||
|
);
|
||||||
|
|
||||||
|
setNewXpTotal((prev) => {
|
||||||
|
if (newXpTotal !== prev) {
|
||||||
|
handleXpUpdates(newXpTotal);
|
||||||
|
}
|
||||||
|
return newXpTotal;
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
xpChange,
|
||||||
|
changeType,
|
||||||
|
levelChosen,
|
||||||
|
xpNew,
|
||||||
|
xpInfo,
|
||||||
|
ruleData,
|
||||||
|
handleXpUpdates,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldReset) {
|
||||||
|
setXpNew(null);
|
||||||
|
setXpChange(null);
|
||||||
|
setLevelChosen(null);
|
||||||
|
setChangeType(XP_CHANGE_TYPE.ADD);
|
||||||
|
setNewXpTotal(xpInfo.currentLevelXp);
|
||||||
|
}
|
||||||
|
}, [shouldReset, xpInfo]);
|
||||||
|
|
||||||
|
const levelOptions = useMemo(() => {
|
||||||
|
let levelOptions: Array<HtmlSelectOption> = [];
|
||||||
|
for (let i = 1; i <= RuleDataUtils.getMaxCharacterLevel(ruleData); i++) {
|
||||||
|
levelOptions.push({
|
||||||
|
label: `${i}`,
|
||||||
|
value: i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return levelOptions;
|
||||||
|
}, [ruleData]);
|
||||||
|
|
||||||
|
const displayCurrentLevelXp = useMemo(() => {
|
||||||
|
return xpInfo.currentLevel === ruleData.maxCharacterLevel
|
||||||
|
? CharacterUtils.deriveCurrentLevelXp(
|
||||||
|
ruleData.maxCharacterLevel - 1,
|
||||||
|
ruleData
|
||||||
|
)
|
||||||
|
: xpInfo.currentLevelXp;
|
||||||
|
}, [xpInfo, ruleData]);
|
||||||
|
|
||||||
|
const handleChooseLevel = (value: string): void => {
|
||||||
|
setXpNew(null);
|
||||||
|
setXpChange(null);
|
||||||
|
setLevelChosen(HelperUtils.parseInputInt(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleXpChange = (evt: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
setXpNew(null);
|
||||||
|
setXpChange(HelperUtils.parseInputInt(evt.target.value));
|
||||||
|
setLevelChosen(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleXpSet = (evt: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
setXpNew(HelperUtils.parseInputInt(evt.target.value));
|
||||||
|
setXpChange(null);
|
||||||
|
setLevelChosen(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...props}>
|
||||||
|
<h2 className={styles.levelTotal}>
|
||||||
|
Current XP Total:{" "}
|
||||||
|
{FormatUtils.renderLocaleNumber(xpInfo.currentLevelXp)} (Level{" "}
|
||||||
|
{xpInfo.currentLevel})
|
||||||
|
</h2>
|
||||||
|
<div className={styles.xpBar}>
|
||||||
|
<XpBar
|
||||||
|
xp={xpInfo.currentLevelXp}
|
||||||
|
showCurrentMarker={true}
|
||||||
|
ruleData={ruleData}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.currentLevelAmounts}>
|
||||||
|
<div>{FormatUtils.renderLocaleNumber(displayCurrentLevelXp)}</div>
|
||||||
|
<div>{FormatUtils.renderLocaleNumber(xpInfo.nextLevelXp)}</div>
|
||||||
|
</div>
|
||||||
|
<div className={clsx([styles.controls, styles.withDivider])}>
|
||||||
|
<div className={styles.control}>
|
||||||
|
<label id="set-level" className={styles.label}>
|
||||||
|
Set Level
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
aria-labelledby="set-level"
|
||||||
|
options={levelOptions}
|
||||||
|
onChange={handleChooseLevel}
|
||||||
|
value={levelChosen}
|
||||||
|
placeholder={"--"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={clsx([styles.control, styles.setXp])}>
|
||||||
|
<label id="set-xp" className={styles.label}>
|
||||||
|
Set XP
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
aria-labelledby="set-xp"
|
||||||
|
type="number"
|
||||||
|
value={xpNew === null ? "" : xpNew}
|
||||||
|
onChange={handleXpSet}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.adjustGroup}>
|
||||||
|
<div className={styles.adjustTabs}>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
size="xx-small"
|
||||||
|
className={clsx([
|
||||||
|
styles.tab,
|
||||||
|
changeType === XP_CHANGE_TYPE.ADD && styles.active,
|
||||||
|
])}
|
||||||
|
onClick={() => setChangeType(XP_CHANGE_TYPE.ADD)}
|
||||||
|
>
|
||||||
|
Add Xp
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
size="xx-small"
|
||||||
|
className={clsx([
|
||||||
|
styles.tab,
|
||||||
|
changeType === XP_CHANGE_TYPE.REMOVE && styles.active,
|
||||||
|
])}
|
||||||
|
onClick={() => setChangeType(XP_CHANGE_TYPE.REMOVE)}
|
||||||
|
>
|
||||||
|
Remove Xp
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={xpChange === null ? "" : xpChange}
|
||||||
|
onChange={handleXpChange}
|
||||||
|
placeholder="Type XP Value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 className={clsx([styles.levelTotal, styles.withDivider])}>
|
||||||
|
New XP Total: {FormatUtils.renderLocaleNumber(newXpTotal)} (Level{" "}
|
||||||
|
{CharacterUtils.deriveXpLevel(newXpTotal, ruleData)})
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
191
ddb_main/config.ts
Normal file
191
ddb_main/config.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
const { Constants } = require("@dndbeyond/character-rules-engine/es");
|
||||||
|
enum EnvTypeEnum {
|
||||||
|
PRODUCTION = "production",
|
||||||
|
STAGING = "staging",
|
||||||
|
TEST = "test",
|
||||||
|
DRAFT = "draft",
|
||||||
|
REVIEW = "review",
|
||||||
|
DEVELOPMENT = "development",
|
||||||
|
LOCAL_APP_SHELL = "local_app_shell",
|
||||||
|
}
|
||||||
|
|
||||||
|
const envConfigs = {
|
||||||
|
[EnvTypeEnum.PRODUCTION]: {
|
||||||
|
apiInfo: {
|
||||||
|
[Constants.ApiTypeEnum.WEBSITE]: "https://www.dndbeyond.com",
|
||||||
|
[Constants.ApiTypeEnum.CHARACTER_SERVICE]:
|
||||||
|
"https://character-service.dndbeyond.com",
|
||||||
|
[Constants.ApiTypeEnum.GAME_DATA_SERVICE]:
|
||||||
|
"https://gamedata-service.dndbeyond.com",
|
||||||
|
},
|
||||||
|
gameLogApiEndpoint: "https://www.dndbeyond.com",
|
||||||
|
ddbApiEndpoint: "https://api.dndbeyond.com",
|
||||||
|
diceAssetEndpoint: "https://www.dndbeyond.com/dice",
|
||||||
|
diceApiEndpoint: "https://dice-service.dndbeyond.com",
|
||||||
|
analyticTrackingId: "UA-26524418-48",
|
||||||
|
authEndpoint: "https://auth-service.dndbeyond.com/v1/cobalt-token",
|
||||||
|
env: EnvTypeEnum.PRODUCTION,
|
||||||
|
ddbMediaUrl: "https://www.dndbeyond.com",
|
||||||
|
},
|
||||||
|
[EnvTypeEnum.STAGING]: {
|
||||||
|
apiInfo: {
|
||||||
|
[Constants.ApiTypeEnum.WEBSITE]: "https://stg.dndbeyond.com",
|
||||||
|
[Constants.ApiTypeEnum.CHARACTER_SERVICE]:
|
||||||
|
"https://character-service-stg.dndbeyond.com",
|
||||||
|
[Constants.ApiTypeEnum.GAME_DATA_SERVICE]:
|
||||||
|
"https://gamedata-service-stg.dndbeyond.com",
|
||||||
|
},
|
||||||
|
gameLogApiEndpoint: "https://stg.dndbeyond.com",
|
||||||
|
ddbApiEndpoint: "https://test-api.dndbeyond.com",
|
||||||
|
diceAssetEndpoint: "https://stg.dndbeyond.com/dice",
|
||||||
|
diceApiEndpoint: "https://dice-service-stg.dndbeyond.com",
|
||||||
|
analyticTrackingId: "UA-26524418-48",
|
||||||
|
authEndpoint: "https://auth-service-stg.dndbeyond.com/v1/cobalt-token",
|
||||||
|
env: EnvTypeEnum.STAGING,
|
||||||
|
ddbMediaUrl: "https://www.dndbeyond.com",
|
||||||
|
},
|
||||||
|
[EnvTypeEnum.DRAFT]: {
|
||||||
|
apiInfo: {
|
||||||
|
[Constants.ApiTypeEnum.WEBSITE]: "https://draft.dndbeyond.com",
|
||||||
|
[Constants.ApiTypeEnum.CHARACTER_SERVICE]:
|
||||||
|
"https://character-service-draft.dndbeyond.com",
|
||||||
|
[Constants.ApiTypeEnum.GAME_DATA_SERVICE]:
|
||||||
|
"https://gamedata-service-draft.dndbeyond.com",
|
||||||
|
},
|
||||||
|
gameLogApiEndpoint: "https://stg.dndbeyond.com",
|
||||||
|
ddbApiEndpoint: "https://test-api.dndbeyond.com",
|
||||||
|
diceAssetEndpoint: "https://stg.dndbeyond.com/dice",
|
||||||
|
diceApiEndpoint: "https://dice-service-stg.dndbeyond.com",
|
||||||
|
analyticTrackingId: "UA-26524418-48",
|
||||||
|
authEndpoint: "https://auth-service-stg.dndbeyond.com/v1/cobalt-token",
|
||||||
|
env: EnvTypeEnum.DRAFT,
|
||||||
|
ddbMediaUrl: "https://www.dndbeyond.com",
|
||||||
|
},
|
||||||
|
[EnvTypeEnum.REVIEW]: {
|
||||||
|
apiInfo: {
|
||||||
|
[Constants.ApiTypeEnum.WEBSITE]: "https://review.dndbeyond.com",
|
||||||
|
[Constants.ApiTypeEnum.CHARACTER_SERVICE]:
|
||||||
|
"https://character-service-review.dndbeyond.com",
|
||||||
|
[Constants.ApiTypeEnum.GAME_DATA_SERVICE]:
|
||||||
|
"https://gamedata-service-review.dndbeyond.com",
|
||||||
|
},
|
||||||
|
gameLogApiEndpoint: "https://stg.dndbeyond.com",
|
||||||
|
ddbApiEndpoint: "https://test-api.dndbeyond.com",
|
||||||
|
diceAssetEndpoint: "https://stg.dndbeyond.com/dice",
|
||||||
|
diceApiEndpoint: "https://dice-service-stg.dndbeyond.com",
|
||||||
|
analyticTrackingId: "UA-26524418-48",
|
||||||
|
authEndpoint: "https://auth-service-stg.dndbeyond.com/v1/cobalt-token",
|
||||||
|
env: EnvTypeEnum.REVIEW,
|
||||||
|
ddbMediaUrl: "https://www.dndbeyond.com",
|
||||||
|
},
|
||||||
|
[EnvTypeEnum.DEVELOPMENT]: {
|
||||||
|
analyticTrackingId: "UA-26524418-48",
|
||||||
|
env: EnvTypeEnum.DEVELOPMENT,
|
||||||
|
diceAssetEndpoint: process.env.REACT_APP_DICE_ASSET_URL,
|
||||||
|
diceApiEndpoint: process.env.REACT_APP_DICE_API_URL,
|
||||||
|
gameLogApiEndpoint: process.env.REACT_APP_GAMELOG_API_URL,
|
||||||
|
ddbApiEndpoint: process.env.REACT_APP_DDB_API_URL,
|
||||||
|
authEndpoint: `${process.env.REACT_APP_DDB_AUTH_URL}`,
|
||||||
|
apiInfo: {
|
||||||
|
[Constants.ApiTypeEnum.WEBSITE]: process.env.REACT_APP_DDB_BASE_URL,
|
||||||
|
[Constants.ApiTypeEnum.CHARACTER_SERVICE]:
|
||||||
|
process.env.REACT_APP_CHARACTER_SERVICE_URL,
|
||||||
|
[Constants.ApiTypeEnum.GAME_DATA_SERVICE]:
|
||||||
|
process.env.REACT_APP_GAMEDATA_API_URL,
|
||||||
|
},
|
||||||
|
ddbMediaUrl: "https://www.dndbeyond.com",
|
||||||
|
},
|
||||||
|
[EnvTypeEnum.LOCAL_APP_SHELL]: {
|
||||||
|
apiInfo: {
|
||||||
|
[Constants.ApiTypeEnum.WEBSITE]: "http://dev.dndbeyond.com:8080",
|
||||||
|
[Constants.ApiTypeEnum.CHARACTER_SERVICE]:
|
||||||
|
"https://character-service-stg.dndbeyond.com",
|
||||||
|
[Constants.ApiTypeEnum.GAME_DATA_SERVICE]:
|
||||||
|
"https://gamedata-service-stg.dndbeyond.com",
|
||||||
|
},
|
||||||
|
gameLogApiEndpoint: "https://stg.dndbeyond.com",
|
||||||
|
ddbApiEndpoint: "https://test-api.dndbeyond.com",
|
||||||
|
diceAssetEndpoint: "https://stg.dndbeyond.com/dice",
|
||||||
|
diceApiEndpoint: "https://dice-service-stg.dndbeyond.com",
|
||||||
|
analyticTrackingId: "UA-26524418-48",
|
||||||
|
authEndpoint: "https://auth-service-stg.dndbeyond.com/v1/cobalt-token",
|
||||||
|
env: EnvTypeEnum.LOCAL_APP_SHELL,
|
||||||
|
ddbMediaUrl: "https://www.dndbeyond.com",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getWebsite(env: EnvTypeEnum): string {
|
||||||
|
return envConfigs[env].apiInfo[Constants.ApiTypeEnum.WEBSITE];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfig() {
|
||||||
|
let envKey: EnvTypeEnum = EnvTypeEnum.PRODUCTION;
|
||||||
|
switch (window.location.origin) {
|
||||||
|
case getWebsite(EnvTypeEnum.PRODUCTION):
|
||||||
|
envKey = EnvTypeEnum.PRODUCTION;
|
||||||
|
break;
|
||||||
|
case getWebsite(EnvTypeEnum.STAGING):
|
||||||
|
envKey = EnvTypeEnum.STAGING;
|
||||||
|
break;
|
||||||
|
case getWebsite(EnvTypeEnum.DRAFT):
|
||||||
|
envKey = EnvTypeEnum.DRAFT;
|
||||||
|
break;
|
||||||
|
case getWebsite(EnvTypeEnum.REVIEW):
|
||||||
|
envKey = EnvTypeEnum.REVIEW;
|
||||||
|
break;
|
||||||
|
case getWebsite(EnvTypeEnum.DEVELOPMENT):
|
||||||
|
envKey = EnvTypeEnum.DEVELOPMENT;
|
||||||
|
break;
|
||||||
|
case getWebsite(EnvTypeEnum.LOCAL_APP_SHELL):
|
||||||
|
envKey = EnvTypeEnum.LOCAL_APP_SHELL;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
envKey = EnvTypeEnum.PRODUCTION;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// this is the config for the current environment
|
||||||
|
let config: Record<string, any> = {
|
||||||
|
...envConfigs[envKey],
|
||||||
|
debug: process.env.NODE_ENV !== "production",
|
||||||
|
launchDarkylyClientId: process.env.REACT_APP_LAUNCH_DARKLY_CLIENT_ID,
|
||||||
|
production: process.env.NODE_ENV === "production",
|
||||||
|
version: process.env.REACT_APP_VERSION,
|
||||||
|
analyticTrackingId: process.env.REACT_APP_TRACKING_ID,
|
||||||
|
env: process.env.NODE_ENV,
|
||||||
|
characterServiceBaseUrl: `${
|
||||||
|
envConfigs[envKey].apiInfo[Constants.ApiTypeEnum.CHARACTER_SERVICE]
|
||||||
|
}/character/v5`,
|
||||||
|
userServiceBaseUrl: `${
|
||||||
|
envConfigs[envKey].apiInfo[Constants.ApiTypeEnum.CHARACTER_SERVICE]
|
||||||
|
}/user/v5`,
|
||||||
|
ddbBaseUrl: envConfigs[envKey].apiInfo[Constants.ApiTypeEnum.WEBSITE],
|
||||||
|
environment: envKey,
|
||||||
|
basePathname: "/characters",
|
||||||
|
};
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export config items individually
|
||||||
|
export const {
|
||||||
|
analyticTrackingId,
|
||||||
|
env,
|
||||||
|
diceAssetEndpoint,
|
||||||
|
diceApiEndpoint,
|
||||||
|
gameLogApiEndpoint,
|
||||||
|
ddbApiEndpoint,
|
||||||
|
authEndpoint,
|
||||||
|
apiInfo,
|
||||||
|
ddbMediaUrl,
|
||||||
|
debug,
|
||||||
|
launchDarkylyClientId,
|
||||||
|
production,
|
||||||
|
version,
|
||||||
|
characterServiceBaseUrl,
|
||||||
|
userServiceBaseUrl,
|
||||||
|
ddbBaseUrl,
|
||||||
|
environment,
|
||||||
|
basePathname,
|
||||||
|
} = getConfig();
|
||||||
|
|
||||||
|
// Export the entire config object
|
||||||
|
export default getConfig();
|
315
ddb_main/constants.ts
Normal file
315
ddb_main/constants.ts
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
import { Constants } from "@dndbeyond/character-rules-engine";
|
||||||
|
|
||||||
|
import config from "./config";
|
||||||
|
|
||||||
|
export const DDB_MEDIA_URL = config.ddbMediaUrl;
|
||||||
|
|
||||||
|
/* Naming convention for GA constants: Session_Category_Action */
|
||||||
|
export const SessionTrackingIds = {
|
||||||
|
DDB: config.production
|
||||||
|
? "UA-26524418-48" // DDB Proper Account
|
||||||
|
: "UA-117447463-5", // Encounter Builder Test Account
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SessionNames = {
|
||||||
|
DDB: "DnDBeyond", // Session names cannot use spaces or special characters
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SessionTrackingIdByName = {
|
||||||
|
[SessionNames.DDB]: SessionTrackingIds.DDB,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CategoryPrefix = "Character Listing";
|
||||||
|
|
||||||
|
export const EventCategories = {
|
||||||
|
DDB_Character: `${CategoryPrefix} Character Card`,
|
||||||
|
DDB_ListingFilter: `${CategoryPrefix} Filter`,
|
||||||
|
DDB_ListingSort: `${CategoryPrefix} Sort`,
|
||||||
|
DDB_PlayerAppBanner: `${CategoryPrefix} Player App Banner`,
|
||||||
|
DDB_Unlock: `${CategoryPrefix} Unlock`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EventActions = {
|
||||||
|
DDB_Character_Campaign: "Campaign",
|
||||||
|
DDB_Character_Copy: "Copy",
|
||||||
|
DDB_Character_Delete: "Delete",
|
||||||
|
DDB_Character_Edit: "Edit",
|
||||||
|
DDB_Character_LeaveCampaign: "Leave Campaign",
|
||||||
|
DDB_Character_View: "View",
|
||||||
|
|
||||||
|
DDB_ListingFilter_SearchChanged: "Search Changed",
|
||||||
|
DDB_ListingFilter_SearchCleared: "Search Cleared",
|
||||||
|
|
||||||
|
DDB_ListingSort_Changed: "Changed",
|
||||||
|
|
||||||
|
DDB_PlayerAppBanner_Dismissed: "Dismissed",
|
||||||
|
DDB_PlayerAppBanner_ClickedCta: "Clicked CTA",
|
||||||
|
|
||||||
|
DDB_Unlock_CharacterLocked: "Character Locked",
|
||||||
|
DDB_Unlock_CharacterUnlocked: "Character Unlocked",
|
||||||
|
DDB_Unlock_FinishUnlockingClicked: "Finish Unlocking Clicked",
|
||||||
|
DDB_Unlock_FinishUnlockingCancelled: "Finish Unlocking Cancelled",
|
||||||
|
DDB_Unlock_FinishUnlockingConfirmed: "Finish Unlocking Confirmed",
|
||||||
|
DDB_Unlock_SubscribeClicked: "Subscribe Clicked",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EventLabels = {
|
||||||
|
Cancelled: "Cancelled",
|
||||||
|
Clicked: "Clicked",
|
||||||
|
Confirmed: "Confirmed",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InputLimits = {
|
||||||
|
characterNameMaxLength: 128,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CharacterNameLimitMsg = `The max length for a name is ${InputLimits.characterNameMaxLength} characters.`;
|
||||||
|
|
||||||
|
export const SourceCategoryDescription = {
|
||||||
|
official:
|
||||||
|
"The sources below add additional character options beyond the 2024 Core Rules. You will only see character options from the Core Rules and content you own and have enabled here.",
|
||||||
|
homebrew:
|
||||||
|
"Character options designed by other players and uploaded to D&D BEYOND. Talk to your DM before including Homebrew content.",
|
||||||
|
partnered:
|
||||||
|
"Content developed by our partner publishers. You will only see character options for content you have purchased.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif"];
|
||||||
|
export const FILE_SIZE_3MB = 3 * 1024 * 1024;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export constants from the rules engine to reduce the number of places where
|
||||||
|
* they're being imported.
|
||||||
|
**/
|
||||||
|
export const ActionCustomizationAdjustmentTypes =
|
||||||
|
Constants.ACTION_CUSTOMIZATION_ADJUSTMENT_TYPES;
|
||||||
|
export const AllArmorList = Constants.ALL_ARMOR_LIST;
|
||||||
|
export const AbilityScoreStatTypeEnum = Constants.AbilityScoreStatTypeEnum;
|
||||||
|
export const AbilityScoreTypeEnum = Constants.AbilityScoreTypeEnum;
|
||||||
|
export const AbilitySkillEnum = Constants.AbilitySkillEnum;
|
||||||
|
export const AbilityStatEnum = Constants.AbilityStatEnum;
|
||||||
|
export const AbilityStatMentalList = Constants.ABILITY_STAT_MENTAL_LIST;
|
||||||
|
export const AbilityStatPhysicalList = Constants.ABILITY_STAT_PHYSICAL_LIST;
|
||||||
|
export const AccessTypeEnum = Constants.AccessTypeEnum;
|
||||||
|
export const ActionTypeEnum = Constants.ActionTypeEnum;
|
||||||
|
export const ActivatableTypeEnum = Constants.ActivatableTypeEnum;
|
||||||
|
export const ActivationTypeEnum = Constants.ActivationTypeEnum;
|
||||||
|
export const AdditionalTypeEnum = Constants.AdditionalTypeEnum;
|
||||||
|
export const AdjustmentConstraintTypeEnum =
|
||||||
|
Constants.AdjustmentConstraintTypeEnum;
|
||||||
|
export const AdjustmentDataTypeEnum = Constants.AdjustmentDataTypeEnum;
|
||||||
|
export const AdjustmentTypeEnum = Constants.AdjustmentTypeEnum;
|
||||||
|
export const ApiTypeEnum = Constants.ApiTypeEnum;
|
||||||
|
export const AppContextTypeEnum = Constants.AppContextTypeEnum;
|
||||||
|
export const ArmorClassExtraTypeEnum = Constants.ArmorClassExtraTypeEnum;
|
||||||
|
export const ArmorClassTypeEnum = Constants.ArmorClassTypeEnum;
|
||||||
|
export const ArmorTypeEnum = Constants.ArmorTypeEnum;
|
||||||
|
export const AttackSourceTypeEnum = Constants.AttackSourceTypeEnum;
|
||||||
|
export const AttackSubtypeEnum = Constants.AttackSubtypeEnum;
|
||||||
|
export const AttackTypeRangeEnum = Constants.AttackTypeRangeEnum;
|
||||||
|
export const BackgroundModifierTypeEnum = Constants.BackgroundModifierTypeEnum;
|
||||||
|
export const BuilderChoiceSubtypeEnum = Constants.BuilderChoiceSubtypeEnum;
|
||||||
|
export const BuilderChoiceTypeEnum = Constants.BuilderChoiceTypeEnum;
|
||||||
|
export const CharacterServiceVersionKey =
|
||||||
|
Constants.CHARACTER_SERVICE_VERSION_KEY;
|
||||||
|
export const CharacterServiceVersionKeyOverride =
|
||||||
|
Constants.CHARACTER_SERVICE_VERSION_KEY_OVERRIDE;
|
||||||
|
export const CreatureCustomizationAdjustmentTypes =
|
||||||
|
Constants.CREATURE_CUSTOMIZATION_ADJUSTMENT_TYPES;
|
||||||
|
export const CurrencyValue = Constants.CURRENCY_VALUE;
|
||||||
|
export const CustomItemDefinitionEntityTypeId =
|
||||||
|
Constants.CUSTOM_ITEM_DEFINITION_ENTITY_TYPE_ID;
|
||||||
|
export const CharacterColorEnum = Constants.CharacterColorEnum;
|
||||||
|
export const CharacterLoadingStatusEnum = Constants.CharacterLoadingStatusEnum;
|
||||||
|
export const CoinNamesEnum = Constants.CoinNamesEnum;
|
||||||
|
export const CoinTypeEnum = Constants.CoinTypeEnum;
|
||||||
|
export const ComponentAdjustmentEnum = Constants.ComponentAdjustmentEnum;
|
||||||
|
export const ComponentCostTypeEnum = Constants.ComponentCostTypeEnum;
|
||||||
|
export const ConditionIdEnum = Constants.ConditionIdEnum;
|
||||||
|
export const ConditionTypeEnum = Constants.ConditionTypeEnum;
|
||||||
|
export const ContainerTypeEnum = Constants.ContainerTypeEnum;
|
||||||
|
export const ContentSharingSettingEnum = Constants.ContentSharingSettingEnum;
|
||||||
|
export const CreatureGroupFlagEnum = Constants.CreatureGroupFlagEnum;
|
||||||
|
export const CreatureSizeEnum = Constants.CreatureSizeEnum;
|
||||||
|
export const CreatureSizeNameEnum = Constants.CreatureSizeNameEnum;
|
||||||
|
export const CustomProficiencyTypeEnum = Constants.CustomProficiencyTypeEnum;
|
||||||
|
export const DamageAdjustmentList = Constants.DAMAGE_ADJUSTMENT_LIST;
|
||||||
|
export const DbStringBookOfAncientSecrets =
|
||||||
|
Constants.DB_STRING_BOOK_OF_ANCIENT_SECRETS;
|
||||||
|
export const DbStringCarapace = Constants.DB_STRING_CARAPACE;
|
||||||
|
export const DbStringDedicatedWeapon = Constants.DB_STRING_DEDICATED_WEAPON;
|
||||||
|
export const DbStringEldritchAdept = Constants.DB_STRING_ELDRITCH_ADEPT;
|
||||||
|
export const DbStringGroupSidekick = Constants.DB_STRING_GROUP_SIDEKICK;
|
||||||
|
export const DbStringImprovedPactWeapon =
|
||||||
|
Constants.DB_STRING_IMPROVED_PACT_WEAPON;
|
||||||
|
export const DbStringInfuseItem = Constants.DB_STRING_INFUSE_ITEM;
|
||||||
|
export const DbStringIntegratedProtection =
|
||||||
|
Constants.DB_STRING_INTEGRATED_PROTECTION;
|
||||||
|
export const DbStringMartialArts = Constants.DB_STRING_MARTIAL_ARTS;
|
||||||
|
export const DbStringMediumArmorMaster =
|
||||||
|
Constants.DB_STRING_MEDIUM_ARMOR_MASTER;
|
||||||
|
export const DbStringPactMagic = Constants.DB_STRING_PACT_MAGIC;
|
||||||
|
export const DbStringRitualCasterBard = Constants.DB_STRING_RITUAL_CASTER_BARD;
|
||||||
|
export const DbStringRitualCasterCleric =
|
||||||
|
Constants.DB_STRING_RITUAL_CASTER_CLERIC;
|
||||||
|
export const DbStringRitualCasterDruid =
|
||||||
|
Constants.DB_STRING_RITUAL_CASTER_DRUID;
|
||||||
|
export const DbStringRitualCasterList = Constants.DB_STRING_RITUAL_CASTER_LIST;
|
||||||
|
export const DbStringRitualCasterSorcerer =
|
||||||
|
Constants.DB_STRING_RITUAL_CASTER_SORCERER;
|
||||||
|
export const DbStringRitualCasterWarlock =
|
||||||
|
Constants.DB_STRING_RITUAL_CASTER_WARLOCK;
|
||||||
|
export const DbStringRitualCasterWizard =
|
||||||
|
Constants.DB_STRING_RITUAL_CASTER_WIZARD;
|
||||||
|
export const DbStringSpellcasting = Constants.DB_STRING_SPELLCASTING;
|
||||||
|
export const DbStringSpellEldritchBlast =
|
||||||
|
Constants.DB_STRING_SPELL_ELDRITCH_BLAST;
|
||||||
|
export const DbStringTagSidekick = Constants.DB_STRING_TAG_SIDEKICK;
|
||||||
|
export const DbStringVerdan = Constants.DB_STRING_VERDAN;
|
||||||
|
export const DefaultFeatureFlagInfo = Constants.DEFAULT_FEATURE_FLAG_INFO;
|
||||||
|
export const DefinitionKeySeparator = Constants.DEFINITION_KEY_SEPARATOR;
|
||||||
|
export const DefinitionServiceVersions = Constants.DEFINITION_SERVICE_VERSIONS;
|
||||||
|
export const DiceRollKeyConceptSeparator =
|
||||||
|
Constants.DICE_ROLL_KEY_CONCEPT_SEPARATOR;
|
||||||
|
export const DiceRollKeyDataSeparator = Constants.DICE_ROLL_KEY_DATA_SEPARATOR;
|
||||||
|
export const DamageAdjustmentTypeEnum = Constants.DamageAdjustmentTypeEnum;
|
||||||
|
export const DataOriginDataInfoKeyEnum = Constants.DataOriginDataInfoKeyEnum;
|
||||||
|
export const DataOriginTypeEnum = Constants.DataOriginTypeEnum;
|
||||||
|
export const DeathCauseEnum = Constants.DeathCauseEnum;
|
||||||
|
export const DefenseAdjustmentTypeEnum = Constants.DefenseAdjustmentTypeEnum;
|
||||||
|
export const DefinitionPoolTypeInfoKeyEnum =
|
||||||
|
Constants.DefinitionPoolTypeInfoKeyEnum;
|
||||||
|
export const DefinitionTypeEnum = Constants.DefinitionTypeEnum;
|
||||||
|
export const DiceAdjustmentRollTypeEnum = Constants.DiceAdjustmentRollTypeEnum;
|
||||||
|
export const DiceAdjustmentTypeEnum = Constants.DiceAdjustmentTypeEnum;
|
||||||
|
export const DiceRollExcludeTypeEnum = Constants.DiceRollExcludeTypeEnum;
|
||||||
|
export const DisplayConfigurationTypeEnum =
|
||||||
|
Constants.DisplayConfigurationTypeEnum;
|
||||||
|
export const DisplayConfigurationValueEnum =
|
||||||
|
Constants.DisplayConfigurationValueEnum;
|
||||||
|
export const DisplayIntentionEnum = Constants.DisplayIntentionEnum;
|
||||||
|
export const DurationUnitEnum = Constants.DurationUnitEnum;
|
||||||
|
export const EntityLimitedUseScaleOperatorEnum =
|
||||||
|
Constants.EntityLimitedUseScaleOperatorEnum;
|
||||||
|
export const EntityTypeEnum = Constants.EntityTypeEnum;
|
||||||
|
export const ExtraGroupTypeEnum = Constants.ExtraGroupTypeEnum;
|
||||||
|
export const ExtraTypeEnum = Constants.ExtraTypeEnum;
|
||||||
|
export const FeatureFlagList = Constants.FEATURE_FLAG_LIST;
|
||||||
|
export const FeetInMiles = Constants.FEET_IN_MILES;
|
||||||
|
export const FutureItemDefinitionType = Constants.FUTURE_ITEM_DEFINITION_TYPE;
|
||||||
|
export const FeatureFlagEnum = Constants.FeatureFlagEnum;
|
||||||
|
export const FeatureTypeEnum = Constants.FeatureTypeEnum;
|
||||||
|
export const HackVehicleGroupId = Constants.HACK_VEHICLE_GROUP_ID;
|
||||||
|
export const HeavyArmorList = Constants.HEAVY_ARMOR_LIST;
|
||||||
|
export const ItemCustomizationAdjustmentTypes =
|
||||||
|
Constants.ITEM_CUSTOMIZATION_ADJUSTMENT_TYPES;
|
||||||
|
export const InfusionItemDataRuleTypeEnum =
|
||||||
|
Constants.InfusionItemDataRuleTypeEnum;
|
||||||
|
export const InfusionModifierDataTypeEnum =
|
||||||
|
Constants.InfusionModifierDataTypeEnum;
|
||||||
|
export const InfusionTypeEnum = Constants.InfusionTypeEnum;
|
||||||
|
export const ItemBaseTypeEnum = Constants.ItemBaseTypeEnum;
|
||||||
|
export const ItemBaseTypeIdEnum = Constants.ItemBaseTypeIdEnum;
|
||||||
|
export const ItemRarityNameEnum = Constants.ItemRarityNameEnum;
|
||||||
|
export const ItemTypeEnum = Constants.ItemTypeEnum;
|
||||||
|
export const LightArmorList = Constants.LIGHT_ARMOR_LIST;
|
||||||
|
export const LimitedUseResetTypeEnum = Constants.LimitedUseResetTypeEnum;
|
||||||
|
export const LimitedUseResetTypeNameEnum =
|
||||||
|
Constants.LimitedUseResetTypeNameEnum;
|
||||||
|
export const LogMessageType = Constants.LogMessageType;
|
||||||
|
export const MagicItemAttackWithStatList =
|
||||||
|
Constants.MAGIC_ITEM_ATTACK_WITH_STAT_LIST;
|
||||||
|
export const MagicItemEntityTypeId = Constants.MAGIC_ITEM_ENTITY_TYPE_ID;
|
||||||
|
export const MediumArmorList = Constants.MEDIUM_ARMOR_LIST;
|
||||||
|
export const ModifierBonusTypeEnum = Constants.ModifierBonusTypeEnum;
|
||||||
|
export const ModifierSubTypeEnum = Constants.ModifierSubTypeEnum;
|
||||||
|
export const ModifierTypeEnum = Constants.ModifierTypeEnum;
|
||||||
|
export const MovementTypeEnum = Constants.MovementTypeEnum;
|
||||||
|
export const MulticlassSpellSlotRoundingEnum =
|
||||||
|
Constants.MulticlassSpellSlotRoundingEnum;
|
||||||
|
export const NoteKeyEnum = Constants.NoteKeyEnum;
|
||||||
|
export const NoteTypeEnum = Constants.NoteTypeEnum;
|
||||||
|
export const NotificationTypeEnum = Constants.NotificationTypeEnum;
|
||||||
|
export const PoundsInTon = Constants.POUNDS_IN_TON;
|
||||||
|
export const PropertyList = Constants.PROPERTY_LIST;
|
||||||
|
export const PartyInventorySharingStateEnum =
|
||||||
|
Constants.PartyInventorySharingStateEnum;
|
||||||
|
export const PreferenceAbilityScoreDisplayTypeEnum =
|
||||||
|
Constants.PreferenceAbilityScoreDisplayTypeEnum;
|
||||||
|
export const PreferenceEncumbranceTypeEnum =
|
||||||
|
Constants.PreferenceEncumbranceTypeEnum;
|
||||||
|
export const PreferenceHitPointTypeEnum = Constants.PreferenceHitPointTypeEnum;
|
||||||
|
export const PreferencePrivacyTypeEnum = Constants.PreferencePrivacyTypeEnum;
|
||||||
|
export const PreferenceProgressionTypeEnum =
|
||||||
|
Constants.PreferenceProgressionTypeEnum;
|
||||||
|
export const PreferenceSharingTypeEnum = Constants.PreferenceSharingTypeEnum;
|
||||||
|
export const PrerequisiteSubTypeEnum = Constants.PrerequisiteSubTypeEnum;
|
||||||
|
export const PrerequisiteTypeEnum = Constants.PrerequisiteTypeEnum;
|
||||||
|
export const ProficiencyAdjustmentTypeEnum =
|
||||||
|
Constants.ProficiencyAdjustmentTypeEnum;
|
||||||
|
export const ProficiencyLevelEnum = Constants.ProficiencyLevelEnum;
|
||||||
|
export const ProficiencyRoundingEnum = Constants.ProficiencyRoundingEnum;
|
||||||
|
export const ProtectionAvailabilityStatusEnum =
|
||||||
|
Constants.ProtectionAvailabilityStatusEnum;
|
||||||
|
export const ProtectionSupplierTypeEnum = Constants.ProtectionSupplierTypeEnum;
|
||||||
|
export const RaceTypeEnum = Constants.RaceTypeEnum;
|
||||||
|
export const RitualCastingTypeEnum = Constants.RitualCastingTypeEnum;
|
||||||
|
export const RuleDataTypeEnum = Constants.RuleDataTypeEnum;
|
||||||
|
export const RuleKeyEnum = Constants.RuleKeyEnum;
|
||||||
|
export const ShieldsList = Constants.SHIELDS_LIST;
|
||||||
|
export const Signed32BitIntMaxValue = Constants.SIGNED_32BIT_INT_MAX_VALUE;
|
||||||
|
export const Signed32BitIntMinValue = Constants.SIGNED_32BIT_INT_MIN_VALUE;
|
||||||
|
export const SizeList = Constants.SIZE_LIST;
|
||||||
|
export const SpellCustomizationAdjustmentTypes =
|
||||||
|
Constants.SPELL_CUSTOMIZATION_ADJUSTMENT_TYPES;
|
||||||
|
export const StatAbilityCheckList = Constants.STAT_ABILITY_CHECK_LIST;
|
||||||
|
export const StatAbilityScoreList = Constants.STAT_ABILITY_SCORE_LIST;
|
||||||
|
export const StatSavingThrowList = Constants.STAT_SAVING_THROW_LIST;
|
||||||
|
export const SaveTypeEnum = Constants.SaveTypeEnum;
|
||||||
|
export const SenseTypeEnum = Constants.SenseTypeEnum;
|
||||||
|
export const SituationalBonusSavingThrowTypeEnum =
|
||||||
|
Constants.SituationalBonusSavingThrowTypeEnum;
|
||||||
|
export const SnippetAbilityKeyEnum = Constants.SnippetAbilityKeyEnum;
|
||||||
|
export const SnippetContentChunkTypeEnum =
|
||||||
|
Constants.SnippetContentChunkTypeEnum;
|
||||||
|
export const SnippetMathOperatorEnum = Constants.SnippetMathOperatorEnum;
|
||||||
|
export const SnippetPostProcessTypeEnum = Constants.SnippetPostProcessTypeEnum;
|
||||||
|
export const SnippetSymbolEnum = Constants.SnippetSymbolEnum;
|
||||||
|
export const SnippetTagDataTypeEnum = Constants.SnippetTagDataTypeEnum;
|
||||||
|
export const SnippetTagValueTypeEnum = Constants.SnippetTagValueTypeEnum;
|
||||||
|
export const SnippetValueModifierTypeEnum =
|
||||||
|
Constants.SnippetValueModifierTypeEnum;
|
||||||
|
export const SourceTypeEnum = Constants.SourceTypeEnum;
|
||||||
|
export const SpeedMovementKeyEnum = Constants.SpeedMovementKeyEnum;
|
||||||
|
export const SpellConditionTypeEnum = Constants.SpellConditionTypeEnum;
|
||||||
|
export const SpellDurationTypeEnum = Constants.SpellDurationTypeEnum;
|
||||||
|
export const SpellGroupEnum = Constants.SpellGroupEnum;
|
||||||
|
export const SpellPrepareTypeEnum = Constants.SpellPrepareTypeEnum;
|
||||||
|
export const SpellRangeTypeEnum = Constants.SpellRangeTypeEnum;
|
||||||
|
export const SpellRangeTypeNameEnum = Constants.SpellRangeTypeNameEnum;
|
||||||
|
export const SpellScaleTypeNameEnum = Constants.SpellScaleTypeNameEnum;
|
||||||
|
export const StartingEquipmentRuleTypeEnum =
|
||||||
|
Constants.StartingEquipmentRuleTypeEnum;
|
||||||
|
export const StartingEquipmentTypeEnum = Constants.StartingEquipmentTypeEnum;
|
||||||
|
export const StatBlockTypeEnum = Constants.StatBlockTypeEnum;
|
||||||
|
export const StealthCheckTypeEnum = Constants.StealthCheckTypeEnum;
|
||||||
|
export const TraitTypeEnum = Constants.TraitTypeEnum;
|
||||||
|
export const UsableDiceAdjustmentTypeEnum =
|
||||||
|
Constants.UsableDiceAdjustmentTypeEnum;
|
||||||
|
export const VehicleComponentGroupTypeEnum =
|
||||||
|
Constants.VehicleComponentGroupTypeEnum;
|
||||||
|
export const VehicleConfigurationDisplayTypeEnum =
|
||||||
|
Constants.VehicleConfigurationDisplayTypeEnum;
|
||||||
|
export const VehicleConfigurationKeyEnum =
|
||||||
|
Constants.VehicleConfigurationKeyEnum;
|
||||||
|
export const VehicleConfigurationPrimaryComponentManageTypeEnum =
|
||||||
|
Constants.VehicleConfigurationPrimaryComponentManageTypeEnum;
|
||||||
|
export const VehicleConfigurationSizeTypeEnum =
|
||||||
|
Constants.VehicleConfigurationSizeTypeEnum;
|
||||||
|
export const WeaponCategoryEnum = Constants.WeaponCategoryEnum;
|
||||||
|
export const WeaponPropertyEnum = Constants.WeaponPropertyEnum;
|
||||||
|
export const WeaponTypeEnum = Constants.WeaponTypeEnum;
|
||||||
|
export const WeightSpeedTypeEnum = Constants.WeightSpeedTypeEnum;
|
||||||
|
export const WeightTypeEnum = Constants.WeightTypeEnum;
|
||||||
|
|
||||||
|
export const DefaultCharacterName = Constants.DefaultCharacterName;
|
21
ddb_main/contexts/Authentication/Authentication.tsx
Normal file
21
ddb_main/contexts/Authentication/Authentication.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
import useUser from "~/hooks/useUser";
|
||||||
|
import type { AppUserState } from "~/hooks/useUser";
|
||||||
|
|
||||||
|
export let AuthContext = createContext<AppUserState>(undefined);
|
||||||
|
|
||||||
|
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const user = useUser();
|
||||||
|
|
||||||
|
if (user === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <AuthContext.Provider value={user}>{children}</AuthContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
return useContext(AuthContext);
|
||||||
|
};
|
85
ddb_main/contexts/CharacterTheme/CharacterTheme.tsx
Normal file
85
ddb_main/contexts/CharacterTheme/CharacterTheme.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { useContext, useEffect } from "react";
|
||||||
|
import { createContext } from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
import {
|
||||||
|
rulesEngineSelectors,
|
||||||
|
CharacterTheme,
|
||||||
|
} from "@dndbeyond/character-rules-engine";
|
||||||
|
|
||||||
|
export let CharacterThemeContext = createContext<CharacterTheme>({
|
||||||
|
isDefault: false,
|
||||||
|
themeColorId: 0,
|
||||||
|
name: "",
|
||||||
|
themeColor: "",
|
||||||
|
backgroundColor: "",
|
||||||
|
isDarkMode: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CharacterThemeProvider = ({ children }) => {
|
||||||
|
const {
|
||||||
|
isDefault,
|
||||||
|
themeColorId,
|
||||||
|
name,
|
||||||
|
themeColor,
|
||||||
|
backgroundColor,
|
||||||
|
isDarkMode,
|
||||||
|
} = useSelector(rulesEngineSelectors.getCharacterTheme);
|
||||||
|
|
||||||
|
const solidBackground =
|
||||||
|
backgroundColor.length > 7 ? backgroundColor.slice(0, -2) : backgroundColor;
|
||||||
|
|
||||||
|
const isThemeColorDark = () => {
|
||||||
|
// Remove #
|
||||||
|
const c = themeColor.substring(1);
|
||||||
|
// convert rrggbb to decimal
|
||||||
|
const rgb = parseInt(c, 16);
|
||||||
|
const r = (rgb >> 16) & 0xff;
|
||||||
|
const g = (rgb >> 8) & 0xff;
|
||||||
|
const b = (rgb >> 0) & 0xff;
|
||||||
|
// Get luma from rgb colors
|
||||||
|
const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||||
|
return luma < 40;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.dataset.theme = isDarkMode ? "dark" : "light";
|
||||||
|
}, [isDarkMode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CharacterThemeContext.Provider
|
||||||
|
value={{
|
||||||
|
isDefault,
|
||||||
|
themeColorId,
|
||||||
|
name,
|
||||||
|
themeColor,
|
||||||
|
backgroundColor,
|
||||||
|
isDarkMode,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<style
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
:root {
|
||||||
|
--theme-color: ${themeColor};
|
||||||
|
--theme-background: ${backgroundColor};
|
||||||
|
--theme-background-solid: ${solidBackground};
|
||||||
|
--theme-contrast: var(--ttui_grey-${isDarkMode ? 50 : 900});
|
||||||
|
|
||||||
|
--theme-transparent: color-mix(in srgb, var(--theme-color), transparent 60%);
|
||||||
|
|
||||||
|
/* Update the character muted color for readability on dark mode */
|
||||||
|
${
|
||||||
|
isDarkMode ? "--character-muted-color: var(--ttui_grey-400);" : ""
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</CharacterThemeContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCharacterTheme = () => {
|
||||||
|
return useContext(CharacterThemeContext);
|
||||||
|
};
|
84
ddb_main/contexts/FeatureFlag/FeatureFlag.tsx
Normal file
84
ddb_main/contexts/FeatureFlag/FeatureFlag.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { FC, createContext, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { toCamelCase } from "~/helpers/casing";
|
||||||
|
import { getFeatureFlagsFromCharacterService } from "~/helpers/characterServiceApi";
|
||||||
|
|
||||||
|
const featureFlagsToGet = [
|
||||||
|
"release-gate-ims",
|
||||||
|
"release-gate-gfs-blessings-ui",
|
||||||
|
"release-gate-character-sheet-tour",
|
||||||
|
"release-gate-premade-characters",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const defaultFlags = {
|
||||||
|
imsFlag: false,
|
||||||
|
gfsBlessingsUiFlag: false,
|
||||||
|
characterSheetTourFlag: false,
|
||||||
|
premadeCharactersFlag: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FeatureFlags = typeof defaultFlags;
|
||||||
|
|
||||||
|
export interface FeatureFlagContextType extends FeatureFlags {
|
||||||
|
featureFlags: FeatureFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeatureFlagContext = createContext<FeatureFlagContextType>(null!);
|
||||||
|
|
||||||
|
interface ProviderProps {
|
||||||
|
defaultOverrides?: Partial<FeatureFlags>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeatureFlagProvider: FC<ProviderProps> = ({
|
||||||
|
children,
|
||||||
|
defaultOverrides,
|
||||||
|
}) => {
|
||||||
|
const [featureFlags, setFeatureFlags] = useState({
|
||||||
|
...defaultFlags,
|
||||||
|
...defaultOverrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
const convertFlagsToKeys = (flags: Object) => {
|
||||||
|
return Object.keys(flags).reduce((acc, flag) => {
|
||||||
|
// Perform any manipulations to remove unwanted characters
|
||||||
|
const formattedFlag = flag
|
||||||
|
// Remove `character-app` from the flag name
|
||||||
|
.replace(/character-app\./i, "")
|
||||||
|
// Remove `release-gate` from the flag name
|
||||||
|
.replace(/release-gate\W/i, "")
|
||||||
|
// Remove `character-sheet` from flag name if not the tour flag
|
||||||
|
.replace(/character-sheet\W(?!tour)/i, "")
|
||||||
|
// Change `2d` to `tactical` since var can't start with number
|
||||||
|
.replace(/2d/i, "tactical");
|
||||||
|
|
||||||
|
const key = toCamelCase(`${formattedFlag}Flag`);
|
||||||
|
return { ...acc, [key]: flags[flag] };
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFeatureFlags = async () => {
|
||||||
|
try {
|
||||||
|
const req = await getFeatureFlagsFromCharacterService(featureFlagsToGet);
|
||||||
|
const res = await req.json();
|
||||||
|
const flags = convertFlagsToKeys(res.data) as FeatureFlags;
|
||||||
|
setFeatureFlags(flags);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!defaultOverrides) getFeatureFlags();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [defaultOverrides]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FeatureFlagContext.Provider value={{ ...featureFlags, featureFlags }}>
|
||||||
|
{children}
|
||||||
|
</FeatureFlagContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFeatureFlags = () => {
|
||||||
|
return useContext(FeatureFlagContext);
|
||||||
|
};
|
45
ddb_main/contexts/Filters/Filters.tsx
Normal file
45
ddb_main/contexts/Filters/Filters.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { createContext, FC, useContext, useState } from "react";
|
||||||
|
|
||||||
|
export interface FiltersContextType {
|
||||||
|
showItemTypes: boolean;
|
||||||
|
setShowItemTypes: (show: boolean) => void;
|
||||||
|
showItemSourceCategories: boolean;
|
||||||
|
setShowItemSourceCategories: (show: boolean) => void;
|
||||||
|
showSpellLevels: boolean;
|
||||||
|
setShowSpellLevels: (show: boolean) => void;
|
||||||
|
showSpellSourceCategories: boolean;
|
||||||
|
setShowSpellSourceCategories: (show: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FiltersContext = createContext<FiltersContextType>(null!);
|
||||||
|
|
||||||
|
export const FiltersProvider: FC = ({ children }) => {
|
||||||
|
const [showItemTypes, setShowItemTypes] = useState(true);
|
||||||
|
const [showItemSourceCategories, setShowItemSourceCategories] =
|
||||||
|
useState(true);
|
||||||
|
|
||||||
|
const [showSpellLevels, setShowSpellLevels] = useState(true);
|
||||||
|
const [showSpellSourceCategories, setShowSpellSourceCategories] =
|
||||||
|
useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FiltersContext.Provider
|
||||||
|
value={{
|
||||||
|
showItemTypes,
|
||||||
|
setShowItemTypes,
|
||||||
|
showItemSourceCategories,
|
||||||
|
setShowItemSourceCategories,
|
||||||
|
showSpellLevels,
|
||||||
|
setShowSpellLevels,
|
||||||
|
showSpellSourceCategories,
|
||||||
|
setShowSpellSourceCategories,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</FiltersContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFiltersContext = () => {
|
||||||
|
return useContext(FiltersContext);
|
||||||
|
};
|
36
ddb_main/contexts/Head/Head.tsx
Normal file
36
ddb_main/contexts/Head/Head.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { createContext, useContext, useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
|
interface HeadContextProps {
|
||||||
|
title: string;
|
||||||
|
setTitle: (title: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HeadContext = createContext<HeadContextProps>(null!);
|
||||||
|
|
||||||
|
export const HeadContextProvider = ({ children }) => {
|
||||||
|
const [title, setTitle] = useState("Character App");
|
||||||
|
|
||||||
|
const setNewTitle = (title) => {
|
||||||
|
setTitle(`${title} - D&D Beyond`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HeadContext.Provider value={{ title, setTitle: setNewTitle }}>
|
||||||
|
<Helmet>
|
||||||
|
<title>{title}</title>
|
||||||
|
</Helmet>
|
||||||
|
{children}
|
||||||
|
</HeadContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useHeadContext = () => {
|
||||||
|
const context = useContext(HeadContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useHead must be used within a HeadProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
174
ddb_main/contexts/Sidebar/Sidebar.tsx
Normal file
174
ddb_main/contexts/Sidebar/Sidebar.tsx
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import { isEqual } from "lodash";
|
||||||
|
import { createContext, FC, useContext, useEffect, useState } from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
import {
|
||||||
|
PaneComponentEnum,
|
||||||
|
PaneComponentInfo,
|
||||||
|
PaneIdentifiers,
|
||||||
|
SidebarAlignmentEnum,
|
||||||
|
SidebarPlacementEnum,
|
||||||
|
} from "~/subApps/sheet/components/Sidebar/types";
|
||||||
|
import { SIDEBAR_FIXED_POSITION_START_WIDTH } from "~/tools/js/CharacterSheet/config";
|
||||||
|
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||||
|
|
||||||
|
export interface SidebarInfo {
|
||||||
|
isVisible: boolean;
|
||||||
|
setIsVisible: (isVisible: boolean) => void;
|
||||||
|
isLocked: boolean;
|
||||||
|
setIsLocked: (isLocked: boolean) => void;
|
||||||
|
placement: SidebarPlacementEnum;
|
||||||
|
setPlacement: (placement: SidebarPlacementEnum) => void;
|
||||||
|
alignment: SidebarAlignmentEnum;
|
||||||
|
setAlignment: (alignment: SidebarAlignmentEnum) => void;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaneInfo {
|
||||||
|
activePane: PaneComponentInfo | null;
|
||||||
|
showControls: boolean;
|
||||||
|
isAtStart: boolean;
|
||||||
|
isAtEnd: boolean;
|
||||||
|
paneHistoryStart: (
|
||||||
|
componentType: PaneComponentEnum,
|
||||||
|
componentIdentifiers?: PaneIdentifiers | null
|
||||||
|
) => void;
|
||||||
|
paneHistoryPush: (
|
||||||
|
componentType: PaneComponentEnum,
|
||||||
|
componentIdentifiers?: PaneIdentifiers | null
|
||||||
|
) => void;
|
||||||
|
paneHistoryPrevious: () => void;
|
||||||
|
paneHistoryNext: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SidebarContextType {
|
||||||
|
sidebar: SidebarInfo;
|
||||||
|
pane: PaneInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidebarContext = createContext<SidebarContextType>(null!);
|
||||||
|
|
||||||
|
export const SidebarProvider: FC = ({ children }) => {
|
||||||
|
const isMobile = useSelector(appEnvSelectors.getIsMobile);
|
||||||
|
const sheetDimensions = useSelector(appEnvSelectors.getDimensions);
|
||||||
|
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const [isLocked, setIsLocked] = useState(false);
|
||||||
|
const [placement, setPlacement] = useState(SidebarPlacementEnum.OVERLAY);
|
||||||
|
const [alignment, setAlignment] = useState(SidebarAlignmentEnum.RIGHT);
|
||||||
|
|
||||||
|
const [paneIdx, setPaneIdx] = useState<number>(0);
|
||||||
|
const [paneHistory, setPaneHistory] = useState<Array<PaneComponentInfo>>([]);
|
||||||
|
|
||||||
|
const [activePane, setActivePane] = useState<PaneComponentInfo | null>(null);
|
||||||
|
|
||||||
|
const width: number = 340;
|
||||||
|
|
||||||
|
//Create a new pane history
|
||||||
|
const paneHistoryStart = (
|
||||||
|
type: PaneComponentEnum,
|
||||||
|
identifiers: PaneIdentifiers | null = null
|
||||||
|
) => {
|
||||||
|
setIsVisible(true);
|
||||||
|
setPaneIdx(0);
|
||||||
|
setPaneHistory([
|
||||||
|
{
|
||||||
|
type,
|
||||||
|
identifiers,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
//Add a new pane to the history
|
||||||
|
const paneHistoryPush = (
|
||||||
|
type: PaneComponentEnum,
|
||||||
|
identifiers: PaneIdentifiers | null = null
|
||||||
|
) => {
|
||||||
|
if (paneIdx === null) {
|
||||||
|
paneHistoryStart(type, identifiers);
|
||||||
|
} else {
|
||||||
|
// get current active pane (last in history)
|
||||||
|
const activeHistoryElement = {
|
||||||
|
identifiers: activePane?.identifiers,
|
||||||
|
type: activePane?.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
// create a new node for the history
|
||||||
|
const newHistoryElement = { identifiers, type };
|
||||||
|
|
||||||
|
// compare the current active pane with the new one (last element in history with new one)
|
||||||
|
if (!isEqual(activeHistoryElement, newHistoryElement)) {
|
||||||
|
// Check if new history element is different than current - otherwise skip to avoid duplicates
|
||||||
|
let newHistory = [
|
||||||
|
...paneHistory.slice(0, paneIdx + 1),
|
||||||
|
newHistoryElement,
|
||||||
|
];
|
||||||
|
setPaneHistory(newHistory);
|
||||||
|
setPaneIdx(newHistory.length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
//Go back to the previous pane in the history
|
||||||
|
const paneHistoryPrevious = () => {
|
||||||
|
setPaneIdx(Math.max(0, paneIdx - 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
//Go to the next pane in the history
|
||||||
|
const paneHistoryNext = () => {
|
||||||
|
setPaneIdx(Math.min(paneHistory.length - 1, paneIdx + 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//If the window less than the fixed position start width (1600px), set the placement to overlay
|
||||||
|
if (
|
||||||
|
isMobile ||
|
||||||
|
sheetDimensions.window.width < SIDEBAR_FIXED_POSITION_START_WIDTH
|
||||||
|
) {
|
||||||
|
setPlacement(SidebarPlacementEnum.OVERLAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
//set Right alignment if mobile
|
||||||
|
setAlignment(isMobile ? SidebarAlignmentEnum.RIGHT : alignment);
|
||||||
|
}, [isMobile, sheetDimensions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const activePane = paneHistory[paneIdx];
|
||||||
|
setActivePane(activePane);
|
||||||
|
}, [paneIdx, paneHistory]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarContext.Provider
|
||||||
|
value={{
|
||||||
|
sidebar: {
|
||||||
|
isVisible,
|
||||||
|
setIsVisible,
|
||||||
|
isLocked,
|
||||||
|
setIsLocked,
|
||||||
|
placement,
|
||||||
|
setPlacement,
|
||||||
|
alignment,
|
||||||
|
setAlignment,
|
||||||
|
width,
|
||||||
|
},
|
||||||
|
pane: {
|
||||||
|
activePane,
|
||||||
|
showControls: paneHistory.length > 1,
|
||||||
|
isAtStart: paneIdx === 0,
|
||||||
|
isAtEnd: paneIdx === paneHistory.length - 1,
|
||||||
|
paneHistoryStart,
|
||||||
|
paneHistoryPush,
|
||||||
|
paneHistoryPrevious,
|
||||||
|
paneHistoryNext,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SidebarContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSidebar = () => {
|
||||||
|
return useContext(SidebarContext);
|
||||||
|
};
|
88
ddb_main/contexts/ThemeManager/ThemeManager.tsx
Normal file
88
ddb_main/contexts/ThemeManager/ThemeManager.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import createCache from "@emotion/cache";
|
||||||
|
import { CacheProvider } from "@emotion/react";
|
||||||
|
import { ThemeProvider } from "@mui/material/styles";
|
||||||
|
import { useState, createContext, useEffect, useMemo } from "react";
|
||||||
|
import { prefixer } from "stylis";
|
||||||
|
|
||||||
|
import { getTheme } from "~/theme";
|
||||||
|
|
||||||
|
type PaletteOpt = "light" | "dark";
|
||||||
|
|
||||||
|
interface Manager {
|
||||||
|
lightOrDark: PaletteOpt;
|
||||||
|
primary?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ThemeManagerContext {
|
||||||
|
manager: Manager;
|
||||||
|
setManager: (manager: Manager) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThemeManager = createContext<ThemeManagerContext>(null!);
|
||||||
|
export const ThemeManagerProvider = ({
|
||||||
|
children,
|
||||||
|
manager: propsManager,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
manager?: Manager;
|
||||||
|
}) => {
|
||||||
|
const [manager, setManager] = useState<Manager>(
|
||||||
|
propsManager || {
|
||||||
|
lightOrDark: "light",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (propsManager) {
|
||||||
|
setManager(propsManager);
|
||||||
|
}
|
||||||
|
}, [propsManager]);
|
||||||
|
// HACK
|
||||||
|
/**
|
||||||
|
* We are doing some hacky things to
|
||||||
|
* win is styling conflicts this can
|
||||||
|
* go away once we no longer need
|
||||||
|
* waterdeep styles
|
||||||
|
*/
|
||||||
|
function hack__createExtraScopePlugin(...extra) {
|
||||||
|
const scopes = extra.map((scope) => `${scope.trim()} `);
|
||||||
|
return (element) => {
|
||||||
|
if (element.type !== "rule") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.root?.type === "@keyframes") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.props = element.props
|
||||||
|
.map((prop) => scopes.map((scope) => scope + prop))
|
||||||
|
.reduce((scopesArray, scope) => scopesArray.concat(scope), []);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hack__cache = useMemo(
|
||||||
|
() =>
|
||||||
|
createCache({
|
||||||
|
key: "ddb-character-app",
|
||||||
|
container: document.body,
|
||||||
|
prepend: true,
|
||||||
|
stylisPlugins: [
|
||||||
|
hack__createExtraScopePlugin("body"),
|
||||||
|
// has to be included manually when customizing `stylisPlugins` if you want to have vendor prefixes added automatically
|
||||||
|
prefixer,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
/**
|
||||||
|
* End the hack
|
||||||
|
*/
|
||||||
|
const theme = getTheme(manager.lightOrDark, manager.primary);
|
||||||
|
return (
|
||||||
|
<ThemeManager.Provider value={{ manager, setManager }}>
|
||||||
|
<CacheProvider value={hack__cache}>
|
||||||
|
<ThemeProvider theme={theme}>{children}</ThemeProvider>
|
||||||
|
</CacheProvider>
|
||||||
|
</ThemeManager.Provider>
|
||||||
|
);
|
||||||
|
};
|
50
ddb_main/handlers/commonHandlers.ts
Normal file
50
ddb_main/handlers/commonHandlers.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Dispatch } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Action,
|
||||||
|
ActionUtils,
|
||||||
|
characterActions,
|
||||||
|
Spell,
|
||||||
|
SpellUtils,
|
||||||
|
} from "@dndbeyond/character-rules-engine/es";
|
||||||
|
|
||||||
|
export const handleActionUseSet = (
|
||||||
|
action: Action,
|
||||||
|
uses: number,
|
||||||
|
dispatch: Dispatch<any>
|
||||||
|
): void => {
|
||||||
|
const id = ActionUtils.getId(action);
|
||||||
|
const entityTypeId = ActionUtils.getEntityTypeId(action);
|
||||||
|
const dataOriginType = ActionUtils.getDataOriginType(action);
|
||||||
|
|
||||||
|
if (id !== null && entityTypeId !== null) {
|
||||||
|
dispatch(
|
||||||
|
characterActions.actionUseSet(id, entityTypeId, uses, dataOriginType)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleSpellUseSet = (
|
||||||
|
spell: Spell,
|
||||||
|
uses: number,
|
||||||
|
dispatch: Dispatch<any>
|
||||||
|
): void => {
|
||||||
|
const mappingId = SpellUtils.getMappingId(spell);
|
||||||
|
const mappingEntityTypeId = SpellUtils.getMappingEntityTypeId(spell);
|
||||||
|
const dataOriginType = SpellUtils.getDataOriginType(spell);
|
||||||
|
|
||||||
|
if (
|
||||||
|
mappingId !== null &&
|
||||||
|
mappingEntityTypeId !== null &&
|
||||||
|
dataOriginType !== null
|
||||||
|
) {
|
||||||
|
dispatch(
|
||||||
|
characterActions.spellUseSet(
|
||||||
|
mappingId,
|
||||||
|
mappingEntityTypeId,
|
||||||
|
uses,
|
||||||
|
dataOriginType
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
343
ddb_main/helpers/analytics/analytics.ts
Normal file
343
ddb_main/helpers/analytics/analytics.ts
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
// GA Docs for using multiple accounts on the same page
|
||||||
|
// https://developers.google.com/analytics/devguides/collection/analyticsjs/creating-trackers#working_with_multiple_trackers
|
||||||
|
import debounce from "debounce";
|
||||||
|
|
||||||
|
import config from "../../config";
|
||||||
|
import {
|
||||||
|
SessionTrackingIdByName,
|
||||||
|
SessionNames,
|
||||||
|
EventCategories,
|
||||||
|
EventActions,
|
||||||
|
EventLabels,
|
||||||
|
} from "../../constants";
|
||||||
|
import {
|
||||||
|
getSearchableTerms,
|
||||||
|
getSearchableTermsAnalyticsLabel,
|
||||||
|
} from "../../state/selectors/characterUtils";
|
||||||
|
import { CharacterData, LogEventOptions } from "../../types";
|
||||||
|
|
||||||
|
let { debug } = config;
|
||||||
|
|
||||||
|
const tryGa = (...params) => {
|
||||||
|
if (typeof window.ga === "function") {
|
||||||
|
window.ga(...params);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line max-params
|
||||||
|
const logGa = (sessionName: string, eventName: string, ...rest: any) =>
|
||||||
|
tryGa(`${sessionName}.send`, eventName, ...rest);
|
||||||
|
|
||||||
|
export const logEvent = (
|
||||||
|
sessionName: string,
|
||||||
|
{ category, action, label, value }: LogEventOptions
|
||||||
|
) => {
|
||||||
|
const success = logGa(sessionName, "event", category, action, label, value);
|
||||||
|
|
||||||
|
if (success && debug) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
"Logged analytics event:",
|
||||||
|
"\nsession:",
|
||||||
|
sessionName,
|
||||||
|
"\ncategory:",
|
||||||
|
category,
|
||||||
|
"\naction:",
|
||||||
|
action,
|
||||||
|
"\nlabel:",
|
||||||
|
label,
|
||||||
|
"\nvalue:",
|
||||||
|
value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line max-params
|
||||||
|
export const logPageView = (sessionName, pageName, ...rest) => {
|
||||||
|
tryGa(`${sessionName}.set`, "page", pageName, ...rest);
|
||||||
|
const success = tryGa(`${sessionName}.send`, "pageview");
|
||||||
|
|
||||||
|
if (success && debug) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
"Logged page view:",
|
||||||
|
"\nsession:",
|
||||||
|
sessionName,
|
||||||
|
"\npage:",
|
||||||
|
pageName,
|
||||||
|
"\nrest:",
|
||||||
|
...rest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initAnalytics = (sessionName: string, forceDebug?: boolean) => {
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions, no-sequences */
|
||||||
|
(function (
|
||||||
|
i: Window,
|
||||||
|
s: Document,
|
||||||
|
o: string,
|
||||||
|
g: string,
|
||||||
|
r: string,
|
||||||
|
a?: HTMLScriptElement,
|
||||||
|
m?: Element
|
||||||
|
) {
|
||||||
|
i["GoogleAnalyticsObject"] = r;
|
||||||
|
(i[r] =
|
||||||
|
i[r] ||
|
||||||
|
function () {
|
||||||
|
(i[r].q = i[r].q || []).push(arguments);
|
||||||
|
}),
|
||||||
|
(i[r].l = 1 * (new Date() as any));
|
||||||
|
(a = s.createElement(o) as HTMLScriptElement),
|
||||||
|
(m = s.getElementsByTagName(o)[0]);
|
||||||
|
a.async = true;
|
||||||
|
a.src = g;
|
||||||
|
m.parentNode?.insertBefore(a, m);
|
||||||
|
})(
|
||||||
|
window,
|
||||||
|
document,
|
||||||
|
"script",
|
||||||
|
"https://www.google-analytics.com/analytics.js",
|
||||||
|
"ga"
|
||||||
|
);
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
tryGa("create", SessionTrackingIdByName[sessionName], "auto", sessionName);
|
||||||
|
|
||||||
|
if (forceDebug) {
|
||||||
|
debug = forceDebug;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an array to a string that can be used for an event label
|
||||||
|
* @param {array} array The array to convert
|
||||||
|
* @param {object} valueToLabelMap An optional value to label mapping object
|
||||||
|
* @returns {string} A string representation of the array
|
||||||
|
*/
|
||||||
|
export const arrayToLabel = (array, valueToLabelMap = {}) =>
|
||||||
|
array
|
||||||
|
.sort()
|
||||||
|
.map((value) => valueToLabelMap[value] || value)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduces an array of filter options ({ label, value }) to a lookup object of value to label
|
||||||
|
* @param {array} filterOptions The list of options to reduce
|
||||||
|
* @returns {object} A lookup object with filter values as the keys and filter labels as the values
|
||||||
|
*/
|
||||||
|
export const filterOptionsToLookup = (filterOptions = []) =>
|
||||||
|
filterOptions.reduce((lookup, { label, value }) => {
|
||||||
|
lookup[value] = label;
|
||||||
|
|
||||||
|
return lookup;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
export const logCharacterCampaignClicked = () => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Character,
|
||||||
|
action: EventActions.DDB_Character_Campaign,
|
||||||
|
label: EventLabels.Clicked,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logCharacterCopyCancelled = () => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Character,
|
||||||
|
action: EventActions.DDB_Character_Copy,
|
||||||
|
label: EventLabels.Cancelled,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logCharacterCopyClicked = () => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Character,
|
||||||
|
action: EventActions.DDB_Character_Copy,
|
||||||
|
label: EventLabels.Clicked,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logCharacterCopyConfirmed = () => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Character,
|
||||||
|
action: EventActions.DDB_Character_Copy,
|
||||||
|
label: EventLabels.Confirmed,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logCharacterDeleteCancelled = () => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Character,
|
||||||
|
action: EventActions.DDB_Character_Delete,
|
||||||
|
label: EventLabels.Cancelled,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logCharacterDeleteClicked = () => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Character,
|
||||||
|
action: EventActions.DDB_Character_Delete,
|
||||||
|
label: EventLabels.Clicked,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logCharacterDeleteConfirmed = () => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Character,
|
||||||
|
action: EventActions.DDB_Character_Delete,
|
||||||
|
label: EventLabels.Confirmed,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logCharacterEditClicked = () => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Character,
|
||||||
|
action: EventActions.DDB_Character_Edit,
|
||||||
|
label: EventLabels.Clicked,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logCharacterLeaveCampaignCancelled = () => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Character,
|
||||||
|
action: EventActions.DDB_Character_LeaveCampaign,
|
||||||
|
label: EventLabels.Cancelled,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logCharacterLeaveCampaignClicked = () => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Character,
|
||||||
|
action: EventActions.DDB_Character_LeaveCampaign,
|
||||||
|
label: EventLabels.Clicked,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logCharacterLeaveCampaignConfirmed = () => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Character,
|
||||||
|
action: EventActions.DDB_Character_LeaveCampaign,
|
||||||
|
label: EventLabels.Confirmed,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logCharacterViewClicked = () => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Character,
|
||||||
|
action: EventActions.DDB_Character_View,
|
||||||
|
label: EventLabels.Clicked,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logListingSearchChanged = debounce(
|
||||||
|
(characters: Array<CharacterData>, matchesSearch: () => any) => {
|
||||||
|
const matchedLabels = characters
|
||||||
|
.map(
|
||||||
|
(character) =>
|
||||||
|
getSearchableTermsAnalyticsLabel(character)[
|
||||||
|
getSearchableTerms(character).findIndex(matchesSearch)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter(
|
||||||
|
(character, index, characters) =>
|
||||||
|
characters.indexOf(character) === index
|
||||||
|
);
|
||||||
|
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_ListingFilter,
|
||||||
|
action: EventActions.DDB_ListingFilter_SearchChanged,
|
||||||
|
label: arrayToLabel(matchedLabels) || "no match",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
|
||||||
|
export const logListingSearchCleared = () => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_ListingFilter,
|
||||||
|
action: EventActions.DDB_ListingFilter_SearchCleared,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logListingSortChanged = (label) => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_ListingSort,
|
||||||
|
action: EventActions.DDB_ListingSort_Changed,
|
||||||
|
label,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logPlayerAppBannerDismissed = () => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_PlayerAppBanner,
|
||||||
|
action: EventActions.DDB_PlayerAppBanner_Dismissed,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logPlayerAppBannerClickedCta = (label) => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_PlayerAppBanner,
|
||||||
|
action: EventActions.DDB_PlayerAppBanner_ClickedCta,
|
||||||
|
label,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logUnlockCharacterLocked = () => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Unlock,
|
||||||
|
action: EventActions.DDB_Unlock_CharacterLocked,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logUnlockCharacterUnlocked = () => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Unlock,
|
||||||
|
action: EventActions.DDB_Unlock_CharacterUnlocked,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logUnlockFinishUnlockingClicked = (
|
||||||
|
unlockedCharacterCount,
|
||||||
|
maxSlots
|
||||||
|
) => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Unlock,
|
||||||
|
action: EventActions.DDB_Unlock_FinishUnlockingClicked,
|
||||||
|
label: `Unlocked Characters: ${unlockedCharacterCount} / ${maxSlots}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logUnlockFinishUnlockingCancelled = (
|
||||||
|
unlockedCharacterCount,
|
||||||
|
maxSlots
|
||||||
|
) => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Unlock,
|
||||||
|
action: EventActions.DDB_Unlock_FinishUnlockingCancelled,
|
||||||
|
label: `Unlocked Characters: ${unlockedCharacterCount} / ${maxSlots}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logUnlockFinishUnlockingConfirmed = (
|
||||||
|
unlockedCharacterCount,
|
||||||
|
maxSlots
|
||||||
|
) => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Unlock,
|
||||||
|
action: EventActions.DDB_Unlock_FinishUnlockingConfirmed,
|
||||||
|
label: `Unlocked Characters: ${unlockedCharacterCount} / ${maxSlots}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logUnlockSubscribeClicked = () => {
|
||||||
|
logEvent(SessionNames.DDB, {
|
||||||
|
category: EventCategories.DDB_Unlock,
|
||||||
|
action: EventActions.DDB_Unlock_SubscribeClicked,
|
||||||
|
});
|
||||||
|
};
|
11
ddb_main/helpers/casing/casing.ts
Normal file
11
ddb_main/helpers/casing/casing.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export const toCamelCase = (str: string) =>
|
||||||
|
str
|
||||||
|
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
|
||||||
|
return index === 0 ? word.toLowerCase() : word.toUpperCase();
|
||||||
|
})
|
||||||
|
.replace(/\W/g, "");
|
||||||
|
|
||||||
|
export const toPascalCase = (str: string) =>
|
||||||
|
str
|
||||||
|
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => word.toUpperCase())
|
||||||
|
.replace(/\W/g, "");
|
189
ddb_main/helpers/characterServiceApi.ts
Normal file
189
ddb_main/helpers/characterServiceApi.ts
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import { CharacterData, UserPreferences } from "~/types";
|
||||||
|
|
||||||
|
import config, {
|
||||||
|
characterServiceBaseUrl,
|
||||||
|
ddbBaseUrl,
|
||||||
|
userServiceBaseUrl,
|
||||||
|
} from "../config";
|
||||||
|
import { getId, getCampaignId } from "../state/selectors/characterUtils";
|
||||||
|
import { applyQueriesClientSide, queryListToMap } from "./queryUtils";
|
||||||
|
import { summon } from "./summon";
|
||||||
|
import { getUserId } from "./userApi";
|
||||||
|
|
||||||
|
export const getCharacters = async () => {
|
||||||
|
try {
|
||||||
|
const userId = await getUserId();
|
||||||
|
const req = await summon(
|
||||||
|
`${characterServiceBaseUrl}/characters/list?userId=${userId}`
|
||||||
|
);
|
||||||
|
return req.json();
|
||||||
|
} catch {
|
||||||
|
throw new Error("Non-OK response from GET characters");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getItems = async ({ queries }) => {
|
||||||
|
const characters = await getCharacters();
|
||||||
|
applyQueriesClientSide(queryListToMap(queries));
|
||||||
|
|
||||||
|
return new Promise((resolve) =>
|
||||||
|
resolve({
|
||||||
|
json: () => new Promise((resolve2) => resolve2(characters)),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const copyCharacter = (character: CharacterData) => {
|
||||||
|
try {
|
||||||
|
const characterId = getId(character);
|
||||||
|
|
||||||
|
return summon(`${characterServiceBaseUrl}/character/copy`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ characterId }),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const claimCharacter = (characterId: number, isAssigned: boolean) => {
|
||||||
|
try {
|
||||||
|
return summon(`${characterServiceBaseUrl}/premade/claim`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ characterId, isAssigned }),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCharacterSlots = async () => {
|
||||||
|
try {
|
||||||
|
const req = await summon(`${config.userServiceBaseUrl}/slot-limit`);
|
||||||
|
const res = await req.json();
|
||||||
|
return res.data;
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteCharacter = (character: CharacterData) => {
|
||||||
|
try {
|
||||||
|
const characterId = getId(character);
|
||||||
|
|
||||||
|
return summon(`${characterServiceBaseUrl}/character`, {
|
||||||
|
method: "DELETE",
|
||||||
|
body: JSON.stringify({ characterId }),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const leaveCampaign = (character: CharacterData) => {
|
||||||
|
try {
|
||||||
|
const campaignId = getCampaignId(character);
|
||||||
|
const characterId = getId(character);
|
||||||
|
|
||||||
|
return summon(
|
||||||
|
`${ddbBaseUrl}/api/campaign/${campaignId}/character/${characterId}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const joinCampaign = (campaignJoinCode: string, characterId: number) => {
|
||||||
|
try {
|
||||||
|
return summon(`${ddbBaseUrl}/api/campaign/join/${campaignJoinCode}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ characterId }),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const unlockCharacters = (characterIds: Array<number>) => {
|
||||||
|
try {
|
||||||
|
return summon(`${userServiceBaseUrl}/unlock`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ characterIds }),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const activateCharacter = (character: CharacterData) => {
|
||||||
|
try {
|
||||||
|
const characterId = getId(character);
|
||||||
|
|
||||||
|
return summon(
|
||||||
|
`${characterServiceBaseUrl}/characters/${characterId}/status/activate`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFeatureFlagsFromCharacterService = (flags: Array<string>) => {
|
||||||
|
try {
|
||||||
|
return summon(`${characterServiceBaseUrl}/featureflag`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ flags }),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all sources that the user is able to use.
|
||||||
|
*/
|
||||||
|
export const getAllEntitledSources = (campaignId?: number) => {
|
||||||
|
try {
|
||||||
|
return summon(
|
||||||
|
`${userServiceBaseUrl}/entitled-sources${
|
||||||
|
campaignId ? `?campaignId=${campaignId}` : ""
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserPreferences = () => {
|
||||||
|
try {
|
||||||
|
return summon(`${config.userServiceBaseUrl}/preferences`);
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateUserPreferences = (preferences: UserPreferences) => {
|
||||||
|
try {
|
||||||
|
return summon(`${config.userServiceBaseUrl}/preferences`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(preferences),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRulesData = () => {
|
||||||
|
try {
|
||||||
|
return summon(`${characterServiceBaseUrl}/rule-data`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
52
ddb_main/helpers/generateCharacterPreferences.ts
Normal file
52
ddb_main/helpers/generateCharacterPreferences.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
MovementTypeEnum,
|
||||||
|
PreferenceAbilityScoreDisplayTypeEnum as AbilityScoreDisplayType,
|
||||||
|
PreferenceEncumbranceTypeEnum,
|
||||||
|
PreferenceHitPointTypeEnum,
|
||||||
|
PreferencePrivacyTypeEnum,
|
||||||
|
PreferenceProgressionTypeEnum,
|
||||||
|
PreferenceSharingTypeEnum,
|
||||||
|
SenseTypeEnum,
|
||||||
|
} from "~/constants";
|
||||||
|
import { FeatureFlags } from "~/contexts/FeatureFlag";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a copy of the `generateCharacterPreferences` function from the rules
|
||||||
|
* engine package. This is necessary because we have restructured the
|
||||||
|
* featureFlagContext, so the `featureFlagInfo` variable is no longer
|
||||||
|
* available.
|
||||||
|
**/
|
||||||
|
export const generateCharacterPreferences = (featureFlags: FeatureFlags) => {
|
||||||
|
// Default values
|
||||||
|
const abilityScoreDisplayType = AbilityScoreDisplayType.MODIFIERS_TOP;
|
||||||
|
const encumbranceType = PreferenceEncumbranceTypeEnum.ENCUMBRANCE;
|
||||||
|
const hitPointType = PreferenceHitPointTypeEnum.FIXED;
|
||||||
|
const primaryMovement = MovementTypeEnum.WALK;
|
||||||
|
const primarySense = SenseTypeEnum.PASSIVE_PERCEPTION;
|
||||||
|
const progressionType = PreferenceProgressionTypeEnum.MILESTONE;
|
||||||
|
const sharingType = PreferenceSharingTypeEnum.LIMITED;
|
||||||
|
const privacyType = PreferencePrivacyTypeEnum.CAMPAIGN_ONLY;
|
||||||
|
|
||||||
|
return {
|
||||||
|
abilityScoreDisplayType,
|
||||||
|
enableContainerCurrency: false,
|
||||||
|
enableDarkMode: false,
|
||||||
|
enableOptionalClassFeatures: false,
|
||||||
|
enableOptionalOrigins: false,
|
||||||
|
encumbranceType,
|
||||||
|
enforceFeatRules: true,
|
||||||
|
enforceMulticlassRules: true,
|
||||||
|
hitPointType,
|
||||||
|
ignoreCoinWeight: true,
|
||||||
|
primaryMovement,
|
||||||
|
primarySense,
|
||||||
|
privacyType,
|
||||||
|
progressionType,
|
||||||
|
sharingType,
|
||||||
|
showCompanions: false,
|
||||||
|
showScaledSpells: true,
|
||||||
|
showUnarmedStrike: true,
|
||||||
|
showWildShape: false,
|
||||||
|
useHomebrewContent: true,
|
||||||
|
};
|
||||||
|
};
|
33
ddb_main/helpers/localStorageUtils.ts
Normal file
33
ddb_main/helpers/localStorageUtils.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
export const tryGet = (key: string) => {
|
||||||
|
try {
|
||||||
|
return window.localStorage.getItem(key);
|
||||||
|
} catch (exception) {
|
||||||
|
// If 3rd party cookies are turned off even though window.localStorage
|
||||||
|
// is accessible or local storage is full this will error
|
||||||
|
// https://github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trySet = (key: string, value: string) => {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(key, value);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (exception) {
|
||||||
|
// Safari can throw an exception when calling localStorage.setItem in a private browsing tab.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem#Exceptions
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tryRemove = (key: string) => {
|
||||||
|
try {
|
||||||
|
window.localStorage.removeItem(key);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (exception) {
|
||||||
|
// Get and trySet can throw exceptions, so this probably can too...
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
13
ddb_main/helpers/reportWebVitals.ts
Normal file
13
ddb_main/helpers/reportWebVitals.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ReportHandler } from "web-vitals";
|
||||||
|
|
||||||
|
export const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||||
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
|
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(onPerfEntry);
|
||||||
|
getFID(onPerfEntry);
|
||||||
|
getFCP(onPerfEntry);
|
||||||
|
getLCP(onPerfEntry);
|
||||||
|
getTTFB(onPerfEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
141
ddb_main/helpers/sortUtils.ts
Normal file
141
ddb_main/helpers/sortUtils.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { SortOrderEnum, SortTypeEnum, CharacterData } from "../types";
|
||||||
|
|
||||||
|
// Creates a sorting predicate to sort for some property
|
||||||
|
export const byProp =
|
||||||
|
(propSelector: Function) =>
|
||||||
|
(a: CharacterData, b: CharacterData): number => {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
|
||||||
|
const aProp = propSelector(a);
|
||||||
|
const bProp = propSelector(b);
|
||||||
|
|
||||||
|
if (aProp < bProp) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aProp > bProp) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Creates a value to be used for sorting
|
||||||
|
export const createSortValue = (
|
||||||
|
sortBy: SortTypeEnum,
|
||||||
|
sortOrder: SortOrderEnum
|
||||||
|
): string => {
|
||||||
|
return `${sortBy}-${sortOrder}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts an array of objects by a given key. If a drilled-down key is needed,
|
||||||
|
* simply provide the path to the key as a string (ex. "key.subkey") and the
|
||||||
|
* function will handle the rest.
|
||||||
|
**/
|
||||||
|
export const orderBy = (
|
||||||
|
arr: Array<any>,
|
||||||
|
key?: string,
|
||||||
|
sort: "asc" | "desc" = "asc"
|
||||||
|
) => {
|
||||||
|
const getKey = (obj: any) => {
|
||||||
|
if (key) {
|
||||||
|
if (key.includes(".")) {
|
||||||
|
const arr = key.split(".");
|
||||||
|
return arr.reduce((acc, curr) => {
|
||||||
|
return acc[curr];
|
||||||
|
}, obj);
|
||||||
|
} else {
|
||||||
|
return obj[key];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSort = (valA: object, valB: object) => {
|
||||||
|
// If a key is provided, sort by that
|
||||||
|
if (key) {
|
||||||
|
// Get values to compare
|
||||||
|
const a = getKey(valA);
|
||||||
|
const b = getKey(valB);
|
||||||
|
// If the values are strings, compare them alphabetically
|
||||||
|
if (a < b) return sort === "asc" ? -1 : 1;
|
||||||
|
if (b < a) return sort === "asc" ? 1 : -1;
|
||||||
|
}
|
||||||
|
// Otherwise, sort by the object itself
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
// Return a new array with the sorted values
|
||||||
|
return arr.concat().sort(handleSort);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Separates a single array of objects into two separate arrays based on
|
||||||
|
* whether or not they include a query or not. If a key is provided, the
|
||||||
|
* function will look for the query by that key. If the key is nested, it will
|
||||||
|
* handle that as well. The function will return both the items which match and
|
||||||
|
* the ones that don't as separate arrays to be used as needed.
|
||||||
|
**/
|
||||||
|
export const sortByMatch = (
|
||||||
|
arr: Array<any>,
|
||||||
|
query: string,
|
||||||
|
key: string = "name",
|
||||||
|
exact: boolean = false
|
||||||
|
) => {
|
||||||
|
// If a key is provided, drill down until that key is found
|
||||||
|
const getKey = (obj: any) => {
|
||||||
|
if (key) {
|
||||||
|
if (key.includes(".")) {
|
||||||
|
const arr = key.split(".");
|
||||||
|
return arr.reduce((acc, curr) => {
|
||||||
|
return acc[curr];
|
||||||
|
}, obj);
|
||||||
|
} else {
|
||||||
|
return obj[key];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create empty arrays to store the objects
|
||||||
|
let arrA: Array<any> = [];
|
||||||
|
let arrB: Array<any> = [];
|
||||||
|
|
||||||
|
// Loop through the provided array and separate the objects
|
||||||
|
arr.forEach((item) => {
|
||||||
|
if (exact) {
|
||||||
|
if (getKey(item) === query) {
|
||||||
|
arrA.push(item);
|
||||||
|
} else {
|
||||||
|
arrB.push(item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (getKey(item).includes(query)) {
|
||||||
|
arrA.push(item);
|
||||||
|
} else {
|
||||||
|
arrB.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return a new array with the sorted values
|
||||||
|
return [arrA, arrB];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sortObjectByKeys = (val: object, dir: "asc" | "desc" = "asc") => {
|
||||||
|
const sortFn = (a, b) => {
|
||||||
|
const numA = parseInt(a);
|
||||||
|
const numB = parseInt(b);
|
||||||
|
return dir === "asc" ? numA - numB : numB - numA;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sorted = Object.keys(val)
|
||||||
|
.sort(sortFn)
|
||||||
|
.reduce((obj, key) => {
|
||||||
|
obj[key] = val[key];
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return sorted;
|
||||||
|
};
|
39
ddb_main/helpers/summon.ts
Normal file
39
ddb_main/helpers/summon.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import AuthUtils from "@dndbeyond/authentication-lib-js";
|
||||||
|
|
||||||
|
import { getStt } from "./tokenUtils";
|
||||||
|
|
||||||
|
export const getAuthHeaders = AuthUtils.makeGetAuthorizationHeaders({
|
||||||
|
madeGetShortTermToken: getStt,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper around fetch that adds the Authorization header and anything else
|
||||||
|
* that may need to be included in every request. If withCookies is true, the
|
||||||
|
* request will add the credentials: "include" option. Otherwise, this function
|
||||||
|
* should work exactly like the native fetch function.
|
||||||
|
*
|
||||||
|
* See https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
|
||||||
|
*/
|
||||||
|
export const summon = async (
|
||||||
|
input: RequestInfo | URL,
|
||||||
|
init?: RequestInit,
|
||||||
|
withCookies?: boolean
|
||||||
|
) => {
|
||||||
|
// Get the Authorization headers from the auth token
|
||||||
|
const authHeaders = await getAuthHeaders();
|
||||||
|
// If withCookies is true, set the credentials to include
|
||||||
|
const credentials = withCookies && { credentials: "include" };
|
||||||
|
// If there is a body, set the Content-Type to application/json
|
||||||
|
const contentType = init?.body && { "Content-Type": "application/json" };
|
||||||
|
|
||||||
|
return window.fetch(input, {
|
||||||
|
...(credentials as { credentials: RequestCredentials } | undefined),
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
...authHeaders,
|
||||||
|
...init?.headers,
|
||||||
|
...contentType,
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
15
ddb_main/helpers/tokenUtils.ts
Normal file
15
ddb_main/helpers/tokenUtils.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import jwtDecode from "jwt-decode";
|
||||||
|
|
||||||
|
import { AuthUtils } from "@dndbeyond/authentication-lib-js";
|
||||||
|
|
||||||
|
import { authEndpoint as authUrl } from "../config";
|
||||||
|
|
||||||
|
export const getStt = AuthUtils.makeGetShortTermToken({
|
||||||
|
authUrl,
|
||||||
|
throwOnHttpStatusError: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getDecodedStt = async () => {
|
||||||
|
const stt = await getStt();
|
||||||
|
return stt ? jwtDecode(stt) : null;
|
||||||
|
};
|
36
ddb_main/helpers/userApi.ts
Normal file
36
ddb_main/helpers/userApi.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { UserUtils } from "@dndbeyond/authentication-lib-js";
|
||||||
|
|
||||||
|
import { getDecodedStt } from "./tokenUtils";
|
||||||
|
|
||||||
|
export const getUserToken = async () => {
|
||||||
|
const stt = await getDecodedStt();
|
||||||
|
return stt;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUser = async () => {
|
||||||
|
const token = await getUserToken();
|
||||||
|
return {
|
||||||
|
...UserUtils.jwtToUser(token),
|
||||||
|
displayName: token.displayName,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserDisplayName = async () => {
|
||||||
|
const user = await getUser();
|
||||||
|
return user.displayName;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserId = async () => {
|
||||||
|
const user = await getUser();
|
||||||
|
return user.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSubscriptionTier = async () => {
|
||||||
|
const user = await getUser();
|
||||||
|
return user.subscriptionTier;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRoles = async () => {
|
||||||
|
const user = await getUser();
|
||||||
|
return user.roles;
|
||||||
|
};
|
11
ddb_main/helpers/validation.ts
Normal file
11
ddb_main/helpers/validation.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* https://github.com/Microsoft/TypeScript/issues/16069#issuecomment-369374214
|
||||||
|
* Used to get around array filter function that errors because it doesn't recognize the filter
|
||||||
|
* is removing nulls
|
||||||
|
* @param input
|
||||||
|
*/
|
||||||
|
export const isNotNullOrUndefined = <T extends Object>(
|
||||||
|
input: null | undefined | T
|
||||||
|
): input is T => {
|
||||||
|
return input !== null;
|
||||||
|
};
|
25
ddb_main/hooks/useAbilities.ts
Normal file
25
ddb_main/hooks/useAbilities.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AbilityManager,
|
||||||
|
FeaturesManager,
|
||||||
|
} from "@dndbeyond/character-rules-engine/es";
|
||||||
|
|
||||||
|
import { AttributesManagerContext } from "~/tools/js/Shared/managers/AttributesManagerContext";
|
||||||
|
|
||||||
|
export function useAbilities() {
|
||||||
|
const { attributesManager } = useContext(AttributesManagerContext);
|
||||||
|
|
||||||
|
const [abilities, setAbilities] = useState<Array<AbilityManager>>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function onUpdate() {
|
||||||
|
const abilities = await attributesManager.getAbilities();
|
||||||
|
|
||||||
|
setAbilities(abilities);
|
||||||
|
}
|
||||||
|
return FeaturesManager.subscribeToUpdates({ onUpdate });
|
||||||
|
}, [attributesManager, setAbilities]);
|
||||||
|
|
||||||
|
return abilities;
|
||||||
|
}
|
48
ddb_main/hooks/useApiCall.ts
Normal file
48
ddb_main/hooks/useApiCall.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
|
const identity = (x) => x;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook that encapsulates loading/error state setting when calling an API promise
|
||||||
|
* Usage:
|
||||||
|
* const [
|
||||||
|
* doThing, // callback to invoke to initiate the API call
|
||||||
|
* isDoingThing, // boolean indicating if the API call is in progress
|
||||||
|
* errorDoingThing, // error object indicating if there was an error caught (whatever param is passed to the .catch() callback)
|
||||||
|
* ] = useApiCall(MyApiUtils.doThing);
|
||||||
|
* @param {function} apiFunc The API function to use for setting loading/error state - Must return a promise!
|
||||||
|
* @param {function} [onSuccess] Optional callback invoked if the apiFunc promise succeeds
|
||||||
|
* @returns {[function, boolean, any]} An array containing a callback to initiate the API call, a isLoading boolean, and an error object
|
||||||
|
*/
|
||||||
|
const useApiCall = (apiFunc, onSuccess = identity) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const makeCall = useCallback(
|
||||||
|
(...args) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
apiFunc(...args)
|
||||||
|
.then(onSuccess)
|
||||||
|
.then(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
})
|
||||||
|
|
||||||
|
.catch((err) => {
|
||||||
|
setIsLoading(false);
|
||||||
|
setError(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[apiFunc, onSuccess]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
makeCall as (...args: any[]) => void,
|
||||||
|
isLoading as boolean,
|
||||||
|
error as any,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useApiCall;
|
428
ddb_main/hooks/useCharacterEngine.ts
Normal file
428
ddb_main/hooks/useCharacterEngine.ts
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AbilityUtils as abilityUtils,
|
||||||
|
AccessUtils as accessUtils,
|
||||||
|
ActionUtils as actionUtils,
|
||||||
|
ActivationUtils as activationUtils,
|
||||||
|
ApiAdapterUtils as apiAdapterUtils,
|
||||||
|
BackgroundUtils as backgroundUtils,
|
||||||
|
CampaignUtils as campaignUtils,
|
||||||
|
CampaignSettingUtils as campaignSettingUtils,
|
||||||
|
CharacterUtils as characterUtils,
|
||||||
|
ChoiceUtils as choiceUtils,
|
||||||
|
ClassFeatureUtils as classFeatureUtils,
|
||||||
|
ClassUtils as classUtils,
|
||||||
|
ConditionUtils as conditionUtils,
|
||||||
|
ConditionLevelUtils as conditionLevelUtils,
|
||||||
|
ContainerUtils as containerUtils,
|
||||||
|
CoreUtils as coreUtils,
|
||||||
|
CreatureUtils as creatureUtils,
|
||||||
|
CreatureRuleUtils as creatureRuleUtils,
|
||||||
|
DataOriginUtils as dataOriginUtils,
|
||||||
|
DecorationUtils as decorationUtils,
|
||||||
|
DefinitionUtils as definitionUtils,
|
||||||
|
DefinitionPoolUtils as definitionPoolUtils,
|
||||||
|
DiceUtils as diceUtils,
|
||||||
|
DurationUtils as durationUtils,
|
||||||
|
EntityUtils as entityUtils,
|
||||||
|
ExtraUtils as extraUtils,
|
||||||
|
FeatUtils as featUtils,
|
||||||
|
FeatureFlagInfoUtils as featureFlagInfoUtils,
|
||||||
|
FeatureUtils as featureUtils,
|
||||||
|
FormatUtils as formatUtils,
|
||||||
|
HelperUtils as helperUtils,
|
||||||
|
InfusionChoiceUtils as infusionChoiceUtils,
|
||||||
|
InfusionUtils as infusionUtils,
|
||||||
|
ItemUtils as itemUtils,
|
||||||
|
KnownInfusionUtils as knownInfusionUtils,
|
||||||
|
LimitedUseUtils as limitedUseUtils,
|
||||||
|
ModifierUtils as modifierUtils,
|
||||||
|
NoteUtils as noteUtils,
|
||||||
|
OptionalClassFeatureUtils as optionalClassFeatureUtils,
|
||||||
|
OptionalOriginUtils as optionalOriginUtils,
|
||||||
|
OptionUtils as optionUtils,
|
||||||
|
OrganizationUtils as organizationUtils,
|
||||||
|
PdfUtils as pdfUtils,
|
||||||
|
PrerequisiteUtils as prerequisiteUtils,
|
||||||
|
RaceUtils as raceUtils,
|
||||||
|
RacialTraitUtils as racialTraitUtils,
|
||||||
|
RuleDataUtils as ruleDataUtils,
|
||||||
|
RuleDataPoolUtils as ruleDataPoolUtils,
|
||||||
|
SkillUtils as skillUtils,
|
||||||
|
SnippetUtils as snippetUtils,
|
||||||
|
SourceUtils as sourceUtils,
|
||||||
|
SpellUtils as spellUtils,
|
||||||
|
StartingEquipmentUtils as startingEquipmentUtils,
|
||||||
|
ValueUtils as valueUtils,
|
||||||
|
VehicleUtils as vehicleUtils,
|
||||||
|
VehicleComponentUtils as vehicleComponentUtils,
|
||||||
|
apiCreatorSelectors as api,
|
||||||
|
rulesEngineSelectors as s,
|
||||||
|
FeatList,
|
||||||
|
characterSelectors as char,
|
||||||
|
characterActions,
|
||||||
|
serviceDataSelectors as sds,
|
||||||
|
serviceDataActions,
|
||||||
|
} from "@dndbeyond/character-rules-engine";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that provides access to the character rules engine state without
|
||||||
|
* having to import rules engine all over the place in your components. This
|
||||||
|
* reduces touchpoints to the rules engine and makes it easier to refactor the
|
||||||
|
* rules engine in the future.
|
||||||
|
*/
|
||||||
|
export const useCharacterEngine = () => ({
|
||||||
|
// Character Actions
|
||||||
|
characterActions,
|
||||||
|
serviceDataActions,
|
||||||
|
// Character Selectors that aren't passed thru to Rules Engine Selectors
|
||||||
|
characterStatusSlug: useSelector(char.getStatusSlug),
|
||||||
|
// Rules Engine Selector Values
|
||||||
|
abilities: useSelector(s.getAbilities),
|
||||||
|
abilityLookup: useSelector(s.getAbilityLookup),
|
||||||
|
abilityKey: useSelector(s.getAbilityKeyLookup),
|
||||||
|
actionLookup: useSelector(s.getActionLookup),
|
||||||
|
acAdjustments: useSelector(s.getAcAdjustments),
|
||||||
|
acSuppliers: useSelector(s.getArmorClassSuppliers),
|
||||||
|
acTotal: useSelector(s.getAcTotal),
|
||||||
|
actions: useSelector(s.getActions),
|
||||||
|
activatables: useSelector(s.getActivatables),
|
||||||
|
activeCharacterSpells: useSelector(s.getActiveCharacterSpells),
|
||||||
|
activeClassSpells: useSelector(s.getActiveClassSpells),
|
||||||
|
activeConditions: useSelector(s.getActiveConditions),
|
||||||
|
activeGroupedImmunities: useSelector(s.getActiveGroupedImmunities),
|
||||||
|
activeGroupedResistances: useSelector(s.getActiveGroupedResistances),
|
||||||
|
activeGroupedVulnerabilities: useSelector(s.getActiveGroupedVulnerabilities),
|
||||||
|
activeImmunities: useSelector(s.getActiveImmunities),
|
||||||
|
activeResistances: useSelector(s.getActiveResistances),
|
||||||
|
activeVulnerabilities: useSelector(s.getActiveVulnerabilities),
|
||||||
|
activeSource: useSelector(s.getActiveSourceLookup),
|
||||||
|
activeSourceCategories: useSelector(s.getActiveSourceCategories),
|
||||||
|
activeSources: useSelector(s.getActiveSources),
|
||||||
|
activeSpellAttackList: useSelector(s.getActiveSpellAttackList),
|
||||||
|
advantageSavingThrowModifiers: useSelector(
|
||||||
|
s.getAdvantageSavingThrowModifiers
|
||||||
|
),
|
||||||
|
alignment: useSelector(s.getAlignment),
|
||||||
|
allInventoryItems: useSelector(s.getAllInventoryItems),
|
||||||
|
armorSpeedAdjustmentAmount: useSelector(s.getArmorSpeedAdjustmentAmount),
|
||||||
|
attacks: useSelector(s.getAttacks),
|
||||||
|
attacksPerActionInfo: useSelector(s.getAttacksPerActionInfo),
|
||||||
|
attunedItems: useSelector(s.getAttunedItems),
|
||||||
|
attunedItemCountMax: useSelector(s.getAttunedItemCountMax),
|
||||||
|
attunedSlots: useSelector(s.getAttunedSlots),
|
||||||
|
availableInfusionChoices: useSelector(s.getAvailableInfusionChoices),
|
||||||
|
availablePactMagicSlotLevels: useSelector(s.getAvailablePactMagicSlotLevels),
|
||||||
|
availableSpellSlotLevels: useSelector(s.getAvailableSpellSlotLevels),
|
||||||
|
backgroundChoice: useSelector(s.getBackgroundChoiceLookup),
|
||||||
|
backgroundInfo: useSelector(s.getBackgroundInfo),
|
||||||
|
backgroundModifier: useSelector(s.getBackgroundModifierLookup),
|
||||||
|
backgroundModifiers: useSelector(s.getBackgroundModifiers),
|
||||||
|
baseClass: useSelector(s.getBaseClassLookup),
|
||||||
|
baseFeat: useSelector(s.getBaseFeatLookup),
|
||||||
|
baseFeats: useSelector(s.getBaseFeats),
|
||||||
|
bonusSavingThrowModifiers: useSelector(s.getBonusSavingThrowModifiers),
|
||||||
|
campaign: useSelector(s.getCampaign),
|
||||||
|
campaignSettings: useSelector(sds.getCampaignSettings),
|
||||||
|
carryingCapacity: useSelector(s.getCarryCapacity),
|
||||||
|
castableSpellSlotLevels: useSelector(s.getCastableSpellSlotLevels),
|
||||||
|
castablePactMagicSlotLevels: useSelector(s.getCastablePactMagicSlotLevels),
|
||||||
|
characterConfig: useSelector(s.getCharacterConfiguration),
|
||||||
|
characterGender: useSelector(s.getGender),
|
||||||
|
characterId: useSelector(s.getId),
|
||||||
|
characterInventoryContainers: useSelector(s.getCharacterInventoryContainers),
|
||||||
|
characterName: useSelector(s.getName),
|
||||||
|
characterNotes: useSelector(s.getCharacterNotes),
|
||||||
|
characterSpells: useSelector(s.getCharacterSpells),
|
||||||
|
characterTheme: useSelector(s.getCharacterTheme),
|
||||||
|
characterTraits: useSelector(s.getCharacterTraits),
|
||||||
|
choiceInfo: useSelector(s.getChoiceInfo),
|
||||||
|
classAction: useSelector(s.getClassActionLookup),
|
||||||
|
classAlwaysKnownSpells: useSelector(sds.getClassAlwaysKnownSpells),
|
||||||
|
classAlwaysPreparedSpells: useSelector(sds.getClassAlwaysPreparedSpells),
|
||||||
|
classChoice: useSelector(s.getClassChoiceLookup),
|
||||||
|
classCreatureRules: useSelector(s.getClassCreatureRules),
|
||||||
|
classFeature: useSelector(s.getClassFeatureLookup),
|
||||||
|
classMappingId: useSelector(s.getClassMappingIdLookupByActiveId),
|
||||||
|
classModifier: useSelector(s.getClassModifierLookup),
|
||||||
|
classOption: useSelector(s.getClassOptionLookup),
|
||||||
|
classSpell: useSelector(s.getClassSpellLookup),
|
||||||
|
classSpells: useSelector(s.getClassSpells),
|
||||||
|
classSpellInfo: useSelector(s.getClassSpellInfoLookup),
|
||||||
|
classSpellLists: useSelector(s.getClassSpellLists),
|
||||||
|
classSpellListSpells: useSelector(s.getClassSpellListSpellsLookup),
|
||||||
|
classes: useSelector(s.getClasses),
|
||||||
|
classesModifiers: useSelector(s.getClassesModifiers),
|
||||||
|
combinedMaxSpellSlotLevel: useSelector(s.getCombinedMaxSpellSlotLevel),
|
||||||
|
combinedSpellSlots: useSelector(s.getCombinedSpellSlots),
|
||||||
|
conModifier: useSelector(s.getConModifier),
|
||||||
|
conditionImmunityData: useSelector(s.getConditionImmunityData),
|
||||||
|
conditionModifier: useSelector(s.getConditionModifierLookup),
|
||||||
|
conditionModifiers: useSelector(s.getConditionModifiers),
|
||||||
|
container: useSelector(s.getContainerLookup),
|
||||||
|
containerCoinWeight: useSelector(s.getCointainerCoinWeight),
|
||||||
|
containerItemWeight: useSelector(s.getContainerItemWeight),
|
||||||
|
containerLookup: useSelector(s.getContainerLookup),
|
||||||
|
creature: useSelector(s.getCreatureLookup),
|
||||||
|
creatureGroupRules: useSelector(s.getCreatureGroupRulesLookup),
|
||||||
|
creatureInfusion: useSelector(s.getCreatureInfusionLookup),
|
||||||
|
creatureOwnerData: useSelector(s.getCreatureOwnerData),
|
||||||
|
creatureRules: useSelector(s.getCreatureRules),
|
||||||
|
creatures: useSelector(s.getCreatures),
|
||||||
|
currentCarriedWeightSpeed: useSelector(s.getCurrentCarriedWeightSpeed),
|
||||||
|
currentCarriedWeightType: useSelector(s.getCurrentCarriedWeightType),
|
||||||
|
currentLevel: useSelector(s.getCurrentLevel),
|
||||||
|
currentXp: useSelector(s.getCurrentXp),
|
||||||
|
currentLevelRacialTraits: useSelector(s.getCurrentLevelRacialTraits),
|
||||||
|
customActions: useSelector(s.getCustomActions),
|
||||||
|
customConditionAdjustments: useSelector(s.getCustomConditionAdjustments),
|
||||||
|
customDamageAdjustments: useSelector(s.getCustomDamageAdjustments),
|
||||||
|
customItems: useSelector(s.getCustomItems),
|
||||||
|
customImmunityDamageAdjustments: useSelector(
|
||||||
|
s.getCustomImmunityDamageAdjustments
|
||||||
|
),
|
||||||
|
customResistanceDamageAdjustments: useSelector(
|
||||||
|
s.getCustomResistanceDamageAdjustments
|
||||||
|
),
|
||||||
|
customVulnerabilityDamageAdjustments: useSelector(
|
||||||
|
s.getCustomVulnerabilityDamageAdjustments
|
||||||
|
),
|
||||||
|
customSkills: useSelector(s.getCustomSkills),
|
||||||
|
customSkillProficiencies: useSelector(s.getCustomSkillProficiencies),
|
||||||
|
damageImmunityData: useSelector(s.getDamageImmunityData),
|
||||||
|
deathSaveInfo: useSelector(s.getDeathSaveInfo),
|
||||||
|
deathCause: useSelector(s.getDeathCause),
|
||||||
|
decorationInfo: useSelector(s.getDecorationInfo),
|
||||||
|
dedicatedWeaponEnabled: useSelector(s.getDedicatedWeaponEnabled),
|
||||||
|
definitionPool: useSelector(sds.getDefinitionPool),
|
||||||
|
dexModifier: useSelector(s.getDexModifier),
|
||||||
|
disadvantageSavingThrowModifiers: useSelector(
|
||||||
|
s.getDisadvantageSavingThrowModifiers
|
||||||
|
),
|
||||||
|
encumberedWeight: useSelector(s.getEncumberedWeight),
|
||||||
|
entityRestrictionData: useSelector(s.getEntityRestrictionData),
|
||||||
|
entityValueLookup: useSelector(s.getCharacterValueLookupByEntity),
|
||||||
|
equippedItems: useSelector(s.getEquippedItems),
|
||||||
|
experienceInfo: useSelector(s.getExperienceInfo),
|
||||||
|
expertiseModifiers: useSelector(s.getExpertiseModifiers),
|
||||||
|
extras: useSelector(s.getExtras),
|
||||||
|
featLookup: useSelector(s.getFeatLookup),
|
||||||
|
feats: useSelector(s.getFeats),
|
||||||
|
featAction: useSelector(s.getFeatActionLookup),
|
||||||
|
featCreatureRules: useSelector(s.getFeatCreatureRules),
|
||||||
|
featModifier: useSelector(s.getFeatModifierLookup),
|
||||||
|
featModifiers: useSelector(s.getFeatModifiers),
|
||||||
|
featOption: useSelector(s.getFeatOptionLookup),
|
||||||
|
featChoice: useSelector(s.getFeatChoiceLookup),
|
||||||
|
gearWeaponItems: useSelector(s.getGearWeaponItems),
|
||||||
|
globalBackgroundSpellListIds: useSelector(s.getGlobalBackgroundSpellListIds),
|
||||||
|
globalRaceSpellListIds: useSelector(s.getGlobalRaceSpellListIds),
|
||||||
|
globalSpellListIds: useSelector(s.getGlobalSpellListIds),
|
||||||
|
hasInitiativeAdvantage: useSelector(s.getHasInitiativeAdvantage),
|
||||||
|
hasMaxAttunedItems: useSelector(s.hasMaxAttunedItems),
|
||||||
|
hasSpells: useSelector(s.hasSpells),
|
||||||
|
heavilyEncumberedWeight: useSelector(s.getHeavilyEncumberedWeight),
|
||||||
|
hexWeaponEnabled: useSelector(s.getHexWeaponEnabled),
|
||||||
|
highestEquippedArmorAC: useSelector(s.getHighestAcEquippedArmor),
|
||||||
|
highestEquippedShieldAC: useSelector(s.getHighestAcEquippedShield),
|
||||||
|
hpInfo: useSelector(s.getHitPointInfo),
|
||||||
|
improvedPactWeaponEnabled: useSelector(s.getImprovedPactWeaponEnabled),
|
||||||
|
infusions: useSelector(s.getInfusions),
|
||||||
|
infusionChoices: useSelector(s.getInfusionChoices),
|
||||||
|
infusionChoiceLookup: useSelector(s.getInfusionChoiceLookup),
|
||||||
|
infusionChoiceInfusion: useSelector(s.getInfusionChoiceInfusionLookup),
|
||||||
|
infusionMappings: useSelector(sds.getInfusionsMappings),
|
||||||
|
innateNaturalActions: useSelector(s.getInnateNaturalActions),
|
||||||
|
inventory: useSelector(s.getInventory),
|
||||||
|
inventoryContainers: useSelector(s.getInventoryContainers),
|
||||||
|
inventoryLookup: useSelector(s.getInventoryLookup),
|
||||||
|
inventoryInfusion: useSelector(s.getInventoryInfusionLookup),
|
||||||
|
isSheetReady: useSelector(s.isCharacterSheetReady),
|
||||||
|
isDead: useSelector(s.isDead),
|
||||||
|
isMulticlassCharacter: useSelector(s.isMulticlassCharacter),
|
||||||
|
itemAttacks: useSelector(s.getItemAttacks),
|
||||||
|
itemModifier: useSelector(s.getItemModifierLookup),
|
||||||
|
itemSpell: useSelector(s.getItemSpellLookup),
|
||||||
|
kenseiModifiers: useSelector(s.getKenseiModifiers),
|
||||||
|
knownInfusion: useSelector(s.getKnownInfusionLookup),
|
||||||
|
knownInfusionByChoice: useSelector(s.getKnownInfusionLookupByChoiceKey),
|
||||||
|
knownInfusions: useSelector(s.getKnownInfusions),
|
||||||
|
knownInfusionMappings: useSelector(sds.getKnownInfusionsMappings),
|
||||||
|
knownReplicatedItems: useSelector(s.getKnownReplicatedItems),
|
||||||
|
languages: useSelector(s.getLanguages),
|
||||||
|
languageModifiers: useSelector(s.getLanguageModifiers),
|
||||||
|
levelSpells: useSelector(s.getLevelSpells),
|
||||||
|
lifestyle: useSelector(s.getLifestyle),
|
||||||
|
loadSpecies: useSelector(api.makeLoadAvailableRaces),
|
||||||
|
maxPactMagicSlotLevel: useSelector(s.getMaxPactMagicSlotLevel),
|
||||||
|
maxSpellSlotLevel: useSelector(s.getMaxSpellSlotLevel),
|
||||||
|
miscModifiers: useSelector(s.getMiscModifiers),
|
||||||
|
modifierData: useSelector(s.getModifierData),
|
||||||
|
optionalClassFeature: useSelector(s.getOptionalClassFeatureLookup),
|
||||||
|
optionalClassFeatures: useSelector(s.getOptionalClassFeatures),
|
||||||
|
optionalOrigin: useSelector(s.getOptionalOriginLookup),
|
||||||
|
optionalOrigins: useSelector(s.getOptionalOrigins),
|
||||||
|
originRef: useSelector(s.getDataOriginRefData),
|
||||||
|
originRefBackground: useSelector(s.getDataOriginRefBackgroundData),
|
||||||
|
originRefClass: useSelector(s.getDataOriginRefClassData),
|
||||||
|
originRefCondition: useSelector(s.getDataOriginRefConditionData),
|
||||||
|
originRefFeat: useSelector(s.getDataOriginRefFeatData),
|
||||||
|
originRefFeatList: useSelector(s.getDataOriginRefFeatListData),
|
||||||
|
originRefItem: useSelector(s.getDataOriginRefItemData),
|
||||||
|
originRefRace: useSelector(s.getDataOriginRefRaceData),
|
||||||
|
originRefVehicle: useSelector(s.getDataOriginRefVehicleData),
|
||||||
|
overallSpellInfo: useSelector(s.getOverallSpellInfo),
|
||||||
|
overridePassiveInsight: useSelector(s.getOverridePassiveInsight),
|
||||||
|
overridePassiveInvestigation: useSelector(s.getOverridePassiveInvestigation),
|
||||||
|
overridePassivePerception: useSelector(s.getOverridePassivePerception),
|
||||||
|
pactMagicConditiones: useSelector(s.getPactMagicClasses),
|
||||||
|
pactMagicSlots: useSelector(s.getPactMagicSlots),
|
||||||
|
pactWeaponEnabled: useSelector(s.getPactWeaponEnabled),
|
||||||
|
partyInfo: useSelector(sds.getPartyInfo),
|
||||||
|
partyInventory: useSelector(s.getPartyInventory),
|
||||||
|
partyInventoryContainers: useSelector(s.getPartyInventoryContainers),
|
||||||
|
partyInventoryItem: useSelector(s.getPartyInventoryLookup),
|
||||||
|
passiveInsight: useSelector(s.getPassiveInsight),
|
||||||
|
passiveInvestigation: useSelector(s.getPassiveInvestigation),
|
||||||
|
passivePerception: useSelector(s.getPassivePerception),
|
||||||
|
pdfBucket1: useSelector(s.getPdfDataBucket1),
|
||||||
|
pdfBucket2: useSelector(s.getPdfDataBucket2),
|
||||||
|
pdfBucket3: useSelector(s.getPdfDataBucket3),
|
||||||
|
pdfBucket4: useSelector(s.getPdfDataBucket4),
|
||||||
|
pdfBucket5: useSelector(s.getPdfDataBucket5),
|
||||||
|
pdfBucket6: useSelector(s.getPdfDataBucket6),
|
||||||
|
pdfExportData: useSelector(s.getPdfExportData),
|
||||||
|
playerId: useSelector(s.getUserId),
|
||||||
|
playerName: useSelector(s.getUsername),
|
||||||
|
preferences: useSelector(s.getCharacterPreferences),
|
||||||
|
prerequisiteData: useSelector(s.getPrerequisiteData),
|
||||||
|
processedInitiative: useSelector(s.getProcessedInitiative),
|
||||||
|
proficiency: useSelector(s.getProficiencyLookup),
|
||||||
|
proficiencyBonus: useSelector(s.getProficiencyBonus),
|
||||||
|
proficiencyGroups: useSelector(s.getProficiencyGroups),
|
||||||
|
proficiencyModifiers: useSelector(s.getProficiencyModifiers),
|
||||||
|
protectionSuppliers: useSelector(s.getProtectionSuppliers),
|
||||||
|
pushDragLiftWeight: useSelector(s.getPushDragLiftWeight),
|
||||||
|
race: useSelector(s.getRace),
|
||||||
|
raceAction: useSelector(s.getRaceActionLookup),
|
||||||
|
raceChoice: useSelector(s.getRaceChoiceLookup),
|
||||||
|
raceCreatureRules: useSelector(s.getRaceCreatureRules),
|
||||||
|
raceModifier: useSelector(s.getRaceModifierLookup),
|
||||||
|
raceModifiers: useSelector(s.getRaceModifiers),
|
||||||
|
raceOption: useSelector(s.getRaceOptionLookup),
|
||||||
|
raceSpell: useSelector(s.getRaceSpellLookup),
|
||||||
|
requiredCharacterServiceParams: useSelector(
|
||||||
|
s.getRequiredCharacterServiceParams
|
||||||
|
),
|
||||||
|
requiredGameDataServiceParams: useSelector(
|
||||||
|
s.getRequiredGameDataServiceParams
|
||||||
|
),
|
||||||
|
resistanceData: useSelector(s.getResistanceData),
|
||||||
|
restrictedBonusSavingThrowModifiers: useSelector(
|
||||||
|
s.getRestrictedBonusSavingThrowModifiers
|
||||||
|
),
|
||||||
|
ritualSpells: useSelector(s.getRitualSpells),
|
||||||
|
ruleData: useSelector(s.getRuleData),
|
||||||
|
ruleDataPool: useSelector(sds.getRuleDataPool),
|
||||||
|
savingThrowDiceAdjustments: useSelector(s.getSavingThrowDiceAdjustments),
|
||||||
|
senseInfo: useSelector(s.getSenseInfo),
|
||||||
|
situationalBonusSavingThrowsLookup: useSelector(
|
||||||
|
s.getSituationalBonusSavingThrowsLookup
|
||||||
|
),
|
||||||
|
situationalBonusSavingThrows: useSelector(s.getSituationalBonusSavingThrows),
|
||||||
|
size: useSelector(s.getSize),
|
||||||
|
skill: useSelector(s.getSkillLookup),
|
||||||
|
skills: useSelector(s.getSkills),
|
||||||
|
snippetData: useSelector(s.getSnippetData),
|
||||||
|
socialImageData: useSelector(s.getSocialImageData),
|
||||||
|
specialWeaponPropertiesEnabled: useSelector(
|
||||||
|
s.hack__getSpecialWeaponPropertiesEnabled
|
||||||
|
),
|
||||||
|
speeds: useSelector(s.getSpeeds),
|
||||||
|
spellClasses: useSelector(s.getSpellClasses),
|
||||||
|
spellcasterInfo: useSelector(s.getSpellCasterInfo),
|
||||||
|
spellcastingClasses: useSelector(s.getSpellcastingClasses),
|
||||||
|
spellListDataOrigin: useSelector(s.getSpellListDataOriginLookup),
|
||||||
|
spellSlots: useSelector(s.getSpellSlots),
|
||||||
|
startingClass: useSelector(s.getStartingClass),
|
||||||
|
strScore: useSelector(s.getStrScore),
|
||||||
|
totalCarriedWeight: useSelector(s.getTotalCarriedWeight),
|
||||||
|
totalClassLevel: useSelector(s.getTotalClassLevel),
|
||||||
|
uniqueProficiencyModifiers: useSelector(s.getUniqueProficiencyModifiers),
|
||||||
|
validEquipmentModifiers: useSelector(s.getValidEquipmentModifiers),
|
||||||
|
validGlobalModifiers: useSelector(s.getValidGlobalModifiers),
|
||||||
|
validImmunityModifiers: useSelector(s.getValidImmunityModifiers),
|
||||||
|
validResistanceModifiers: useSelector(s.getValidResistanceModifiers),
|
||||||
|
validVulnerabilityModifiers: useSelector(s.getValidVulnerabilityModifiers),
|
||||||
|
valueLookup: useSelector(s.getCharacterValueLookup),
|
||||||
|
vehicle: useSelector(s.getVehicleLookup),
|
||||||
|
vehicles: useSelector(s.getVehicles),
|
||||||
|
vehicleComponent: useSelector(s.getVehicleComponentLookup),
|
||||||
|
vehicleComponentMappings: useSelector(sds.getVehicleComponentMappings),
|
||||||
|
vehicleMappings: useSelector(sds.getVehicleMappings),
|
||||||
|
vulnerabilityData: useSelector(s.getVulnerabilityData),
|
||||||
|
weaponSpellDamageGroups: useSelector(s.getWeaponSpellDamageGroups),
|
||||||
|
weightSpeeds: useSelector(s.getWeightSpeeds),
|
||||||
|
|
||||||
|
// Utility Functions
|
||||||
|
abilityUtils,
|
||||||
|
accessUtils,
|
||||||
|
actionUtils,
|
||||||
|
activationUtils,
|
||||||
|
apiAdapterUtils,
|
||||||
|
backgroundUtils,
|
||||||
|
campaignUtils,
|
||||||
|
campaignSettingUtils,
|
||||||
|
characterUtils,
|
||||||
|
choiceUtils,
|
||||||
|
classFeatureUtils,
|
||||||
|
classUtils,
|
||||||
|
conditionUtils,
|
||||||
|
conditionLevelUtils,
|
||||||
|
containerUtils,
|
||||||
|
coreUtils,
|
||||||
|
creatureUtils,
|
||||||
|
creatureRuleUtils,
|
||||||
|
dataOriginUtils,
|
||||||
|
decorationUtils,
|
||||||
|
definitionUtils,
|
||||||
|
definitionPoolUtils,
|
||||||
|
diceUtils,
|
||||||
|
durationUtils,
|
||||||
|
entityUtils,
|
||||||
|
extraUtils,
|
||||||
|
featUtils,
|
||||||
|
featureFlagInfoUtils,
|
||||||
|
featureUtils,
|
||||||
|
formatUtils,
|
||||||
|
helperUtils,
|
||||||
|
infusionChoiceUtils,
|
||||||
|
infusionUtils,
|
||||||
|
itemUtils,
|
||||||
|
knownInfusionUtils,
|
||||||
|
limitedUseUtils,
|
||||||
|
modifierUtils,
|
||||||
|
noteUtils,
|
||||||
|
optionalClassFeatureUtils,
|
||||||
|
optionalOriginUtils,
|
||||||
|
optionUtils,
|
||||||
|
organizationUtils,
|
||||||
|
pdfUtils,
|
||||||
|
prerequisiteUtils,
|
||||||
|
raceUtils,
|
||||||
|
racialTraitUtils,
|
||||||
|
ruleDataUtils,
|
||||||
|
ruleDataPoolUtils,
|
||||||
|
skillUtils,
|
||||||
|
snippetUtils,
|
||||||
|
sourceUtils,
|
||||||
|
spellUtils,
|
||||||
|
startingEquipmentUtils,
|
||||||
|
valueUtils,
|
||||||
|
vehicleUtils,
|
||||||
|
vehicleComponentUtils,
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
FeatList,
|
||||||
|
});
|
77
ddb_main/hooks/useClaimCharacter.ts
Normal file
77
ddb_main/hooks/useClaimCharacter.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
|
import { characterSelectors } from "@dndbeyond/character-rules-engine/es";
|
||||||
|
|
||||||
|
import { toastMessageActions } from "~/tools/js/Shared/actions";
|
||||||
|
|
||||||
|
import {
|
||||||
|
claimCharacter as claimCharacterApi,
|
||||||
|
joinCampaign as joinCampaignApi,
|
||||||
|
} from "../helpers/characterServiceApi";
|
||||||
|
|
||||||
|
interface UseClaimCharacterProps {
|
||||||
|
campaignJoinCode: string | null;
|
||||||
|
isAssigned: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useClaimCharacter = ({
|
||||||
|
campaignJoinCode,
|
||||||
|
isAssigned,
|
||||||
|
}: UseClaimCharacterProps) => {
|
||||||
|
const [isFinished, setIsFinished] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const [newCharacterId, setNewCharacterId] = useState<number | null>(null);
|
||||||
|
const [campaignId, setCampaignId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const characterId = useSelector(characterSelectors.getId);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const claimCharacter = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
let clonedId;
|
||||||
|
|
||||||
|
const claimResponse = await claimCharacterApi(characterId, isAssigned);
|
||||||
|
if (claimResponse.ok) {
|
||||||
|
const data = await claimResponse.json();
|
||||||
|
clonedId = data.id;
|
||||||
|
setNewCharacterId(clonedId);
|
||||||
|
} else {
|
||||||
|
throw new Error(`There was an error claiming character ${characterId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (campaignJoinCode) {
|
||||||
|
const joinResponse = await joinCampaignApi(campaignJoinCode, clonedId);
|
||||||
|
if (joinResponse.ok) {
|
||||||
|
const data = await joinResponse.json();
|
||||||
|
setCampaignId(data.campaignId);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`There was an error adding character ${clonedId} to campaign join code ${campaignJoinCode}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
toastMessageActions.toastError("Error", "An unexpected error occurred.")
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsFinished(true);
|
||||||
|
}
|
||||||
|
}, [campaignJoinCode, characterId, dispatch, isAssigned]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
claimCharacter,
|
||||||
|
isLoading,
|
||||||
|
isFinished,
|
||||||
|
newCharacterId,
|
||||||
|
campaignId,
|
||||||
|
] as const;
|
||||||
|
};
|
33
ddb_main/hooks/useErrorHandling/useErrorHandling.tsx
Normal file
33
ddb_main/hooks/useErrorHandling/useErrorHandling.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { HTMLAttributes, useState } from "react";
|
||||||
|
import styles from '../../styles/errors.module.css';
|
||||||
|
|
||||||
|
export interface ErrorHandlerOptions {
|
||||||
|
initialState: boolean;
|
||||||
|
errMsg: string;
|
||||||
|
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ErrorHandler {
|
||||||
|
showError: boolean;
|
||||||
|
setShowError: (value: boolean) => void;
|
||||||
|
ErrorMessage: () => JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useErrorHandling = (
|
||||||
|
initialState: boolean,
|
||||||
|
errMsg: string,
|
||||||
|
errorMessageAttributes: HTMLAttributes<HTMLDivElement> | null = null
|
||||||
|
): ErrorHandler => {
|
||||||
|
const [showError, setShowError] = useState(initialState);
|
||||||
|
const ErrorMessage = (): JSX.Element => (
|
||||||
|
<div className={styles.inputError} {...errorMessageAttributes}>
|
||||||
|
{errMsg}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
showError,
|
||||||
|
setShowError,
|
||||||
|
ErrorMessage,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,49 @@
|
|||||||
|
import { HTMLAttributes } from "react";
|
||||||
|
import { useErrorHandling } from "./useErrorHandling";
|
||||||
|
|
||||||
|
export interface MaxLengthErrorHandler {
|
||||||
|
handleMaxLengthErrorMsg: (value: string) => void;
|
||||||
|
hideError: () => void;
|
||||||
|
MaxLengthErrorMessage: () => JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMaxLengthErrorHandling = (
|
||||||
|
initialState: boolean,
|
||||||
|
maxLength: number | null,
|
||||||
|
errMsg: string = "",
|
||||||
|
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>
|
||||||
|
): MaxLengthErrorHandler => {
|
||||||
|
errMsg ||= `The max length is ${maxLength} characters.`;
|
||||||
|
|
||||||
|
const {
|
||||||
|
showError,
|
||||||
|
setShowError,
|
||||||
|
ErrorMessage,
|
||||||
|
} = useErrorHandling(initialState, errMsg, errorMessageAttributes);
|
||||||
|
|
||||||
|
const handleMaxLengthErrorMsg = (value: string): void => {
|
||||||
|
if (!maxLength) {
|
||||||
|
// Skip if maxLength is 0 or not set.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTooLong = (value?.length ?? 0) >= (maxLength ?? 0);
|
||||||
|
if (isTooLong !== showError) {
|
||||||
|
setShowError(isTooLong);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideError = () => {
|
||||||
|
setShowError(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MaxLengthErrorMessage = (): JSX.Element => showError && errMsg
|
||||||
|
? (<ErrorMessage />)
|
||||||
|
: <></>;
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleMaxLengthErrorMsg,
|
||||||
|
MaxLengthErrorMessage,
|
||||||
|
hideError
|
||||||
|
};
|
||||||
|
}
|
26
ddb_main/hooks/useExtras.ts
Normal file
26
ddb_main/hooks/useExtras.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ExtraManager,
|
||||||
|
FeaturesManager,
|
||||||
|
} from "@dndbeyond/character-rules-engine/es";
|
||||||
|
|
||||||
|
import { ExtrasManagerContext } from "~/tools/js/Shared/managers/ExtrasManagerContext";
|
||||||
|
|
||||||
|
//TODO: need to look at subscribeToUpdates for how many times its being called on state updates
|
||||||
|
export function useExtras() {
|
||||||
|
const { extrasManager } = useContext(ExtrasManagerContext);
|
||||||
|
|
||||||
|
const [extras, setExtras] = useState<Array<ExtraManager>>(
|
||||||
|
extrasManager.getCharacterExtraManagers()
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onUpdate = () => {
|
||||||
|
setExtras(extrasManager.getCharacterExtraManagers());
|
||||||
|
};
|
||||||
|
return FeaturesManager.subscribeToUpdates({ onUpdate });
|
||||||
|
}, [extrasManager]);
|
||||||
|
|
||||||
|
return extras;
|
||||||
|
}
|
59
ddb_main/hooks/useLocalStorage.ts
Normal file
59
ddb_main/hooks/useLocalStorage.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
import { tryGet, tryRemove, trySet } from "../helpers/localStorageUtils";
|
||||||
|
|
||||||
|
//got most of the typing for this at https://usehooks-ts.com/react-hook/use-local-storage
|
||||||
|
|
||||||
|
type SetValue<T> = Dispatch<SetStateAction<T>>;
|
||||||
|
type RemoveValue<T> = Dispatch<SetStateAction<T>>;
|
||||||
|
function useLocalStorage<T>(
|
||||||
|
key: string,
|
||||||
|
defaultValue: T
|
||||||
|
): [T, SetValue<T>, RemoveValue<T>] {
|
||||||
|
const getValue = useCallback(() => {
|
||||||
|
const savedValue = tryGet(key);
|
||||||
|
|
||||||
|
return savedValue === null
|
||||||
|
? (defaultValue as T)
|
||||||
|
: (parseJSON(savedValue) as T);
|
||||||
|
}, [key, defaultValue]);
|
||||||
|
|
||||||
|
const [value, setValue] = useState(getValue());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(getValue());
|
||||||
|
}, [getValue]);
|
||||||
|
|
||||||
|
const set = useCallback(
|
||||||
|
(newValue) => {
|
||||||
|
setValue(newValue);
|
||||||
|
trySet(key, newValue);
|
||||||
|
},
|
||||||
|
[key, setValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
const remove = () => {
|
||||||
|
setValue(defaultValue as T);
|
||||||
|
tryRemove(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [value, set, remove];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useLocalStorage;
|
||||||
|
|
||||||
|
// A wrapper for "JSON.parse()"" to support "undefined" value
|
||||||
|
function parseJSON<T>(value: any): T | undefined {
|
||||||
|
try {
|
||||||
|
return value === "undefined" ? undefined : JSON.parse(value ?? "");
|
||||||
|
} catch (error) {
|
||||||
|
console.log("parsing error on", { value });
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
58
ddb_main/hooks/usePdfExport.ts
Normal file
58
ddb_main/hooks/usePdfExport.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ApiAdapterUtils,
|
||||||
|
ApiRequests,
|
||||||
|
characterActions,
|
||||||
|
rulesEngineSelectors,
|
||||||
|
syncTransactionActions,
|
||||||
|
} from "@dndbeyond/character-rules-engine";
|
||||||
|
|
||||||
|
import { toastMessageActions } from "~/tools/js/Shared/actions";
|
||||||
|
|
||||||
|
export interface ExportPdfData {
|
||||||
|
exportPdf: () => void;
|
||||||
|
pdfUrl: string | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
isFinished: boolean;
|
||||||
|
}
|
||||||
|
export const usePdfExport = (): ExportPdfData => {
|
||||||
|
const [isFinished, setIsFinished] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
||||||
|
const pdfData = useSelector(rulesEngineSelectors.getPdfExportData);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const exportPdf = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
let syncActivationId: string = "PDF-SYNC";
|
||||||
|
dispatch(syncTransactionActions.activate(syncActivationId));
|
||||||
|
|
||||||
|
//This will update the character's spell data before export is created
|
||||||
|
//(in case a user is in the builder and has not updated their class spells)
|
||||||
|
dispatch(characterActions.loadLazyCharacterData());
|
||||||
|
|
||||||
|
const response = await ApiRequests.postCharacterPdf({
|
||||||
|
exportData: JSON.stringify(pdfData),
|
||||||
|
});
|
||||||
|
const pdfUrl = ApiAdapterUtils.getResponseData(response);
|
||||||
|
|
||||||
|
if (pdfUrl) {
|
||||||
|
setPdfUrl(pdfUrl);
|
||||||
|
dispatch(syncTransactionActions.deactivate());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(
|
||||||
|
toastMessageActions.toastError("Error", "An unexpected error occurred.")
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsFinished(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
return { exportPdf, pdfUrl, isLoading, isFinished };
|
||||||
|
};
|
62
ddb_main/hooks/usePositioning.ts
Normal file
62
ddb_main/hooks/usePositioning.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
import { useSidebar } from "~/contexts/Sidebar";
|
||||||
|
import {
|
||||||
|
SidebarAlignmentEnum,
|
||||||
|
SidebarPlacementEnum,
|
||||||
|
SidebarPositionInfo,
|
||||||
|
} from "~/subApps/sheet/components/Sidebar/types";
|
||||||
|
import { SheetPositioningInfo } from "~/tools/js/CharacterSheet/typings";
|
||||||
|
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||||
|
|
||||||
|
export const usePositioning = () => {
|
||||||
|
const {
|
||||||
|
sidebar: { isVisible, placement, alignment, width },
|
||||||
|
} = useSidebar();
|
||||||
|
|
||||||
|
const getSheetPositioning = (): SheetPositioningInfo => {
|
||||||
|
let offset: number = 0;
|
||||||
|
if (!isVisible) {
|
||||||
|
return { offset, left: offset };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placement === SidebarPlacementEnum.FIXED) {
|
||||||
|
offset = width / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
offset,
|
||||||
|
left: alignment === SidebarAlignmentEnum.LEFT ? offset : -1 * offset,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSidebarPositioning = (): SidebarPositionInfo => {
|
||||||
|
const sheetDimensions = useSelector(appEnvSelectors.getDimensions);
|
||||||
|
const sheetPosition = getSheetPositioning();
|
||||||
|
|
||||||
|
const { sheet, window } = sheetDimensions;
|
||||||
|
|
||||||
|
let posX: number = 0;
|
||||||
|
let sidebarGutter: number = 15;
|
||||||
|
let screenGutterSize: number = (window.width - sheet.width) / 2;
|
||||||
|
|
||||||
|
if (placement === SidebarPlacementEnum.FIXED) {
|
||||||
|
posX = screenGutterSize + sheetPosition.offset - sidebarGutter - width;
|
||||||
|
} else {
|
||||||
|
screenGutterSize -= sidebarGutter;
|
||||||
|
let overflowOffset: number =
|
||||||
|
width > screenGutterSize ? width - screenGutterSize : 0;
|
||||||
|
posX = screenGutterSize - width + overflowOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: alignment === SidebarAlignmentEnum.LEFT ? posX : "auto",
|
||||||
|
right: alignment === SidebarAlignmentEnum.RIGHT ? posX : "auto",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getSheetPositioning,
|
||||||
|
getSidebarPositioning,
|
||||||
|
};
|
||||||
|
};
|
104
ddb_main/hooks/useRuleData.ts
Normal file
104
ddb_main/hooks/useRuleData.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
import {
|
||||||
|
RuleDataUtils as ruleDataUtils,
|
||||||
|
ruleDataSelectors as s,
|
||||||
|
} from "@dndbeyond/character-rules-engine";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that provides access to the rule data state without having to
|
||||||
|
* import rules engine all over the place in your components. This reduces
|
||||||
|
* touchpoints to the rules engine and makes it easier to refactor the rules
|
||||||
|
* engine in the future.
|
||||||
|
*/
|
||||||
|
export const useRuleData = () => ({
|
||||||
|
// Export all data coming from the RuleData selectors
|
||||||
|
allData: useSelector(s.getAllData),
|
||||||
|
abilityScoreDisplayTypes: useSelector(s.getAbilityScoreDisplayTypes),
|
||||||
|
abilitySkills: useSelector(s.getAbilitySkills),
|
||||||
|
activationTypes: useSelector(s.getActivationTypes),
|
||||||
|
additionalLevelTypes: useSelector(s.getAdditionalLevelTypes),
|
||||||
|
adjustmentDataTypes: useSelector(s.getAdjustmentDataTypes),
|
||||||
|
adjustmentTypes: useSelector(s.getAdjustmentTypes),
|
||||||
|
alignments: useSelector(s.getAlignments),
|
||||||
|
aoeTypes: useSelector(s.getAoeTypes),
|
||||||
|
armor: useSelector(s.getArmor),
|
||||||
|
basicActions: useSelector(s.getBasicActions),
|
||||||
|
baseWeaponReach: useSelector(s.getBaseWeaponReach),
|
||||||
|
basicMaxStatScore: useSelector(s.getBasicMaxStatScore),
|
||||||
|
challengeRatings: useSelector(s.getChallengeRatings),
|
||||||
|
conditionTypes: useSelector(s.getConditionTypes),
|
||||||
|
conditions: useSelector(s.getConditions),
|
||||||
|
creatureGroupCategories: useSelector(s.getCreatureGroupCategories),
|
||||||
|
creatureGroupFlags: useSelector(s.getCreatureGroupFlags),
|
||||||
|
creatureGroups: useSelector(s.getCreatureGroups),
|
||||||
|
creatureSizes: useSelector(s.getCreatureSizes),
|
||||||
|
currencyData: useSelector(s.getCurrencyData),
|
||||||
|
damageAdjustments: useSelector(s.getDamageAdjustments),
|
||||||
|
damageTypes: useSelector(s.getDamageTypes),
|
||||||
|
defaultArmorImageUrl: useSelector(s.getDefaultArmorImageUrl),
|
||||||
|
defaultAttunedItemCountMax: useSelector(s.getDefaultAttunedItemCountMax),
|
||||||
|
defaultGearImageUrl: useSelector(s.getDefaultGearImageUrl),
|
||||||
|
defaultRacePortraitUrl: useSelector(s.getDefaultRacePortraitUrl),
|
||||||
|
defaultWeaponImageUrl: useSelector(s.getDefaultWeaponImageUrl),
|
||||||
|
diceValues: useSelector(s.getDiceValues),
|
||||||
|
environments: useSelector(s.getEnvironments),
|
||||||
|
initiativeScore: useSelector(s.getInitiativeScore),
|
||||||
|
languageTypeId: useSelector(s.getLanguageTypeId),
|
||||||
|
languages: useSelector(s.getLanguages),
|
||||||
|
levelExperiencePoints: useSelector(s.getLevelExperiencePoints),
|
||||||
|
levelProficiencyBonuses: useSelector(s.getLevelProficiencyBonuses),
|
||||||
|
lifestyles: useSelector(s.getLifestyles),
|
||||||
|
limitedUseResetTypes: useSelector(s.getLimitedUseResetTypes),
|
||||||
|
longRestMinHitDiceUsedRecovered: useSelector(
|
||||||
|
s.getLongRestMinimumHitDiceUsedRecovered
|
||||||
|
),
|
||||||
|
maxAttunedItemCountMax: useSelector(s.getMaxAttunedItemCountMax),
|
||||||
|
maxCharacterLevel: useSelector(s.getMaxCharacterLevel),
|
||||||
|
maxDeathsavesFail: useSelector(s.getMaxDeathsavesFail),
|
||||||
|
maxDeathsavesSuccess: useSelector(s.getMaxDeathsavesSuccess),
|
||||||
|
maxSpellLevel: useSelector(s.getMaxSpellLevel),
|
||||||
|
maxStatScore: useSelector(s.getMaxStatScore),
|
||||||
|
minAttunedItemCountMax: useSelector(s.getMinAttunedItemCountMax),
|
||||||
|
minStatScore: useSelector(s.getMinStatScore),
|
||||||
|
minimumHpTotal: useSelector(s.getMinimumHpTotal),
|
||||||
|
minimumLimitedUseMaxUse: useSelector(s.getMinimumLimitedUseMaxUse),
|
||||||
|
monsterSubTypes: useSelector(s.getMonsterSubTypes),
|
||||||
|
monsterTypes: useSelector(s.getMonsterTypes),
|
||||||
|
movements: useSelector(s.getMovements),
|
||||||
|
multiClassSpellSlots: useSelector(s.getMultiClassSpellSlots),
|
||||||
|
naturalActions: useSelector(s.getNaturalActions),
|
||||||
|
noArmorAcAmount: useSelector(s.getNoArmorAcAmount),
|
||||||
|
pactMagicMultiClassSpellSlots: useSelector(
|
||||||
|
s.getPactMagicMultiClassSpellSlots
|
||||||
|
),
|
||||||
|
privacyTypes: useSelector(s.getPrivacyTypes),
|
||||||
|
proficiencyGroups: useSelector(s.getProficiencyGroups),
|
||||||
|
raceGroups: useSelector(s.getRaceGroups),
|
||||||
|
rangeTypes: useSelector(s.getRangeTypes),
|
||||||
|
ritualCastingTimeMinuteAddition: useSelector(
|
||||||
|
s.getRitualCastingTimeMinuteAddition
|
||||||
|
),
|
||||||
|
rules: useSelector(s.getRules),
|
||||||
|
senses: useSelector(s.getSenses),
|
||||||
|
sharingTypes: useSelector(s.getSharingTypes),
|
||||||
|
sourceCategories: useSelector(s.getSourceCategories),
|
||||||
|
sources: useSelector(s.getSources),
|
||||||
|
spellComponents: useSelector(s.getSpellComponents),
|
||||||
|
spellConditionTypes: useSelector(s.getSpellConditionTypes),
|
||||||
|
spellRangeTypes: useSelector(s.getSpellRangeTypes),
|
||||||
|
statModifiers: useSelector(s.getStatModifiers),
|
||||||
|
stats: useSelector(s.getStats),
|
||||||
|
stealthCheckTypes: useSelector(s.getStealthCheckTypes),
|
||||||
|
stringMartialArts: useSelector(s.getStringMartialArts),
|
||||||
|
stringPactMagic: useSelector(s.getStringPactMagic),
|
||||||
|
stringSpellCasting: useSelector(s.getStringSpellCasting),
|
||||||
|
stringSpellEldritchBlast: useSelector(s.getStringSpellEldritchBlast),
|
||||||
|
weapons: useSelector(s.getWeapons),
|
||||||
|
weaponCategories: useSelector(s.getWeaponCategories),
|
||||||
|
weaponProperties: useSelector(s.getWeaponProperties),
|
||||||
|
weaponPropertyReachDistance: useSelector(s.getWeaponPropertyReachDistance),
|
||||||
|
|
||||||
|
// Export all utils from RuleDataUtils
|
||||||
|
ruleDataUtils,
|
||||||
|
});
|
179
ddb_main/hooks/useSource.ts
Normal file
179
ddb_main/hooks/useSource.ts
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import { orderBy } from "lodash";
|
||||||
|
|
||||||
|
import {
|
||||||
|
BuilderChoiceOptionContract,
|
||||||
|
EntityRestrictionData,
|
||||||
|
HelperUtils,
|
||||||
|
HtmlSelectOptionGroup,
|
||||||
|
RuleDataUtils,
|
||||||
|
SimpleSourcedDefinitionContract,
|
||||||
|
SourceMappingContract,
|
||||||
|
SourceUtils,
|
||||||
|
} from "@dndbeyond/character-rules-engine";
|
||||||
|
|
||||||
|
import { TypeScriptUtils } from "~/tools/js/Shared/utils";
|
||||||
|
|
||||||
|
import { useCharacterEngine } from "./useCharacterEngine";
|
||||||
|
import { useRuleData } from "./useRuleData";
|
||||||
|
|
||||||
|
interface SourceCategoryGroup<T> {
|
||||||
|
name: string;
|
||||||
|
id: number;
|
||||||
|
items: Array<T>;
|
||||||
|
sortOrder: number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SourceCategoryKey =
|
||||||
|
| "avatarUrl"
|
||||||
|
| "description"
|
||||||
|
| "id"
|
||||||
|
| "isEnabledByDefault"
|
||||||
|
| "isHideable"
|
||||||
|
| "isPartneredContent"
|
||||||
|
| "isToggleable"
|
||||||
|
| "name";
|
||||||
|
|
||||||
|
export const useSource = () => {
|
||||||
|
const {
|
||||||
|
ruleData,
|
||||||
|
helperUtils: { lookupDataOrFallback },
|
||||||
|
ruleDataUtils: { getSourceCategoryLookup },
|
||||||
|
} = useCharacterEngine();
|
||||||
|
const {
|
||||||
|
ruleDataUtils: { getSourceDataLookup },
|
||||||
|
} = useRuleData();
|
||||||
|
const allSources = getSourceDataLookup(ruleData);
|
||||||
|
const allSourceCategories = getSourceCategoryLookup(ruleData);
|
||||||
|
|
||||||
|
// Returns the source ID from a given source
|
||||||
|
const getSourceId = (source: SourceMappingContract) => source?.sourceId;
|
||||||
|
|
||||||
|
const getSource = (sourceId: number | string) =>
|
||||||
|
lookupDataOrFallback(allSources, sourceId);
|
||||||
|
|
||||||
|
const getSourceName = (sourceId: number | string) =>
|
||||||
|
lookupDataOrFallback(allSources, sourceId)?.name;
|
||||||
|
|
||||||
|
const getSourceDescription = (sourceId: number | string) =>
|
||||||
|
lookupDataOrFallback(allSources, sourceId)?.description;
|
||||||
|
|
||||||
|
// Source Category direct access with category id
|
||||||
|
const getSourceCategory = (sourceCategoryId: number) =>
|
||||||
|
lookupDataOrFallback(allSourceCategories, sourceCategoryId);
|
||||||
|
|
||||||
|
const getSourceCategoryDescription = (sourceCategoryId: number) =>
|
||||||
|
getSourceCategory(sourceCategoryId)?.description;
|
||||||
|
|
||||||
|
// Takes an array of any entity that contains a sources key and returns an array of sorted SourceCategoryGroup representing each Source Category with the category data and an array of the entities that belong to that category.
|
||||||
|
const getSourceCategoryGroups = <
|
||||||
|
T extends {
|
||||||
|
sources: SourceMappingContract[] | null;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
items: Array<T>
|
||||||
|
): Array<SourceCategoryGroup<T>> => {
|
||||||
|
let groups: Array<SourceCategoryGroup<T>> = [];
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
const sourceContracts = item.sources ?? [];
|
||||||
|
|
||||||
|
//get the source data for each source contract
|
||||||
|
const sources = sourceContracts
|
||||||
|
.map((source) =>
|
||||||
|
HelperUtils.lookupDataOrFallback(
|
||||||
|
RuleDataUtils.getSourceDataLookup(ruleData),
|
||||||
|
source.sourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter(TypeScriptUtils.isNotNullOrUndefined);
|
||||||
|
|
||||||
|
//get the source category name for the first source contract or default to Homebrew
|
||||||
|
const sourceCategoryName =
|
||||||
|
sources.length > 0 &&
|
||||||
|
sources[0].sourceCategory &&
|
||||||
|
sources[0].sourceCategory.name
|
||||||
|
? sources[0].sourceCategory.name
|
||||||
|
: "Homebrew";
|
||||||
|
|
||||||
|
// Search the availableGroupedOptions array for the source category
|
||||||
|
const index = groups.findIndex(
|
||||||
|
(element) => element.name === sourceCategoryName
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the source category wasn't found, add it as a new option group.
|
||||||
|
if (index < 0) {
|
||||||
|
groups.push({
|
||||||
|
name: sourceCategoryName,
|
||||||
|
id: sources[0]?.sourceCategory?.id || 0,
|
||||||
|
sortOrder: sources[0]?.sourceCategory?.sortOrder,
|
||||||
|
items: [item],
|
||||||
|
});
|
||||||
|
// Otherwise, add the option to the existing source category's array of options.
|
||||||
|
} else {
|
||||||
|
groups[index].items.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return orderBy(groups, "sortOrder");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Takes an array of any entity's DefinitionContract and returns an array of sorted HtmlSelectOptionGroup representing each Source Category and a sorted array of options.
|
||||||
|
const getGroupedOptionsBySourceCategory = <
|
||||||
|
T extends {
|
||||||
|
sources: SourceMappingContract[] | null;
|
||||||
|
name: string | null;
|
||||||
|
id: number;
|
||||||
|
description?: string | null;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
items: Array<T>,
|
||||||
|
optionValue?: number | null,
|
||||||
|
entityRestrictionData?: EntityRestrictionData,
|
||||||
|
labelFallback?: string
|
||||||
|
): HtmlSelectOptionGroup[] => {
|
||||||
|
return SourceUtils.getGroupedOptionsBySourceCategory(
|
||||||
|
items,
|
||||||
|
ruleData,
|
||||||
|
optionValue,
|
||||||
|
entityRestrictionData,
|
||||||
|
labelFallback
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
//Given an array of choiceOptionContracts, return an array of SimpleSourcedDefinitionContracts (these are used to create grouped options in a select dropdown)
|
||||||
|
const getSimpleSourcedDefinitionContracts = (
|
||||||
|
choiceOptionContracts: Array<BuilderChoiceOptionContract>
|
||||||
|
): Array<SimpleSourcedDefinitionContract> => {
|
||||||
|
return SourceUtils.getSimpleSourcedDefinitionContracts(
|
||||||
|
choiceOptionContracts
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compares a source's category to a given match and returns a boolean value
|
||||||
|
const matchSourceCategory = (
|
||||||
|
sourceId: number,
|
||||||
|
match: string,
|
||||||
|
key: SourceCategoryKey = "name"
|
||||||
|
) => {
|
||||||
|
if (!sourceId) return false;
|
||||||
|
|
||||||
|
const source = lookupDataOrFallback(allSources, sourceId);
|
||||||
|
if (!source) return false;
|
||||||
|
|
||||||
|
if (source.sourceCategory?.[key] === match) return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
allSources,
|
||||||
|
getSource,
|
||||||
|
getSourceId,
|
||||||
|
getSourceCategory,
|
||||||
|
getSourceCategoryDescription,
|
||||||
|
getSourceDescription,
|
||||||
|
getSourceName,
|
||||||
|
matchSourceCategory,
|
||||||
|
getGroupedOptionsBySourceCategory,
|
||||||
|
getSimpleSourcedDefinitionContracts,
|
||||||
|
getSourceCategoryGroups,
|
||||||
|
};
|
||||||
|
};
|
26
ddb_main/hooks/useSubscriptionTier.ts
Normal file
26
ddb_main/hooks/useSubscriptionTier.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
import { getSubscriptionTier } from "../helpers/userApi";
|
||||||
|
|
||||||
|
export const FREE_TIER = "free";
|
||||||
|
export const HERO_TIER = "hero";
|
||||||
|
export const MASTER_TIER = "master";
|
||||||
|
export const LEGENDARY_TIER = "legendary";
|
||||||
|
|
||||||
|
const useSubscriptionTier = (defaultSubscription?: string) => {
|
||||||
|
const [subscriptionTier, setSubscriptionTier] = useState<string | undefined>(
|
||||||
|
defaultSubscription
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getSubscriptionTier().then((tier) => {
|
||||||
|
if (tier) {
|
||||||
|
setSubscriptionTier(tier as unknown as string);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (subscriptionTier || FREE_TIER).toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSubscriptionTier;
|
21
ddb_main/hooks/useUnpropagatedClick.ts
Normal file
21
ddb_main/hooks/useUnpropagatedClick.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { MouseEvent, useCallback } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents click events from propagating to parent elements.
|
||||||
|
* @param onClick The function to call when the element is clicked.
|
||||||
|
* @returns A function that will prevent the click event from propagating.
|
||||||
|
*/
|
||||||
|
export const useUnpropagatedClick = (
|
||||||
|
onClick?: (event: MouseEvent, ...args: unknown[]) => void
|
||||||
|
) => {
|
||||||
|
const handleClick = useCallback(
|
||||||
|
(event: MouseEvent, ...args: unknown[]) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
|
onClick?.(event, ...args);
|
||||||
|
},
|
||||||
|
[onClick]
|
||||||
|
);
|
||||||
|
|
||||||
|
return handleClick;
|
||||||
|
};
|
42
ddb_main/hooks/useUser.ts
Normal file
42
ddb_main/hooks/useUser.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
import type { User } from "@dndbeyond/authentication-lib-js";
|
||||||
|
|
||||||
|
import { getUser } from "../helpers/userApi";
|
||||||
|
|
||||||
|
export interface AppUser extends User {
|
||||||
|
displayName: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
roles: Array<string>;
|
||||||
|
subscription: string;
|
||||||
|
subscriptionTier: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* AppUserState has three states:
|
||||||
|
* - undefined: We currently don't know if the user is authenticated or not. ie loading state
|
||||||
|
* - null: user is not authenticated
|
||||||
|
* - User: user is authenticated and is a registered user
|
||||||
|
*/
|
||||||
|
const useUserInitialState = undefined;
|
||||||
|
type UseUserUnauthenticated = null;
|
||||||
|
export type AppUserState =
|
||||||
|
| AppUser
|
||||||
|
| UseUserUnauthenticated
|
||||||
|
| typeof useUserInitialState;
|
||||||
|
|
||||||
|
const useUser = () => {
|
||||||
|
const [user, setUser] = useState<AppUserState>(useUserInitialState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getUser()
|
||||||
|
.then(setUser)
|
||||||
|
.catch(() => {
|
||||||
|
setUser(null);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useUser;
|
17
ddb_main/hooks/useUserId.ts
Normal file
17
ddb_main/hooks/useUserId.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
import { getUserId } from "../helpers/userApi";
|
||||||
|
|
||||||
|
const useUserId = (defaultUserId?: number) => {
|
||||||
|
const [userId, setUserId] = useState<number | undefined>(defaultUserId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getUserId().then((user) => {
|
||||||
|
setUserId(user as unknown as number);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return userId;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useUserId;
|
19
ddb_main/hooks/useUserName.ts
Normal file
19
ddb_main/hooks/useUserName.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
import { getUserDisplayName } from "../helpers/userApi";
|
||||||
|
|
||||||
|
const useUserName = () => {
|
||||||
|
const [userName, setUserName] = useState<string | null>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getUserDisplayName()
|
||||||
|
.then(setUserName)
|
||||||
|
.catch(() => {
|
||||||
|
setUserName(null);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return userName;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useUserName;
|
19
ddb_main/hooks/useUserRoles.ts
Normal file
19
ddb_main/hooks/useUserRoles.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
import { getRoles } from "../helpers/userApi";
|
||||||
|
|
||||||
|
const useUserRoles = () => {
|
||||||
|
const [userRoles, setUserRoles] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getRoles()
|
||||||
|
.then(setUserRoles)
|
||||||
|
.catch(() => {
|
||||||
|
setUserRoles([]);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return userRoles;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useUserRoles;
|
69
ddb_main/index.tsx
Normal file
69
ddb_main/index.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import { QueryClient, QueryClientProvider } from "react-query";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
|
||||||
|
import { FontProvider } from "@dndbeyond/ftui-components";
|
||||||
|
import "@dndbeyond/ttui/styles/palette.css";
|
||||||
|
import wayOfLight from "@dndbeyond/ttui/themes/wayOfLight";
|
||||||
|
|
||||||
|
import { Layout } from "./components/Layout";
|
||||||
|
import { AuthProvider } from "./contexts/Authentication";
|
||||||
|
import { FeatureFlagProvider } from "./contexts/FeatureFlag";
|
||||||
|
import { FiltersProvider } from "./contexts/Filters";
|
||||||
|
import { HeadContextProvider } from "./contexts/Head";
|
||||||
|
import { ThemeManagerProvider } from "./contexts/ThemeManager";
|
||||||
|
import { reportWebVitals } from "./helpers/reportWebVitals";
|
||||||
|
import { Routes } from "./routes";
|
||||||
|
import "./styles/global.css";
|
||||||
|
import "./styles/index.scss";
|
||||||
|
import "./styles/reset.css";
|
||||||
|
import "./styles/variables.css";
|
||||||
|
import "./styles/waterdeep-stat-blocks.css";
|
||||||
|
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: { queries: { staleTime: 60 * 60 * 1000 } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const fonts = [
|
||||||
|
{
|
||||||
|
family: "Tiamat Condensed SC",
|
||||||
|
type: "woff2",
|
||||||
|
url: "https://www.dndbeyond.com/fonts/tiamatcondensedsc-regular-webfont.woff2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
family: "MrsEavesSmallCaps",
|
||||||
|
type: "truetype",
|
||||||
|
url: "https://www.dndbeyond.com/content/skins/waterdeep/fonts/MrsEavesSmallCaps.ttf",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const App = () => (
|
||||||
|
<>
|
||||||
|
<style>{wayOfLight}</style>
|
||||||
|
<BrowserRouter>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<ThemeManagerProvider manager={{ lightOrDark: "light" }}>
|
||||||
|
<FontProvider fonts={fonts} />
|
||||||
|
<HeadContextProvider>
|
||||||
|
<AuthProvider>
|
||||||
|
<FeatureFlagProvider>
|
||||||
|
<FiltersProvider>
|
||||||
|
<Layout>
|
||||||
|
<Routes />
|
||||||
|
</Layout>
|
||||||
|
</FiltersProvider>
|
||||||
|
</FeatureFlagProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
</HeadContextProvider>
|
||||||
|
</ThemeManagerProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
ReactDOM.render(<App />, document.getElementById("character-tools-target"));
|
||||||
|
|
||||||
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
|
reportWebVitals();
|
13
ddb_main/node_modules/@babel/polyfill/lib/index.js
generated
vendored
Normal file
13
ddb_main/node_modules/@babel/polyfill/lib/index.js
generated
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
require("./noConflict");
|
||||||
|
|
||||||
|
var _global = _interopRequireDefault(require("core-js/library/fn/global"));
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
|
||||||
|
|
||||||
|
if (_global["default"]._babelPolyfill && typeof console !== "undefined" && console.warn) {
|
||||||
|
console.warn("@babel/polyfill is loaded more than once on this page. This is probably not desirable/intended " + "and may have consequences if different versions of the polyfills are applied sequentially. " + "If you do need to load the polyfill more than once, use @babel/polyfill/noConflict " + "instead to bypass the warning.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_global["default"]._babelPolyfill = true;
|
29
ddb_main/node_modules/@babel/polyfill/lib/noConflict.js
generated
vendored
Normal file
29
ddb_main/node_modules/@babel/polyfill/lib/noConflict.js
generated
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
require("core-js/es6");
|
||||||
|
|
||||||
|
require("core-js/fn/array/includes");
|
||||||
|
|
||||||
|
require("core-js/fn/array/flat-map");
|
||||||
|
|
||||||
|
require("core-js/fn/string/pad-start");
|
||||||
|
|
||||||
|
require("core-js/fn/string/pad-end");
|
||||||
|
|
||||||
|
require("core-js/fn/string/trim-start");
|
||||||
|
|
||||||
|
require("core-js/fn/string/trim-end");
|
||||||
|
|
||||||
|
require("core-js/fn/symbol/async-iterator");
|
||||||
|
|
||||||
|
require("core-js/fn/object/get-own-property-descriptors");
|
||||||
|
|
||||||
|
require("core-js/fn/object/values");
|
||||||
|
|
||||||
|
require("core-js/fn/object/entries");
|
||||||
|
|
||||||
|
require("core-js/fn/promise/finally");
|
||||||
|
|
||||||
|
require("core-js/web");
|
||||||
|
|
||||||
|
require("regenerator-runtime/runtime");
|
139
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/es6/index.js
generated
vendored
Normal file
139
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/es6/index.js
generated
vendored
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
require('../modules/es6.symbol');
|
||||||
|
require('../modules/es6.object.create');
|
||||||
|
require('../modules/es6.object.define-property');
|
||||||
|
require('../modules/es6.object.define-properties');
|
||||||
|
require('../modules/es6.object.get-own-property-descriptor');
|
||||||
|
require('../modules/es6.object.get-prototype-of');
|
||||||
|
require('../modules/es6.object.keys');
|
||||||
|
require('../modules/es6.object.get-own-property-names');
|
||||||
|
require('../modules/es6.object.freeze');
|
||||||
|
require('../modules/es6.object.seal');
|
||||||
|
require('../modules/es6.object.prevent-extensions');
|
||||||
|
require('../modules/es6.object.is-frozen');
|
||||||
|
require('../modules/es6.object.is-sealed');
|
||||||
|
require('../modules/es6.object.is-extensible');
|
||||||
|
require('../modules/es6.object.assign');
|
||||||
|
require('../modules/es6.object.is');
|
||||||
|
require('../modules/es6.object.set-prototype-of');
|
||||||
|
require('../modules/es6.object.to-string');
|
||||||
|
require('../modules/es6.function.bind');
|
||||||
|
require('../modules/es6.function.name');
|
||||||
|
require('../modules/es6.function.has-instance');
|
||||||
|
require('../modules/es6.parse-int');
|
||||||
|
require('../modules/es6.parse-float');
|
||||||
|
require('../modules/es6.number.constructor');
|
||||||
|
require('../modules/es6.number.to-fixed');
|
||||||
|
require('../modules/es6.number.to-precision');
|
||||||
|
require('../modules/es6.number.epsilon');
|
||||||
|
require('../modules/es6.number.is-finite');
|
||||||
|
require('../modules/es6.number.is-integer');
|
||||||
|
require('../modules/es6.number.is-nan');
|
||||||
|
require('../modules/es6.number.is-safe-integer');
|
||||||
|
require('../modules/es6.number.max-safe-integer');
|
||||||
|
require('../modules/es6.number.min-safe-integer');
|
||||||
|
require('../modules/es6.number.parse-float');
|
||||||
|
require('../modules/es6.number.parse-int');
|
||||||
|
require('../modules/es6.math.acosh');
|
||||||
|
require('../modules/es6.math.asinh');
|
||||||
|
require('../modules/es6.math.atanh');
|
||||||
|
require('../modules/es6.math.cbrt');
|
||||||
|
require('../modules/es6.math.clz32');
|
||||||
|
require('../modules/es6.math.cosh');
|
||||||
|
require('../modules/es6.math.expm1');
|
||||||
|
require('../modules/es6.math.fround');
|
||||||
|
require('../modules/es6.math.hypot');
|
||||||
|
require('../modules/es6.math.imul');
|
||||||
|
require('../modules/es6.math.log10');
|
||||||
|
require('../modules/es6.math.log1p');
|
||||||
|
require('../modules/es6.math.log2');
|
||||||
|
require('../modules/es6.math.sign');
|
||||||
|
require('../modules/es6.math.sinh');
|
||||||
|
require('../modules/es6.math.tanh');
|
||||||
|
require('../modules/es6.math.trunc');
|
||||||
|
require('../modules/es6.string.from-code-point');
|
||||||
|
require('../modules/es6.string.raw');
|
||||||
|
require('../modules/es6.string.trim');
|
||||||
|
require('../modules/es6.string.iterator');
|
||||||
|
require('../modules/es6.string.code-point-at');
|
||||||
|
require('../modules/es6.string.ends-with');
|
||||||
|
require('../modules/es6.string.includes');
|
||||||
|
require('../modules/es6.string.repeat');
|
||||||
|
require('../modules/es6.string.starts-with');
|
||||||
|
require('../modules/es6.string.anchor');
|
||||||
|
require('../modules/es6.string.big');
|
||||||
|
require('../modules/es6.string.blink');
|
||||||
|
require('../modules/es6.string.bold');
|
||||||
|
require('../modules/es6.string.fixed');
|
||||||
|
require('../modules/es6.string.fontcolor');
|
||||||
|
require('../modules/es6.string.fontsize');
|
||||||
|
require('../modules/es6.string.italics');
|
||||||
|
require('../modules/es6.string.link');
|
||||||
|
require('../modules/es6.string.small');
|
||||||
|
require('../modules/es6.string.strike');
|
||||||
|
require('../modules/es6.string.sub');
|
||||||
|
require('../modules/es6.string.sup');
|
||||||
|
require('../modules/es6.date.now');
|
||||||
|
require('../modules/es6.date.to-json');
|
||||||
|
require('../modules/es6.date.to-iso-string');
|
||||||
|
require('../modules/es6.date.to-string');
|
||||||
|
require('../modules/es6.date.to-primitive');
|
||||||
|
require('../modules/es6.array.is-array');
|
||||||
|
require('../modules/es6.array.from');
|
||||||
|
require('../modules/es6.array.of');
|
||||||
|
require('../modules/es6.array.join');
|
||||||
|
require('../modules/es6.array.slice');
|
||||||
|
require('../modules/es6.array.sort');
|
||||||
|
require('../modules/es6.array.for-each');
|
||||||
|
require('../modules/es6.array.map');
|
||||||
|
require('../modules/es6.array.filter');
|
||||||
|
require('../modules/es6.array.some');
|
||||||
|
require('../modules/es6.array.every');
|
||||||
|
require('../modules/es6.array.reduce');
|
||||||
|
require('../modules/es6.array.reduce-right');
|
||||||
|
require('../modules/es6.array.index-of');
|
||||||
|
require('../modules/es6.array.last-index-of');
|
||||||
|
require('../modules/es6.array.copy-within');
|
||||||
|
require('../modules/es6.array.fill');
|
||||||
|
require('../modules/es6.array.find');
|
||||||
|
require('../modules/es6.array.find-index');
|
||||||
|
require('../modules/es6.array.species');
|
||||||
|
require('../modules/es6.array.iterator');
|
||||||
|
require('../modules/es6.regexp.constructor');
|
||||||
|
require('../modules/es6.regexp.exec');
|
||||||
|
require('../modules/es6.regexp.to-string');
|
||||||
|
require('../modules/es6.regexp.flags');
|
||||||
|
require('../modules/es6.regexp.match');
|
||||||
|
require('../modules/es6.regexp.replace');
|
||||||
|
require('../modules/es6.regexp.search');
|
||||||
|
require('../modules/es6.regexp.split');
|
||||||
|
require('../modules/es6.promise');
|
||||||
|
require('../modules/es6.map');
|
||||||
|
require('../modules/es6.set');
|
||||||
|
require('../modules/es6.weak-map');
|
||||||
|
require('../modules/es6.weak-set');
|
||||||
|
require('../modules/es6.typed.array-buffer');
|
||||||
|
require('../modules/es6.typed.data-view');
|
||||||
|
require('../modules/es6.typed.int8-array');
|
||||||
|
require('../modules/es6.typed.uint8-array');
|
||||||
|
require('../modules/es6.typed.uint8-clamped-array');
|
||||||
|
require('../modules/es6.typed.int16-array');
|
||||||
|
require('../modules/es6.typed.uint16-array');
|
||||||
|
require('../modules/es6.typed.int32-array');
|
||||||
|
require('../modules/es6.typed.uint32-array');
|
||||||
|
require('../modules/es6.typed.float32-array');
|
||||||
|
require('../modules/es6.typed.float64-array');
|
||||||
|
require('../modules/es6.reflect.apply');
|
||||||
|
require('../modules/es6.reflect.construct');
|
||||||
|
require('../modules/es6.reflect.define-property');
|
||||||
|
require('../modules/es6.reflect.delete-property');
|
||||||
|
require('../modules/es6.reflect.enumerate');
|
||||||
|
require('../modules/es6.reflect.get');
|
||||||
|
require('../modules/es6.reflect.get-own-property-descriptor');
|
||||||
|
require('../modules/es6.reflect.get-prototype-of');
|
||||||
|
require('../modules/es6.reflect.has');
|
||||||
|
require('../modules/es6.reflect.is-extensible');
|
||||||
|
require('../modules/es6.reflect.own-keys');
|
||||||
|
require('../modules/es6.reflect.prevent-extensions');
|
||||||
|
require('../modules/es6.reflect.set');
|
||||||
|
require('../modules/es6.reflect.set-prototype-of');
|
||||||
|
module.exports = require('../modules/_core');
|
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/array/flat-map.js
generated
vendored
Normal file
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/array/flat-map.js
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
require('../../modules/es7.array.flat-map');
|
||||||
|
module.exports = require('../../modules/_core').Array.flatMap;
|
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/array/includes.js
generated
vendored
Normal file
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/array/includes.js
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
require('../../modules/es7.array.includes');
|
||||||
|
module.exports = require('../../modules/_core').Array.includes;
|
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/object/entries.js
generated
vendored
Normal file
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/object/entries.js
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
require('../../modules/es7.object.entries');
|
||||||
|
module.exports = require('../../modules/_core').Object.entries;
|
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/object/get-own-property-descriptors.js
generated
vendored
Normal file
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/object/get-own-property-descriptors.js
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
require('../../modules/es7.object.get-own-property-descriptors');
|
||||||
|
module.exports = require('../../modules/_core').Object.getOwnPropertyDescriptors;
|
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/object/values.js
generated
vendored
Normal file
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/object/values.js
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
require('../../modules/es7.object.values');
|
||||||
|
module.exports = require('../../modules/_core').Object.values;
|
4
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/promise/finally.js
generated
vendored
Normal file
4
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/promise/finally.js
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
'use strict';
|
||||||
|
require('../../modules/es6.promise');
|
||||||
|
require('../../modules/es7.promise.finally');
|
||||||
|
module.exports = require('../../modules/_core').Promise['finally'];
|
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/string/pad-end.js
generated
vendored
Normal file
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/string/pad-end.js
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
require('../../modules/es7.string.pad-end');
|
||||||
|
module.exports = require('../../modules/_core').String.padEnd;
|
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/string/pad-start.js
generated
vendored
Normal file
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/string/pad-start.js
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
require('../../modules/es7.string.pad-start');
|
||||||
|
module.exports = require('../../modules/_core').String.padStart;
|
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/string/trim-end.js
generated
vendored
Normal file
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/string/trim-end.js
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
require('../../modules/es7.string.trim-right');
|
||||||
|
module.exports = require('../../modules/_core').String.trimRight;
|
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/string/trim-start.js
generated
vendored
Normal file
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/string/trim-start.js
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
require('../../modules/es7.string.trim-left');
|
||||||
|
module.exports = require('../../modules/_core').String.trimLeft;
|
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/symbol/async-iterator.js
generated
vendored
Normal file
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/symbol/async-iterator.js
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
require('../../modules/es7.symbol.async-iterator');
|
||||||
|
module.exports = require('../../modules/_wks-ext').f('asyncIterator');
|
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/fn/global.js
generated
vendored
Normal file
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/fn/global.js
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
require('../modules/es7.global');
|
||||||
|
module.exports = require('../modules/_core').global;
|
4
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/modules/_a-function.js
generated
vendored
Normal file
4
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/modules/_a-function.js
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module.exports = function (it) {
|
||||||
|
if (typeof it != 'function') throw TypeError(it + ' is not a function!');
|
||||||
|
return it;
|
||||||
|
};
|
5
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/modules/_an-object.js
generated
vendored
Normal file
5
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/modules/_an-object.js
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
var isObject = require('./_is-object');
|
||||||
|
module.exports = function (it) {
|
||||||
|
if (!isObject(it)) throw TypeError(it + ' is not an object!');
|
||||||
|
return it;
|
||||||
|
};
|
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/modules/_core.js
generated
vendored
Normal file
2
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/modules/_core.js
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
var core = module.exports = { version: '2.6.12' };
|
||||||
|
if (typeof __e == 'number') __e = core; // eslint-disable-line no-undef
|
20
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/modules/_ctx.js
generated
vendored
Normal file
20
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/modules/_ctx.js
generated
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// optional / simple context binding
|
||||||
|
var aFunction = require('./_a-function');
|
||||||
|
module.exports = function (fn, that, length) {
|
||||||
|
aFunction(fn);
|
||||||
|
if (that === undefined) return fn;
|
||||||
|
switch (length) {
|
||||||
|
case 1: return function (a) {
|
||||||
|
return fn.call(that, a);
|
||||||
|
};
|
||||||
|
case 2: return function (a, b) {
|
||||||
|
return fn.call(that, a, b);
|
||||||
|
};
|
||||||
|
case 3: return function (a, b, c) {
|
||||||
|
return fn.call(that, a, b, c);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return function (/* ...args */) {
|
||||||
|
return fn.apply(that, arguments);
|
||||||
|
};
|
||||||
|
};
|
4
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/modules/_descriptors.js
generated
vendored
Normal file
4
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/modules/_descriptors.js
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Thank's IE8 for his funny defineProperty
|
||||||
|
module.exports = !require('./_fails')(function () {
|
||||||
|
return Object.defineProperty({}, 'a', { get: function () { return 7; } }).a != 7;
|
||||||
|
});
|
7
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/modules/_dom-create.js
generated
vendored
Normal file
7
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/modules/_dom-create.js
generated
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
var isObject = require('./_is-object');
|
||||||
|
var document = require('./_global').document;
|
||||||
|
// typeof document.createElement is 'object' in old IE
|
||||||
|
var is = isObject(document) && isObject(document.createElement);
|
||||||
|
module.exports = function (it) {
|
||||||
|
return is ? document.createElement(it) : {};
|
||||||
|
};
|
62
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/modules/_export.js
generated
vendored
Normal file
62
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/modules/_export.js
generated
vendored
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
var global = require('./_global');
|
||||||
|
var core = require('./_core');
|
||||||
|
var ctx = require('./_ctx');
|
||||||
|
var hide = require('./_hide');
|
||||||
|
var has = require('./_has');
|
||||||
|
var PROTOTYPE = 'prototype';
|
||||||
|
|
||||||
|
var $export = function (type, name, source) {
|
||||||
|
var IS_FORCED = type & $export.F;
|
||||||
|
var IS_GLOBAL = type & $export.G;
|
||||||
|
var IS_STATIC = type & $export.S;
|
||||||
|
var IS_PROTO = type & $export.P;
|
||||||
|
var IS_BIND = type & $export.B;
|
||||||
|
var IS_WRAP = type & $export.W;
|
||||||
|
var exports = IS_GLOBAL ? core : core[name] || (core[name] = {});
|
||||||
|
var expProto = exports[PROTOTYPE];
|
||||||
|
var target = IS_GLOBAL ? global : IS_STATIC ? global[name] : (global[name] || {})[PROTOTYPE];
|
||||||
|
var key, own, out;
|
||||||
|
if (IS_GLOBAL) source = name;
|
||||||
|
for (key in source) {
|
||||||
|
// contains in native
|
||||||
|
own = !IS_FORCED && target && target[key] !== undefined;
|
||||||
|
if (own && has(exports, key)) continue;
|
||||||
|
// export native or passed
|
||||||
|
out = own ? target[key] : source[key];
|
||||||
|
// prevent global pollution for namespaces
|
||||||
|
exports[key] = IS_GLOBAL && typeof target[key] != 'function' ? source[key]
|
||||||
|
// bind timers to global for call from export context
|
||||||
|
: IS_BIND && own ? ctx(out, global)
|
||||||
|
// wrap global constructors for prevent change them in library
|
||||||
|
: IS_WRAP && target[key] == out ? (function (C) {
|
||||||
|
var F = function (a, b, c) {
|
||||||
|
if (this instanceof C) {
|
||||||
|
switch (arguments.length) {
|
||||||
|
case 0: return new C();
|
||||||
|
case 1: return new C(a);
|
||||||
|
case 2: return new C(a, b);
|
||||||
|
} return new C(a, b, c);
|
||||||
|
} return C.apply(this, arguments);
|
||||||
|
};
|
||||||
|
F[PROTOTYPE] = C[PROTOTYPE];
|
||||||
|
return F;
|
||||||
|
// make static versions for prototype methods
|
||||||
|
})(out) : IS_PROTO && typeof out == 'function' ? ctx(Function.call, out) : out;
|
||||||
|
// export proto methods to core.%CONSTRUCTOR%.methods.%NAME%
|
||||||
|
if (IS_PROTO) {
|
||||||
|
(exports.virtual || (exports.virtual = {}))[key] = out;
|
||||||
|
// export proto methods to core.%CONSTRUCTOR%.prototype.%NAME%
|
||||||
|
if (type & $export.R && expProto && !expProto[key]) hide(expProto, key, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// type bitmap
|
||||||
|
$export.F = 1; // forced
|
||||||
|
$export.G = 2; // global
|
||||||
|
$export.S = 4; // static
|
||||||
|
$export.P = 8; // proto
|
||||||
|
$export.B = 16; // bind
|
||||||
|
$export.W = 32; // wrap
|
||||||
|
$export.U = 64; // safe
|
||||||
|
$export.R = 128; // real proto method for `library`
|
||||||
|
module.exports = $export;
|
7
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/modules/_fails.js
generated
vendored
Normal file
7
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/library/modules/_fails.js
generated
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = function (exec) {
|
||||||
|
try {
|
||||||
|
return !!exec();
|
||||||
|
} catch (e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user