Skip to content

Commit

Permalink
feat(project): support watchlists
Browse files Browse the repository at this point in the history
- New endpoint /apps/watchlists is now used to get favorites and continue_watching
- Max number of favorites and continue_watching videos added (30 for each)
- Alert component to be shown in case of exceeding for favorites
- The size of externalData string stored at the back-end side reduced
- Local storage watchlists info save irrespective of the user account presence
- New validation to check that both content array item present and the watchlist are set
- Favorites row can be hidden now
- e2e tests added for favorites and continue_watching
  • Loading branch information
“Anton committed Jun 8, 2022
1 parent e2e103f commit 0124428
Show file tree
Hide file tree
Showing 44 changed files with 522 additions and 245 deletions.
18 changes: 16 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,9 @@ The eight-character Playlists IDs from the JW Player dashboard. These IDs popula

**content[].type**

It is possible to use 'playlist', 'continue_watching' or 'favorites' as a type. With this, you can change the position of the shelves and turn on/off extra `continue_watching` shelf (just include / exclude it in / from the array). Example:
It is possible to use 'playlist', 'continue_watching' or 'favorites' as a type. With this, you can change the position of the shelves and turn on/off extra `continue_watching` and `favorites` shelves.

If you want to include `favorites` / `continue_watching` shelf, you should also add a corresponding playlist with `watchlist` type to features section (`features.favorites_list` and `features.continue_watching_list`). To exclude the shelves, remove a corresponding array item and a playlist in `features`.

