fix(ui): hide 'Recently Added' & 'Recent Requests' sliders when empty (#2190)

* fix(ui): hide 'Recently Added' & 'Recent Requests' sliders when empty

* fix(ui): hide 'errored' sliders too

* fix: type import

* fix: remove unneeded React import

* fix: missing TmdbTitleCard props

* refactor: remove isEmpty param for never-empty sliders

* fix: display empty watchlist message if autorequest enabled

* fix: pr suggestion

* fix(lang): remove no-longer-needed string
This commit is contained in:
TheCatLady
2022-08-30 16:51:55 -07:00
committed by GitHub
parent 410ad0d4b4
commit 03d5e56678
7 changed files with 192 additions and 121 deletions

View File

@@ -23,10 +23,11 @@ const messages = defineMessages({
populartv: 'Popular Series', populartv: 'Popular Series',
upcomingtv: 'Upcoming Series', upcomingtv: 'Upcoming Series',
recentlyAdded: 'Recently Added', recentlyAdded: 'Recently Added',
noRequests: 'No requests.',
upcoming: 'Upcoming Movies', upcoming: 'Upcoming Movies',
trending: 'Trending', trending: 'Trending',
plexwatchlist: 'Your Plex Watchlist', plexwatchlist: 'Your Plex Watchlist',
emptywatchlist:
'Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.',
}); });
const Discover = () => { const Discover = () => {
@@ -58,76 +59,97 @@ const Discover = () => {
return ( return (
<> <>
<PageTitle title={intl.formatMessage(messages.discover)} /> <PageTitle title={intl.formatMessage(messages.discover)} />
{hasPermission([Permission.MANAGE_REQUESTS, Permission.RECENT_VIEW], { {(!media || !!media.results.length) &&
type: 'or', !mediaError &&
}) && ( hasPermission([Permission.MANAGE_REQUESTS, Permission.RECENT_VIEW], {
<> type: 'or',
<div className="slider-header"> }) && (
<div className="slider-title"> <>
<span>{intl.formatMessage(messages.recentlyAdded)}</span> <div className="slider-header">
<div className="slider-title">
<span>{intl.formatMessage(messages.recentlyAdded)}</span>
</div>
</div> </div>
</div> <Slider
<Slider sliderKey="media"
sliderKey="media" isLoading={!media}
isLoading={!media && !mediaError} items={(media?.results ?? []).map((item) => (
isEmpty={!!media && !mediaError && media.results.length === 0} <TmdbTitleCard
items={media?.results?.map((item) => ( key={`media-slider-item-${item.id}`}
<TmdbTitleCard id={item.id}
key={`media-slider-item-${item.id}`} tmdbId={item.tmdbId}
id={item.id} tvdbId={item.tvdbId}
tmdbId={item.tmdbId} type={item.mediaType}
tvdbId={item.tvdbId} />
type={item.mediaType} ))}
/> />
))} </>
/> )}
</> {(!requests || !!requests.results.length) && !requestError && (
)}
<div className="slider-header">
<Link href="/requests?filter=all">
<a className="slider-title">
<span>{intl.formatMessage(messages.recentrequests)}</span>
<ArrowCircleRightIcon />
</a>
</Link>
</div>
<Slider
sliderKey="requests"
isLoading={!requests && !requestError}
isEmpty={!!requests && !requestError && requests.results.length === 0}
items={(requests?.results ?? []).map((request) => (
<RequestCard
key={`request-slider-item-${request.id}`}
request={request}
/>
))}
placeholder={<RequestCard.Placeholder />}
emptyMessage={intl.formatMessage(messages.noRequests)}
/>
{(!watchlistItems || !!watchlistItems.results.length) && !watchlistError && (
<> <>
<div className="slider-header"> <div className="slider-header">
<Link href="/discover/watchlist"> <Link href="/requests?filter=all">
<a className="slider-title"> <a className="slider-title">
<span>{intl.formatMessage(messages.plexwatchlist)}</span> <span>{intl.formatMessage(messages.recentrequests)}</span>
<ArrowCircleRightIcon /> <ArrowCircleRightIcon />
</a> </a>
</Link> </Link>
</div> </div>
<Slider <Slider
sliderKey="watchlist" sliderKey="requests"
isLoading={!watchlistItems && !watchlistError} isLoading={!requests}
items={watchlistItems?.results.map((item) => ( items={(requests?.results ?? []).map((request) => (
<TmdbTitleCard <RequestCard
id={item.tmdbId} key={`request-slider-item-${request.id}`}
key={`watchlist-slider-item-${item.ratingKey}`} request={request}
tmdbId={item.tmdbId}
type={item.mediaType}
/> />
))} ))}
placeholder={<RequestCard.Placeholder />}
/> />
</> </>
)} )}
{user?.userType === UserType.PLEX &&
(!watchlistItems ||
!!watchlistItems.results.length ||
user.settings?.watchlistSyncMovies ||
user.settings?.watchlistSyncTv) &&
!watchlistError && (
<>
<div className="slider-header">
<Link href="/discover/watchlist">
<a className="slider-title">
<span>{intl.formatMessage(messages.plexwatchlist)}</span>
<ArrowCircleRightIcon />
</a>
</Link>
</div>
<Slider
sliderKey="watchlist"
isLoading={!watchlistItems}
isEmpty={!!watchlistItems && watchlistItems.results.length === 0}
emptyMessage={intl.formatMessage(messages.emptywatchlist, {
PlexWatchlistSupportLink: (msg: React.ReactNode) => (
<a
href="https://support.plex.tv/articles/universal-watchlist/"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
})}
items={watchlistItems?.results.map((item) => (
<TmdbTitleCard
id={item.tmdbId}
key={`watchlist-slider-item-${item.ratingKey}`}
tmdbId={item.tmdbId}
type={item.mediaType}
/>
))}
/>
</>
)}
<MediaSlider <MediaSlider
sliderKey="trending" sliderKey="trending"
title={intl.formatMessage(messages.trending)} title={intl.formatMessage(messages.trending)}

View File

@@ -20,7 +20,6 @@ import * as Yup from 'yup';
const messages = defineMessages({ const messages = defineMessages({
validationMessageRequired: 'You must provide a description', validationMessageRequired: 'You must provide a description',
issomethingwrong: 'Is there a problem with {title}?',
whatswrong: "What's wrong?", whatswrong: "What's wrong?",
providedetail: providedetail:
'Please provide a detailed explanation of the issue you encountered.', 'Please provide a detailed explanation of the issue you encountered.',

View File

@@ -44,6 +44,7 @@ const RequestList = () => {
const { user } = useUser({ const { user } = useUser({
id: Number(router.query.userId), id: Number(router.query.userId),
}); });
const { user: currentUser } = useUser();
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING); const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING);
const [currentSort, setCurrentSort] = useState<Sort>('added'); const [currentSort, setCurrentSort] = useState<Sort>('added');
const [currentPageSize, setCurrentPageSize] = useState<number>(10); const [currentPageSize, setCurrentPageSize] = useState<number>(10);
@@ -60,7 +61,11 @@ const RequestList = () => {
`/api/v1/request?take=${currentPageSize}&skip=${ `/api/v1/request?take=${currentPageSize}&skip=${
pageIndex * currentPageSize pageIndex * currentPageSize
}&filter=${currentFilter}&sort=${currentSort}${ }&filter=${currentFilter}&sort=${currentSort}${
router.query.userId ? `&requestedBy=${router.query.userId}` : '' router.pathname.startsWith('/profile')
? `&requestedBy=${currentUser?.id}`
: router.query.userId
? `&requestedBy=${router.query.userId}`
: ''
}` }`
); );
@@ -116,7 +121,11 @@ const RequestList = () => {
<div className="mb-4 flex flex-col justify-between lg:flex-row lg:items-end"> <div className="mb-4 flex flex-col justify-between lg:flex-row lg:items-end">
<Header <Header
subtext={ subtext={
router.query.userId ? ( router.pathname.startsWith('/profile') ? (
<Link href={`/profile`}>
<a className="hover:underline">{currentUser?.displayName}</a>
</Link>
) : router.query.userId ? (
<Link href={`/users/${user?.id}`}> <Link href={`/users/${user?.id}`}>
<a className="hover:underline">{user?.displayName}</a> <a className="hover:underline">{user?.displayName}</a>
</Link> </Link>

View File

@@ -11,7 +11,7 @@ interface SliderProps {
items?: JSX.Element[]; items?: JSX.Element[];
isLoading: boolean; isLoading: boolean;
isEmpty?: boolean; isEmpty?: boolean;
emptyMessage?: string; emptyMessage?: React.ReactNode;
placeholder?: React.ReactNode; placeholder?: React.ReactNode;
} }
@@ -192,7 +192,7 @@ const Slider = ({
</div> </div>
))} ))}
{isEmpty && ( {isEmpty && (
<div className="mt-16 mb-16 text-center text-white"> <div className="mt-16 mb-16 text-center font-medium text-gray-400">
{emptyMessage {emptyMessage
? emptyMessage ? emptyMessage
: intl.formatMessage(globalMessages.noresults)} : intl.formatMessage(globalMessages.noresults)}

View File

@@ -20,12 +20,11 @@ import type { TvDetails } from '@server/models/Tv';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { defineMessages, FormattedNumber, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages({
recentrequests: 'Recent Requests', recentrequests: 'Recent Requests',
norequests: 'No requests.',
limit: '{remaining} of {limit}', limit: '{remaining} of {limit}',
requestsperdays: '{limit} remaining', requestsperdays: '{limit} remaining',
unlimited: 'Unlimited', unlimited: 'Unlimited',
@@ -35,6 +34,8 @@ const messages = defineMessages({
seriesrequest: 'Series Requests', seriesrequest: 'Series Requests',
recentlywatched: 'Recently Watched', recentlywatched: 'Recently Watched',
plexwatchlist: 'Plex Watchlist', plexwatchlist: 'Plex Watchlist',
emptywatchlist:
'Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.',
}); });
type MediaTitle = MovieDetails | TvDetails; type MediaTitle = MovieDetails | TvDetails;
@@ -70,22 +71,24 @@ const UserProfile = () => {
? `/api/v1/user/${user.id}/quota` ? `/api/v1/user/${user.id}/quota`
: null : null
); );
const { data: watchData } = useSWR<UserWatchDataResponse>( const { data: watchData, error: watchDataError } =
user?.userType === UserType.PLEX && useSWR<UserWatchDataResponse>(
(user.id === currentUser?.id || currentHasPermission(Permission.ADMIN)) user?.userType === UserType.PLEX &&
? `/api/v1/user/${user.id}/watch_data` (user.id === currentUser?.id || currentHasPermission(Permission.ADMIN))
: null ? `/api/v1/user/${user.id}/watch_data`
); : null
);
const { data: watchlistItems, error: watchlistError } = const { data: watchlistItems, error: watchlistError } =
useSWR<WatchlistResponse>( useSWR<WatchlistResponse>(
user?.id === currentUser?.id || user?.userType === UserType.PLEX &&
currentHasPermission( (user.id === currentUser?.id ||
[Permission.MANAGE_REQUESTS, Permission.WATCHLIST_VIEW], currentHasPermission(
{ [Permission.MANAGE_REQUESTS, Permission.WATCHLIST_VIEW],
type: 'or', {
} type: 'or',
) }
? `/api/v1/user/${user?.id}/watchlist` ))
? `/api/v1/user/${user.id}/watchlist`
: null, : null,
{ {
revalidateOnMount: true, revalidateOnMount: true,
@@ -146,7 +149,15 @@ const UserProfile = () => {
{intl.formatMessage(messages.totalrequests)} {intl.formatMessage(messages.totalrequests)}
</dt> </dt>
<dd className="mt-1 text-3xl font-semibold text-white"> <dd className="mt-1 text-3xl font-semibold text-white">
<FormattedNumber value={user.requestCount} /> <Link
href={
user.id === currentUser?.id
? '/profile/requests?filter=all'
: `/users/${user?.id}/requests?filter=all`
}
>
<a>{intl.formatNumber(user.requestCount)}</a>
</Link>
</dd> </dd>
</div> </div>
<div <div
@@ -266,40 +277,49 @@ const UserProfile = () => {
currentHasPermission( currentHasPermission(
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW], [Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
{ type: 'or' } { type: 'or' }
)) && (
<>
<div className="slider-header">
<Link href={`/users/${user?.id}/requests?filter=all`}>
<a className="slider-title">
<span>{intl.formatMessage(messages.recentrequests)}</span>
<ArrowCircleRightIcon />
</a>
</Link>
</div>
<Slider
sliderKey="requests"
isLoading={!requests && !requestError}
isEmpty={
!!requests && !requestError && requests.results.length === 0
}
items={(requests?.results ?? []).map((request) => (
<RequestCard
key={`request-slider-item-${request.id}`}
request={request}
onTitleData={updateAvailableTitles}
/>
))}
placeholder={<RequestCard.Placeholder />}
emptyMessage={intl.formatMessage(messages.norequests)}
/>
</>
)}
{(user.id === currentUser?.id ||
currentHasPermission(
[Permission.MANAGE_REQUESTS, Permission.WATCHLIST_VIEW],
{ type: 'or' }
)) && )) &&
(!watchlistItems || !!watchlistItems.results.length) && (!requests || !!requests.results.length) &&
!requestError && (
<>
<div className="slider-header">
<Link
href={
user.id === currentUser?.id
? '/profile/requests?filter=all'
: `/users/${user?.id}/requests?filter=all`
}
>
<a className="slider-title">
<span>{intl.formatMessage(messages.recentrequests)}</span>
<ArrowCircleRightIcon />
</a>
</Link>
</div>
<Slider
sliderKey="requests"
isLoading={!requests}
items={(requests?.results ?? []).map((request) => (
<RequestCard
key={`request-slider-item-${request.id}`}
request={request}
onTitleData={updateAvailableTitles}
/>
))}
placeholder={<RequestCard.Placeholder />}
/>
</>
)}
{user.userType === UserType.PLEX &&
(user.id === currentUser?.id ||
currentHasPermission(
[Permission.MANAGE_REQUESTS, Permission.WATCHLIST_VIEW],
{ type: 'or' }
)) &&
(!watchlistItems ||
!!watchlistItems.results.length ||
(user.id === currentUser?.id &&
(user.settings?.watchlistSyncMovies ||
user.settings?.watchlistSyncTv))) &&
!watchlistError && ( !watchlistError && (
<> <>
<div className="slider-header"> <div className="slider-header">
@@ -318,7 +338,20 @@ const UserProfile = () => {
</div> </div>
<Slider <Slider
sliderKey="watchlist" sliderKey="watchlist"
isLoading={!watchlistItems && !watchlistError} isLoading={!watchlistItems}
isEmpty={!!watchlistItems && watchlistItems.results.length === 0}
emptyMessage={intl.formatMessage(messages.emptywatchlist, {
PlexWatchlistSupportLink: (msg: React.ReactNode) => (
<a
href="https://support.plex.tv/articles/universal-watchlist/"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
})}
items={watchlistItems?.results.map((item) => ( items={watchlistItems?.results.map((item) => (
<TmdbTitleCard <TmdbTitleCard
id={item.tmdbId} id={item.tmdbId}
@@ -330,9 +363,11 @@ const UserProfile = () => {
/> />
</> </>
)} )}
{(user.id === currentUser?.id || {user.userType === UserType.PLEX &&
currentHasPermission(Permission.ADMIN)) && (user.id === currentUser?.id ||
!!watchData?.recentlyWatched.length && ( currentHasPermission(Permission.ADMIN)) &&
(!watchData || !!watchData.recentlyWatched.length) &&
!watchDataError && (
<> <>
<div className="slider-header"> <div className="slider-header">
<div className="slider-title"> <div className="slider-title">
@@ -342,8 +377,7 @@ const UserProfile = () => {
<Slider <Slider
sliderKey="media" sliderKey="media"
isLoading={!watchData} isLoading={!watchData}
isEmpty={!watchData?.recentlyWatched.length} items={watchData?.recentlyWatched.map((item) => (
items={watchData.recentlyWatched.map((item) => (
<TmdbTitleCard <TmdbTitleCard
key={`media-slider-item-${item.id}`} key={`media-slider-item-${item.id}`}
id={item.id} id={item.id}

View File

@@ -23,7 +23,7 @@
"components.Discover.discover": "Discover", "components.Discover.discover": "Discover",
"components.Discover.discovermovies": "Popular Movies", "components.Discover.discovermovies": "Popular Movies",
"components.Discover.discovertv": "Popular Series", "components.Discover.discovertv": "Popular Series",
"components.Discover.noRequests": "No requests.", "components.Discover.emptywatchlist": "Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.",
"components.Discover.plexwatchlist": "Your Plex Watchlist", "components.Discover.plexwatchlist": "Your Plex Watchlist",
"components.Discover.popularmovies": "Popular Movies", "components.Discover.popularmovies": "Popular Movies",
"components.Discover.populartv": "Popular Series", "components.Discover.populartv": "Popular Series",
@@ -91,7 +91,6 @@
"components.IssueModal.CreateIssueModal.allseasons": "All Seasons", "components.IssueModal.CreateIssueModal.allseasons": "All Seasons",
"components.IssueModal.CreateIssueModal.episode": "Episode {episodeNumber}", "components.IssueModal.CreateIssueModal.episode": "Episode {episodeNumber}",
"components.IssueModal.CreateIssueModal.extras": "Extras", "components.IssueModal.CreateIssueModal.extras": "Extras",
"components.IssueModal.CreateIssueModal.issomethingwrong": "Is there a problem with {title}?",
"components.IssueModal.CreateIssueModal.problemepisode": "Affected Episode", "components.IssueModal.CreateIssueModal.problemepisode": "Affected Episode",
"components.IssueModal.CreateIssueModal.problemseason": "Affected Season", "components.IssueModal.CreateIssueModal.problemseason": "Affected Season",
"components.IssueModal.CreateIssueModal.providedetail": "Please provide a detailed explanation of the issue you encountered.", "components.IssueModal.CreateIssueModal.providedetail": "Please provide a detailed explanation of the issue you encountered.",
@@ -1048,9 +1047,9 @@
"components.UserProfile.UserSettings.menuNotifications": "Notifications", "components.UserProfile.UserSettings.menuNotifications": "Notifications",
"components.UserProfile.UserSettings.menuPermissions": "Permissions", "components.UserProfile.UserSettings.menuPermissions": "Permissions",
"components.UserProfile.UserSettings.unauthorizedDescription": "You do not have permission to modify this user's settings.", "components.UserProfile.UserSettings.unauthorizedDescription": "You do not have permission to modify this user's settings.",
"components.UserProfile.emptywatchlist": "Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.",
"components.UserProfile.limit": "{remaining} of {limit}", "components.UserProfile.limit": "{remaining} of {limit}",
"components.UserProfile.movierequests": "Movie Requests", "components.UserProfile.movierequests": "Movie Requests",
"components.UserProfile.norequests": "No requests.",
"components.UserProfile.pastdays": "{type} (past {days} days)", "components.UserProfile.pastdays": "{type} (past {days} days)",
"components.UserProfile.plexwatchlist": "Plex Watchlist", "components.UserProfile.plexwatchlist": "Plex Watchlist",
"components.UserProfile.recentlywatched": "Recently Watched", "components.UserProfile.recentlywatched": "Recently Watched",

View File

@@ -0,0 +1,8 @@
import RequestList from '@app/components/RequestList';
import type { NextPage } from 'next';
const UserRequestsPage: NextPage = () => {
return <RequestList />;
};
export default UserRequestsPage;