New source found from dndbeyond.com

This commit is contained in:
2026-07-08 01:00:09 -07:00
parent 9a983a6d7b
commit dcefa0d2f2
2865 changed files with 222467 additions and 49053 deletions
@@ -0,0 +1,30 @@
import type { FC } from 'react';
import type { AmplienceContentItem, BaseContentItem } from '../../types';
import { GenericSlot } from '../GenericSlot';
import { PromoCarouselAdapter } from '../PromoCarouselAdapter';
export const getAmplienceLocale = (appLocale: string) => {
switch (appLocale) {
case 'de':
return 'de-DE';
case 'en':
default:
return 'en';
}
};
const getComponentForSchema = (schemaName: string) => {
switch (schemaName) {
case 'https://content.hasbro.com/slots/generic-slot':
return GenericSlot;
case 'https://content.hasbro.com/models/promotion-message':
return PromoCarouselAdapter;
default:
return () => null;
}
};
export const AmplienceComponent = ({ item, locale }: AmplienceContentItem<BaseContentItem>) => {
const Component = getComponentForSchema(item._meta?.schema) as FC<AmplienceContentItem<BaseContentItem>>;
return <Component item={item} locale={getAmplienceLocale(locale)} />;
};
@@ -0,0 +1,15 @@
import type { GenericContentSlot } from '../../schemas/generic-slot-schema';
import type { AmplienceContentItem, BaseContentItem } from '../../types';
import { AmplienceComponent } from '../AmplienceComponent';
export const GenericSlot = ({ item: { content }, locale }: AmplienceContentItem<GenericContentSlot>) => {
// Amplience Generic Slot is a localized component, unlike Amplience Core components.
// Because of this, it receives two different types of responses that we need to handle below.
const item = (content?.values?.find((item) => item.locale === locale)?.value ?? content) as
| BaseContentItem
| undefined;
if (!item) return null;
return <AmplienceComponent item={item} locale={locale} />;
};
@@ -0,0 +1,19 @@
import { ROUTING_LOCALES } from '../../shared/constants';
type Locale = (typeof ROUTING_LOCALES)[number];
function stripLocalePrefix(pathname: string): string {
const segments = pathname.split('/');
const [_, potentialLocale, ...rest] = segments;
if (ROUTING_LOCALES.includes(potentialLocale as Locale)) {
const remainder = rest.join('/');
return remainder ? `/${remainder}` : '/';
}
return pathname;
}
export const getPathname = () => {
return typeof window !== 'undefined' ? stripLocalePrefix(window.location.pathname) : '/';
};
@@ -0,0 +1,18 @@
'use client';
import type { Title as PromotionMessage } from '../../schemas/promotion-message-schema';
import type { AmplienceContentItem } from '../../types';
import { PromoCarousel } from '../TopNavigation/PromoCarousel';
export const PromoCarouselAdapter = ({ item: { promoMessages } }: AmplienceContentItem<PromotionMessage>) => {
if (!promoMessages || !promoMessages.length) return;
const adaptedData = promoMessages
.filter((promoMessage) => promoMessage?.content?.richTextField)
.map((promoMessage) => ({
text: promoMessage.content?.richTextField ?? '',
color: promoMessage.color,
}));
return <PromoCarousel promotions={adaptedData} />;
};
@@ -0,0 +1,92 @@
'use client';
import ChevronLeft from '@dndbeyond/fontawesome-cache/svgs/regular/chevron-left.svg';
import ChevronRight from '@dndbeyond/fontawesome-cache/svgs/regular/chevron-right.svg';
import clsx from 'clsx';
import useEmblaCarousel from 'embla-carousel-react';
import { useCallback, useEffect, useState } from 'react';
import Markdown from 'react-markdown';
import { getItemAttributes, getSubElementAttributes } from '../../../bi/helpers';
import styles from './PromoCarousel.module.css';
type PromoCarouselType = {
promotions: {
text: string;
color?: string;
}[];
};
export const PromoCarousel = ({ promotions }: PromoCarouselType) => {
const [emblaRef, emblaApi] = useEmblaCarousel();
const [prevBtnDisabled, setPrevBtnDisabled] = useState(true);
const [nextBtnDisabled, setNextBtnDisabled] = useState(true);
const [activeSlide, setActiveSlide] = useState(0);
const onPrevButtonClick = () => {
emblaApi?.scrollPrev();
determineSliderPosition();
if (emblaApi) setActiveSlide(emblaApi.selectedScrollSnap());
};
const onNextButtonClick = () => {
emblaApi?.scrollNext();
determineSliderPosition();
if (emblaApi) setActiveSlide(emblaApi.selectedScrollSnap());
};
const determineSliderPosition = useCallback(() => {
setPrevBtnDisabled(!emblaApi?.canScrollPrev());
setNextBtnDisabled(!emblaApi?.canScrollNext());
}, [emblaApi]);
useEffect(() => {
if (emblaApi) setActiveSlide(emblaApi.selectedScrollSnap());
determineSliderPosition();
}, [determineSliderPosition]);
return (
<div className={styles.carouselContainer}>
<button
onClick={onPrevButtonClick}
className={clsx(
styles.navigationButton,
prevBtnDisabled && styles.navigationButtonDisabled,
promotions.length === 1 && styles.navigationButtonHidden,
)}
disabled={prevBtnDisabled}
{...getItemAttributes('Previous', 'button', 'scroll', undefined, undefined)}
>
<ChevronLeft />
</button>
<div
className={styles.carousel}
ref={emblaRef}
{...getSubElementAttributes('Promo Carousel', 'main tile', undefined)}
>
<div className={styles.carouselScrollContainer}>
{promotions.map((promo, index) => (
// React versions below 19 remove the 'inert' attribute when provided as a boolean. Passing it as a string "true" as a workaround.
<div
inert={activeSlide !== index && ('inert' as unknown as boolean)}
key={index + promo.text}
className={styles.slide}
>
<Markdown>{promo.text}</Markdown>
</div>
))}
</div>
</div>
<button
onClick={onNextButtonClick}
className={clsx(
styles.navigationButton,
nextBtnDisabled && styles.navigationButtonDisabled,
promotions.length === 1 && styles.navigationButtonHidden,
)}
disabled={nextBtnDisabled}
{...getItemAttributes('Next', 'button', 'scroll', undefined, undefined)}
>
<ChevronRight />
</button>
</div>
);
};
@@ -0,0 +1,53 @@
'use client';
import { Button } from '@dndbeyond/ttui/components/Button';
import clsx from 'clsx';
import { forwardRef } from 'react';
import { getItemAttributes, getSubElementAttributes } from '../../bi/helpers';
import type { Meta, Title as PromoCarouselAdapterSchema } from '../../schemas/promotion-message-schema';
import type { LanguageType, TopNavigationMessages } from '../../types';
import { AmplienceComponent } from '../AmplienceComponent';
import styles from './TopNavigation.module.css';
type TopNavigationType = {
className?: string;
promoSlotData?: {
content: PromoCarouselAdapterSchema;
_meta: Meta;
};
messages: TopNavigationMessages;
locale: LanguageType;
onDismissBanner?: () => void;
};
export const TopNavigation = forwardRef<HTMLDivElement, TopNavigationType>(
({ className, promoSlotData, messages, locale, onDismissBanner }, ref) => {
return (
<div
ref={ref}
className={clsx(styles.promoBannerWrapper, className)}
data-testid="topNavigation-wrapper"
{...getSubElementAttributes('Top Navigation', 'main tile', undefined)}
>
<div className={styles.promoBanner}>
<div></div>
<div>{promoSlotData && <AmplienceComponent item={promoSlotData} locale={locale} />}</div>
{!!onDismissBanner && (
<Button
variant="outline"
color="primary"
size="x-small"
className={styles.dismissButton}
onClick={onDismissBanner}
data-testid="dismiss-button"
{...getItemAttributes('Dismiss Promo Banner', 'button', 'close', undefined, undefined)}
>
{messages.dismiss}
</Button>
)}
</div>
</div>
);
},
);
TopNavigation.displayName = 'TopNavigation';