```
{
Expand Down Expand Up @@ -278,6 +280,18 @@ The eight-character Playlist ID of the Search playlist that you want to use to e

---

**features.favorites_list** (optional)

The eight-character Playlist ID of the Watchlist playlist that you want to use to populate the "Favorites" shelf in your site.

---

**features.continue_watching_list** (optional)

The eight-character Playlist ID of the Watchlist playlist that you want to use to populate the "Continue Watching" shelf in your site.

---

**integrations.cleeng**

Use the `integrations.cleeng` object to to integrate with Cleeng.
Expand Down Expand Up @@ -348,7 +362,7 @@ official [URL Signing Documentation](https://developer.jwplayer.com/jwplayer/doc
**contentSigningService.drmPolicyId** (optional)

When DRM is enabled for your JW Dashboard Property, all playlist and media requests MUST use the DRM specific endpoints.
When this property is configured, OTT Web App automatically does this automatically for you but all DRM requests must be
When this property is configured, OTT Web App automatically does this for you but all DRM requests must be
signed as well.

For this to work the entitlement service must implement the following endpoints:
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
"test": "TZ=UTC vitest run",
"test-watch": "TZ=UTC vitest",
"test-coverage": "TZ=UTC vitest run --coverage",
"test-commit": "TZ=UTC vitest run --changed HEAD~1 --coverage",
"i18next": "i18next src/{components,containers,screens}/**/{**/,/}*.tsx && node ./scripts/i18next/generate.js",
"format": "prettier --write \"{**/*,*}.{js,ts,jsx,tsx}\"",
"lint": "prettier --check \"{**/*,*}.{js,ts,jsx,tsx}\" && eslint \"{**/*,*}.{js,ts,jsx,tsx}\"",
"lint:styles": "stylelint \"src/**/*.scss\"",
"commit-msg": "commitlint --edit $1",
"pre-commit": "depcheck && lint-staged && TZ=UTC yarn test --coverage",
"pre-commit": "depcheck && lint-staged && TZ=UTC yarn test-commit",
"codecept:mobile": "cd test-e2e && codeceptjs -c codecept.mobile.js run --steps",
"codecept:desktop": "cd test-e2e && codeceptjs -c codecept.desktop.js run --steps",
"deploy:github": "node ./scripts/deploy-github.js"
Expand Down
8 changes: 0 additions & 8 deletions public/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,6 @@
"type": "playlist",
"contentId": "JSKF03bk"
},
{
"enableText": true,
"type": "favorites"
},
{
"enableText": true,
"type": "continue_watching"
},
{
"enableText": true,
"type": "playlist",
Expand Down
10 changes: 6 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import QueryProvider from '#src/providers/QueryProvider';
import { restoreWatchHistory } from '#src/stores/WatchHistoryController';
import { initializeAccount } from '#src/stores/AccountController';
import { initializeFavorites } from '#src/stores/FavoritesController';
import { PersonalShelf } from '#src/enum/PersonalShelf';

import '#src/i18n/config';
import '#src/styles/main.scss';
Expand All @@ -28,11 +27,14 @@ class App extends Component {
}

async initializeServices(config: Config) {
if (config.content.some((el) => el.type === PersonalShelf.ContinueWatching)) {
// We only request favorites and continue_watching data if these features are enabled
if (config?.features?.continue_watching_list) {
await restoreWatchHistory();
}

await initializeFavorites();
if (config?.features?.favorites_list) {
await initializeFavorites();
}

if (config?.integrations?.cleeng?.id) {
await initializeAccount();
Expand All @@ -45,7 +47,7 @@ class App extends Component {

configErrorHandler = (error: Error) => {
this.setState({ error });
console.info('Error while loading the config.json:', error);
console.error('Error while loading the config.json:', error);
};

configValidationCompletedHandler = async (config: Config) => {
Expand Down
17 changes: 17 additions & 0 deletions src/components/Alert/Alert.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@use '../../styles/variables';
@use '../../styles/theme';

.title {
margin-bottom: 24px;
font-family: theme.$body-font-family;
font-weight: 700;
font-size: 24px;
}

.body {
font-family: theme.$body-font-family;
}

.confirmButton {
margin-bottom: 8px;
}
12 changes: 12 additions & 0 deletions src/components/Alert/Alert.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import { render } from '@testing-library/react';

import Alert from './Alert';

describe('<Alert>', () => {
test('renders and matches snapshot', () => {
const { container } = render(<Alert body="Body" title="Title" open={true} onClose={vi.fn()} />);

expect(container).toMatchSnapshot();
});
});
28 changes: 28 additions & 0 deletions src/components/Alert/Alert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { useTranslation } from 'react-i18next';

import Dialog from '../Dialog/Dialog';
import Button from '../Button/Button';

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

type Props = {
open: boolean;
title: string;
body: string;
onClose: () => void;
};

const Alert: React.FC<Props> = ({ open, title, body, onClose }: Props) => {
const { t } = useTranslation('common');

return (
<Dialog open={open} onClose={onClose}>
<h2 className={styles.title}>{title}</h2>
<p className={styles.body}>{body}</p>
<Button label={t('alert.close')} variant="outlined" onClick={onClose} fullWidth />
</Dialog>
);
};

export default Alert;
3 changes: 3 additions & 0 deletions src/components/Alert/__snapshots__/Alert.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Vitest Snapshot v1

exports[`<Alert> > renders and matches snapshot 1`] = `<div />`;
9 changes: 1 addition & 8 deletions src/components/ConfirmationDialog/ConfirmationDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,7 @@ const ConfirmationDialog: React.FC<Props> = ({ open, title, body, onConfirm, onC
<Dialog open={open} onClose={onClose}>
<h2 className={styles.title}>{title}</h2>
<p className={styles.body}>{body}</p>
<Button
className={styles.confirmButton}
label={t('confirmation_dialog.confirm')}
variant="contained"
color="primary"
onClick={onConfirm}
fullWidth
/>
<Button className={styles.confirmButton} label={t('confirmation_dialog.confirm')} variant="contained" color="primary" onClick={onConfirm} fullWidth />
<Button label={t('confirmation_dialog.close')} variant="outlined" onClick={onClose} fullWidth />
</Dialog>
);
Expand Down
1 change: 1 addition & 0 deletions src/components/Video/Video.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('<Video>', () => {
onShareClick={vi.fn()}
enableSharing
isFavorited={false}
isFavoritesEnabled={true}
onFavoriteButtonClick={vi.fn()}
playTrailer={false}
onTrailerClick={vi.fn()}
Expand Down
20 changes: 12 additions & 8 deletions src/components/Video/Video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type Props = {
goBack: () => void;
onComplete?: () => void;
isFavorited: boolean;
isFavoritesEnabled: boolean;
onFavoriteButtonClick: () => void;
poster: Poster;
enableSharing: boolean;
Expand Down Expand Up @@ -59,6 +60,7 @@ const Video: React.FC<Props> = ({
hasShared,
onShareClick,
isFavorited,
isFavoritesEnabled,
onFavoriteButtonClick,
children,
playTrailer,
Expand Down Expand Up @@ -132,14 +134,16 @@ const Video: React.FC<Props> = ({
fullWidth={breakpoint < Breakpoint.md}
/>
)}
<Button
label={t('video:favorite')}
aria-label={isFavorited ? t('video:remove_from_favorites') : t('video:add_to_favorites')}
startIcon={isFavorited ? <Favorite /> : <FavoriteBorder />}
onClick={onFavoriteButtonClick}
color={isFavorited ? 'primary' : 'default'}
fullWidth={breakpoint < Breakpoint.md}
/>
{isFavoritesEnabled && (
<Button
label={t('video:favorite')}
aria-label={isFavorited ? t('video:remove_from_favorites') : t('video:add_to_favorites')}
startIcon={isFavorited ? <Favorite /> : <FavoriteBorder />}
onClick={onFavoriteButtonClick}
color={isFavorited ? 'primary' : 'default'}
fullWidth={breakpoint < Breakpoint.md}
/>
)}
{enableSharing && (
<Button
label={hasShared ? t('video:copied_url') : t('video:share')}
Expand Down
1 change: 1 addition & 0 deletions src/constants/watchlist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MAX_WATCHLIST_ITEMS_COUNT = 30;
29 changes: 14 additions & 15 deletions src/containers/Cinema/Cinema.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import classNames from 'classnames';

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

import { VideoProgressMinMax } from '#src/config';
import { useWatchHistoryListener } from '#src/hooks/useWatchHistoryListener';
import { useWatchHistoryStore } from '#src/stores/WatchHistoryStore';
import { ConfigContext } from '#src/providers/ConfigProvider';
import { addScript } from '#src/utils/dom';
import useOttAnalytics from '#src/hooks/useOttAnalytics';
import { deepCopy } from '#src/utils/collection';
import type { JWPlayer } from '#types/jwplayer';
import type { PlaylistItem } from '#types/playlist';
import type { Config } from '#types/Config';
import { useConfigStore } from '#src/stores/ConfigStore';
import { saveItem } from '#src/stores/WatchHistoryController';
import type { VideoProgress } from '#types/video';
import { PersonalShelf } from '#src/enum/PersonalShelf';
import { usePlaylistItemCallback } from '#src/hooks/usePlaylistItemCallback';

type Props = {
Expand All @@ -31,25 +28,27 @@ type Props = {
};

const Cinema: React.FC<Props> = ({ item, onPlay, onPause, onComplete, onUserActive, onUserInActive, feedId, isTrailer = false }: Props) => {
const config: Config = useContext(ConfigContext);
const { player, continue_watching_list } = useConfigStore(({ config }) => ({
player: config.player,
continue_watching_list: config.features?.continue_watching_list,
}));
const playerElementRef = useRef<HTMLDivElement>(null);
const playerRef = useRef<JWPlayer>();
const loadingRef = useRef(false);
const seekToRef = useRef(-1);
const [libLoaded, setLibLoaded] = useState(!!window.jwplayer);
const scriptUrl = `https://content.jwplatform.com/libraries/${config.player}.js`;
const enableWatchHistory = config.content.some((el) => el.type === PersonalShelf.ContinueWatching) && !isTrailer;
const scriptUrl = `https://content.jwplatform.com/libraries/${player}.js`;
const enableWatchHistory = continue_watching_list && !isTrailer;
const setPlayer = useOttAnalytics(item, feedId);
const handlePlaylistItemCallback = usePlaylistItemCallback();

