refactor(frontend): move discovery sliders into reusable MediaSlider component

pull/655/head
sct 4 years ago
parent b04d00ef50
commit 5cfdfac37b

@ -1,20 +1,13 @@
import React, { useContext } from 'react'; import React from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import type {
MovieResult,
TvResult,
PersonResult,
} from '../../../server/models/Search';
import TitleCard from '../TitleCard';
import PersonCard from '../PersonCard';
import TmdbTitleCard from '../TitleCard/TmdbTitleCard'; import TmdbTitleCard from '../TitleCard/TmdbTitleCard';
import Slider from '../Slider'; import Slider from '../Slider';
import Link from 'next/link'; import Link from 'next/link';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { LanguageContext } from '../../context/LanguageContext';
import type { MediaResultsResponse } from '../../../server/interfaces/api/mediaInterfaces'; import type { MediaResultsResponse } from '../../../server/interfaces/api/mediaInterfaces';
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces'; import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
import RequestCard from '../RequestCard'; import RequestCard from '../RequestCard';
import MediaSlider from '../MediaSlider';
const messages = defineMessages({ const messages = defineMessages({
recentrequests: 'Recent Requests', recentrequests: 'Recent Requests',
@ -26,47 +19,8 @@ const messages = defineMessages({
trending: 'Trending', trending: 'Trending',
}); });
interface MovieDiscoverResult {
page: number;
totalResults: number;
totalPages: number;
results: MovieResult[];
}
interface TvDiscoverResult {
page: number;
totalResults: number;
totalPages: number;
results: TvResult[];
}
interface MixedResult {
page: number;
totalResults: number;
totalPages: number;
results: (TvResult | MovieResult | PersonResult)[];
}
const Discover: React.FC = () => { const Discover: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const { locale } = useContext(LanguageContext);
const { data: movieData, error: movieError } = useSWR<MovieDiscoverResult>(
`/api/v1/discover/movies?language=${locale}`
);
const { data: tvData, error: tvError } = useSWR<TvDiscoverResult>(
`/api/v1/discover/tv?language=${locale}`
);
const {
data: movieUpcomingData,
error: movieUpcomingError,
} = useSWR<MovieDiscoverResult>(
`/api/v1/discover/movies/upcoming?language=${locale}`
);
const { data: trendingData, error: trendingError } = useSWR<MixedResult>(
`/api/v1/discover/trending?language=${locale}`
);
const { data: media, error: mediaError } = useSWR<MediaResultsResponse>( const { data: media, error: mediaError } = useSWR<MediaResultsResponse>(
'/api/v1/media?filter=available&take=20&sort=mediaAdded' '/api/v1/media?filter=available&take=20&sort=mediaAdded'
@ -140,202 +94,29 @@ const Discover: React.FC = () => {
placeholder={<RequestCard.Placeholder />} placeholder={<RequestCard.Placeholder />}
emptyMessage={intl.formatMessage(messages.nopending)} emptyMessage={intl.formatMessage(messages.nopending)}
/> />
<div className="mt-6 mb-4 md:flex md:items-center md:justify-between"> <MediaSlider
<div className="flex-1 min-w-0">
<Link href="/discover/movies/upcoming">
<a className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate">
<span>
<FormattedMessage {...messages.upcoming} />
</span>
<svg
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a>
</Link>
</div>
</div>
<Slider
sliderKey="upcoming" sliderKey="upcoming"
isLoading={!movieUpcomingData && !movieUpcomingError} title={intl.formatMessage(messages.upcoming)}
isEmpty={false} linkUrl="/discover/movies/upcoming"
items={movieUpcomingData?.results.map((title) => ( url="/api/v1/discover/movies/upcoming"
<TitleCard
key={`upcoming-movie-slider-${title.id}`}
id={title.id}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
year={title.releaseDate}
mediaType={title.mediaType}
/>
))}
/> />
<div className="mt-6 mb-4 md:flex md:items-center md:justify-between"> <MediaSlider
<div className="flex-1 min-w-0">
<Link href="/discover/trending">
<a className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate">
<span>
<FormattedMessage {...messages.trending} />
</span>
<svg
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a>
</Link>
</div>
</div>
<Slider
sliderKey="trending" sliderKey="trending"
isLoading={!trendingData && !trendingError} title={intl.formatMessage(messages.trending)}
isEmpty={false} url="/api/v1/discover/trending"
items={trendingData?.results.map((title) => { linkUrl="/discover/trending"
switch (title.mediaType) {
case 'movie':
return (
<TitleCard
id={title.id}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
year={title.releaseDate}
mediaType={title.mediaType}
/>
);
case 'tv':
return (
<TitleCard
id={title.id}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.name}
userScore={title.voteAverage}
year={title.firstAirDate}
mediaType={title.mediaType}
/>
);
case 'person':
return (
<PersonCard
personId={title.id}
name={title.name}
profilePath={title.profilePath}
/>
);
}
})}
/> />
<div className="mt-6 mb-4 md:flex md:items-center md:justify-between"> <MediaSlider
<div className="flex-1 min-w-0"> sliderKey="popular-movies"
<Link href="/discover/movies"> title={intl.formatMessage(messages.popularmovies)}
<a className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate"> url="/api/v1/discover/movies"
<span> linkUrl="/discover/movies"
<FormattedMessage {...messages.popularmovies} />
</span>
<svg
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a>
</Link>
</div>
</div>
<Slider
sliderKey="movies"
isLoading={!movieData && !movieError}
isEmpty={false}
items={movieData?.results.map((title) => (
<TitleCard
key={`popular-movie-slider-${title.id}`}
id={title.id}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
year={title.releaseDate}
mediaType={title.mediaType}
/>
))}
/> />
<div className="mt-4 mb-4 md:flex md:items-center md:justify-between"> <MediaSlider
<div className="flex-1 min-w-0"> sliderKey="popular-tv"
<Link href="/discover/tv"> title={intl.formatMessage(messages.populartv)}
<a className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate"> url="/api/v1/discover/tv"
<span> linkUrl="/discover/tv"
<FormattedMessage {...messages.populartv} />
</span>
<svg
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a>
</Link>
</div>
</div>
<Slider
sliderKey="tv"
isLoading={!tvData && !tvError}
isEmpty={false}
items={tvData?.results.map((title) => (
<TitleCard
key={`popular-tv-slider-${title.id}`}
id={title.id}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.name}
userScore={title.voteAverage}
year={title.firstAirDate}
mediaType={title.mediaType}
/>
))}
/> />
</> </>
); );

@ -0,0 +1,121 @@
import Link from 'next/link';
import React, { useContext } from 'react';
import useSWR from 'swr';
import type {
MovieResult,
PersonResult,
TvResult,
} from '../../../server/models/Search';
import { LanguageContext } from '../../context/LanguageContext';
import PersonCard from '../PersonCard';
import Slider from '../Slider';
import TitleCard from '../TitleCard';
interface MixedResult {
page: number;
totalResults: number;
totalPages: number;
results: (TvResult | MovieResult | PersonResult)[];
}
interface MediaSliderProps {
title: string;
url: string;
linkUrl?: string;
sliderKey: string;
hideWhenEmpty?: boolean;
}
const MediaSlider: React.FC<MediaSliderProps> = ({
title,
url,
linkUrl,
sliderKey,
hideWhenEmpty = false,
}) => {
const { locale } = useContext(LanguageContext);
const { data, error } = useSWR<MixedResult>(`${url}?language=${locale}`);
if (hideWhenEmpty && (data?.results ?? []).length === 0) {
return null;
}
return (
<>
<div className="mt-6 mb-4 md:flex md:items-center md:justify-between">
<div className="flex-1 min-w-0">
{linkUrl ? (
<Link href={linkUrl}>
<a className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate">
<span>{title}</span>
<svg
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a>
</Link>
) : (
<div className="inline-flex items-center text-xl leading-7 text-gray-300 sm:text-2xl sm:leading-9 sm:truncate">
<span>{title}</span>
</div>
)}
</div>
</div>
<Slider
sliderKey={sliderKey}
isLoading={!data && !error}
isEmpty={false}
items={data?.results.map((title) => {
switch (title.mediaType) {
case 'movie':
return (
<TitleCard
id={title.id}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
year={title.releaseDate}
mediaType={title.mediaType}
/>
);
case 'tv':
return (
<TitleCard
id={title.id}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.name}
userScore={title.voteAverage}
year={title.firstAirDate}
mediaType={title.mediaType}
/>
);
case 'person':
return (
<PersonCard
personId={title.id}
name={title.name}
profilePath={title.profilePath}
/>
);
}
})}
/>
</>
);
};
export default MediaSlider;

@ -10,10 +10,8 @@ import type { MovieDetails as MovieDetailsType } from '../../../server/models/Mo
import useSWR from 'swr'; import useSWR from 'swr';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Button from '../Common/Button'; import Button from '../Common/Button';
import type { MovieResult } from '../../../server/models/Search';
import Link from 'next/link'; import Link from 'next/link';
import Slider from '../Slider'; import Slider from '../Slider';
import TitleCard from '../TitleCard';
import PersonCard from '../PersonCard'; import PersonCard from '../PersonCard';
import { LanguageContext } from '../../context/LanguageContext'; import { LanguageContext } from '../../context/LanguageContext';
import LoadingSpinner from '../Common/LoadingSpinner'; import LoadingSpinner from '../Common/LoadingSpinner';
@ -34,6 +32,7 @@ import ExternalLinkBlock from '../ExternalLinkBlock';
import { sortCrewPriority } from '../../utils/creditHelpers'; import { sortCrewPriority } from '../../utils/creditHelpers';
import StatusBadge from '../StatusBadge'; import StatusBadge from '../StatusBadge';
import RequestButton from '../RequestButton'; import RequestButton from '../RequestButton';
import MediaSlider from '../MediaSlider';
const messages = defineMessages({ const messages = defineMessages({
releasedate: 'Release Date', releasedate: 'Release Date',
@ -70,13 +69,6 @@ interface MovieDetailsProps {
movie?: MovieDetailsType; movie?: MovieDetailsType;
} }
interface SearchResult {
page: number;
totalResults: number;
totalPages: number;
results: MovieResult[];
}
const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => { const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
const { hasPermission } = useUser(); const { hasPermission } = useUser();
const router = useRouter(); const router = useRouter();
@ -89,12 +81,6 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
initialData: movie, initialData: movie,
} }
); );
const { data: recommended, error: recommendedError } = useSWR<SearchResult>(
`/api/v1/movie/${router.query.movieId}/recommendations?language=${locale}`
);
const { data: similar, error: similarError } = useSWR<SearchResult>(
`/api/v1/movie/${router.query.movieId}/similar?language=${locale}`
);
const { data: ratingData } = useSWR<RTRating>( const { data: ratingData } = useSWR<RTRating>(
`/api/v1/movie/${router.query.movieId}/ratings` `/api/v1/movie/${router.query.movieId}/ratings`
); );
@ -525,106 +511,20 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
/> />
))} ))}
/> />
{(recommended?.results ?? []).length > 0 && ( <MediaSlider
<> sliderKey="recommendations"
<div className="mt-6 mb-4 md:flex md:items-center md:justify-between"> title={intl.formatMessage(messages.recommendations)}
<div className="flex-1 min-w-0"> url={`/api/v1/movie/${router.query.movieId}/recommendations`}
<Link linkUrl={`/movie/${data.id}/recommendations`}
href="/movie/[movieId]/recommendations" hideWhenEmpty
as={`/movie/${data.id}/recommendations`} />
> <MediaSlider
<a className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate"> sliderKey="similar"
<span> title={intl.formatMessage(messages.similar)}
<FormattedMessage {...messages.recommendations} /> url={`/api/v1/movie/${router.query.movieId}/similar`}
</span> linkUrl={`/movie/${data.id}/similar`}
<svg hideWhenEmpty
className="w-6 h-6 ml-2" />
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a>
</Link>
</div>
</div>
<Slider
sliderKey="recommendations"
isLoading={!recommended && !recommendedError}
isEmpty={false}
items={recommended?.results.map((title) => (
<TitleCard
key={`recommended-${title.id}`}
id={title.id}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
year={title.releaseDate}
mediaType={title.mediaType}
/>
))}
/>
</>
)}
{(similar?.results ?? []).length > 0 && (
<>
<div className="mt-6 mb-4 md:flex md:items-center md:justify-between">
<div className="flex-1 min-w-0">
<Link
href="/movie/[movieId]/similar"
as={`/movie/${data.id}/similar`}
>
<a className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate">
<span>
<FormattedMessage {...messages.similar} />
</span>
<svg
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a>
</Link>
</div>
</div>
<Slider
sliderKey="similar"
isLoading={!similar && !similarError}
isEmpty={false}
items={similar?.results.map((title) => (
<TitleCard
key={`recommended-${title.id}`}
id={title.id}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
year={title.releaseDate}
mediaType={title.mediaType}
/>
))}
/>
</>
)}
<div className="pb-8" /> <div className="pb-8" />
</div> </div>
); );

