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