const getProgress = useCallback((): VideoProgress | null => {
const getProgress = useCallback((): number | null => {
if (!playerRef.current) return null;

const duration = playerRef.current.getDuration();
const progress = playerRef.current.getPosition() / duration;
const progress = playerRef.current.getPosition() / item.duration;

return { duration, progress };
}, []);
return progress;
}, [item]);

useWatchHistoryListener(() => (enableWatchHistory ? saveItem(item, getProgress()) : null));

Expand Down Expand Up @@ -99,7 +98,7 @@ const Cinema: React.FC<Props> = ({ item, onPlay, onPause, onComplete, onUserActi
}, [scriptUrl]);

useEffect(() => {
if (!config.player) {
if (!player) {
return;
}

Expand Down Expand Up @@ -171,7 +170,7 @@ const Cinema: React.FC<Props> = ({ item, onPlay, onPause, onComplete, onUserActi
if (libLoaded) {
initializePlayer();
}
}, [libLoaded, item, onPlay, onPause, onUserActive, onUserInActive, onComplete, config.player, enableWatchHistory, setPlayer, handlePlaylistItemCallback]);
}, [libLoaded, item, onPlay, onPause, onUserActive, onUserInActive, onComplete, player, enableWatchHistory, setPlayer, handlePlaylistItemCallback]);

useEffect(() => {
return () => {
Expand Down
2 changes: 1 addition & 1 deletion src/containers/Layout/Layout.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
.layout {
display: flex;
flex-direction: column;
min-height: calc(100vh - calc(100vh - 100%));
height: 100vh;
}

.main {
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/en_US/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"close": "Cancel",
"confirm": "Yes"
},
"alert": {
"close": "Close"
},
"filter_videos_by_genre": "Filter videos by genre",
"home": "Home",
"live": "LIVE",
Expand Down
6 changes: 5 additions & 1 deletion src/i18n/locales/en_US/video.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,9 @@
"trailer": "Trailer",
"video_not_found": "Video not found",
"watch_trailer": "Watch the trailer",
"share_video": "Share this video"
"share_video": "Share this video",
"favorites_warning": {
"title": "Maximum amount of favorite videos exceeded",
"body": "You can only add up to {{ count }} favorite videos. Please delete one and repeat the operation."
}
}
7 changes: 7 additions & 0 deletions src/providers/ConfigProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ const ConfigProvider: FunctionComponent<ProviderProps> = ({ children, configLoca
onValidationError(error);
});

if (!config) {
onLoading(false);
setLoading(false);

return;
}

validateConfig(config)
.then((configValidated) => {
const configWithDefaults = merge({}, defaultConfig, configValidated);
Expand Down
Loading

0 comments on commit 0124428

Please sign in to comment.