@ -8,10 +8,8 @@ import {
import useSWR from 'swr'; import useSWR from 'swr';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Button from '../Common/Button'; import Button from '../Common/Button';
import type { TvResult } from '../../../server/models/Search';
import Link from 'next/link'; import Link from 'next/link';
import Slider from '../Slider'; import Slider from '../Slider';
import TitleCard from '../TitleCard';
import PersonCard from '../PersonCard'; import PersonCard from '../PersonCard';
import { LanguageContext } from '../../context/LanguageContext'; import { LanguageContext } from '../../context/LanguageContext';
import LoadingSpinner from '../Common/LoadingSpinner'; import LoadingSpinner from '../Common/LoadingSpinner';
@ -36,6 +34,7 @@ import { sortCrewPriority } from '../../utils/creditHelpers';
import { Crew } from '../../../server/models/common'; import { Crew } from '../../../server/models/common';
import StatusBadge from '../StatusBadge'; import StatusBadge from '../StatusBadge';
import RequestButton from '../RequestButton'; import RequestButton from '../RequestButton';
import MediaSlider from '../MediaSlider';
const messages = defineMessages({ const messages = defineMessages({
firstAirDate: 'First Air Date', firstAirDate: 'First Air Date',
@ -70,13 +69,6 @@ interface TvDetailsProps {
tv?: TvDetailsType; tv?: TvDetailsType;
} }
interface SearchResult {
page: number;
totalResults: number;
totalPages: number;
results: TvResult[];
}
const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => { const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
const { hasPermission } = useUser(); const { hasPermission } = useUser();
const router = useRouter(); const router = useRouter();
@ -90,12 +82,6 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
initialData: tv, initialData: tv,
} }
); );
const { data: recommended, error: recommendedError } = useSWR<SearchResult>(
`/api/v1/tv/${router.query.tvId}/recommendations?language=${locale}`
);
const { data: similar, error: similarError } = useSWR<SearchResult>(
`/api/v1/tv/${router.query.tvId}/similar?language=${locale}`
);
const { data: ratingData } = useSWR<RTRating>( const { data: ratingData } = useSWR<RTRating>(
`/api/v1/tv/${router.query.tvId}/ratings` `/api/v1/tv/${router.query.tvId}/ratings`
@ -528,103 +514,20 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
/> />
))} ))}
/> />
{(recommended?.results ?? []).length > 0 && ( <MediaSlider
<> sliderKey="recommendations"
<div className="mt-6 mb-4 md:flex md:items-center md:justify-between"> title={intl.formatMessage(messages.recommendations)}
<div className="flex-1 min-w-0"> url={`/api/v1/tv/${router.query.tvId}/recommendations`}
<Link linkUrl={`/tv/${data.id}/recommendations`}
href="/tv/[tvId]/recommendations" hideWhenEmpty
as={`/tv/${data.id}/recommendations`} />
> <MediaSlider
<a className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate"> sliderKey="similar"
<span> title={intl.formatMessage(messages.similar)}
<FormattedMessage {...messages.recommendations} /> url={`/api/v1/tv/${router.query.tvId}/similar`}
</span> linkUrl={`/tv/${data.id}/similar`}
<svg hideWhenEmpty
className="w-6 h-6 ml-2" />
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a>
</Link>
</div>
</div>
<Slider
sliderKey="recommendations"
isLoading={!recommended && !recommendedError}
isEmpty={false}
items={recommended?.results.map((title) => (
<TitleCard
key={`recommended-${title.id}`}
id={title.id}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.name}
userScore={title.voteAverage}
year={title.firstAirDate}
mediaType={title.mediaType}
/>
))}
/>
</>
)}
{(similar?.results ?? []).length > 0 && (
<>
<div className="mt-6 mb-4 md:flex md:items-center md:justify-between">
<div className="flex-1 min-w-0">
<Link href="/tv/[tvId]/similar" as={`/tv/${data.id}/similar`}>
<a className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate">
<span>
<FormattedMessage {...messages.similar} />
</span>
<svg
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a>
</Link>
</div>
</div>
<Slider
sliderKey="similar"
isLoading={!similar && !similarError}
isEmpty={false}
items={similar?.results.map((title) => (
<TitleCard
key={`recommended-${title.id}`}
id={title.id}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.name}
userScore={title.voteAverage}
year={title.firstAirDate}
mediaType={title.mediaType}
/>
))}
/>
</>
)}
<div className="pb-8" /> <div className="pb-8" />
</div> </div>
); );

Loading…
Cancel
Save