diff --git a/src/components/Card/Card.module.scss b/src/components/Card/Card.module.scss
index d80ab1e92..e67210b19 100644
--- a/src/components/Card/Card.module.scss
+++ b/src/components/Card/Card.module.scss
@@ -19,7 +19,7 @@
& .poster {
box-shadow: 0 0 0 3px var(--highlight-color, variables.$white), 0 8px 10px rgb(0 0 0 / 14%), 0 3px 14px rgb(0 0 0 / 12%),
- 0 4px 5px rgb(0 0 0 / 20%);
+ 0 4px 5px rgb(0 0 0 / 20%);
}
}
}
@@ -162,12 +162,18 @@ $aspects: ((1, 1), (2, 1), (2, 3), (4, 3), (5, 3), (16, 9), (9, 16));
color: var(--card-color);
}
+.tags {
+ display: flex;
+}
+
.tag {
+ display: flex;
+ align-items: center;
padding: 4px 8px;
color: var(--card-color);
font-family: var(--body-font-family);
font-weight: 600;
- font-size: 13px;
+ font-size: 16px;
white-space: nowrap;
background-color: rgba(variables.$black, 0.6);
border-radius: 4px;
@@ -176,6 +182,15 @@ $aspects: ((1, 1), (2, 1), (2, 3), (4, 3), (5, 3), (16, 9), (9, 16));
}
}
+.lock {
+ margin-right: variables.$base-spacing / 2;
+ padding: 2px 6px;
+ > svg {
+ width: 16px;
+ height: 21px;
+ }
+}
+
.live {
background-color: variables.$red;
}
diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx
index 2fd77d14d..be3afef06 100644
--- a/src/components/Card/Card.tsx
+++ b/src/components/Card/Card.tsx
@@ -3,6 +3,7 @@ import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { formatDurationTag } from '../../utils/formatting';
+import Lock from '../../icons/Lock';
import styles from './Card.module.scss';
@@ -21,6 +22,7 @@ type CardProps = {
disabled?: boolean;
loading?: boolean;
isCurrent?: boolean;
+ isLocked?: boolean;
currentLabel?: string;
enableTitle?: boolean;
};
@@ -41,6 +43,7 @@ function Card({
disabled = false,
loading = false,
isCurrent = false,
+ isLocked = true,
currentLabel,
}: CardProps): JSX.Element {
const { t } = useTranslation('common');
@@ -85,7 +88,14 @@ function Card({
{!loading && (
{featured && !disabled && enableTitle &&
{title}
}
- {renderTag()}
+
+ {isLocked && (
+
+
+
+ )}
+ {renderTag()}
+
)}
{progress ? (
diff --git a/src/components/CardGrid/CardGrid.tsx b/src/components/CardGrid/CardGrid.tsx
index 99cf79614..f65fcbce9 100644
--- a/src/components/CardGrid/CardGrid.tsx
+++ b/src/components/CardGrid/CardGrid.tsx
@@ -27,6 +27,8 @@ type CardGridProps = {
cols?: Breakpoints;
currentCardItem?: PlaylistItem;
currentCardLabel?: string;
+ hasActiveSubscription: boolean;
+ requiresSubscription: boolean;
};
function CardGrid({
@@ -39,6 +41,8 @@ function CardGrid({
cols = defaultCols,
currentCardItem,
currentCardLabel,
+ requiresSubscription,
+ hasActiveSubscription,
}: CardGridProps) {
const breakpoint: Breakpoint = useBreakpoint();
const isLargeScreen = breakpoint >= Breakpoint.md;
@@ -69,6 +73,7 @@ function CardGrid({
loading={isLoading}
isCurrent={currentCardItem && currentCardItem.mediaid === mediaid}
currentLabel={currentCardLabel}
+ isLocked={requiresSubscription && !hasActiveSubscription && playlistItem.requiresSubscription !== 'false'}
/>
diff --git a/src/components/Shelf/Shelf.tsx b/src/components/Shelf/Shelf.tsx
index e7343bfad..9d83560e9 100644
--- a/src/components/Shelf/Shelf.tsx
+++ b/src/components/Shelf/Shelf.tsx
@@ -39,6 +39,8 @@ export type ShelfProps = {
loading?: boolean;
error?: unknown;
title?: string;
+ hasActiveSubscription: boolean;
+ requiresSubscription: boolean;
};
const Shelf: React.FC = ({
@@ -52,6 +54,8 @@ const Shelf: React.FC = ({
featured = false,
loading = false,
error = null,
+ requiresSubscription,
+ hasActiveSubscription,
}: ShelfProps) => {
const breakpoint: Breakpoint = useBreakpoint();
const { t } = useTranslation('common');
@@ -76,9 +80,21 @@ const Shelf: React.FC = ({
featured={featured}
disabled={!isInView}
loading={loading}
+ isLocked={requiresSubscription && !hasActiveSubscription && item.requiresSubscription !== 'false'}
/>
),
- [enableCardTitles, featured, imageSourceWidth, loading, onCardClick, onCardHover, playlist.feedid, watchHistory],
+ [
+ enableCardTitles,
+ featured,
+ imageSourceWidth,
+ loading,
+ onCardClick,
+ onCardHover,
+ playlist.feedid,
+ watchHistory,
+ requiresSubscription,
+ hasActiveSubscription,
+ ],
);
const renderRightControl = useCallback(
diff --git a/src/containers/Subscription/SubscriptionContainer.ts b/src/containers/Subscription/SubscriptionContainer.ts
index 3d449965e..114cc6a63 100644
--- a/src/containers/Subscription/SubscriptionContainer.ts
+++ b/src/containers/Subscription/SubscriptionContainer.ts
@@ -37,7 +37,7 @@ const SubscriptionContainer = ({ children }: Props): JSX.Element => {
const { data: transactions, isLoading: isTransactionsLoading } = getTransactionsQuery;
return children({
- activeSubscription: subscriptions?.responseData.items.find(
+ activeSubscription: subscriptions?.responseData?.items.find(
(subscription) => subscription.status !== 'expired' && subscription.status !== 'terminated',
),
activePaymentDetail: paymentDetails?.responseData.paymentDetails.find((paymentDetails) => paymentDetails.active),
diff --git a/src/icons/Lock.tsx b/src/icons/Lock.tsx
new file mode 100644
index 000000000..59f29324a
--- /dev/null
+++ b/src/icons/Lock.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+
+import createIcon from './Icon';
+
+export default createIcon(
+ '0 0 24 24',
+ ,
+);
diff --git a/src/screens/Home/Home.tsx b/src/screens/Home/Home.tsx
index 87516ac67..0bedb67ec 100644
--- a/src/screens/Home/Home.tsx
+++ b/src/screens/Home/Home.tsx
@@ -1,23 +1,25 @@
-import React, { CSSProperties, useContext, useRef, useEffect, useCallback } from 'react';
+import React, { CSSProperties, useRef, useEffect, useCallback } from 'react';
import memoize from 'memoize-one';
import WindowScroller from 'react-virtualized/dist/commonjs/WindowScroller';
import List from 'react-virtualized/dist/commonjs/List';
import { useHistory } from 'react-router-dom';
-import type { Config, Content } from 'types/Config';
+import type { Content } from 'types/Config';
import type { PlaylistItem } from 'types/playlist';
import classNames from 'classnames';
import PlaylistContainer from '../../containers/Playlist/PlaylistContainer';
import { favoritesStore } from '../../stores/FavoritesStore';
+import { AccountStore } from '../../stores/AccountStore';
+import { ConfigStore } from '../../stores/ConfigStore';
import { PersonalShelf } from '../../enum/PersonalShelf';
import { useWatchHistory } from '../../stores/WatchHistoryStore';
import useBlurImageUpdater from '../../hooks/useBlurImageUpdater';
import ShelfComponent, { featuredTileBreakpoints, tileBreakpoints } from '../../components/Shelf/Shelf';
-import { ConfigContext } from '../../providers/ConfigProvider';
import usePlaylist from '../../hooks/usePlaylist';
import useBreakpoint, { Breakpoint } from '../../hooks/useBreakpoint';
import scrollbarSize from '../../utils/dom';
import { cardUrl } from '../../utils/formatting';
+import { configHasCleengOffer } from '../../utils/cleeng';
import styles from './Home.module.scss';
@@ -35,10 +37,11 @@ const createItemData = memoize((content) => ({ content }));
const Home = (): JSX.Element => {
const history = useHistory();
- const config: Config = useContext(ConfigContext);
+ const config = ConfigStore.useState((state) => state.config);
const breakpoint = useBreakpoint();
const listRef = useRef() as React.MutableRefObject;
const content: Content[] = config?.content;
+ const itemData: ItemData = createItemData(content);
const { getPlaylist: getWatchHistoryPlaylist, getDictionary: getWatchHistoryDictionary } = useWatchHistory();
const watchHistory = getWatchHistoryPlaylist();
@@ -48,6 +51,9 @@ const Home = (): JSX.Element => {
const { data: { playlist } = { playlist: [] } } = usePlaylist(content[0]?.playlistId);
const updateBlurImage = useBlurImageUpdater(playlist);
+ const hasActiveSubscription = !!AccountStore.useState((state) => state.subscription);
+ const requiresSubscription = !!config.cleengId && configHasCleengOffer(config);
+
const onCardClick = useCallback(
(playlistItem: PlaylistItem, playlistId?: string) => {
history.push(cardUrl(playlistItem, playlistId, playlistId === PersonalShelf.ContinueWatching));
@@ -56,8 +62,6 @@ const Home = (): JSX.Element => {
);
const onCardHover = useCallback((playlistItem: PlaylistItem) => updateBlurImage(playlistItem.image), [updateBlurImage]);
- const itemData: ItemData = createItemData(content);
-
const rowRenderer = ({ index, key, style, itemData }: rowData) => {
if (!itemData?.content?.[index]) return null;
@@ -79,6 +83,8 @@ const Home = (): JSX.Element => {
enableCardTitles={config.options.shelveTitles}
title={playlist.title}
featured={contentItem.featured === true}
+ hasActiveSubscription={hasActiveSubscription}
+ requiresSubscription={requiresSubscription}
/>
@@ -122,13 +128,13 @@ const Home = (): JSX.Element => {
useEffect(() => {
if (favorites || watchHistory) {
- ((listRef.current as unknown) as List)?.recomputeRowHeights();
+ (listRef.current as unknown as List)?.recomputeRowHeights();
}
}, [favorites, watchHistory]);
return (
-
((listRef.current as unknown) as List)?.recomputeRowHeights()}>
+ (listRef.current as unknown as List)?.recomputeRowHeights()}>
{({ height, isScrolling, onChildScroll, scrollTop }) => (
): JSX.
});
const { data: subscriptionsResult, isLoading: isSubscriptionsLoading } = getSubscriptionsQuery;
const subscriptions = subscriptionsResult?.responseData?.items;
- const hasActiveSubscription = subscriptions?.find(
+ const hasActiveSubscription = !!subscriptions?.find(
(subscription: Subscription) => subscription.status === 'active' || subscription.status === 'cancelled',
);
const allowedToWatch = useMemo(
@@ -205,6 +205,8 @@ const Movie = ({ match, location }: RouteComponentProps): JSX.
currentCardItem={item}
currentCardLabel={t('currently_playing')}
enableCardTitles={options.shelveTitles}
+ hasActiveSubscription={hasActiveSubscription}
+ requiresSubscription={!!cleengId && configHasOffer}
/>
>
) : undefined}
diff --git a/src/screens/Playlist/Playlist.tsx b/src/screens/Playlist/Playlist.tsx
index dce7c6b42..25cb5d613 100644
--- a/src/screens/Playlist/Playlist.tsx
+++ b/src/screens/Playlist/Playlist.tsx
@@ -1,10 +1,8 @@
-import React, { useContext, useEffect, useMemo, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import { RouteComponentProps, useHistory } from 'react-router-dom';
import type { PlaylistItem } from 'types/playlist';
-import type { Config } from 'types/Config';
import { Helmet } from 'react-helmet';
-import { ConfigContext } from '../../providers/ConfigProvider';
import { cardUrl } from '../../utils/formatting';
import usePlaylist from '../../hooks/usePlaylist';
import { filterPlaylist, getFiltersFromConfig } from '../../utils/collection';
@@ -12,6 +10,9 @@ import CardGrid from '../../components/CardGrid/CardGrid';
import ErrorPage from '../../components/ErrorPage/ErrorPage';
import Filter from '../../components/Filter/Filter';
import useBlurImageUpdater from '../../hooks/useBlurImageUpdater';
+import { AccountStore } from '../../stores/AccountStore';
+import { ConfigStore } from '../../stores/ConfigStore';
+import { configHasCleengOffer } from '../../utils/cleeng';
import styles from './Playlist.module.scss';
@@ -25,7 +26,7 @@ function Playlist({
},
}: RouteComponentProps) {
const history = useHistory();
- const config: Config = useContext(ConfigContext);
+ const config = ConfigStore.useState((state) => state.config);
const { isLoading, isPlaceholderData, error, data: { title, playlist } = { title: '', playlist: [] } } = usePlaylist(id);
const [filter, setFilter] = useState('');
@@ -34,6 +35,9 @@ function Playlist({
const filteredPlaylist = useMemo(() => filterPlaylist(playlist, filter), [playlist, filter]);
const updateBlurImage = useBlurImageUpdater(filteredPlaylist);
+ const hasActiveSubscription = !!AccountStore.useState((state) => state.subscription);
+ const requiresSubscription = !!config.cleengId && configHasCleengOffer(config);
+
useEffect(() => {
// reset filter when the playlist id changes
setFilter('');
@@ -66,6 +70,8 @@ function Playlist({
onCardHover={onCardHover}
isLoading={isLoading}
enableCardTitles={config.options.shelveTitles}
+ hasActiveSubscription={hasActiveSubscription}
+ requiresSubscription={requiresSubscription}
/>
diff --git a/src/screens/Search/Search.tsx b/src/screens/Search/Search.tsx
index 7ec121025..de7ab1199 100644
--- a/src/screens/Search/Search.tsx
+++ b/src/screens/Search/Search.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect } from 'react';
+import React, { useEffect } from 'react';
import type { RouteComponentProps } from 'react-router-dom';
import { useHistory } from 'react-router';
import { Helmet } from 'react-helmet';
@@ -10,10 +10,12 @@ import useSearchQueryUpdater from '../../hooks/useSearchQueryUpdater';
import ErrorPage from '../../components/ErrorPage/ErrorPage';
import type { PlaylistItem } from '../../../types/playlist';
import CardGrid from '../../components/CardGrid/CardGrid';
-import { ConfigContext } from '../../providers/ConfigProvider';
import { cardUrl } from '../../utils/formatting';
import useFirstRender from '../../hooks/useFirstRender';
import useSearchPlaylist from '../../hooks/useSearchPlaylist';
+import { AccountStore } from '../../stores/AccountStore';
+import { ConfigStore } from '../../stores/ConfigStore';
+import { configHasCleengOffer } from '../../utils/cleeng';
import styles from './Search.module.scss';
@@ -27,7 +29,8 @@ const Search: React.FC> = ({
},
}) => {
const { t } = useTranslation('search');
- const { siteName, searchPlaylist, options } = useContext(ConfigContext);
+ const config = ConfigStore.useState((state) => state.config);
+ const { siteName, searchPlaylist, options } = config;
const firstRender = useFirstRender();
const searchQuery = UIStore.useState((s) => s.searchQuery);
const { updateSearchQuery } = useSearchQueryUpdater();
@@ -36,6 +39,9 @@ const Search: React.FC> = ({
const updateBlurImage = useBlurImageUpdater(playlist);
+ const hasActiveSubscription = !!AccountStore.useState((state) => state.subscription);
+ const requiresSubscription = !!config.cleengId && configHasCleengOffer(config);
+
// Update the search bar query to match the route param on mount
useEffect(() => {
if (!firstRender) {
@@ -100,6 +106,8 @@ const Search: React.FC> = ({
onCardHover={onCardHover}
isLoading={firstRender}
enableCardTitles={options.shelveTitles}
+ hasActiveSubscription={hasActiveSubscription}
+ requiresSubscription={requiresSubscription}
/>
diff --git a/src/screens/Series/Series.tsx b/src/screens/Series/Series.tsx
index 235a4bf5d..ef54117d1 100644
--- a/src/screens/Series/Series.tsx
+++ b/src/screens/Series/Series.tsx
@@ -92,7 +92,7 @@ const Series = ({ match, location }: RouteComponentProps): JS
});
const { data: subscriptionsResult, isLoading: isSubscriptionsLoading } = getSubscriptionsQuery;
const subscriptions = subscriptionsResult?.responseData?.items;
- const hasActiveSubscription = subscriptions?.find(
+ const hasActiveSubscription = !!subscriptions?.find(
(subscription: Subscription) => subscription.status === 'active' || subscription.status === 'cancelled',
);
const allowedToWatch = useMemo(
@@ -236,6 +236,8 @@ const Series = ({ match, location }: RouteComponentProps): JS
currentCardItem={item}
currentCardLabel={t('current_episode')}
enableCardTitles={options.shelveTitles}
+ hasActiveSubscription={hasActiveSubscription}
+ requiresSubscription={!!cleengId && configHasOffer}
/>
>