Skip to content

Commit

Permalink
feat(project): add bcl live events
Browse files Browse the repository at this point in the history
* feat: add bcl live events

* chore: fix failing tests

* chore: pr rework

* chore: pr feedback
  • Loading branch information
RCVZ authored and ChristiaanScheermeijer committed May 30, 2023
1 parent 8237d96 commit 0c53b06
Show file tree
Hide file tree
Showing 22 changed files with 450 additions and 39 deletions.
6 changes: 6 additions & 0 deletions src/components/Card/Card.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ $aspects: ((1, 1), (2, 1), (2, 3), (4, 3), (5, 3), (16, 9), (9, 16), (9, 13));
background-color: variables.$red;
}

svg.scheduled {
width: 18px;
height: 18px;
margin-right: 4px;
}

.progressContainer {
position: absolute;
right: 0;
Expand Down
16 changes: 14 additions & 2 deletions src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { formatDurationTag, formatSeriesMetaString } from '#src/utils/formatting
import Lock from '#src/icons/Lock';
import Image from '#components/Image/Image';
import type { ImageData } from '#types/playlist';
import Today from '#src/icons/Today';

export const cardAspectRatios = ['2:1', '16:9', '5:3', '4:3', '1:1', '9:13', '2:3', '9:16'] as const;

Expand All @@ -30,6 +31,8 @@ type CardProps = {
isCurrent?: boolean;
isLocked?: boolean;
currentLabel?: string;
isLive?: boolean;
isScheduled?: boolean;
};

function Card({
Expand All @@ -49,8 +52,10 @@ function Card({
isCurrent = false,
isLocked = true,
currentLabel,
isLive = false,
isScheduled = false,
}: CardProps): JSX.Element {
const { t } = useTranslation('common');
const { t } = useTranslation(['common', 'video']);
const [imageLoaded, setImageLoaded] = useState(false);
const cardClassName = classNames(styles.card, {
[styles.featured]: featured,
Expand All @@ -72,8 +77,15 @@ function Card({
return <div className={styles.tag}>{formatSeriesMetaString(seasonNumber, episodeNumber)}</div>;
} else if (duration) {
return <div className={styles.tag}>{formatDurationTag(duration)}</div>;
} else if (duration === 0) {
} else if (isLive) {
return <div className={classNames(styles.tag, styles.live)}>{t('live')}</div>;
} else if (isScheduled) {
return (
<div className={styles.tag}>
<Today className={styles.scheduled} />
{t('scheduled')}
</div>
);
}
};

Expand Down
4 changes: 4 additions & 0 deletions src/components/Shelf/Shelf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { isLocked } from '#src/utils/entitlements';
import TileDock from '#components/TileDock/TileDock';
import Card, { type PosterAspectRatio } from '#components/Card/Card';
import type { Playlist, PlaylistItem } from '#types/playlist';
import { isLiveChannel } from '#src/utils/media';
import { MediaStatus } from '#src/utils/liveEvent';

export const tileBreakpoints: Breakpoints = {
[Breakpoint.xs]: 1,
Expand Down Expand Up @@ -87,6 +89,8 @@ const Shelf = ({
loading={loading}
isLocked={isLocked(accessModel, isLoggedIn, hasSubscription, item)}
posterAspect={posterAspect}
isLive={item.mediaStatus === MediaStatus.LIVE || isLiveChannel(item)}
isScheduled={item.mediaStatus === MediaStatus.SCHEDULED}
/>
),
[watchHistory, onCardHover, featured, loading, accessModel, isLoggedIn, hasSubscription, posterAspect, onCardClick, playlist.feedid, type],
Expand Down
21 changes: 21 additions & 0 deletions src/components/StatusIcon/StatusIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useTranslation } from 'react-i18next';

import Tag from '#components/Tag/Tag';
import Today from '#src/icons/Today';
import { MediaStatus } from '#src/utils/liveEvent';

type Props = {
mediaStatus?: MediaStatus;
};

export default function StatusIcon({ mediaStatus }: Props) {
const { t } = useTranslation('video');

if (mediaStatus === MediaStatus.SCHEDULED) {
return <Today />;
} else if (mediaStatus === MediaStatus.LIVE) {
return <Tag isLive>{t('live')}</Tag>;
}

return null;
}
6 changes: 6 additions & 0 deletions src/components/VideoDetails/VideoDetails.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,16 @@
}

.primaryMetadata {
display: flex;
align-items: center;
margin-bottom: 8px;
font-size: 16px;
line-height: 18px;
letter-spacing: 0.15px;

> :first-child {
margin-right: 8px;
}

@include responsive.mobile-only() {
order: 2;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,17 @@ div.title {
}

.primaryMetadata {
display: flex;
flex: 1;
flex-basis: 100%;
align-items: center;
margin-right: auto;
margin-bottom: 0;
font-size: 16px;
line-height: 18px;
letter-spacing: 0.15px;

> div.live {
> :first-child {
display: inline-block;
margin-right: 8px;

Expand Down
14 changes: 2 additions & 12 deletions src/components/VideoDetailsInline/VideoDetailsInline.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React from 'react';
import { useTranslation } from 'react-i18next';

import styles from './VideoDetailsInline.module.scss';

import Tag from '#components/Tag/Tag';
import CollapsibleText from '#components/CollapsibleText/CollapsibleText';
import useBreakpoint, { Breakpoint } from '#src/hooks/useBreakpoint';
import { testId } from '#src/utils/common';
Expand All @@ -18,8 +16,7 @@ type Props = {
live?: boolean;
};

const VideoDetailsInline: React.FC<Props> = ({ title, description, primaryMetadata, shareButton, favoriteButton, trailerButton, live }) => {
const { t } = useTranslation('common');
const VideoDetailsInline: React.FC<Props> = ({ title, description, primaryMetadata, shareButton, favoriteButton, trailerButton }) => {
const breakpoint: Breakpoint = useBreakpoint();
const isMobile = breakpoint === Breakpoint.xs;

Expand All @@ -29,14 +26,7 @@ const VideoDetailsInline: React.FC<Props> = ({ title, description, primaryMetada
<div className={styles.details} data-testid={testId('video-details-inline')}>
<TitleComponent className={styles.title}>{title}</TitleComponent>
<div className={styles.inlinePlayerMetadata}>
<div className={styles.primaryMetadata}>
{live && (
<Tag className={styles.live} isLive>
{t('live')}
</Tag>
)}
{primaryMetadata}
</div>
<div className={styles.primaryMetadata}>{primaryMetadata}</div>
{trailerButton}
{favoriteButton}
{shareButton}
Expand Down
6 changes: 5 additions & 1 deletion src/components/VideoList/VideoList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { isLocked } from '#src/utils/entitlements';
import { testId } from '#src/utils/common';
import type { AccessModel } from '#types/Config';
import type { Playlist, PlaylistItem } from '#types/playlist';
import { isLiveChannel } from '#src/utils/media';
import { MediaStatus } from '#src/utils/liveEvent';

type Props = {
playlist?: Playlist;
Expand Down Expand Up @@ -43,7 +45,7 @@ function VideoList({
{!!header && header}
{playlist &&
playlist.playlist.map((playlistItem: PlaylistItem) => {
const { mediaid, title, duration, seriesId, episodeNumber, seasonNumber, shelfImage } = playlistItem;
const { mediaid, title, duration, seriesId, episodeNumber, seasonNumber, shelfImage, mediaStatus } = playlistItem;

return (
<VideoListItem
Expand All @@ -61,6 +63,8 @@ function VideoList({
isActive={activeMediaId === mediaid}
activeLabel={activeLabel}
isLocked={isLocked(accessModel, isLoggedIn, hasSubscription, playlistItem)}
isLive={mediaStatus === MediaStatus.LIVE || isLiveChannel(playlistItem)}
isScheduled={mediaStatus === MediaStatus.SCHEDULED}
/>
);
})}
Expand Down
8 changes: 8 additions & 0 deletions src/components/VideoListItem/VideoListItem.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ svg.lock {
border-radius: 4px;
}

svg.scheduled {
width: 16px;
height: 16px;
margin-right: 4px;
}

.tag {
margin-left: 4px;
padding: 2px 6px;
Expand All @@ -116,6 +122,8 @@ svg.lock {
background-color: variables.$red;
}



.progressContainer {
position: absolute;
right: 0;
Expand Down
16 changes: 14 additions & 2 deletions src/components/VideoListItem/VideoListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Image from '#components/Image/Image';
import Lock from '#src/icons/Lock';
import Tag from '#components/Tag/Tag';
import { formatDurationTag, formatSeriesMetaString } from '#src/utils/formatting';
import Today from '#src/icons/Today';

type VideoListItemProps = {
onClick?: () => void;
Expand All @@ -24,6 +25,8 @@ type VideoListItemProps = {
isActive?: boolean;
activeLabel?: string;
isLocked?: boolean;
isLive?: boolean;
isScheduled?: boolean;
};

function VideoListItem({
Expand All @@ -40,6 +43,8 @@ function VideoListItem({
activeLabel,
isLocked = true,
image,
isLive = false,
isScheduled = false,
}: VideoListItemProps): JSX.Element {
const { t } = useTranslation('common');
const [imageLoaded, setImageLoaded] = useState(false);
Expand All @@ -56,8 +61,15 @@ function VideoListItem({
return formatSeriesMetaString(seasonNumber, episodeNumber);
} else if (duration) {
return formatDurationTag(duration);
} else if (duration === 0) {
} else if (isLive) {
return t('live');
} else if (isScheduled) {
return (
<>
<Today className={styles.scheduled} />
{t('scheduled')}
</>
);
}
};

Expand All @@ -78,7 +90,7 @@ function VideoListItem({
{isActive && <div className={styles.activeLabel}>{activeLabel}</div>}
<div className={styles.tags}>
{isLocked && <Lock className={styles.lock} />}
<Tag className={classNames(styles.tag, { [styles.live]: duration === 0 })}>{renderTagLabel()}</Tag>
<Tag className={classNames(styles.tag, { [styles.live]: isLive })}>{renderTagLabel()}</Tag>
</div>

{progress ? (
Expand Down
20 changes: 13 additions & 7 deletions src/containers/InlinePlayer/InlinePlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Props = {
startWatchingButton: React.ReactNode;
isLoggedIn: boolean;
paywall: boolean;
playable?: boolean;
autostart?: boolean;
};

Expand All @@ -41,6 +42,7 @@ const InlinePlayer: React.FC<Props> = ({
isLoggedIn,
paywall,
autostart,
playable = true,
}: Props) => {
const siteName = useConfigStore((s) => s.config.siteName);
const { t } = useTranslation();
Expand All @@ -53,17 +55,21 @@ const InlinePlayer: React.FC<Props> = ({

return (
<div className={styles.inlinePlayer}>
<Fade open={paywall}>
<Fade open={!playable || paywall}>
<div className={styles.paywall}>
<Image className={styles.poster} image={item.backgroundImage} alt={item.title} width={1280} />
<Lock className={styles.lock} />
<h2 className={styles.title}>{t('video:sign_up_to_start_watching')}</h2>
<span className={styles.text}>{t('account:choose_offer.watch_this_on_platform', { siteName })}</span>
{startWatchingButton}
{!isLoggedIn && <Button onClick={loginButtonClickHandler} label={t('common:sign_in')} />}
{paywall && (
<>
<Lock className={styles.lock} />
<h2 className={styles.title}>{t('video:sign_up_to_start_watching')}</h2>
<span className={styles.text}>{t('account:choose_offer.watch_this_on_platform', { siteName })}</span>
{startWatchingButton}
{!isLoggedIn && <Button onClick={loginButtonClickHandler} label={t('common:sign_in')} />}
</>
)}
</div>
</Fade>
{!paywall && (
{!paywall && playable && (
<PlayerContainer
item={item}
feedId={feedId}
Expand Down
9 changes: 9 additions & 0 deletions src/hooks/useLiveEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { PlaylistItem } from '../../types/playlist';
import { isLiveEvent, isPlayable } from '../utils/liveEvent';

export function useLiveEvent(media: PlaylistItem) {
return {
isLiveEvent: isLiveEvent(media),
isPlayable: isPlayable(media),
};
}
15 changes: 14 additions & 1 deletion src/hooks/usePlaylist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { generatePlaylistPlaceholder } from '#src/utils/collection';
import type { GetPlaylistParams } from '#types/playlist';
import { getPlaylistById } from '#src/services/api.service';
import { queryClient } from '#src/containers/QueryProvider/QueryProvider';
import { isScheduledOrLiveMedia } from '#src/utils/liveEvent';

const placeholderData = generatePlaylistPlaceholder(30);

Expand All @@ -25,7 +26,19 @@ export default function usePlaylist(playlistId?: string, params: GetPlaylistPara
return useQuery(queryKey, () => callback(playlistId, params), {
enabled: isEnabled,
placeholderData: usePlaceholderData && isEnabled ? placeholderData : undefined,
refetchInterval: (data, _) => (data?.refetch ? 1000 * 30 : false),
refetchInterval: (data, _) => {
if (!data) return false;

let shouldRefetchPlaylist = data.refetch;

for (const media of data.playlist) {
if (shouldRefetchPlaylist) break;

shouldRefetchPlaylist = isScheduledOrLiveMedia(media);
}

return shouldRefetchPlaylist ? 1000 * 30 : false;
},
retry: false,
});
}
11 changes: 11 additions & 0 deletions src/icons/Today.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';

import createIcon from './Icon';

export default createIcon(
'0 0 24 24',
<g>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z" />
</g>,
);
Loading

0 comments on commit 0c53b06

Please sign in to comment.