New source found from dndbeyond.com
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
import ArrowDown from "@dndbeyond/fontawesome-cache/svgs/regular/arrow-down-to-line.svg";
|
||||
|
||||
import { Button } from "~/components/Button";
|
||||
import { MaxCharactersDialog } from "~/components/MaxCharactersDialog";
|
||||
import config from "~/config";
|
||||
import { UserPreferenceProvider } from "~/tools/js/smartComponents/UserPreference";
|
||||
|
||||
import { logUnlockSubscribeClicked } from "../../../../helpers/analytics";
|
||||
import { byProp, createSortValue } from "../../../../helpers/sortUtils";
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
SortOrderEnum,
|
||||
SortTypeEnum,
|
||||
} from "../../../../types";
|
||||
import { UserPreferencesProvider } from "../../contexts/UserPreferences";
|
||||
import { ApiStatusIndicator } from "../ApiStatusIndicator";
|
||||
import { CharacterCard } from "../CharacterCard";
|
||||
import { SubscriptionBanner } from "../SubscriptionBanner";
|
||||
@@ -76,6 +77,7 @@ export const CharacterGrid: React.FC<CharacterGridProps> = ({
|
||||
}) => {
|
||||
const userId = useUserId();
|
||||
const subscriptionTier = useSubscriptionTier();
|
||||
const { search: searchParams } = useLocation();
|
||||
|
||||
const [sortPreference, setSortPreference] = useLocalStorage<string>(
|
||||
`MY_CHARACTERS_SORT_PREFERENCE:${userId}`,
|
||||
@@ -190,15 +192,17 @@ export const CharacterGrid: React.FC<CharacterGridProps> = ({
|
||||
maxCharacterSlotsAllowed !== null &&
|
||||
characterCount >= maxCharacterSlotsAllowed;
|
||||
|
||||
const createCharacterHref = !hasMaxCharacters
|
||||
? `${config.basePathname}/builder`
|
||||
: undefined;
|
||||
const createCharacterOnClick = hasMaxCharacters
|
||||
? () => setIsMaxCharacterMessageOpen(true)
|
||||
: undefined;
|
||||
const createCharacterHref =
|
||||
!hasMaxCharacters && !hasLockedCharacters
|
||||
? `${config.basePathname}/builder${searchParams}`
|
||||
: undefined;
|
||||
const createCharacterOnClick =
|
||||
hasMaxCharacters || hasLockedCharacters
|
||||
? () => setIsMaxCharacterMessageOpen(true)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<UserPreferenceProvider>
|
||||
<UserPreferencesProvider>
|
||||
<div
|
||||
className={[className, "ddb-characters-listing"]
|
||||
.filter(Boolean)
|
||||
@@ -403,8 +407,9 @@ export const CharacterGrid: React.FC<CharacterGridProps> = ({
|
||||
<MaxCharactersDialog
|
||||
open={isMaxCharacterMessageOpen}
|
||||
onClose={() => setIsMaxCharacterMessageOpen(false)}
|
||||
hasLockedCharacters={hasLockedCharacters}
|
||||
/>
|
||||
</div>
|
||||
</UserPreferenceProvider>
|
||||
</UserPreferencesProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,11 +14,11 @@ import { Button } from "@dndbeyond/ttui/components/Button";
|
||||
import { Select } from "@dndbeyond/ttui/components/Select";
|
||||
|
||||
import PreferenceUpdateLocation from "~/tools/js/Shared/constants/PreferenceUpdateLocation";
|
||||
import CharacterSettingsModal from "~/tools/js/smartComponents/CharacterSettingsModal";
|
||||
import { SortOrderEnum, SortTypeEnum } from "~/types";
|
||||
|
||||
import { logListingSortChanged } from "../../../../../helpers/analytics";
|
||||
import { createSortValue } from "../../../../../helpers/sortUtils";
|
||||
import { CharacterSettingsModal } from "../../CharacterSettingsModal";
|
||||
import { SortState } from "../CharacterGrid";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
@@ -150,8 +150,7 @@ export const SearchSort: FC<SearchSortProps> = ({
|
||||
</Button>
|
||||
<CharacterSettingsModal
|
||||
open={showSettingsModal}
|
||||
updateLocation={PreferenceUpdateLocation.CharacterListing}
|
||||
handleClose={() => setShowSettingsModal(false)}
|
||||
onClose={() => setShowSettingsModal(false)}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
import clsx from "clsx";
|
||||
import { FC } from "react";
|
||||
|
||||
import { RuleDataUtils } from "@dndbeyond/character-rules-engine";
|
||||
import GearIcon from "@dndbeyond/fontawesome-cache/svgs/regular/gear.svg";
|
||||
import CloseIcon from "@dndbeyond/fontawesome-cache/svgs/regular/xmark.svg";
|
||||
|
||||
import { Button } from "~/components/Button";
|
||||
import { CheckboxGroup, CheckboxInfo } from "~/components/CheckboxGroup";
|
||||
import { Dialog, DialogProps } from "~/components/Dialog";
|
||||
import { PrivacyTypeRadio } from "~/components/PrivacyTypeRadio";
|
||||
import { SourceCategoryDescription } from "~/constants";
|
||||
import { PreferenceAbilityScoreDisplayTypeEnum } from "~/constants";
|
||||
import { useFeatureFlags } from "~/contexts/FeatureFlag";
|
||||
import { generateCharacterPreferences } from "~/helpers/generateCharacterPreferences";
|
||||
import PreferenceUpdateLocation from "~/tools/js/Shared/constants/PreferenceUpdateLocation";
|
||||
import AbilitySummary from "~/tools/js/smartComponents/AbilitySummary";
|
||||
|
||||
import { useUserPreferences } from "../../contexts/UserPreferences";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
type AbilityScoreDisplayTypes = {
|
||||
label: string;
|
||||
value: number;
|
||||
}[];
|
||||
|
||||
export interface CharacterSettingsModalProps extends DialogProps {}
|
||||
|
||||
export const CharacterSettingsModal: FC<CharacterSettingsModalProps> = ({
|
||||
className,
|
||||
open,
|
||||
onClose,
|
||||
...props
|
||||
}) => {
|
||||
const {
|
||||
privacyType,
|
||||
isDarkModeEnabled,
|
||||
abilityScoreDisplayType,
|
||||
updatePreferences,
|
||||
ruleData,
|
||||
defaultEnabledSourceCategories,
|
||||
isHomebrewEnabled,
|
||||
} = useUserPreferences();
|
||||
|
||||
const { featureFlags } = useFeatureFlags();
|
||||
|
||||
const updateLocation = PreferenceUpdateLocation.CharacterListing;
|
||||
|
||||
// Build an array of display types from enum
|
||||
const abilityScoreDisplayTypes: AbilityScoreDisplayTypes = Object.entries(
|
||||
PreferenceAbilityScoreDisplayTypeEnum
|
||||
)
|
||||
// Remove any duplicates which don't have numeric values
|
||||
.filter((opt) => opt.some((val) => typeof val === "number"))
|
||||
// Map to object with label and value formatting
|
||||
.map(([label, value]) => ({
|
||||
label: label.replace("_", " "),
|
||||
value: Number(value),
|
||||
}));
|
||||
|
||||
const preferences = generateCharacterPreferences(featureFlags);
|
||||
|
||||
const getTheme = (darkMode: boolean) => ({
|
||||
name: "DDB Red",
|
||||
backgroundColor: darkMode ? "#10161ADB" : "#FEFEFE",
|
||||
isDefault: true,
|
||||
themeColor: "#C53131",
|
||||
themeColorId: null,
|
||||
isDarkMode: darkMode || false,
|
||||
});
|
||||
|
||||
const rollContext = {
|
||||
entityId: "52962664",
|
||||
entityType: "character",
|
||||
name: "Stor Hornraven",
|
||||
avatarUrl:
|
||||
"https://stg.dndbeyond.com/avatars/18589/680/1581111423-52962664.jpeg?width=150&height=150&fit=crop&quality=95&auto=webp",
|
||||
};
|
||||
|
||||
const handleSourceCategoryChange = (
|
||||
sourceId: number,
|
||||
isActive: boolean
|
||||
): void => {
|
||||
const newSourceCategories = {
|
||||
...defaultEnabledSourceCategories,
|
||||
[sourceId]: isActive,
|
||||
};
|
||||
|
||||
updatePreferences({
|
||||
privacyType,
|
||||
isDarkModeEnabled,
|
||||
abilityScoreDisplayType,
|
||||
updateLocation,
|
||||
defaultEnabledSourceCategories: newSourceCategories,
|
||||
isHomebrewEnabled,
|
||||
});
|
||||
};
|
||||
|
||||
let sourceToggles: Array<CheckboxInfo> = [];
|
||||
let partneredSourceCheckboxes: Array<CheckboxInfo> = [];
|
||||
let allPartneredSources: Array<number> = [];
|
||||
|
||||
if (ruleData) {
|
||||
const activeSourceCategories = Object.keys(defaultEnabledSourceCategories)
|
||||
.filter((key) => defaultEnabledSourceCategories[key])
|
||||
.map(Number);
|
||||
RuleDataUtils.getSourceCategories(ruleData).forEach((sourceCategory) => {
|
||||
if (!sourceCategory.isToggleable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const checkbox: CheckboxInfo = {
|
||||
label: `${sourceCategory.name}`,
|
||||
initiallyEnabled: activeSourceCategories.includes(sourceCategory.id),
|
||||
onChange: (e) =>
|
||||
handleSourceCategoryChange(sourceCategory.id, e.target.checked),
|
||||
sortOrder: sourceCategory.sortOrder,
|
||||
description: sourceCategory.description ?? "",
|
||||
};
|
||||
|
||||
if (sourceCategory.isPartneredContent) {
|
||||
delete checkbox.description;
|
||||
partneredSourceCheckboxes.push(checkbox);
|
||||
allPartneredSources.push(sourceCategory.id);
|
||||
} else {
|
||||
sourceToggles.push(checkbox);
|
||||
}
|
||||
});
|
||||
|
||||
//Add Homebrew to sources for display
|
||||
sourceToggles.push({
|
||||
label: "Homebrew",
|
||||
initiallyEnabled: isHomebrewEnabled,
|
||||
description: SourceCategoryDescription.homebrew,
|
||||
onChange: (e) => {
|
||||
updatePreferences({
|
||||
privacyType,
|
||||
isDarkModeEnabled,
|
||||
abilityScoreDisplayType,
|
||||
updateLocation,
|
||||
defaultEnabledSourceCategories,
|
||||
isHomebrewEnabled: e.target.checked,
|
||||
});
|
||||
},
|
||||
sortOrder: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const handleChangeUnderdarkMode = () => {
|
||||
const isChecked = !isDarkModeEnabled;
|
||||
|
||||
updatePreferences({
|
||||
privacyType,
|
||||
abilityScoreDisplayType,
|
||||
updateLocation,
|
||||
isDarkModeEnabled: isChecked,
|
||||
defaultEnabledSourceCategories,
|
||||
isHomebrewEnabled,
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeDisplayType = (value: number) => {
|
||||
if (!isNaN(value))
|
||||
updatePreferences({
|
||||
privacyType,
|
||||
isDarkModeEnabled,
|
||||
updateLocation,
|
||||
abilityScoreDisplayType: value,
|
||||
defaultEnabledSourceCategories,
|
||||
isHomebrewEnabled,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
className={clsx([styles.characterSettingsModal, className])}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
modal
|
||||
{...props}
|
||||
>
|
||||
<button
|
||||
className={styles.closeButton}
|
||||
onClick={onClose}
|
||||
aria-label="Close the Character Settings Modal"
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
<header className={styles.header}>
|
||||
<GearIcon />
|
||||
<h2>Default Character Settings</h2>
|
||||
</header>
|
||||
<section className={styles.body}>
|
||||
<p className={styles.helpText}>
|
||||
These settings apply defaults to all new Characters you create. Any
|
||||
settings you apply to a specific Character will override the choices
|
||||
you make here.
|
||||
</p>
|
||||
<CheckboxGroup
|
||||
className={styles.checkboxGroup}
|
||||
title="Sources"
|
||||
description={SourceCategoryDescription.official}
|
||||
checkboxes={sourceToggles}
|
||||
name="sources"
|
||||
showAccordion={true}
|
||||
accordionDefaultOpen={true}
|
||||
/>
|
||||
<CheckboxGroup
|
||||
className={styles.checkboxGroup}
|
||||
title="Partnered Content"
|
||||
description={SourceCategoryDescription.partnered}
|
||||
checkboxes={partneredSourceCheckboxes}
|
||||
name="partnered-content"
|
||||
showAccordion={true}
|
||||
accordionSummary="Choose Partners"
|
||||
/>
|
||||
<PrivacyTypeRadio
|
||||
className={styles.privacyTypeRadio}
|
||||
initialValue={privacyType}
|
||||
onChange={(e) =>
|
||||
updatePreferences({
|
||||
isDarkModeEnabled,
|
||||
abilityScoreDisplayType,
|
||||
updateLocation,
|
||||
privacyType: parseInt(e.target.value),
|
||||
defaultEnabledSourceCategories,
|
||||
isHomebrewEnabled,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<div className={styles.displayGrid}>
|
||||
<div className={styles.displayFields}>
|
||||
<CheckboxGroup
|
||||
className={styles.checkboxGroup}
|
||||
title="Display"
|
||||
name="underdark-mode"
|
||||
data-testid="underdark-mode-toggle"
|
||||
checkboxes={[
|
||||
{
|
||||
label: "Underdark Mode",
|
||||
initiallyEnabled: isDarkModeEnabled,
|
||||
onChange: handleChangeUnderdarkMode,
|
||||
description: "Activate dark mode for Character Sheets.",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className={styles.abilityScores}>
|
||||
<p className={styles.label}>Ability Scores / Modifiers</p>
|
||||
<p className={styles.description}>
|
||||
Reverse the display of Ability Scores and Modifiers.
|
||||
</p>
|
||||
<div className={styles.buttonGroup}>
|
||||
{abilityScoreDisplayTypes
|
||||
.reverse()
|
||||
.map(({ label, value }, i) => (
|
||||
<Button
|
||||
className={clsx([
|
||||
styles.buttonGroupButton,
|
||||
abilityScoreDisplayType === value &&
|
||||
styles.buttonGroupSelected,
|
||||
])}
|
||||
onClick={() => handleChangeDisplayType(Number(value))}
|
||||
variant="outline"
|
||||
size="small"
|
||||
key={`ability-score-display-type-${value}`}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.displayPreview}>
|
||||
<div className={styles.previewContainer}>
|
||||
<p className={styles.previewText}>Preview</p>
|
||||
<AbilitySummary
|
||||
ability={{ name: "Strength", score: 14, modifier: 2 }}
|
||||
preferences={{
|
||||
...preferences,
|
||||
abilityScoreDisplayType: abilityScoreDisplayType,
|
||||
}}
|
||||
theme={getTheme(isDarkModeEnabled || false)}
|
||||
onClick={(e) => e.getBaseScore()}
|
||||
rollContext={rollContext}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -96,7 +96,6 @@ export const MyCharacters = ({ characterQuery }: Props) => {
|
||||
const { isLoading, data, refetch, error } = characterQuery;
|
||||
const { characters, hasLockedCharacters, maxCharacterSlotsAllowed } =
|
||||
generateAppData(userName, data);
|
||||
// Check for max active characters
|
||||
const hasMaxCharacters =
|
||||
characters?.filter(
|
||||
(character) => getStatus(character) === CharacterStatusEnum.Active
|
||||
|
||||
Reference in New Issue
Block a user