Feat/BOOK-257 videoplayer with card

* feat(BOOK-257): Added VideoPlayer with card component
* feat(BOOK-257): Added queries and component for VideoCard block to Content and Collection pages
* fix(BOOK-257): Only setting object-fit: cover on the video if it is not fullscreen
* feat(BOOK-257): Added queries and component for VideoCard block to Startpage
* feat(BOOK-257): Added queries and component for Video block to content/collection/start page

Approved-by: Chuma Mcphoy (We Ahead)
This commit is contained in:
Erik Tiekstra
2025-12-12 06:34:32 +00:00
parent 5770147af4
commit 0597b09c08
31 changed files with 1226 additions and 52 deletions

View File

@@ -0,0 +1,82 @@
import { VariantProps } from 'class-variance-authority'
import { Typography } from '../../Typography'
import { variants } from './variants'
import { VideoPlayer, VideoPlayerProps } from '..'
import styles from './videoWithCard.module.css'
interface TextCardProps {
variant: 'text'
heading: string
text?: string
}
interface QuoteCardProps {
variant: 'quote'
quote: string
author: string
authorDescription?: string
}
type VideoWithCardProps = VariantProps<typeof variants> &
(TextCardProps | QuoteCardProps) & {
video: Pick<VideoPlayerProps, 'src' | 'captions' | 'focalPoint'>
}
export function VideoWithCard(props: VideoWithCardProps) {
const { variant, style, video } = props
const classNames = variants({
variant,
style,
})
return (
<div className={styles.videoWithCardWrapper}>
<div className={styles.videoWithCard}>
<VideoPlayer variant="inline" {...video} />
<article className={classNames}>
<CardContent {...props} />
</article>
</div>
</div>
)
}
function CardContent(props: VideoWithCardProps) {
if (props.variant === 'quote') {
const { quote, author, authorDescription } = props
return (
<>
<Typography variant="Title/smLowCase">
<blockquote className={styles.blockquote}>{quote}</blockquote>
</Typography>
<cite className={styles.cite}>
<Typography variant="Body/Paragraph/mdBold">
<span>{author}</span>
</Typography>
{authorDescription ? (
<Typography variant="Body/Paragraph/mdRegular">
<span>{authorDescription}</span>
</Typography>
) : null}
</cite>
</>
)
}
const { heading, text } = props
return (
<>
<Typography variant="Title/smLowCase">
<h3 className={styles.heading}>{heading}</h3>
</Typography>
{text ? (
<Typography variant="Body/Paragraph/mdRegular">
<p>{text}</p>
</Typography>
) : null}
</>
)
}

View File

@@ -0,0 +1,207 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { VideoWithCard } from '.'
import { config } from './variants'
const meta: Meta<typeof VideoWithCard> = {
title: 'Core Components//🚧 Video 🚧//VideoWithCard',
component: VideoWithCard,
parameters: {
docs: {
description: {
component:
'A component to display a VideoPlayer and content inside a card connected to the video. The size and gaps are determined by the parent container.',
},
},
},
argTypes: {
variant: {
control: 'select',
options: Object.keys(config.variants.variant),
table: {
defaultValue: {
summary: config.defaultVariants.variant,
},
type: {
summary: Object.keys(config.variants.variant).join(' | '),
},
},
description:
'The variant of the card, which determines its style of the text and what content is shown.',
},
style: {
control: 'select',
options: Object.keys(config.variants.style),
table: {
defaultValue: {
summary: config.defaultVariants.style,
},
type: {
summary: Object.keys(config.variants.style).join(' | '),
},
},
description:
'This decides the background color and text color of the card.',
},
heading: {
table: {
type: { summary: 'string' },
},
description: 'The heading text. Only applicable for the text variant.',
},
text: {
table: {
type: { summary: 'string' },
},
description: 'The body text. Only applicable for the text variant.',
},
quote: {
table: {
type: { summary: 'string' },
},
description: 'The quote text. Only applicable for the quote variant.',
},
author: {
table: {
type: { summary: 'string' },
},
description:
'The author of the quote. Only applicable for the quote variant.',
},
authorDescription: {
table: {
type: { summary: 'string' },
},
description:
'The description of the author. Only applicable for the quote variant.',
},
video: {
control: false,
table: {
type: {
summary:
'{ src: string; captions?: Caption[]; focalPoint?: FocalPoint}',
},
},
description:
'The video props including source URL, captions and focal point. Please note that not all props from the VideoPlayer component are supported in this wrapper component.',
},
},
}
export default meta
type Story = StoryObj<typeof VideoWithCard>
const videoProps = {
src: 'https://eu-assets.contentstack.com/v3/assets/bltfd73aa2de3a5c4e3/bltad0fe3c2ce340947/68eced6c14e5a8150ebba18c/Scandic_EB_Master.mp4',
}
const quoteCardProps = {
variant: 'quote' as const,
quote: 'Download our membership App for smoother & richer experience',
author: 'Hans Christian Andersen',
authorDescription: 'The famed Danish storyteller.',
video: videoProps,
}
const textCardProps = {
variant: 'text' as const,
heading: 'Download our membership App now',
text: 'Hans Christian Andersen, the famed Danish storyteller, spent many years of his life in Nyhavn, drawing inspiration from its lively atmosphere and picturesque setting.',
video: videoProps,
}
const smallDecorator = (Story: React.FC) => (
<div
style={{
width: '792px',
paddingRight: '2rem',
}}
>
<Story />
</div>
)
const largeDecorator = (Story: React.FC) => (
<div
style={{
width: '1200px',
paddingRight: '2rem',
}}
>
<Story />
</div>
)
export const QuotePrimary1Small: Story = {
args: {
...quoteCardProps,
style: 'primary-1',
},
decorators: [smallDecorator],
render: (args) => <VideoWithCard {...args} />,
}
export const QuotePrimary2Small: Story = {
args: {
...quoteCardProps,
style: 'primary-2',
},
decorators: [smallDecorator],
render: (args) => <VideoWithCard {...args} />,
}
export const TextPrimary1Small: Story = {
args: {
...textCardProps,
style: 'primary-1',
},
decorators: [smallDecorator],
render: (args) => <VideoWithCard {...args} />,
}
export const TextPrimary2Small: Story = {
args: {
...textCardProps,
style: 'primary-2',
},
decorators: [smallDecorator],
render: (args) => <VideoWithCard {...args} />,
}
export const QuotePrimary1Large: Story = {
args: {
...quoteCardProps,
style: 'primary-1',
},
decorators: [largeDecorator],
render: (args) => <VideoWithCard {...args} />,
}
export const QuotePrimary2Large: Story = {
args: {
...quoteCardProps,
style: 'primary-2',
},
decorators: [largeDecorator],
render: (args) => <VideoWithCard {...args} />,
}
export const TextPrimary1Large: Story = {
args: {
...textCardProps,
style: 'primary-1',
},
decorators: [largeDecorator],
render: (args) => <VideoWithCard {...args} />,
}
export const TextPrimary2Large: Story = {
args: {
...textCardProps,
style: 'primary-2',
},
decorators: [largeDecorator],
render: (args) => <VideoWithCard {...args} />,
}

