feat: improve hero showcase details

This commit is contained in:
Aleksi Lassila
2025-02-09 04:10:13 +02:00
parent db58ed99e8
commit d4030cda9d
15 changed files with 154 additions and 216 deletions

View File

@@ -1,19 +1,30 @@
<script lang="ts">
import classNames from 'classnames';
import { DotFilled } from 'radix-icons-svelte';
import HeroTitleInfo from '$lib/pages/TitlePages/HeroTitleInfo.svelte';
import { createEventDispatcher } from 'svelte';
import { get } from 'svelte/store';
import { TMDB_IMAGES_ORIGINAL, TMDB_POSTER_SMALL } from '../../constants';
import { registrars } from '../../selectable.js';
import HeroCarousel from '../HeroCarousel/HeroCarousel.svelte';
import type { ShowcaseItemProps } from './HeroShowcase';
type ShowcaseItem = {
id: number;
type: 'movie' | 'tv';
posterUri: string;
backdropUri: string;
trailerUrl?: string;
title: string;
overview: string;
infoProperties: { label: string; href?: string }[];
url?: string;
};
export let items: Promise<ShowcaseItem[]> = Promise.resolve([]);
const dispatch = createEventDispatcher<{
select: ShowcaseItemProps | undefined;
select: ShowcaseItem | undefined;
}>();
export let items: Promise<ShowcaseItemProps[]> = Promise.resolve([]);
let awaitedItems: undefined | ShowcaseItemProps[];
let awaitedItems: undefined | ShowcaseItem[];
items.then((items) => (awaitedItems = items));
function openItem() {
@@ -24,7 +35,7 @@
</script>
<HeroCarousel
urls={items.then((items) => items.map((i) => `${TMDB_IMAGES_ORIGINAL}${i.backdropUrl}`))}
urls={items.then((items) => items.map((i) => `${TMDB_IMAGES_ORIGINAL}${i.backdropUri}`))}
bind:index={showcaseIndex}
on:enter
on:navigate={({ detail }) => {
@@ -50,46 +61,20 @@
<div class="flex-1 flex items-end">
<div class="mr-8">
<!-- <Card orientation="portrait" backdropUrl={TMDB_POSTER_SMALL + item.posterUrl} />-->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="bg-center bg-cover rounded-xl w-44 h-64 cursor-pointer"
style={`background-image: url("${TMDB_POSTER_SMALL + item.posterUrl}")`}
style={`background-image: url("${TMDB_POSTER_SMALL + item.posterUri}")`}
on:click={openItem}
/>
</div>
<div class="flex flex-col">
<div
class={classNames(
'text-left font-medium tracking-wider text-stone-200 hover:text-amber-200 max-w-xl mt-2',
{
'text-4xl sm:text-5xl 2xl:text-6xl': item?.title.length < 15,
'text-3xl sm:text-4xl 2xl:text-5xl': item?.title.length >= 15
}
)}
on:click={openItem}
>
{item?.title}
</div>
<div
class="flex items-center gap-1 uppercase text-zinc-300 font-semibold tracking-wider mt-2"
>
<p class="flex-shrink-0">{item.year}</p>
<!-- <DotFilled />
<p class="flex-shrink-0">{item.runtime}</p> -->
<DotFilled />
<p class="flex-shrink-0"><a href={item.url}>{item.rating} TMDB</a></p>
</div>
<div class="text-stone-300 font-medium line-clamp-3 opacity-75 max-w-2xl mt-4">
{item.overview}
</div>
<!-- <div class="flex items-center">
{#each item?.genres.slice(0, 3) as genre}
<span
class="backdrop-blur-lg rounded-full bg-zinc-400 bg-opacity-20 p-1.5 px-4 font-medium text-sm flex-grow-0 mr-4"
>
{genre}
</span>
{/each}
</div> -->
<HeroTitleInfo
title={item.title}
properties={item.infoProperties}
overview={item.overview ?? ''}
onClickTitle={openItem}
/>
</div>
</div>
{/if}

View File

@@ -1,59 +0,0 @@
import { formatMinutesToTime } from '../../utils';
import type { tmdbApi } from '../../apis/tmdb/tmdb-api';
export type RatingSource = 'tmdb'; // TODO: Add more rating sources & move elsewhere
export type ShowcaseItemProps = {
posterUrl: string;
backdropUrl: string;
id: number;
trailerUrl?: string;
title: string;
overview?: string;
year?: number;
runtime?: string;
rating?: string;
ratingSource?: RatingSource;
genres: string[];
url?: string;
};
export async function getShowcasePropsFromTmdbMovie(
response: Awaited<ReturnType<typeof tmdbApi.getPopularMovies>>
): Promise<ShowcaseItemProps[]> {
return response.slice(0, 10).map((movie) => ({
id: movie.id || 0,
title: movie.title || '',
posterUrl: movie.poster_path || '',
backdropUrl: movie.backdrop_path || '',
rating: movie.vote_average?.toFixed(1) || '0',
genres: [], //(movie as any)?.genres?.map((genre: any) => genre?.name),
year: movie.release_date ? new Date(movie.release_date).getFullYear() : undefined,
runtime: formatMinutesToTime((movie as any).runtime || 0),
ratingSource: 'tmdb',
trailerUrl: '',
url: `https://www.themoviedb.org/movie/${movie.id}`,
overview: movie.overview || ''
}));
}
export async function getShowcasePropsFromTmdbSeries(
response: Awaited<ReturnType<typeof tmdbApi.getPopularSeries>>
): Promise<ShowcaseItemProps[]> {
return response.slice(0, 10).map((series) => ({
id: series.id || 0,
title: series.name || '',
posterUrl: series.poster_path || '',
backdropUrl: series.backdrop_path || '',
rating: series.vote_average?.toFixed(1) || '0',
genres: [], //(series as any)?.genres?.map((genre: any) => genre?.name),
year: series.first_air_date ? new Date(series.first_air_date).getFullYear() : undefined,
runtime: formatMinutesToTime((series as any).runtime || 0),
ratingSource: 'tmdb',
trailerUrl: '',
url: `https://www.themoviedb.org/movie/${series.id}`,
overview: series.overview || ''
}));
}

View File

@@ -0,0 +1,37 @@
<script lang="ts">
import type { TmdbMovie2 } from '$lib/apis/tmdb/tmdb-api';
import { formatMinutesToTime, formatThousands } from '$lib/utils';
import { navigate } from '../StackRouter/StackRouter';
import HeroShowcase from './HeroShowcase.svelte';
export let movies: Promise<TmdbMovie2[]>;
$: items = movies.then((movies) =>
movies.map((movie) => ({
id: movie.id ?? 0,
type: 'movie' as const,
posterUri: movie.poster_path ?? '',
backdropUri: movie.backdrop_path ?? '',
title: `${movie.title}`,
overview: movie.overview ?? '',
infoProperties: [
...(movie.release_date
? [{ label: new Date(movie.release_date).getFullYear().toString() }]
: []),
...(movie.runtime ? [{ label: formatMinutesToTime(movie.runtime) }] : []),
...(movie.vote_average
? [
{
label: `${movie.vote_average.toFixed(1)} TMDB (${formatThousands(
movie.vote_count ?? 0
)})`,
href: `https://www.themoviedb.org/movie/${movie.id}`
}
]
: []),
...(movie.genres ? [{ label: movie.genres.map((genre) => genre.name).join(', ') }] : [])
]
}))
);
</script>
<HeroShowcase on:select={({ detail }) => navigate(`/movie/${detail?.id}`)} {items} />

View File

@@ -0,0 +1,38 @@
<script lang="ts">
import type { TmdbMovie2, TmdbSeries2 } from '$lib/apis/tmdb/tmdb-api';
import { formatMinutesToTime, formatThousands } from '$lib/utils';
import { navigate } from '../StackRouter/StackRouter';
import HeroShowcase from './HeroShowcase.svelte';
export let series: Promise<TmdbSeries2[]>;
$: items = series.then((series) =>
series.map((series) => ({
id: series.id ?? 0,
type: 'movie' as const,
posterUri: series.poster_path ?? '',
backdropUri: series.backdrop_path ?? '',
title: `${series.name}`,
overview: series.overview ?? '',
infoProperties: [
...(series.status !== 'Ended'
? [{ label: `Since ${new Date(series.first_air_date ?? 0).getFullYear()}` }]
: series.last_air_date
? [{ label: `Ended ${new Date(series.last_air_date).getFullYear()}` }]
: []),
...(series.vote_average
? [
{
label: `${series.vote_average.toFixed(1)} TMDB (${formatThousands(
series.vote_count ?? 0
)})`,
href: `https://www.themoviedb.org/tv/${series.id}`
}
]
: []),
...(series.genres ? [{ label: series.genres.map((genre) => genre.name).join(', ') }] : [])
]
}))
);
</script>
<HeroShowcase on:select={({ detail }) => navigate(`/series/${detail?.id}`)} {items} />

View File

@@ -6,7 +6,7 @@
<button
class={classNames(
'text-zinc-300 hover:text-zinc-50 p-1 flex items-center justify-center selectable rounded-sm flex-shrink-0 bg-transparent',
'text-zinc-300 hover:text-zinc-50 p-1 flex items-center justify-center rounded-sm flex-shrink-0 bg-transparent',
{
'opacity-30 cursor-not-allowed pointer-events-none': disabled,
'cursor-pointer': !disabled

View File

@@ -105,7 +105,7 @@
<!-- Background -->
<div
class={classNames(
'absolute inset-y-0 left-0 w-[25vw] transition-opacity bg-gradient-to-r from-secondary-900 to-transparent',
'absolute inset-y-0 left-0 min-w-[40rem] w-[25vw] transition-opacity bg-gradient-to-r from-secondary-900 to-transparent',
{
'opacity-0': !$isNavBarOpen,
'group-hover:opacity-100 pointer-events-none': true
@@ -146,7 +146,7 @@
<Person class="w-8 h-8" />
<span
class={classNames(
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-20',
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-20 text-nowrap',
{
'opacity-0 pointer-events-none': $isNavBarOpen === false,
'group-hover:opacity-100 group-hover:pointer-events-auto': true