Grabbed dndbeyond's source code
``` ~/go/bin/sourcemapper -output ddb -jsurl https://media.dndbeyond.com/character-app/static/js/main.90aa78c5.js ```
This commit is contained in:
@@ -0,0 +1,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>
|
||||
);
|
||||
};
|
||||
+116
@@ -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>
|
||||
);
|
||||
};
|
||||
+57
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user