Skip to content

Commit

Permalink
feat(series): use separate pages for deprecated and new flows
Browse files Browse the repository at this point in the history
  • Loading branch information
AntonLantukh committed May 23, 2023
1 parent d9b997e commit df65db2
Show file tree
Hide file tree
Showing 21 changed files with 690 additions and 166 deletions.
4 changes: 2 additions & 2 deletions src/containers/AppRoutes/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import About from '#src/pages/About/About';
import Home from '#src/pages/Home/Home';
import Search from '#src/pages/Search/Search';
import User from '#src/pages/User/User';
import Series from '#src/pages/Series/Series';
import DeprecatedSeries from '#src/pages/DeprecatedSeries/DeprecatedSeries';
import MediaScreenRouter from '#src/pages/ScreenRouting/MediaScreenRouter';
import PlaylistScreenRouter from '#src/pages/ScreenRouting/PlaylistScreenRouter';
import Layout from '#src/containers/Layout/Layout';
Expand All @@ -22,7 +22,7 @@ export default function AppRoutes() {
<Route index element={<Home />} />
<Route path="/p/:id" element={<PlaylistScreenRouter />} />
<Route path="/m/:id/*" element={<MediaScreenRouter />} />
<Route path="/s/:id/*" element={<Series />} />
<Route path="/s/:id/*" element={<DeprecatedSeries />} />
<Route path="/q/*" element={<Search />} />
<Route path="/u/*" element={<User />} />
<Route path="/o/about" element={<About />} />
Expand Down
2 changes: 1 addition & 1 deletion src/containers/ShelfList/ShelfList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const ShelfList = ({ rows }: Props) => {

const onCardClick = useCallback(
(playlistItem, playlistId, type) => {
navigate(mediaURL(playlistItem, playlistId, type === PersonalShelf.ContinueWatching));
navigate(mediaURL({ media: playlistItem, playlistId, play: type === PersonalShelf.ContinueWatching }));
},
[navigate],
);
Expand Down
15 changes: 11 additions & 4 deletions src/containers/StartWatchingButton/StartWatchingButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ import { useAccountStore } from '#src/stores/AccountStore';

type Props = {
item: PlaylistItem;
playUrl: string;
playUrl?: string;
disabled?: boolean;
onClick?: () => void;
};

const StartWatchingButton: React.VFC<Props> = ({ item, playUrl, disabled = false }) => {
const StartWatchingButton: React.VFC<Props> = ({ item, playUrl, disabled = false, onClick }) => {
const { t } = useTranslation('video');
const navigate = useNavigate();
const location = useLocation();
Expand Down Expand Up @@ -48,12 +49,18 @@ const StartWatchingButton: React.VFC<Props> = ({ item, playUrl, disabled = false
}, [isEntitled, isLoggedIn, hasMediaOffers, videoProgress, t]);

const handleStartWatchingClick = useCallback(() => {
if (isEntitled) return playUrl && navigate(playUrl);
if (isEntitled) {
if (onClick) {
onClick();
return;
}
return playUrl && navigate(playUrl);
}
if (!isLoggedIn) return navigate(addQueryParam(location, 'u', 'create-account'));
if (hasMediaOffers) return navigate(addQueryParam(location, 'u', 'choose-offer'));

return navigate('/u/payments');
}, [isEntitled, playUrl, navigate, isLoggedIn, location, hasMediaOffers]);
}, [isEntitled, playUrl, navigate, isLoggedIn, location, hasMediaOffers, onClick]);

useEffect(() => {
// set the TVOD mediaOffers in the checkout store
Expand Down
6 changes: 2 additions & 4 deletions src/hooks/series/useEpisodeMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ export const useEpisodeMetadata = (
series: Series | undefined,
options: { enabled: boolean },
): { isLoading: boolean; data: EpisodeMetadata | undefined } => {
const oldFlowMetadata = { episodeNumber: episode?.episodeNumber || '0', seasonNumber: episode?.seasonNumber || '0' };

const { isLoading, data }: UseQueryResult<EpisodeMetadata | undefined, ApiError | null> = useQuery(
['episodeId', series?.series_id, episode?.mediaid],
async () => {
Expand All @@ -24,7 +22,7 @@ export const useEpisodeMetadata = (
// Get an item details of the associated series (we need its episode and season)
const { season_number, episode_number } = (seriesDictionary?.[episode.mediaid] || []).find((el) => el.series_id === series?.series_id) || {};

return { episodeNumber: String(episode_number || 0), seasonNumber: String(season_number || 0) };
return { episodeNumber: episode_number && String(episode_number), seasonNumber: season_number && String(season_number) };
},
{
// Only enable this query when having new series flow
Expand All @@ -34,6 +32,6 @@ export const useEpisodeMetadata = (

return {
isLoading,
data: isLoading || !options.enabled ? data : series ? data : oldFlowMetadata,
data,
};
};
2 changes: 1 addition & 1 deletion src/hooks/series/useSeriesData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const useSeriesData = (
},
);

const usePlaylistFallback = isSeriesError && seriesError?.code === 404;
const usePlaylistFallback = isSeriesError && seriesError?.code === 404 && !isSeriesLoading;

// We enable it only after new series api unsuccessful load (404 error showing that such the series with the following id doesn't exist)
const { data: playlistData, isLoading: isPlaylistLoading, isError: isPlaylistError } = usePlaylist(legacySeriesPlaylistId, {}, usePlaylistFallback, false);
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/series/useSeriesId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import { useQuery } from 'react-query';

import type { PlaylistItem } from '#types/playlist';
import { getSeriesByMediaIds } from '#src/services/api.service';
import useQueryParam from '#src/hooks/useQueryParam';
import { getSeriesIdFromEpisode } from '#src/utils/media';

const useGetSeriesId = (item: PlaylistItem) => {
const staticSeriesId = useQueryParam('seriesId') || getSeriesIdFromEpisode(item);
const staticSeriesId = getSeriesIdFromEpisode(item);

const { isLoading, data } = useQuery(['seriesId', item.mediaid], async () => {
// get all series for the given media id
Expand All @@ -19,7 +18,8 @@ const useGetSeriesId = (item: PlaylistItem) => {

return {
isLoading,
seriesId: data || staticSeriesId,
deprecatedPlaylistId: staticSeriesId,
seriesId: data,
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,16 @@ import { Helmet } from 'react-helmet';
import { useTranslation } from 'react-i18next';
import shallow from 'zustand/shallow';

import { filterSeries, getEpisodesInSeason, getFiltersFromSeries, getNextItem } from './utils';

import { generateLegacyEpisodeJSONLD } from '#src/utils/structuredData';
import VideoLayout from '#components/VideoLayout/VideoLayout';
import InlinePlayer from '#src/containers/InlinePlayer/InlinePlayer';
import { isLocked } from '#src/utils/entitlements';
import useEntitlement from '#src/hooks/useEntitlement';
import { formatSeriesMetaString, formatVideoMetaString, seriesURL } from '#src/utils/formatting';
import { formatSeriesMetaString, formatVideoMetaString, deprecatedSeriesURL, formatPlaylistMetaString } from '#src/utils/formatting';
import useMedia from '#src/hooks/useMedia';
import { useSeriesData } from '#src/hooks/series/useSeriesData';
import { useEpisodes } from '#src/hooks/series/useEpisodes';
import { useEpisodeMetadata } from '#src/hooks/series/useEpisodeMetadata';
import { useNextEpisode } from '#src/hooks/series/useNextEpisode';
import ErrorPage from '#components/ErrorPage/ErrorPage';
import { generateEpisodeJSONLD } from '#src/utils/structuredData';
import { filterSeries, getEpisodesInSeason, getFiltersFromSeries, getFirstEpisode } from '#src/utils/series';
import { useWatchHistoryStore } from '#src/stores/WatchHistoryStore';
import { useConfigStore } from '#src/stores/ConfigStore';
import { useAccountStore } from '#src/stores/AccountStore';
Expand All @@ -28,11 +25,12 @@ import ShareButton from '#components/ShareButton/ShareButton';
import FavoriteButton from '#src/containers/FavoriteButton/FavoriteButton';
import Button from '#components/Button/Button';
import PlayTrailer from '#src/icons/PlayTrailer';
import type { PlaylistItem } from '#types/playlist';
import type { PlaylistItem, ImageData } from '#types/playlist';
import useQueryParam from '#src/hooks/useQueryParam';
import Loading from '#src/pages/Loading/Loading';
import usePlaylist from '#src/hooks/usePlaylist';

const Series = () => {
const DeprecatedSeries = () => {
const breakpoint = useBreakpoint();
const { t } = useTranslation('video');
const [playTrailer, setPlayTrailer] = useState<boolean>(false);
Expand All @@ -46,14 +44,14 @@ const Series = () => {
const episodeId = useQueryParam('e');

// Main data
const { isLoading: isSeriesDataLoading, isPlaylistError, data } = useSeriesData(seriesId, seriesId);
const { series, media: seriesMedia, playlist: seriesPlaylist } = data || {};
const { isLoading: isSeriesPlaylistLoading, data: seriesPlaylist, isError: isPlaylistError } = usePlaylist(seriesId, {}, true, false);
const { isLoading: isEpisodeLoading, data: episode } = useMedia(episodeId || '');
const { isLoading: isTrailerLoading, data: trailerItem } = useMedia(episode?.trailerId || '');
const { data: episodeMetadata, isLoading: isEpisodeMetadataLoading } = useEpisodeMetadata(episode, series, { enabled: !!episode && !!series });
const episodeMetadata = useMemo(() => ({ episodeNumber: episode?.episodeNumber || '0', seasonNumber: episode?.seasonNumber || '0' }), [episode]);

// Whether we show series or episode information. For old series flow we only have access to the playlist
const selectedItem = episode || seriesMedia || seriesPlaylist;
const selectedItem = episode || seriesPlaylist;
const selectedItemImage = (selectedItem?.image as string) || '';

// Config
const { config, accessModel } = useConfigStore(({ config, accessModel }) => ({ config, accessModel }), shallow);
Expand All @@ -62,20 +60,13 @@ const Series = () => {
const inlineLayout = Boolean(custom?.inlinePlayer);

// Filters
const filters = useMemo(() => getFiltersFromSeries(seriesPlaylist, series), [seriesPlaylist, series]);
const filters = useMemo(() => getFiltersFromSeries(seriesPlaylist), [seriesPlaylist]);
const [seasonFilter, setSeasonFilter] = useState<string | undefined>(undefined);

// Season / episodes data
const {
data: episodes,
fetchNextPage: fetchNextEpisodes,
hasNextPage: hasNextEpisodesPage,
} = useEpisodes(seriesId, seasonFilter, { enabled: seasonFilter !== undefined && !!series });

const firstEpisode = useMemo(() => getFirstEpisode(seriesPlaylist, episodes), [seriesPlaylist, episodes]);
const filteredPlaylist = useMemo(() => filterSeries(seriesPlaylist, episodes, seasonFilter), [seriesPlaylist, episodes, seasonFilter]);
const episodesInSeason = getEpisodesInSeason(episode, episodeMetadata, seriesPlaylist, series);
const nextItem = useNextEpisode({ episode, seriesPlaylist, series, episodeMetadata });
const firstEpisode = useMemo(() => seriesPlaylist?.playlist?.[0], [seriesPlaylist]);
const filteredPlaylist = useMemo(() => filterSeries(seriesPlaylist, seasonFilter), [seriesPlaylist, seasonFilter]);
const episodesInSeason = getEpisodesInSeason(episode, seriesPlaylist);
const nextItem = useMemo(() => getNextItem(episode, seriesPlaylist), [episode, seriesPlaylist]);

// Watch history
const watchHistoryDictionary = useWatchHistoryStore((state) => state.getDictionaryWithEpisodes());
Expand All @@ -87,11 +78,11 @@ const Series = () => {
const hasSubscription = !!subscription;

// Handlers
const goBack = () => episode && navigate(seriesURL({ episodeId: episode.mediaid, seriesId, play: false, playlistId: feedId }));
const goBack = () => episode && navigate(deprecatedSeriesURL({ episodeId: episode.mediaid, seriesId, play: false, playlistId: feedId }));
const onCardClick = (toEpisode: PlaylistItem) =>
seriesPlaylist && navigate(seriesURL({ episodeId: toEpisode.mediaid, seriesId, play: false, playlistId: feedId }));
seriesPlaylist && navigate(deprecatedSeriesURL({ episodeId: toEpisode.mediaid, seriesId, play: false, playlistId: feedId }));
const handleComplete = useCallback(async () => {
navigate(seriesURL({ episodeId: nextItem?.mediaid, seriesId, play: !!nextItem, playlistId: feedId }));
navigate(deprecatedSeriesURL({ episodeId: nextItem?.mediaid, seriesId, play: !!nextItem, playlistId: feedId }));
}, [navigate, nextItem, seriesId, feedId]);

// Effects
Expand All @@ -100,27 +91,28 @@ const Series = () => {
}, [episode]);

useEffect(() => {
if (isSeriesDataLoading || isEpisodeMetadataLoading || isEpisodeLoading) {
if (isSeriesPlaylistLoading || isEpisodeLoading) {
return;
}

if (seasonFilter === undefined) {
setSeasonFilter(episodeMetadata?.seasonNumber || episode?.seasonNumber || filters?.[0] || '');
setSeasonFilter(parseInt(episodeMetadata?.seasonNumber, 10) ? episodeMetadata?.seasonNumber : filters?.[0] || '');
}
}, [episodeMetadata, episode, seasonFilter, isEpisodeMetadataLoading, isSeriesDataLoading, isEpisodeLoading, filters]);
}, [episodeMetadata, seasonFilter, isSeriesPlaylistLoading, isEpisodeLoading, filters]);

// UI
const isLoading = isSeriesDataLoading || isEpisodeMetadataLoading || isEpisodeLoading;
const isLoading = isSeriesPlaylistLoading || isEpisodeLoading;
if (isLoading) return <Loading />;
if (isPlaylistError) return <ErrorPage title={t('series_error')} />;
if (isPlaylistError || !seriesPlaylist || !selectedItem || !firstEpisode) return <ErrorPage title={t('series_error')} />;

const pageTitle = `${selectedItem.title} - ${siteName}`;
const canonicalUrl = `${window.location.origin}${seriesURL({ episodeId: episode?.mediaid, seriesId })}`;
const pageDescription = selectedItem?.description || '';
const canonicalUrl = `${window.location.origin}${deprecatedSeriesURL({ episodeId: episode?.mediaid, seriesId })}`;
const backgroundImage = (selectedItem.backgroundImage as ImageData) || undefined;

const primaryMetadata = formatVideoMetaString(
selectedItem,
t('video:total_episodes', { count: series ? series.episode_count : seriesPlaylist.playlist.length }),
);
const primaryMetadata = episode
? formatVideoMetaString(episode, t('video:total_episodes', { count: seriesPlaylist?.playlist?.length }))
: formatPlaylistMetaString(seriesPlaylist, t('video:total_episodes', { count: seriesPlaylist?.playlist?.length }));
const secondaryMetadata = episodeMetadata && episode && (
<>
<strong>{formatSeriesMetaString(episodeMetadata.seasonNumber, episodeMetadata.episodeNumber)}</strong> - {episode.title}
Expand All @@ -129,16 +121,16 @@ const Series = () => {
const filterMetadata =
episodeMetadata &&
` ${t('video:season')} ${episodeMetadata.seasonNumber}/${filters?.length} - ${t('video:episode')} ${episodeMetadata.episodeNumber}/${episodesInSeason}`;
const shareButton = <ShareButton title={selectedItem?.title} description={selectedItem.description} url={canonicalUrl} />;
const shareButton = <ShareButton title={selectedItem?.title} description={pageDescription} url={canonicalUrl} />;
const startWatchingButton = (
<StartWatchingButton
item={episode || firstEpisode}
playUrl={seriesURL({ episodeId: episode?.mediaid || firstEpisode?.mediaid, seriesId, play: true, playlistId: feedId })}
playUrl={deprecatedSeriesURL({ episodeId: episode?.mediaid || firstEpisode?.mediaid, seriesId, play: true, playlistId: feedId })}
/>
);

// For the old series approach we mark episodes as favorite items. New approach is applied to the series
const favoriteButton = isFavoritesEnabled && <FavoriteButton item={seriesMedia || firstEpisode} />;
const favoriteButton = isFavoritesEnabled && <FavoriteButton item={episode || firstEpisode} />;
const trailerButton = (!!trailerItem || isTrailerLoading) && (
<Button
label={t('video:trailer')}
Expand All @@ -156,37 +148,38 @@ const Series = () => {
<Helmet>
<title>{pageTitle}</title>
<link rel="canonical" href={canonicalUrl} />
<meta name="description" content={selectedItem.description} />
<meta property="og:description" content={selectedItem.description} />
<meta name="description" content={pageDescription} />
<meta property="og:description" content={pageDescription} />
<meta property="og:title" content={pageTitle} />
<meta property="og:type" content={episode ? 'video.episode' : 'video.series'} />
{selectedItem.image && <meta property="og:image" content={selectedItem.image?.replace(/^https:/, 'http:')} />}
{selectedItem.image && <meta property="og:image:secure_url" content={selectedItem.image?.replace(/^http:/, 'https:')} />}
{selectedItem.image && <meta property="og:image:width" content={selectedItem.image ? '720' : ''} />}
{selectedItem.image && <meta property="og:image:height" content={selectedItem.image ? '406' : ''} />}
{selectedItemImage && <meta property="og:image" content={selectedItemImage?.replace(/^https:/, 'http:')} />}
{selectedItemImage && <meta property="og:image:secure_url" content={selectedItemImage?.replace(/^http:/, 'https:')} />}
{selectedItemImage && <meta property="og:image:width" content={selectedItemImage ? '720' : ''} />}
{selectedItemImage && <meta property="og:image:height" content={selectedItemImage ? '406' : ''} />}
<meta name="twitter:title" content={pageTitle} />
<meta name="twitter:description" content={selectedItem.description} />
<meta name="twitter:image" content={selectedItem.image} />
<meta name="twitter:description" content={pageDescription} />
{selectedItemImage && <meta name="twitter:image" content={selectedItemImage} />}
<meta property="og:video" content={canonicalUrl.replace(/^https:/, 'http:')} />
<meta property="og:video:secure_url" content={canonicalUrl.replace(/^http:/, 'https:')} />
<meta property="og:video:type" content="text/html" />
<meta property="og:video:width" content="1280" />
<meta property="og:video:height" content="720" />
{selectedItem.tags?.split(',').map((tag: string) => (
<meta property="og:video:tag" content={tag} key={tag} />
))}
{selectedItem.tags &&
String(selectedItem.tags)
.split(',')
.map((tag: string) => <meta property="og:video:tag" content={tag} key={tag} />)}
{seriesPlaylist && selectedItem ? (
<script type="application/ld+json">{generateEpisodeJSONLD(seriesPlaylist, series, episode, episodeMetadata, seriesId)}</script>
<script type="application/ld+json">{generateLegacyEpisodeJSONLD(seriesPlaylist, episode, episodeMetadata, seriesId)}</script>
) : null}
</Helmet>
<VideoLayout
item={selectedItem}
title={inlineLayout ? selectedItem.title : seriesPlaylist.title}
description={selectedItem.description}
item={episode}
title={selectedItem.title}
description={pageDescription}
inlineLayout={inlineLayout}
primaryMetadata={primaryMetadata}
secondaryMetadata={secondaryMetadata}
image={selectedItem.backgroundImage}
image={backgroundImage}
shareButton={shareButton}
favoriteButton={favoriteButton}
trailerButton={trailerButton}
Expand All @@ -196,7 +189,7 @@ const Series = () => {
isLoggedIn={isLoggedIn}
hasSubscription={hasSubscription}
playlist={filteredPlaylist}
relatedTitle={inlineLayout ? seriesPlaylist.title : t('episodes')}
relatedTitle={inlineLayout ? selectedItem.title : t('episodes')}
onItemClick={onCardClick}
setFilter={setSeasonFilter}
currentFilter={seasonFilter}
Expand All @@ -206,8 +199,6 @@ const Series = () => {
watchHistory={watchHistoryDictionary}
filterMetadata={filterMetadata}
filters={filters}
hasLoadMore={series && hasNextEpisodesPage}
loadMore={series && fetchNextEpisodes}
player={
inlineLayout && (episode || firstEpisode) ? (
<InlinePlayer
Expand All @@ -224,7 +215,7 @@ const Series = () => {
open={play && isEntitled}
onClose={goBack}
item={episode || firstEpisode}
title={seriesPlaylist.title}
title={episode?.title || firstEpisode.title}
primaryMetadata={primaryMetadata}
secondaryMetadata={secondaryMetadata}
onComplete={handleComplete}
Expand All @@ -238,4 +229,4 @@ const Series = () => {
);
};

export default Series;
export default DeprecatedSeries;
Loading

0 comments on commit df65db2

Please sign in to comment.