mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
refactor(frontend): move discovery sliders into reusable MediaSlider component
This commit is contained in:
@@ -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}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
121
src/components/MediaSlider/index.tsx
Normal file
121
src/components/MediaSlider/index.tsx
Normal file
@@ -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>
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user