From 7560ed73cf4823da527cb61d741bf0c08a31c287 Mon Sep 17 00:00:00 2001 From: Anna Beddow Date: Tue, 28 Jan 2025 09:27:00 +0000 Subject: [PATCH] Video timestamp pills on cards (#13208) * Add media icons to pills * update audio text colour * add standalone video pill to bottom of card * Display video pill in bottom left of card content if video article * Use pill component for timestamp on youtube video overlay * Use pill for live indicator * Switch to new video pill placment for beta containers * Fix linting * improve comments for video cards * Reorder imports for linter --- dotcom-rendering/src/components/Card/Card.tsx | 87 ++++++++++++++----- .../src/components/FeatureCard.tsx | 10 +-- .../YoutubeAtom/YoutubeAtomOverlay.tsx | 63 ++++++-------- dotcom-rendering/src/lib/formatTime.ts | 8 ++ 4 files changed, 100 insertions(+), 68 deletions(-) diff --git a/dotcom-rendering/src/components/Card/Card.tsx b/dotcom-rendering/src/components/Card/Card.tsx index a8f9636dc7..74f82d37fb 100644 --- a/dotcom-rendering/src/components/Card/Card.tsx +++ b/dotcom-rendering/src/components/Card/Card.tsx @@ -12,6 +12,7 @@ import { ArticleSpecial, } from '../../lib/articleFormat'; import { isMediaCard as isAMediaCard } from '../../lib/cardHelpers'; +import { isWithinTwelveHours, secondsToDuration } from '../../lib/formatTime'; import { getZIndex } from '../../lib/getZIndex'; import { DISCUSSION_ID_DATA_ATTRIBUTE } from '../../lib/useCommentCount'; import { palette } from '../../palette'; @@ -36,7 +37,6 @@ import type { Loading } from '../CardPicture'; import { CardPicture } from '../CardPicture'; import { Island } from '../Island'; import { LatestLinks } from '../LatestLinks.importable'; -import { MediaDuration } from '../MediaDuration'; import { MediaMeta } from '../MediaMeta'; import { Pill } from '../Pill'; import { Slideshow } from '../Slideshow'; @@ -342,13 +342,13 @@ const getHeadlinePosition = ({ return 'inner'; }; -export const isWithinTwelveHours = (webPublicationDate: string): boolean => { - const timeDiffMs = Math.abs( - new Date().getTime() - new Date(webPublicationDate).getTime(), - ); - const timeDiffHours = timeDiffMs / (1000 * 60 * 60); - return timeDiffHours <= 12; -}; +const liveBulletStyles = css` + width: 9px; + height: 9px; + border-radius: 50%; + background-color: ${palette('--pill-bullet')}; + margin-right: ${space[1]}px; +`; export const Card = ({ linkTo, @@ -426,6 +426,20 @@ export const Card = ({ const isBetaContainer = BETA_CONTAINERS.includes(containerType ?? ''); + /** + * A "video article" refers to standalone video content presented as the main focus of the article. + * It is treated as a media card in the design system. + */ + const isVideoArticle = + mainMedia?.type === 'Video' && format.design === ArticleDesign.Video; + + /** + * Articles with a video as the main media but not classified as "video articles" + * are styled differently and are not treated as media cards. + */ + const isVideoMainMedia = + mainMedia?.type === 'Video' && format.design !== ArticleDesign.Video; + const decideAge = () => { if (!webPublicationDate) return undefined; const withinTwelveHours = isWithinTwelveHours(webPublicationDate); @@ -485,10 +499,29 @@ export const Card = ({ margin-top: auto; `} > + {isVideoArticle && ( + <> + {mainMedia.duration === 0 ? ( + } + iconSize={'small'} + /> + ) : ( + } + iconSize={'small'} + /> + )} + + )} + {mainMedia?.type === 'Audio' && ( } + iconSize={'small'} /> )} {mainMedia?.type === 'Gallery' && ( @@ -514,10 +547,7 @@ export const Card = ({ * Check media type to determine if pill, or article metadata & icon shown. * Currently pills are only shown within beta containers. */ - const showPill = - isBetaContainer && - mainMedia && - (mainMedia.type === 'Audio' || mainMedia.type === 'Gallery'); + const showPill = isBetaContainer && !!mainMedia; const media = getMedia({ imageUrl: image?.src, @@ -855,7 +885,11 @@ export const Card = ({ } index={index} duration={ - media.mainMedia.duration + isBetaContainer && + isVideoArticle + ? undefined + : media.mainMedia + .duration } posterImage={ media.mainMedia.images @@ -938,18 +972,23 @@ export const Card = ({ roundedCorners={isOnwardContent} aspectRatio={aspectRatio} /> - {mainMedia?.type === 'Video' && - mainMedia.duration > 0 && ( - 0 && ( +
+ } + iconSize={'small'} /> - )} +
+ )} )} {media.type === 'crossword' && ( diff --git a/dotcom-rendering/src/components/FeatureCard.tsx b/dotcom-rendering/src/components/FeatureCard.tsx index f9b8deb8f2..cbfd5b6f24 100644 --- a/dotcom-rendering/src/components/FeatureCard.tsx +++ b/dotcom-rendering/src/components/FeatureCard.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/react'; import { space } from '@guardian/source/foundations'; import { Link, SvgMediaControlsPlay } from '@guardian/source/react-components'; import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat'; -import { secondsToDuration } from '../lib/formatTime'; +import { isWithinTwelveHours, secondsToDuration } from '../lib/formatTime'; import { getZIndex } from '../lib/getZIndex'; import { DISCUSSION_ID_DATA_ATTRIBUTE } from '../lib/useCommentCount'; import { palette } from '../palette'; @@ -218,14 +218,6 @@ const getMedia = ({ return undefined; }; -export const isWithinTwelveHours = (webPublicationDate: string): boolean => { - const timeDiffMs = Math.abs( - new Date().getTime() - new Date(webPublicationDate).getTime(), - ); - const timeDiffHours = timeDiffMs / (1000 * 60 * 60); - return timeDiffHours <= 12; -}; - const CardAge = ({ showClock, absoluteServerTimes, diff --git a/dotcom-rendering/src/components/YoutubeAtom/YoutubeAtomOverlay.tsx b/dotcom-rendering/src/components/YoutubeAtom/YoutubeAtomOverlay.tsx index ea970ceb01..306d2e2237 100644 --- a/dotcom-rendering/src/components/YoutubeAtom/YoutubeAtomOverlay.tsx +++ b/dotcom-rendering/src/components/YoutubeAtom/YoutubeAtomOverlay.tsx @@ -6,7 +6,6 @@ import { headlineMedium20, palette as sourcePalette, space, - textSansBold12, } from '@guardian/source/foundations'; import type { ArticleFormat } from '../../lib/articleFormat'; import { secondsToDuration } from '../../lib/formatTime'; @@ -19,6 +18,8 @@ import type { import { PlayIcon } from '../Card/components/PlayIcon'; import { FormatBoundary } from '../FormatBoundary'; import { Kicker } from '../Kicker'; +import { Pill } from '../Pill'; +import { SvgMediaControlsPlay } from '../SvgMediaControlsPlay'; import { YoutubeAtomPicture } from './YoutubeAtomPicture'; type Props = { @@ -72,38 +73,14 @@ const pillStyles = css` position: absolute; top: ${space[2]}px; right: ${space[2]}px; - ${textSansBold12}; - color: ${palette('--pill-text')}; -`; - -const durationPillStyles = css` - background-color: rgba(0, 0, 0, 0.7); - border-radius: ${space[3]}px; - padding: ${space[1]}px ${space[3]}px; - display: inline-flex; - line-height: ${space[4]}px; -`; - -const livePillStyles = css` - border-radius: ${space[10]}px; - padding: ${space[1]}px ${space[2]}px; - gap: ${space[2]}px; - background-color: ${palette('--pill-background')}; - display: flex; - align-items: center; `; const liveBulletStyles = css` - ::before { - content: ''; - width: 9px; - height: 9px; - border-radius: 50%; - background-color: ${palette('--pill-bullet')}; - display: inline-block; - position: relative; - margin-right: 0.1875rem; - } + width: 9px; + height: 9px; + border-radius: 50%; + background-color: ${palette('--pill-bullet')}; + margin-right: ${space[1]}px; `; const textOverlayStyles = css` @@ -151,7 +128,7 @@ export const YoutubeAtomOverlay = ({ const id = `youtube-overlay-${uniqueId}`; const hasDuration = !isUndefined(duration) && duration > 0; //** We infer that a video is a livestream if the duration is set to 0. This is a soft contract with Editorial who manual set the duration of videos */ - const isLiveStream = duration === 0; + const isLiveStream = !isUndefined(duration) && duration === 0; const image = overrideImage ?? posterImage; const hidePillOnMobile = imagePositionOnMobile === 'right' || imagePositionOnMobile === 'left'; @@ -175,8 +152,20 @@ export const YoutubeAtomOverlay = ({ /> )} {isLiveStream && ( -
- Live +
+ } + iconSize={'small'} + />
)} {hasDuration && ( @@ -186,10 +175,14 @@ export const YoutubeAtomOverlay = ({ ? css` display: none; ` - : [pillStyles, durationPillStyles] + : pillStyles } > - {secondsToDuration(duration)} + } + iconSize={'small'} + />
)} { } return duration.join(':'); }; + +export const isWithinTwelveHours = (webPublicationDate: string): boolean => { + const timeDiffMs = Math.abs( + new Date().getTime() - new Date(webPublicationDate).getTime(), + ); + const timeDiffHours = timeDiffMs / (1000 * 60 * 60); + return timeDiffHours <= 12; +};