Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Video timestamp pills on cards #13208

Merged
merged 11 commits into from
Jan 28, 2025
87 changes: 63 additions & 24 deletions dotcom-rendering/src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -485,10 +499,29 @@ export const Card = ({
margin-top: auto;
`}
>
{isVideoArticle && (
<>
{mainMedia.duration === 0 ? (
<Pill
content={'Live'}
icon={<div css={liveBulletStyles} />}
iconSize={'small'}
/>
) : (
<Pill
content={secondsToDuration(mainMedia.duration)}
icon={<SvgMediaControlsPlay />}
iconSize={'small'}
/>
)}
</>
)}

{mainMedia?.type === 'Audio' && (
<Pill
content={audioDuration ?? ''}
icon={<SvgMediaControlsPlay />}
iconSize={'small'}
/>
)}
{mainMedia?.type === 'Gallery' && (
Expand All @@ -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,
Expand Down Expand Up @@ -855,7 +885,11 @@ export const Card = ({
}
index={index}
duration={
media.mainMedia.duration
isBetaContainer &&
isVideoArticle
? undefined
: media.mainMedia
.duration
}
posterImage={
media.mainMedia.images
Expand Down Expand Up @@ -938,18 +972,23 @@ export const Card = ({
roundedCorners={isOnwardContent}
aspectRatio={aspectRatio}
/>
{mainMedia?.type === 'Video' &&
mainMedia.duration > 0 && (
<MediaDuration
mediaDuration={mainMedia.duration}
imagePositionOnDesktop={
imagePositionOnDesktop
}
imagePositionOnMobile={
imagePositionOnMobile
}
{isVideoMainMedia && mainMedia.duration > 0 && (
<div
css={css`
position: absolute;
top: ${space[2]}px;
right: ${space[2]}px;
`}
>
<Pill
content={secondsToDuration(
mainMedia.duration,
)}
icon={<SvgMediaControlsPlay />}
iconSize={'small'}
/>
)}
</div>
)}
</>
)}
{media.type === 'crossword' && (
Expand Down
10 changes: 1 addition & 9 deletions dotcom-rendering/src/components/FeatureCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 = {
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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';
Expand All @@ -175,8 +152,20 @@ export const YoutubeAtomOverlay = ({
/>
)}
{isLiveStream && (
<div css={[pillStyles, livePillStyles, liveBulletStyles]}>
Live
<div
css={
hidePillOnMobile
? css`
display: none;
`
: pillStyles
}
>
<Pill
content={'Live'}
icon={<div css={[liveBulletStyles]} />}
iconSize={'small'}
/>
</div>
)}
{hasDuration && (
Expand All @@ -186,10 +175,14 @@ export const YoutubeAtomOverlay = ({
? css`
display: none;
`
: [pillStyles, durationPillStyles]
: pillStyles
}
>
{secondsToDuration(duration)}
<Pill
content={secondsToDuration(duration)}
icon={<SvgMediaControlsPlay />}
iconSize={'small'}
/>
</div>
)}
<PlayIcon
Expand Down
8 changes: 8 additions & 0 deletions dotcom-rendering/src/lib/formatTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,11 @@ export const secondsToDuration = (secs?: number): string => {
}
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;
};
Loading