Grabbed dndbeyond's source code

```
~/go/bin/sourcemapper -output ddb -jsurl https://media.dndbeyond.com/character-app/static/js/main.90aa78c5.js
```
This commit is contained in:
2025-05-28 11:50:03 -07:00
commit 8df9031d27
3592 changed files with 319051 additions and 0 deletions
@@ -0,0 +1,50 @@
import { useState } from "react";
import { Button } from "@dndbeyond/ttui/components/Button";
import { CharacterData } from "~/types";
import { logUnlockCharacterUnlocked } from "../../../../../helpers/analytics";
import { UnlockConfirmation } from "../../CharacterGrid/UnlockConfirmation";
import { ConfirmationModal } from "../../ConfirmationModal";
interface ActivateButtonProps {
character: CharacterData;
activateCharacter: (character: CharacterData) => void;
maxCharacterSlotsAllowed: number | null;
}
export const ActivateButton: React.FC<ActivateButtonProps> = ({
character,
activateCharacter,
maxCharacterSlotsAllowed,
}) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<ConfirmationModal
isOpen={isOpen}
onClose={() => {
setIsOpen(false);
}}
onConfirm={() => {
activateCharacter(character);
logUnlockCharacterUnlocked();
setIsOpen(false);
}}
title="Activate these characters?"
confirmText="Activate"
message={
<UnlockConfirmation
charactersToUnlock={[character]}
maxCharacterSlotsAllowed={maxCharacterSlotsAllowed}
/>
}
/>
<Button size="small" variant="text" onClick={() => setIsOpen(true)}>
Activate
</Button>
</div>
);
};
@@ -0,0 +1,239 @@
import clsx from "clsx";
import { FC } from "react";
import Lock from "@dndbeyond/fontawesome-cache/svgs/regular/lock.svg";
import Check from "@dndbeyond/fontawesome-cache/svgs/sharp-regular/check.svg";
import { Button } from "@dndbeyond/ttui/components/Button";
import useApiCall from "~/hooks/useApiCall";
import {
logCharacterCampaignClicked,
logUnlockCharacterLocked,
logUnlockCharacterUnlocked,
} from "../../../../helpers/analytics";
import {
copyCharacter as copyCharacterApi,
deleteCharacter as deleteCharacterApi,
leaveCampaign as leaveCampaignApi,
activateCharacter as activateCharacterApi,
} from "../../../../helpers/characterServiceApi";
import {
getAvatarUrl,
getBackdropUrl,
getCampaignDetailsLink,
getCampaignName,
getCoverImageUrl,
getDetailsLink,
getIsAssigned,
getName,
getSecondaryInfo,
getStatus,
getStatusSlug,
isInCampaign,
} from "../../../../state/selectors/characterUtils";
import { CharacterData, CharacterStatusEnum } from "../../../../types";
import { ApiStatusIndicator } from "../ApiStatusIndicator";
import "./CharacterCard.scss";
import { CharacterCardLinks } from "./CharacterCardLinks";
import { LeaveCampaignButton } from "./LeaveCampaignButton";
import styles from "./styles.module.css";
export interface CharacterCardProps {
character: CharacterData;
reloadListing: () => void;
lockCharacter: (character: CharacterData) => void;
unlockCharacter: (character: CharacterData) => void;
canUnlockMore: boolean;
hasLockedCharacters: boolean;
overCharacterLimit: boolean;
maxCharacterSlotsAllowed: number | null;
currentCharacterCount: number;
}
export const CharacterCard: FC<CharacterCardProps> = ({
character,
reloadListing,
lockCharacter,
unlockCharacter,
canUnlockMore,
hasLockedCharacters,
overCharacterLimit,
maxCharacterSlotsAllowed,
currentCharacterCount,
}) => {
const [copyCharacter, isCopyingCharacter, errorCopyingCharacter] = useApiCall(
copyCharacterApi,
reloadListing
);
const [deleteCharacter, isDeletingCharacter, errorDeletingCharacter] =
useApiCall(deleteCharacterApi, reloadListing);
const [leaveCampaign, isLeavingCampaign, errorLeavingCampaign] = useApiCall(
leaveCampaignApi,
reloadListing
);
const [activateCharacter, isActivatingCharacter, errorActivatingCharacter] =
useApiCall(activateCharacterApi, reloadListing);
const isLocked = getStatus(character) === CharacterStatusEnum.Locked;
const handleToggleLock = () => {
if (isLocked) {
unlockCharacter(character);
logUnlockCharacterUnlocked();
} else {
lockCharacter(character);
logUnlockCharacterLocked();
}
};
return (
<li className="ddb-campaigns-character-card-wrapper j-characters-listing__item">
<div
className={clsx([`status-${getStatusSlug(character)}`, styles.card])}
>
<div className="ddb-campaigns-character-card-header">
{getBackdropUrl(character) ? (
<div
className="ddb-campaigns-character-card-header-cover-image ddb-campaigns-character-card-header-cover-image-user-backdrop"
style={{
backgroundImage: `url(${getBackdropUrl(character)})`,
}}
/>
) : (
<div
className="ddb-campaigns-character-card-header-cover-image"
style={{
backgroundImage: `url(${getCoverImageUrl(character)})`,
}}
/>
)}
<div className="ddb-campaigns-character-card-header-upper">
{!hasLockedCharacters &&
getStatus(character) === CharacterStatusEnum.Active && (
// eslint-disable-next-line jsx-a11y/anchor-has-content
<a
href={getDetailsLink(character)}
className="ddb-campaigns-character-card-header-upper-details-link"
// There already is a view link - this is just making the top area clickable so it shouldn't be tabbed to or announced
tabIndex={-1}
aria-hidden={true}
/>
)}
<div className="ddb-campaigns-character-card-header-upper-portrait">
{getAvatarUrl(character) ? (
<div
className="image user-selected-avatar"
style={{
backgroundImage: `url(${getAvatarUrl(character)})`,
}}
/>
) : (
<div className="image default-character-avatar" />
)}
</div>
<div className="ddb-campaigns-character-card-header-upper-character-info">
<h2 className={styles.name}>{getName(character)}</h2>
<div className="ddb-campaigns-character-card-header-upper-character-info-secondary">
{getSecondaryInfo(character)}
</div>
</div>
</div>
</div>
{hasLockedCharacters && (
<Button
className={clsx([styles.cornerButton, isLocked && styles.locked])}
color="primary"
aria-label={isLocked ? "Click to unlock" : "Click to lock"}
onClick={handleToggleLock}
disabled={isLocked && !canUnlockMore}
>
{isLocked ? (
<Lock className={styles.lockIcon} />
) : (
<Check className={styles.checkIcon} />
)}
</Button>
)}
{isInCampaign(character) && (
<div className="ddb-campaigns-character-card-campaign-links">
<div className="ddb-campaigns-character-card-campaign-links-campaign">
<strong>Campaign:</strong>{" "}
{!hasLockedCharacters ? (
<a
className="ddb-campaigns-character-card-campaign-links-campaign-link"
href={getCampaignDetailsLink(character)}
onClick={() => logCharacterCampaignClicked()}
>
{getCampaignName(character)}
</a>
) : (
<span>{getCampaignName(character)}</span>
)}
{!getIsAssigned(character) && " (Unassigned)"}
</div>
<div className="ddb-campaigns-character-card-campaign-links-actions">
{!hasLockedCharacters && (
<LeaveCampaignButton
character={character}
leaveCampaign={() => leaveCampaign(character)}
/>
)}
</div>
</div>
)}
<div className="ddb-campaigns-character-card-footer">
<div
className={[
"ddb-campaigns-character-card-footer-links",
hasLockedCharacters && "is-unlocking",
]
.filter(Boolean)
.join(" ")}
>
<CharacterCardLinks
character={character}
hasLockedCharacters={hasLockedCharacters}
overCharacterLimit={overCharacterLimit}
copyCharacter={() => copyCharacter(character)}
deleteCharacter={() => deleteCharacter(character)}
lockCharacter={lockCharacter}
unlockCharacter={unlockCharacter}
canUnlockMore={canUnlockMore}
maxCharacterSlotsAllowed={maxCharacterSlotsAllowed}
activateCharacter={() => activateCharacter(character)}
currentCharacterCount={currentCharacterCount}
/>
</div>
<ApiStatusIndicator
isLoading={isCopyingCharacter}
error={errorCopyingCharacter}
loadingMessage={`Copying ${getName(character)}`}
errorMessage={`Error copying ${getName(character)}`}
/>
<ApiStatusIndicator
isLoading={isDeletingCharacter}
error={errorDeletingCharacter}
loadingMessage={`Deleting ${getName(character)}`}
errorMessage={`Error deleting ${getName(character)}`}
/>
<ApiStatusIndicator
isLoading={isActivatingCharacter}
error={errorActivatingCharacter}
loadingMessage={`Activating ${getName(character)}`}
errorMessage={`Error activating ${getName(character)}`}
/>
<ApiStatusIndicator
isLoading={isLeavingCampaign}
error={errorLeavingCampaign}
loadingMessage={`Leaving ${getCampaignName(character)}`}
errorMessage={`Error leaving ${getCampaignName(character)}`}
/>
</div>
</div>
</li>
);
};
@@ -0,0 +1,116 @@
import { Button } from "@dndbeyond/ttui/components/Button";
import { CharacterStatusEnum } from "~/types";
import {
logCharacterEditClicked,
logCharacterViewClicked,
logUnlockCharacterLocked,
logUnlockCharacterUnlocked,
} from "../../../../../helpers/analytics";
import {
getDetailsLink,
getEditLink,
getStatus,
} from "../../../../../state/selectors/characterUtils";
import { ActivateButton } from "../ActivateButton";
import type { CharacterCardProps } from "../CharacterCard";
import { CopyButton } from "../CopyButton";
import { DeleteButton } from "../DeleteButton";
import styles from "./styles.module.css";
interface CharacterCardLinksProps
extends Omit<CharacterCardProps, "reloadListing"> {
copyCharacter: () => void;
deleteCharacter: () => void;
activateCharacter: () => void;
}
export const CharacterCardLinks: React.FC<CharacterCardLinksProps> = ({
character,
hasLockedCharacters,
overCharacterLimit,
copyCharacter,
deleteCharacter,
lockCharacter,
unlockCharacter,
canUnlockMore,
maxCharacterSlotsAllowed,
activateCharacter,
currentCharacterCount,
}) => {
const isLocked = getStatus(character) === CharacterStatusEnum.Locked;
const showActivateButton =
maxCharacterSlotsAllowed === null ||
currentCharacterCount < maxCharacterSlotsAllowed;
const handleToggleLock = () => {
if (isLocked) {
unlockCharacter(character);
logUnlockCharacterUnlocked();
} else {
lockCharacter(character);
logUnlockCharacterLocked();
}
};
if (hasLockedCharacters)
return (
<>
<Button
size="small"
variant={isLocked ? "outline" : "solid"}
onClick={handleToggleLock}
disabled={isLocked && !canUnlockMore}
title={
isLocked && !canUnlockMore
? "Max characters selected to unlock. Lock other characters to unlock this character."
: undefined
}
>
{isLocked ? "Select" : "Deselect"}
</Button>
<p className={styles.text}>
{isLocked
? "This Character will be deactivated."
: "This Character will be activated."}
</p>
</>
);
return (
<>
{!isLocked && (
<>
<Button
size="small"
variant="text"
href={getDetailsLink(character)}
onClick={() => logCharacterViewClicked()}
>
View
</Button>
<Button
size="small"
variant="text"
href={getEditLink(character)}
onClick={() => logCharacterEditClicked()}
>
Edit
</Button>
{!overCharacterLimit && (
<CopyButton character={character} copyCharacter={copyCharacter} />
)}
</>
)}
{isLocked && showActivateButton && (
<ActivateButton
character={character}
activateCharacter={activateCharacter}
maxCharacterSlotsAllowed={maxCharacterSlotsAllowed}
/>
)}
<DeleteButton character={character} deleteCharacter={deleteCharacter} />
</>
);
};
@@ -0,0 +1,54 @@
import { useState } from "react";
import { Button } from "@dndbeyond/ttui/components/Button";
import {
logCharacterCopyCancelled,
logCharacterCopyClicked,
logCharacterCopyConfirmed,
} from "../../../../../helpers/analytics";
import { getName } from "../../../../../state/selectors/characterUtils";
import { CharacterData } from "../../../../../types";
import { ConfirmationModal } from "../../ConfirmationModal";
interface CopyProps {
character: CharacterData;
copyCharacter: () => void;
}
export const CopyButton: React.FC<CopyProps> = ({
character,
copyCharacter,
}) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<ConfirmationModal
onClose={() => {
setIsOpen(false);
logCharacterCopyCancelled();
}}
isOpen={isOpen}
onConfirm={() => {
copyCharacter();
logCharacterCopyConfirmed();
setIsOpen(false);
}}
message={`Are you sure you want to copy ${getName(character)}?`}
title="Copy this character?"
confirmText="Copy"
/>
<Button
variant="text"
size="small"
onClick={() => {
logCharacterCopyClicked();
setIsOpen(true);
}}
>
Copy
</Button>
</div>
);
};
@@ -0,0 +1,58 @@
import { useState } from "react";
import { Button } from "@dndbeyond/ttui/components/Button";
import {
logCharacterDeleteCancelled,
logCharacterDeleteClicked,
logCharacterDeleteConfirmed,
} from "../../../../../helpers/analytics";
import { getName } from "../../../../../state/selectors/characterUtils";
import { CharacterData } from "../../../../../types";
import { ConfirmationModal } from "../../ConfirmationModal";
interface DeleteButtonProps {
character: CharacterData;
deleteCharacter: () => void;
}
export const DeleteButton: React.FC<DeleteButtonProps> = ({
character,
deleteCharacter,
}) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<ConfirmationModal
isOpen={isOpen}
onClose={() => {
logCharacterDeleteCancelled();
setIsOpen(false);
}}
onConfirm={() => {
deleteCharacter();
logCharacterDeleteConfirmed();
setIsOpen(false);
}}
message={`To delete ${getName(
character
)}, type the word DELETE into the field below.`}
title="Delete this character?"
typeValueToConfirm="delete"
confirmText="Delete"
/>
<Button
size="small"
variant="text"
color="error"
onClick={() => {
logCharacterDeleteClicked();
setIsOpen(true);
}}
>
Delete
</Button>
</div>
);
};
@@ -0,0 +1,57 @@
import { useState } from "react";
import {
logCharacterLeaveCampaignCancelled,
logCharacterLeaveCampaignClicked,
logCharacterLeaveCampaignConfirmed,
} from "../../../../../helpers/analytics";
import {
getCampaignName,
getName,
} from "../../../../../state/selectors/characterUtils";
import { CharacterData } from "../../../../../types";
import { ConfirmationModal } from "../../ConfirmationModal";
import styles from "./styles.module.css";
interface LeaveCampaignProps {
character: CharacterData;
leaveCampaign: () => void;
}
export const LeaveCampaignButton: React.FC<LeaveCampaignProps> = ({
character,
leaveCampaign,
}) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<ConfirmationModal
isOpen={isOpen}
onClose={() => {
logCharacterLeaveCampaignCancelled();
setIsOpen(false);
}}
onConfirm={() => {
leaveCampaign();
logCharacterLeaveCampaignConfirmed();
setIsOpen(false);
}}
message={`Are you sure you want ${getName(
character
)} to leave ${getCampaignName(character)}?`}
title="Leave this Campaign?"
confirmText="Leave"
/>
<button
className={styles.button}
onClick={() => {
logCharacterLeaveCampaignClicked();
setIsOpen(true);
}}
>
Leave Campaign
</button>
</div>
);
};