mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(ui): add movie/series genre list pages (#1194)
This commit is contained in:
@@ -37,7 +37,7 @@ const ListView: React.FC<ListViewProps> = ({
|
|||||||
{intl.formatMessage(messages.noresults)}
|
{intl.formatMessage(messages.noresults)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ul className="cardList">
|
<ul className="cards-vertical">
|
||||||
{items?.map((title, index) => {
|
{items?.map((title, index) => {
|
||||||
let titleCard: React.ReactNode;
|
let titleCard: React.ReactNode;
|
||||||
|
|
||||||
|
@@ -13,7 +13,7 @@ const CompanyCard: React.FC<CompanyCardProps> = ({ image, url, name }) => {
|
|||||||
return (
|
return (
|
||||||
<Link href={url}>
|
<Link href={url}>
|
||||||
<a
|
<a
|
||||||
className={`relative flex items-center justify-center h-32 w-64 sm:h-36 sm:w-72 p-8 shadow transition ease-in-out duration-300 cursor-pointer transform-gpu ring-1 ${
|
className={`relative flex items-center justify-center h-32 w-56 sm:h-36 sm:w-72 p-8 shadow transition ease-in-out duration-300 cursor-pointer transform-gpu ring-1 ${
|
||||||
isHovered
|
isHovered
|
||||||
? 'bg-gray-700 scale-105 ring-gray-500'
|
? 'bg-gray-700 scale-105 ring-gray-500'
|
||||||
: 'bg-gray-800 scale-100 ring-gray-700'
|
: 'bg-gray-800 scale-100 ring-gray-700'
|
||||||
|
56
src/components/Discover/MovieGenreList/index.tsx
Normal file
56
src/components/Discover/MovieGenreList/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import GenreCard from '../../GenreCard';
|
||||||
|
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
|
||||||
|
import { LanguageContext } from '../../../context/LanguageContext';
|
||||||
|
import { genreColorMap } from '../constants';
|
||||||
|
import PageTitle from '../../Common/PageTitle';
|
||||||
|
import Header from '../../Common/Header';
|
||||||
|
import LoadingSpinner from '../../Common/LoadingSpinner';
|
||||||
|
import Error from '../../../pages/_error';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
moviegenres: 'Movie Genres',
|
||||||
|
});
|
||||||
|
|
||||||
|
const MovieGenreList: React.FC = () => {
|
||||||
|
const { locale } = useContext(LanguageContext);
|
||||||
|
const intl = useIntl();
|
||||||
|
const { data, error } = useSWR<GenreSliderItem[]>(
|
||||||
|
`/api/v1/discover/genreslider/movie?language=${locale}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data && !error) {
|
||||||
|
return <LoadingSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return <Error statusCode={404} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageTitle title={intl.formatMessage(messages.moviegenres)} />
|
||||||
|
<div className="mt-1 mb-5">
|
||||||
|
<Header>{intl.formatMessage(messages.moviegenres)}</Header>
|
||||||
|
</div>
|
||||||
|
<ul className="cards-horizontal">
|
||||||
|
{data.map((genre, index) => (
|
||||||
|
<li key={`genre-${genre.id}-${index}`}>
|
||||||
|
<GenreCard
|
||||||
|
name={genre.name}
|
||||||
|
image={`https://www.themoviedb.org/t/p/w1280_filter(duotone,${
|
||||||
|
genreColorMap[genre.id] ?? genreColorMap[0]
|
||||||
|
})${genre.backdrops[4]}`}
|
||||||
|
url={`/discover/movies/genre/${genre.id}`}
|
||||||
|
canExpand
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieGenreList;
|
@@ -6,6 +6,7 @@ import Slider from '../../Slider';
|
|||||||
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
|
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
|
||||||
import { LanguageContext } from '../../../context/LanguageContext';
|
import { LanguageContext } from '../../../context/LanguageContext';
|
||||||
import { genreColorMap } from '../constants';
|
import { genreColorMap } from '../constants';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
moviegenres: 'Movie Genres',
|
moviegenres: 'Movie Genres',
|
||||||
@@ -25,9 +26,25 @@ const MovieGenreSlider: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="slider-header">
|
<div className="slider-header">
|
||||||
<div className="slider-title">
|
<Link href="/discover/movies/genres">
|
||||||
<span>{intl.formatMessage(messages.moviegenres)}</span>
|
<a className="slider-title">
|
||||||
</div>
|
<span>{intl.formatMessage(messages.moviegenres)}</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
|
<Slider
|
||||||
sliderKey="movie-genres"
|
sliderKey="movie-genres"
|
||||||
|
56
src/components/Discover/TvGenreList/index.tsx
Normal file
56
src/components/Discover/TvGenreList/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import GenreCard from '../../GenreCard';
|
||||||
|
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
|
||||||
|
import { LanguageContext } from '../../../context/LanguageContext';
|
||||||
|
import { genreColorMap } from '../constants';
|
||||||
|
import PageTitle from '../../Common/PageTitle';
|
||||||
|
import Header from '../../Common/Header';
|
||||||
|
import LoadingSpinner from '../../Common/LoadingSpinner';
|
||||||
|
import Error from '../../../pages/_error';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
seriesgenres: 'Series Genres',
|
||||||
|
});
|
||||||
|
|
||||||
|
const TvGenreList: React.FC = () => {
|
||||||
|
const { locale } = useContext(LanguageContext);
|
||||||
|
const intl = useIntl();
|
||||||
|
const { data, error } = useSWR<GenreSliderItem[]>(
|
||||||
|
`/api/v1/discover/genreslider/tv?language=${locale}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data && !error) {
|
||||||
|
return <LoadingSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return <Error statusCode={404} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageTitle title={intl.formatMessage(messages.seriesgenres)} />
|
||||||
|
<div className="mt-1 mb-5">
|
||||||
|
<Header>{intl.formatMessage(messages.seriesgenres)}</Header>
|
||||||
|
</div>
|
||||||
|
<ul className="cards-horizontal">
|
||||||
|
{data.map((genre, index) => (
|
||||||
|
<li key={`genre-${genre.id}-${index}`}>
|
||||||
|
<GenreCard
|
||||||
|
name={genre.name}
|
||||||
|
image={`https://www.themoviedb.org/t/p/w1280_filter(duotone,${
|
||||||
|
genreColorMap[genre.id] ?? genreColorMap[0]
|
||||||
|
})${genre.backdrops[4]}`}
|
||||||
|
url={`/discover/tv/genre/${genre.id}`}
|
||||||
|
canExpand
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TvGenreList;
|
@@ -6,6 +6,7 @@ import Slider from '../../Slider';
|
|||||||
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
|
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
|
||||||
import { LanguageContext } from '../../../context/LanguageContext';
|
import { LanguageContext } from '../../../context/LanguageContext';
|
||||||
import { genreColorMap } from '../constants';
|
import { genreColorMap } from '../constants';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
tvgenres: 'Series Genres',
|
tvgenres: 'Series Genres',
|
||||||
@@ -25,9 +26,25 @@ const TvGenreSlider: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="slider-header">
|
<div className="slider-header">
|
||||||
<div className="slider-title">
|
<Link href="/discover/tv/genres">
|
||||||
<span>{intl.formatMessage(messages.tvgenres)}</span>
|
<a className="slider-title">
|
||||||
</div>
|
<span>{intl.formatMessage(messages.tvgenres)}</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
|
<Slider
|
||||||
sliderKey="tv-genres"
|
sliderKey="tv-genres"
|
||||||
|
@@ -6,15 +6,23 @@ interface GenreCardProps {
|
|||||||
name: string;
|
name: string;
|
||||||
image: string;
|
image: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
canExpand?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GenreCard: React.FC<GenreCardProps> = ({ image, url, name }) => {
|
const GenreCard: React.FC<GenreCardProps> = ({
|
||||||
|
image,
|
||||||
|
url,
|
||||||
|
name,
|
||||||
|
canExpand = false,
|
||||||
|
}) => {
|
||||||
const [isHovered, setHovered] = useState(false);
|
const [isHovered, setHovered] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={url}>
|
<Link href={url}>
|
||||||
<a
|
<a
|
||||||
className={`relative flex items-center justify-center h-32 w-56 sm:h-40 sm:w-72 p-8 shadow transition ease-in-out duration-300 cursor-pointer transform-gpu ring-1 ${
|
className={`relative flex items-center justify-center h-32 sm:h-36 ${
|
||||||
|
canExpand ? 'w-full' : 'w-56 sm:w-72'
|
||||||
|
} p-8 shadow transition ease-in-out duration-300 cursor-pointer transform-gpu ring-1 ${
|
||||||
isHovered
|
isHovered
|
||||||
? 'bg-gray-700 scale-105 ring-gray-500 bg-opacity-100'
|
? 'bg-gray-700 scale-105 ring-gray-500 bg-opacity-100'
|
||||||
: 'bg-gray-800 scale-100 ring-gray-700 bg-opacity-80'
|
: 'bg-gray-800 scale-100 ring-gray-700 bg-opacity-80'
|
||||||
|
@@ -45,7 +45,7 @@ const MovieCast: React.FC = () => {
|
|||||||
{intl.formatMessage(messages.fullcast)}
|
{intl.formatMessage(messages.fullcast)}
|
||||||
</Header>
|
</Header>
|
||||||
</div>
|
</div>
|
||||||
<ul className="cardList">
|
<ul className="cards-vertical">
|
||||||
{data?.credits.cast.map((person, index) => {
|
{data?.credits.cast.map((person, index) => {
|
||||||
return (
|
return (
|
||||||
<li key={`cast-${person.id}-${index}`}>
|
<li key={`cast-${person.id}-${index}`}>
|
||||||
|
@@ -45,7 +45,7 @@ const MovieCrew: React.FC = () => {
|
|||||||
{intl.formatMessage(messages.fullcrew)}
|
{intl.formatMessage(messages.fullcrew)}
|
||||||
</Header>
|
</Header>
|
||||||
</div>
|
</div>
|
||||||
<ul className="cardList">
|
<ul className="cards-vertical">
|
||||||
{data?.credits.crew.map((person, index) => {
|
{data?.credits.crew.map((person, index) => {
|
||||||
return (
|
return (
|
||||||
<li key={`crew-${person.id}-${index}`}>
|
<li key={`crew-${person.id}-${index}`}>
|
||||||
|
@@ -90,7 +90,7 @@ const PersonDetails: React.FC = () => {
|
|||||||
<span>{intl.formatMessage(messages.appearsin)}</span>
|
<span>{intl.formatMessage(messages.appearsin)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul className="cardList">
|
<ul className="cards-vertical">
|
||||||
{sortedCast?.map((media, index) => {
|
{sortedCast?.map((media, index) => {
|
||||||
return (
|
return (
|
||||||
<li key={`list-cast-item-${media.id}-${index}`}>
|
<li key={`list-cast-item-${media.id}-${index}`}>
|
||||||
@@ -130,7 +130,7 @@ const PersonDetails: React.FC = () => {
|
|||||||
<span>{intl.formatMessage(messages.crewmember)}</span>
|
<span>{intl.formatMessage(messages.crewmember)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul className="cardList">
|
<ul className="cards-vertical">
|
||||||
{sortedCrew?.map((media, index) => {
|
{sortedCrew?.map((media, index) => {
|
||||||
return (
|
return (
|
||||||
<li key={`list-crew-item-${media.id}-${index}`}>
|
<li key={`list-crew-item-${media.id}-${index}`}>
|
||||||
|
@@ -81,12 +81,13 @@ const TitleCard: React.FC<TitleCardProps> = ({
|
|||||||
onCancel={closeModal}
|
onCancel={closeModal}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`transition duration-300 transform-gpu scale-100 outline-none cursor-default titleCard rounded-xl ring-1 ${
|
className={`transition duration-300 transform-gpu scale-100 outline-none cursor-default relative bg-gray-800 bg-cover rounded-xl ring-1 ${
|
||||||
showDetail
|
showDetail
|
||||||
? 'scale-105 shadow-lg ring-gray-500'
|
? 'scale-105 shadow-lg ring-gray-500'
|
||||||
: 'shadow ring-gray-700'
|
: 'shadow ring-gray-700'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
|
paddingBottom: '150%',
|
||||||
backgroundImage: image
|
backgroundImage: image
|
||||||
? `url(//image.tmdb.org/t/p/w300_and_h450_face${image})`
|
? `url(//image.tmdb.org/t/p/w300_and_h450_face${image})`
|
||||||
: `url('/images/overseerr_poster_not_found_logo_top.png')`,
|
: `url('/images/overseerr_poster_not_found_logo_top.png')`,
|
||||||
|
@@ -47,7 +47,7 @@ const TvCast: React.FC = () => {
|
|||||||
{intl.formatMessage(messages.fullseriescast)}
|
{intl.formatMessage(messages.fullseriescast)}
|
||||||
</Header>
|
</Header>
|
||||||
</div>
|
</div>
|
||||||
<ul className="cardList">
|
<ul className="cards-vertical">
|
||||||
{data?.credits.cast.map((person) => {
|
{data?.credits.cast.map((person) => {
|
||||||
return (
|
return (
|
||||||
<li key={person.id}>
|
<li key={person.id}>
|
||||||
|
@@ -47,7 +47,7 @@ const TvCrew: React.FC = () => {
|
|||||||
{intl.formatMessage(messages.fullseriescrew)}
|
{intl.formatMessage(messages.fullseriescrew)}
|
||||||
</Header>
|
</Header>
|
||||||
</div>
|
</div>
|
||||||
<ul className="cardList">
|
<ul className="cards-vertical">
|
||||||
{data?.credits.crew.map((person, index) => {
|
{data?.credits.crew.map((person, index) => {
|
||||||
return (
|
return (
|
||||||
<li key={`crew-${person.id}-${index}`}>
|
<li key={`crew-${person.id}-${index}`}>
|
||||||
|
@@ -20,9 +20,11 @@
|
|||||||
"components.Discover.DiscoverStudio.studioMovies": "{studio} Movies",
|
"components.Discover.DiscoverStudio.studioMovies": "{studio} Movies",
|
||||||
"components.Discover.DiscoverTvGenre.genreSeries": "{genre} Series",
|
"components.Discover.DiscoverTvGenre.genreSeries": "{genre} Series",
|
||||||
"components.Discover.DiscoverTvLanguage.languageSeries": "{language} Series",
|
"components.Discover.DiscoverTvLanguage.languageSeries": "{language} Series",
|
||||||
|
"components.Discover.MovieGenreList.moviegenres": "Movie Genres",
|
||||||
"components.Discover.MovieGenreSlider.moviegenres": "Movie Genres",
|
"components.Discover.MovieGenreSlider.moviegenres": "Movie Genres",
|
||||||
"components.Discover.NetworkSlider.networks": "Networks",
|
"components.Discover.NetworkSlider.networks": "Networks",
|
||||||
"components.Discover.StudioSlider.studios": "Studios",
|
"components.Discover.StudioSlider.studios": "Studios",
|
||||||
|
"components.Discover.TvGenreList.seriesgenres": "Series Genres",
|
||||||
"components.Discover.TvGenreSlider.tvgenres": "Series Genres",
|
"components.Discover.TvGenreSlider.tvgenres": "Series Genres",
|
||||||
"components.Discover.discover": "Discover",
|
"components.Discover.discover": "Discover",
|
||||||
"components.Discover.discovermovies": "Popular Movies",
|
"components.Discover.discovermovies": "Popular Movies",
|
||||||
|
9
src/pages/discover/movies/genres.tsx
Normal file
9
src/pages/discover/movies/genres.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { NextPage } from 'next';
|
||||||
|
import MovieGenreList from '../../../components/Discover/MovieGenreList';
|
||||||
|
|
||||||
|
const MovieGenresPage: NextPage = () => {
|
||||||
|
return <MovieGenreList />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieGenresPage;
|
9
src/pages/discover/tv/genres.tsx
Normal file
9
src/pages/discover/tv/genres.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { NextPage } from 'next';
|
||||||
|
import TvGenreList from '../../../components/Discover/TvGenreList';
|
||||||
|
|
||||||
|
const TvGenresPage: NextPage = () => {
|
||||||
|
return <TvGenreList />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TvGenresPage;
|
@@ -16,18 +16,17 @@ body {
|
|||||||
background: #f19a30;
|
background: #f19a30;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.cardList {
|
ul.cards-vertical,
|
||||||
|
ul.cards-horizontal {
|
||||||
@apply grid gap-4;
|
@apply grid gap-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.cards-vertical {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.cardList > li {
|
ul.cards-horizontal {
|
||||||
@apply flex flex-col items-center col-span-1 text-center;
|
grid-template-columns: repeat(auto-fill, minmax(16.5rem, 1fr));
|
||||||
}
|
|
||||||
|
|
||||||
.titleCard {
|
|
||||||
@apply relative bg-gray-800 bg-cover;
|
|
||||||
padding-bottom: 150%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider-header {
|
.slider-header {
|
||||||
|
Reference in New Issue
Block a user