View File

@@ -0,0 +1,81 @@
import { VariantProps } from 'class-variance-authority'
import { Typography } from '../../Typography'
import { variants } from './variants'
import { VideoPlayer, VideoPlayerProps } from '..'
import styles from './videoWithCard.module.css'
interface TextCardProps {
variant: 'text'
heading: string
text?: string
}
interface QuoteCardProps {
variant: 'quote'
quote: string
author: string
authorDescription?: string
}
type VideoWithCardProps = VariantProps<typeof variants> &
(TextCardProps | QuoteCardProps) & {
video: Pick<VideoPlayerProps, 'src' | 'captions' | 'focalPoint'>
}
export function VideoWithCard(props: VideoWithCardProps) {
const { variant, style, video } = props
const classNames = variants({
variant,
style,
})
return (
<div className={styles.videoWithCardWrapper}>
<div className={styles.videoWithCard}>
<VideoPlayer variant="inline" {...video} />
<article className={classNames}>
<CardContent {...props} />
</article>
</div>
</div>
)
}
function CardContent(props: VideoWithCardProps) {
if (props.variant === 'quote') {
const { quote, author, authorDescription } = props
return (
<>
<Typography variant="Title/smLowCase">
<blockquote className={styles.blockquote}>{quote}</blockquote>
</Typography>
<cite className={styles.cite}>
<Typography variant="Body/Paragraph/mdBold">
<span>{author}</span>
</Typography>
{authorDescription ? (
<Typography variant="Body/Paragraph/mdRegular">
<span>{authorDescription}</span>
</Typography>
) : null}
</cite>
</>
)
}
const { heading, text } = props
return (
<>
<Typography variant="Title/smLowCase">
<h3 className={styles.heading}>{heading}</h3>
</Typography>
{text ? (
<Typography variant="Body/Paragraph/mdRegular">
<p>{text}</p>
</Typography>
) : null}
</>
)
}

View File

@@ -0,0 +1,22 @@
import { cva } from 'class-variance-authority'
import styles from './videoWithCard.module.css'
export const config = {
variants: {
variant: {
text: styles['variant-text'],
quote: styles['variant-quote'],
},
style: {
'primary-1': styles['style-primary-1'],
'primary-2': styles['style-primary-2'],
},
},
defaultVariants: {
style: 'primary-1',
variant: 'text',
},
} as const
export const variants = cva(styles.card, config)

View File

@@ -0,0 +1,84 @@
.videoWithCardWrapper {
width: 100%;
container-type: inline-size;
container-name: videoWithCardWrapper;
@media screen and (min-width: 768px) {
.videoWithCard {
grid-template-columns: 1fr auto;
min-height: 261px;
gap: var(--Space-x1);
.card {
width: 320px;
}
}
@container videoWithCardWrapper (min-width: 793px) {
.videoWithCard {
min-height: 445px;
gap: var(--Space-x2);
.card {
width: 391px;
}
}
}
}
}
.videoWithCard {
display: grid;
width: 100%;
gap: var(--Space-x025);
}
.card {
display: grid;
min-height: 200px;
height: 100%;
width: 100%;
padding: var(--Space-x3) var(--Space-x4);
align-content: center;
border-radius: var(--Corner-radius-lg);
&.style-primary-1 {
background: var(--Surface-Brand-Primary-1-Default);
color: var(--Text-Brand-OnPrimary-1-Heading);
}
&.style-primary-2 {
background: var(--Surface-Brand-Primary-2-Default);
color: var(--Text-Brand-OnPrimary-2-Default);
.heading {
color: var(--Text-Brand-OnPrimary-2-Heading);
}
}
&.variant-quote {
gap: var(--Space-x3);
@media screen and (min-width: 768px) {
gap: var(--Space-x4);
}
}
&.variant-text {
gap: var(--Space-x2);
}
}
.blockquote {
&::before {
content: '“';
}
&::after {
content: '”';
}
}
.cite {
font-style: normal;
display: grid;
}