feat: genre sliders (experiment) (#1182)
parent
8dc69bddd9
commit
1c4515a1ae
@ -0,0 +1,5 @@
|
|||||||
|
export interface GenreSliderItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
backdrops: string[];
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import GenreCard from '../../GenreCard';
|
||||||
|
import Slider from '../../Slider';
|
||||||
|
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
|
||||||
|
import { LanguageContext } from '../../../context/LanguageContext';
|
||||||
|
import { genreColorMap } from '../constants';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
moviegenres: 'Movie Genres',
|
||||||
|
});
|
||||||
|
|
||||||
|
const MovieGenreSlider: React.FC = () => {
|
||||||
|
const { locale } = useContext(LanguageContext);
|
||||||
|
const intl = useIntl();
|
||||||
|
const { data, error } = useSWR<GenreSliderItem[]>(
|
||||||
|
`/api/v1/discover/genreslider/movie?language=${locale}`,
|
||||||
|
{
|
||||||
|
refreshInterval: 0,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="slider-header">
|
||||||
|
<div className="slider-title">
|
||||||
|
<span>{intl.formatMessage(messages.moviegenres)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
sliderKey="movie-genres"
|
||||||
|
isLoading={!data && !error}
|
||||||
|
isEmpty={false}
|
||||||
|
items={(data ?? []).map((genre, index) => (
|
||||||
|
<GenreCard
|
||||||
|
key={`genre-${genre.id}-${index}`}
|
||||||
|
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}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
placeholder={<GenreCard.Placeholder />}
|
||||||
|
emptyMessage=""
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(MovieGenreSlider);
|
@ -0,0 +1,53 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import GenreCard from '../../GenreCard';
|
||||||
|
import Slider from '../../Slider';
|
||||||
|
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
|
||||||
|
import { LanguageContext } from '../../../context/LanguageContext';
|
||||||
|
import { genreColorMap } from '../constants';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
tvgenres: 'Series Genres',
|
||||||
|
});
|
||||||
|
|
||||||
|
const TvGenreSlider: React.FC = () => {
|
||||||
|
const { locale } = useContext(LanguageContext);
|
||||||
|
const intl = useIntl();
|
||||||
|
const { data, error } = useSWR<GenreSliderItem[]>(
|
||||||
|
`/api/v1/discover/genreslider/tv?language=${locale}`,
|
||||||
|
{
|
||||||
|
refreshInterval: 0,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="slider-header">
|
||||||
|
<div className="slider-title">
|
||||||
|
<span>{intl.formatMessage(messages.tvgenres)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
sliderKey="tv-genres"
|
||||||
|
isLoading={!data && !error}
|
||||||
|
isEmpty={false}
|
||||||
|
items={(data ?? []).map((genre, index) => (
|
||||||
|
<GenreCard
|
||||||
|
key={`genre-tv-${genre.id}-${index}`}
|
||||||
|
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}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
placeholder={<GenreCard.Placeholder />}
|
||||||
|
emptyMessage=""
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(TvGenreSlider);
|
@ -0,0 +1,62 @@
|
|||||||
|
type AvailableColors =
|
||||||
|
| 'black'
|
||||||
|
| 'red'
|
||||||
|
| 'darkred'
|
||||||
|
| 'blue'
|
||||||
|
| 'lightblue'
|
||||||
|
| 'darkblue'
|
||||||
|
| 'orange'
|
||||||
|
| 'darkorange'
|
||||||
|
| 'green'
|
||||||
|
| 'lightgreen'
|
||||||
|
| 'purple'
|
||||||
|
| 'darkpurple'
|
||||||
|
| 'yellow'
|
||||||
|
| 'pink';
|
||||||
|
|
||||||
|
export const colorTones: Record<AvailableColors, [string, string]> = {
|
||||||
|
red: ['991B1B', 'FCA5A5'],
|
||||||
|
darkred: ['1F2937', 'F87171'],
|
||||||
|
blue: ['032541', '01b4e4'],
|
||||||
|
lightblue: ['1F2937', '60A5FA'],
|
||||||
|
darkblue: ['1F2937', '2864d2'],
|
||||||
|
orange: ['92400E', 'FCD34D'],
|
||||||
|
lightgreen: ['065F46', '6EE7B7'],
|
||||||
|
green: ['087d29', '21cb51'],
|
||||||
|
purple: ['5B21B6', 'C4B5FD'],
|
||||||
|
yellow: ['777e0d', 'e4ed55'],
|
||||||
|
darkorange: ['552c01', 'd47c1d'],
|
||||||
|
black: ['1F2937', 'D1D5DB'],
|
||||||
|
pink: ['9D174D', 'F9A8D4'],
|
||||||
|
darkpurple: ['480c8b', 'a96bef'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const genreColorMap: Record<number, [string, string]> = {
|
||||||
|
0: colorTones.black,
|
||||||
|
28: colorTones.red,
|
||||||
|
12: colorTones.blue,
|
||||||
|
16: colorTones.orange,
|
||||||
|
35: colorTones.lightgreen,
|
||||||
|
80: colorTones.darkblue,
|
||||||
|
99: colorTones.green,
|
||||||
|
18: colorTones.purple,
|
||||||
|
10751: colorTones.yellow,
|
||||||
|
14: colorTones.darkorange,
|
||||||
|
36: colorTones.green,
|
||||||
|
27: colorTones.black,
|
||||||
|
10402: colorTones.blue,
|
||||||
|
9648: colorTones.purple,
|
||||||
|
10749: colorTones.pink,
|
||||||
|
878: colorTones.lightblue,
|
||||||
|
10770: colorTones.red,
|
||||||
|
53: colorTones.darkpurple,
|
||||||
|
10752: colorTones.darkred,
|
||||||
|
37: colorTones.orange,
|
||||||
|
10759: colorTones.blue, // Action & Adventure
|
||||||
|
10762: colorTones.blue, // Kids
|
||||||
|
10764: colorTones.red, // Reality
|
||||||
|
10765: colorTones.lightblue, // Sci-Fi & Fantasy
|
||||||
|
10766: colorTones.darkpurple, // Soap
|
||||||
|
10767: colorTones.lightgreen, // Talk
|
||||||
|
10768: colorTones.darkred, // War & Politics
|
||||||
|
};
|
@ -0,0 +1,58 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { withProperties } from '../../utils/typeHelpers';
|
||||||
|
|
||||||
|
interface GenreCardProps {
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GenreCard: React.FC<GenreCardProps> = ({ image, url, name }) => {
|
||||||
|
const [isHovered, setHovered] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={url}>
|
||||||
|
<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 ${
|
||||||
|
isHovered
|
||||||
|
? 'bg-gray-700 scale-105 ring-gray-500 bg-opacity-100'
|
||||||
|
: 'bg-gray-800 scale-100 ring-gray-700 bg-opacity-80'
|
||||||
|
} rounded-xl bg-cover bg-center overflow-hidden`}
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url("${image}")`,
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
setHovered(true);
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => setHovered(false)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
setHovered(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="link"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`absolute z-10 inset-0 w-full h-full transition duration-300 bg-gray-800 ${
|
||||||
|
isHovered ? 'bg-opacity-10' : 'bg-opacity-30'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div className="relative z-20 w-full text-2xl font-bold text-center text-white truncate whitespace-normal sm:text-3xl">
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const GenreCardPlaceholder: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative h-32 w-56 sm:h-40 sm:w-72 animate-pulse rounded-xl bg-gray-700`}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withProperties(GenreCard, { Placeholder: GenreCardPlaceholder });
|
Loading…
Reference in new issue