New source found from dndbeyond.com
This commit is contained in:
Generated
Vendored
+80
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
arrow,
|
||||
autoPlacement,
|
||||
autoUpdate,
|
||||
FloatingArrow,
|
||||
FloatingPortal,
|
||||
offset as offsetFn,
|
||||
useFloating,
|
||||
type Placement,
|
||||
} from "@floating-ui/react";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
cloneElement,
|
||||
useRef,
|
||||
type PropsWithChildren,
|
||||
type ReactElement,
|
||||
type Ref,
|
||||
} from "react";
|
||||
import styles from "./AnchoredPopover.module.css";
|
||||
|
||||
interface AnchoredPopoverProps<T extends HTMLElement>
|
||||
extends PropsWithChildren {
|
||||
referenceComponent: ReactElement<{ ref?: Ref<T> }>;
|
||||
allowedPlacements: Placement[];
|
||||
className?: string;
|
||||
offset?: number;
|
||||
open?: boolean;
|
||||
floatingArrow?: boolean;
|
||||
floatingArrowClassName?: string;
|
||||
}
|
||||
|
||||
export const AnchoredPopover = <T extends HTMLElement>({
|
||||
className,
|
||||
children,
|
||||
open,
|
||||
referenceComponent,
|
||||
offset,
|
||||
allowedPlacements,
|
||||
floatingArrow,
|
||||
floatingArrowClassName,
|
||||
...rest
|
||||
}: AnchoredPopoverProps<T>) => {
|
||||
const arrowRef = useRef(null);
|
||||
const { refs, floatingStyles, context, isPositioned } = useFloating({
|
||||
strategy: "fixed",
|
||||
transform: false,
|
||||
open,
|
||||
middleware: [
|
||||
offset ? offsetFn(offset) : null,
|
||||
arrow({ element: arrowRef }),
|
||||
autoPlacement({
|
||||
allowedPlacements,
|
||||
}),
|
||||
].filter(Boolean),
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{cloneElement(referenceComponent, { ref: refs.setReference })}
|
||||
<FloatingPortal>
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
className={clsx([styles.wrapper, className])}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
{floatingArrow && isPositioned && (
|
||||
<FloatingArrow
|
||||
ref={arrowRef}
|
||||
context={context}
|
||||
className={floatingArrowClassName}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</FloatingPortal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -21,7 +21,7 @@ export interface ButtonProps
|
||||
className?: string;
|
||||
color?: "primary" | "secondary" | "success" | "info" | "warning" | "error";
|
||||
variant?: "solid" | "outline" | "text" | "tool";
|
||||
size?: "x-small" | "small" | "medium" | "large" | "x-large";
|
||||
size?: "x-small" | "smaller" | "small" | "medium" | "large" | "x-large";
|
||||
isDiv?: boolean;
|
||||
}
|
||||
|
||||
|
||||
+30
-11
@@ -40,6 +40,12 @@ export const Dialog: React.FC<DialogProps> = ({
|
||||
const handleClickBackdrop = (e?: MouseEvent) => {
|
||||
if (!e) return;
|
||||
|
||||
// Keyboard-triggered clicks (e.g. pressing Enter on a button) report
|
||||
// clientX/clientY as 0, which falls outside the dialog rect and would
|
||||
// incorrectly be treated as a backdrop click.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail
|
||||
if (e.detail === 0) return;
|
||||
|
||||
const rect = dialogRef.current?.getBoundingClientRect();
|
||||
const isClickInDialog =
|
||||
rect &&
|
||||
@@ -49,17 +55,14 @@ export const Dialog: React.FC<DialogProps> = ({
|
||||
e.clientX <= rect.left + rect.width;
|
||||
|
||||
if (!isClickInDialog) {
|
||||
// Stop the click from bubbling to a parent dialog's backdrop handler,
|
||||
// so only the innermost dialog closes when its backdrop is clicked.
|
||||
e.stopPropagation();
|
||||
onBackdropClick?.();
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleEsc = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && open) {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
// Get the element that was clicked to open the dialog
|
||||
@@ -78,11 +81,27 @@ export const Dialog: React.FC<DialogProps> = ({
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
document.addEventListener("keydown", handleEsc);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleEsc);
|
||||
};
|
||||
if (modal) {
|
||||
// The native cancel event is only fired for modal dialogs opened via
|
||||
// showModal(). The browser adds its own Escape key listener and ensures
|
||||
// the cancel event only fires on the topmost modal, so stacked dialogs
|
||||
// close one at a time.
|
||||
const dialog = dialogRef.current;
|
||||
const handleCancel = (e: Event) => {
|
||||
e.preventDefault();
|
||||
handleClose();
|
||||
};
|
||||
dialog?.addEventListener("cancel", handleCancel);
|
||||
return () => dialog?.removeEventListener("cancel", handleCancel);
|
||||
} else {
|
||||
// Non-modal dialogs don't receive the cancel event, so we fall back to
|
||||
// a document-level keydown listener.
|
||||
const handleEsc = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && open) handleClose();
|
||||
};
|
||||
document.addEventListener("keydown", handleEsc);
|
||||
return () => document.removeEventListener("keydown", handleEsc);
|
||||
}
|
||||
},
|
||||
//eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[modal, open],
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import clsx from "clsx";
|
||||
import type { FC } from "react";
|
||||
import { Button, type ButtonProps } from "../Button";
|
||||
import styles from "./FancyButton.module.css";
|
||||
|
||||
interface FancyButtonProps extends Omit<ButtonProps, "color"> {
|
||||
color: "light" | "red" | "dark";
|
||||
}
|
||||
|
||||
export const FancyButton: FC<FancyButtonProps> = ({
|
||||
className,
|
||||
color = "light",
|
||||
children,
|
||||
...props
|
||||
}) => (
|
||||
<Button
|
||||
className={clsx([styles.button, styles[`${color}Button`], className])}
|
||||
{...props}
|
||||
>
|
||||
<img
|
||||
className={styles.image}
|
||||
src={
|
||||
color === "red"
|
||||
? "https://media.dndbeyond.com/images/campaigns/redButtonBorder.png"
|
||||
: "https://media.dndbeyond.com/images/campaigns/goldButtonBorder.png"
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
+696
File diff suppressed because one or more lines are too long
-49
@@ -1,49 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import type { FC, HTMLAttributes } from "react";
|
||||
import styles from "./InfoItem.module.css";
|
||||
|
||||
interface InfoItemProps extends HTMLAttributes<HTMLDivElement> {
|
||||
center?: boolean;
|
||||
color?: "primary" | "secondary" | "success" | "info" | "warning" | "error";
|
||||
label: string;
|
||||
inline?: boolean;
|
||||
italic?: boolean;
|
||||
}
|
||||
|
||||
export const InfoItem: FC<InfoItemProps> = ({
|
||||
center,
|
||||
color,
|
||||
className,
|
||||
inline,
|
||||
italic,
|
||||
label,
|
||||
children,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={clsx([
|
||||
center && styles.center,
|
||||
inline && styles.inline,
|
||||
className,
|
||||
])}
|
||||
{...props}
|
||||
>
|
||||
<p
|
||||
className={clsx([
|
||||
styles.label,
|
||||
color && styles[color],
|
||||
italic && styles.italic,
|
||||
])}
|
||||
>
|
||||
{label}
|
||||
</p>
|
||||
{typeof children === "string" ? (
|
||||
<p
|
||||
className={clsx([styles.value, color && styles[color]])}
|
||||
dangerouslySetInnerHTML={{ __html: children }}
|
||||
/>
|
||||
) : (
|
||||
<p className={clsx([styles.value, color && styles[color]])}>{children}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
Generated
Vendored
-45
@@ -1,45 +0,0 @@
|
||||
const ddbImageBase = "https://media.dndbeyond.com/mega-menu";
|
||||
|
||||
export const cardList = [
|
||||
{
|
||||
label: "Classes",
|
||||
imageUrl: `${ddbImageBase}/aef3e4b2ac24457aa251c761d41ff07b.jpg`,
|
||||
link: "https://www.dndbeyond.com/classes",
|
||||
},
|
||||
{
|
||||
label: "Backgrounds",
|
||||
imageUrl: `${ddbImageBase}/6e31ff6ea1f04ca2af9896f3f6d1b383.jpg`,
|
||||
link: "https://www.dndbeyond.com/backgrounds",
|
||||
},
|
||||
{
|
||||
label: "Species",
|
||||
imageUrl: `${ddbImageBase}/8b551f9a1c2c4db09d65cdd7c139f9d5.jpg`,
|
||||
link: `https://www.dndbeyond.com/species`,
|
||||
},
|
||||
{
|
||||
label: "Feats",
|
||||
imageUrl: `${ddbImageBase}/a69ab5bf67b03308893b582dbef700e9.jpg`,
|
||||
link: "https://www.dndbeyond.com/feats",
|
||||
},
|
||||
{
|
||||
label: "Spells",
|
||||
imageUrl: `${ddbImageBase}/a9eac9081b6240ca8d6325ca4766345e.png`,
|
||||
link: "https://www.dndbeyond.com/spells",
|
||||
},
|
||||
{
|
||||
label: "Equipment",
|
||||
imageUrl: `${ddbImageBase}/b778ff3ca3f18e5f75ad4b348615cab5.jpg`,
|
||||
link: "https://www.dndbeyond.com/equipment",
|
||||
},
|
||||
{
|
||||
label: "Magic Items",
|
||||
imageUrl: `${ddbImageBase}/c06b79eae8ee234d1cea4688e117152b.jpg`,
|
||||
link: "https://www.dndbeyond.com/magic-items",
|
||||
},
|
||||
{
|
||||
label: "Monsters",
|
||||
imageUrl: `${ddbImageBase}/36ee49066331fc36e3b37147d123463a.jpg`,
|
||||
imagePosition: "center 11%",
|
||||
link: "https://www.dndbeyond.com/monsters",
|
||||
},
|
||||
];
|
||||
Generated
Vendored
-26
@@ -1,26 +0,0 @@
|
||||
import { twitchUrl, youtubeUrl } from "../../../shared/constants";
|
||||
|
||||
const ddbImageBase = "https://media.dndbeyond.com/mega-menu";
|
||||
|
||||
export const primaryItems = [
|
||||
{
|
||||
label: "Community Update",
|
||||
imageUrl: `${ddbImageBase}/c2979f81837d8ee7c113a874da2909db.png`,
|
||||
link: "https://www.dndbeyond.com/community-update",
|
||||
},
|
||||
{
|
||||
label: "Twitch",
|
||||
imageUrl: `${ddbImageBase}/443085c3173d345e90d29881d8442a59.png`,
|
||||
link: twitchUrl,
|
||||
},
|
||||
{
|
||||
label: "Youtube",
|
||||
imageUrl: `${ddbImageBase}/d5e1b6d07482834ebfcbc1f04bd6476d.png`,
|
||||
link: youtubeUrl,
|
||||
},
|
||||
{
|
||||
label: "Changelog",
|
||||
imageUrl: `${ddbImageBase}/4af3d4c196428ab0809cf71d332d540d.png`,
|
||||
link: "https://www.dndbeyond.com/changelog",
|
||||
},
|
||||
];
|
||||
Generated
Vendored
-45
@@ -1,45 +0,0 @@
|
||||
import type { MegaMenuCardProps } from "../MegaMenuCard";
|
||||
|
||||
const ddbImageBase = "https://media.dndbeyond.com/mega-menu";
|
||||
|
||||
export const groupOne: MegaMenuCardProps[] = [
|
||||
{
|
||||
label: "Character Builder",
|
||||
imageUrl: `${ddbImageBase}/character_builder.png`,
|
||||
link: "https://www.dndbeyond.com/characters/builder",
|
||||
},
|
||||
{
|
||||
label: "Sigil 3D VTT",
|
||||
imageUrl: `${ddbImageBase}/playtest_sigil.png`,
|
||||
link: "https://www.dndbeyond.com/project-sigil",
|
||||
flags: [{ label: "New", variant: "success" }],
|
||||
},
|
||||
];
|
||||
|
||||
export const groupTwo: MegaMenuCardProps[] = [
|
||||
{
|
||||
label: "Maps VTT",
|
||||
imageUrl: `${ddbImageBase}/049ddb9085342521d25c5230451cfd45.jpg`,
|
||||
link: "https://www.dndbeyond.com/games",
|
||||
flags: [{ label: "Beta", variant: "info" }],
|
||||
},
|
||||
{
|
||||
label: "Encounters",
|
||||
imageUrl: `${ddbImageBase}/e434a8385f9619f0d52480c3c3987059.jpg`,
|
||||
link: "https://www.dndbeyond.com/encounter-builder",
|
||||
flags: [{ label: "Beta", variant: "info" }],
|
||||
},
|
||||
];
|
||||
|
||||
export const groupThree: MegaMenuCardProps[] = [
|
||||
{
|
||||
label: "Mobile App",
|
||||
imageUrl: `${ddbImageBase}/3aa58aac2d02bb52d62204e158b48ce6.jpg`,
|
||||
link: "https://www.dndbeyond.com/player-app",
|
||||
},
|
||||
{
|
||||
label: "Avrae Discord Bot",
|
||||
imageUrl: `${ddbImageBase}/28d923cd45ad209411ef1ff07993721b.jpg`,
|
||||
link: "https://avrae.io/",
|
||||
},
|
||||
];
|
||||
Generated
Vendored
+75
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import type { FC, HTMLAttributes } from "react";
|
||||
import { FancyButton } from "../FancyButton";
|
||||
import { SpinnerIcon } from "../Icons";
|
||||
import styles from "./PremadeCharacterCard.module.css";
|
||||
|
||||
interface PremadeCharacterCardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
characterLevel: number;
|
||||
characterName: string;
|
||||
characterId?: number;
|
||||
primaryClassName: string;
|
||||
speciesName: string;
|
||||
longDescription: string;
|
||||
imageUrl: string;
|
||||
imageAlt: string;
|
||||
createButtonText?: string;
|
||||
showProgress: boolean;
|
||||
previewLink: string;
|
||||
onClaim: () => void;
|
||||
disableClaim?: boolean;
|
||||
}
|
||||
|
||||
export const PremadeCharacterCard: FC<PremadeCharacterCardProps> = ({
|
||||
characterLevel,
|
||||
characterName,
|
||||
characterId,
|
||||
primaryClassName,
|
||||
speciesName,
|
||||
longDescription,
|
||||
imageUrl,
|
||||
imageAlt,
|
||||
createButtonText = "Create",
|
||||
showProgress,
|
||||
previewLink,
|
||||
onClaim,
|
||||
disableClaim = false,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.container} {...props}>
|
||||
<img className={styles.image} src={imageUrl} alt={imageAlt} />
|
||||
<h2 className={styles.title} id={characterId?.toString()}>
|
||||
{characterName}
|
||||
</h2>
|
||||
<h3 className={styles.subTitle}>
|
||||
Level {characterLevel}: {speciesName}{" "}
|
||||
<strong>{primaryClassName}</strong>
|
||||
</h3>
|
||||
|
||||
<p className={styles.description}>{longDescription}</p>
|
||||
<FancyButton
|
||||
className={styles.button}
|
||||
color="light"
|
||||
href={previewLink}
|
||||
aria-label={`Preview ${speciesName} ${primaryClassName}`}
|
||||
>
|
||||
Preview
|
||||
</FancyButton>
|
||||
<FancyButton
|
||||
className={styles.fancyButton}
|
||||
onClick={disableClaim ? undefined : onClaim}
|
||||
color={"dark"}
|
||||
disabled={disableClaim}
|
||||
aria-label={
|
||||
showProgress
|
||||
? `Loading ${speciesName} ${primaryClassName}...`
|
||||
: `${createButtonText} ${speciesName} ${primaryClassName}`
|
||||
}
|
||||
>
|
||||
{showProgress ? <SpinnerIcon /> : createButtonText}
|
||||
</FancyButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+5
-2
@@ -64,7 +64,7 @@ export const Select: FC<SelectProps> = ({
|
||||
}
|
||||
// If value is string, show string
|
||||
else if (!multiple && value) {
|
||||
return value;
|
||||
return options.find((option) => option.value === value)?.label || value;
|
||||
}
|
||||
// If there is no value, show the placeholder
|
||||
else {
|
||||
@@ -114,7 +114,10 @@ export const Select: FC<SelectProps> = ({
|
||||
type="radio"
|
||||
name={name}
|
||||
value={option.value}
|
||||
onChange={(e) => onChange?.(e)}
|
||||
onChange={(e) => {
|
||||
onChange?.(e);
|
||||
setIsVisible(false);
|
||||
}}
|
||||
checked={value === option.value}
|
||||
/>
|
||||
{option.label}
|
||||
|
||||
Reference in New Issue
Block a user