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} />