Files
SEEN/frontend/src/pages/movies-page.tsx
T
2026-04-10 12:06:24 +02:00

167 lines
7.2 KiB
TypeScript

import { Clapperboard, Clock3, Flame, Star } from 'lucide-solid'
import { For, Show, createMemo, createResource } from 'solid-js'
import { MediaCard } from '@/components/media/media-card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { DataRow } from '@/components/ui/data-row'
import { EmptyState } from '@/components/ui/empty-state'
import { SectionHeading } from '@/components/ui/section-heading'
import { Skeleton } from '@/components/ui/skeleton'
import { discoverService } from '@/services/discover-service'
import type { MediaItem } from '@/types/domain'
import { formatDate } from '@/utils/format'
import { mediaBadgeVariant, mediaTypeLabel } from '@/utils/media'
const moviesOnly = (items: MediaItem[]) => items.filter((item) => item.type === 'movie')
export const MoviesPage = () => {
const [discoverData, { refetch }] = createResource(() => discoverService.getSections({ page: 1, pageSize: 5 }))
const allSections = createMemo(() => discoverData() ?? [])
const trendingMovies = createMemo(() =>
moviesOnly(allSections().find((section) => section.kind === 'trending')?.items ?? []),
)
const topRatedMovies = createMemo(() =>
moviesOnly(allSections().find((section) => section.kind === 'top-rated')?.items ?? []),
)
const upcomingMovies = createMemo(() =>
moviesOnly(allSections().find((section) => section.kind === 'upcoming')?.items ?? []),
)
const averageRating = createMemo(() => {
const items = topRatedMovies()
if (items.length === 0) return 0
return (items.reduce((acc, item) => acc + item.rating, 0) / items.length).toFixed(1)
})
return (
<section class="space-y-6" data-testid="movies-page">
<Card class="animate-stagger">
<CardContent class="space-y-6 pt-6">
<SectionHeading
eyebrow="Movies"
title="A dedicated movie surface with trending, top rated, and upcoming picks."
description="Focused on film browsing without mixing in episodic or game noise."
badge={<Badge variant="neutral">{trendingMovies().length + topRatedMovies().length} loaded cards</Badge>}
action={<Button variant="subtle" size="sm" onClick={() => refetch()}>Refresh</Button>}
/>
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<div class="rounded-2xl border border-border bg-muted/40 p-4">
<p class="text-xs uppercase tracking-[0.2em] text-muted-fg">Trending</p>
<p class="mt-2 text-2xl font-semibold text-fg">{trendingMovies().length}</p>
</div>
<div class="rounded-2xl border border-border bg-muted/40 p-4">
<p class="text-xs uppercase tracking-[0.2em] text-muted-fg">Top rated</p>
<p class="mt-2 text-2xl font-semibold text-fg">{topRatedMovies().length}</p>
</div>
<div class="rounded-2xl border border-border bg-muted/40 p-4">
<p class="text-xs uppercase tracking-[0.2em] text-muted-fg">Upcoming</p>
<p class="mt-2 text-2xl font-semibold text-fg">{upcomingMovies().length}</p>
</div>
<div class="rounded-2xl border border-border bg-muted/40 p-4">
<p class="text-xs uppercase tracking-[0.2em] text-muted-fg">Avg rating</p>
<p class="mt-2 text-2xl font-semibold text-fg">{averageRating()}</p>
</div>
</div>
</CardContent>
</Card>
<Show when={discoverData.loading && allSections().length === 0}>
<div class="grid gap-4 md:grid-cols-3">
<For each={Array.from({ length: 6 }, (_, index) => index)}>{() => <Skeleton class="h-64" />}</For>
</div>
</Show>
<div class="grid gap-6 xl:grid-cols-[minmax(0,1.05fr)_360px]">
<Card>
<CardContent class="space-y-5 pt-6">
<SectionHeading
title="Trending movies"
description="Films currently drawing the most attention."
badge={<Badge variant="accent">{trendingMovies().length} active</Badge>}
/>
<Show
when={trendingMovies().length > 0}
fallback={<EmptyState title="No trending movies" description="Movie feed is currently empty." />}
>
<div class="grid gap-4 md:grid-cols-2">
<For each={trendingMovies().slice(0, 6)}>{(item) => <MediaCard item={item} />}</For>
</div>
</Show>
</CardContent>
</Card>
<Card>
<CardContent class="space-y-5 pt-6">
<SectionHeading
title="Signals"
description="Quick indicators for film curation quality."
badge={<Badge variant="secondary">Live</Badge>}
/>
<div class="space-y-3">
<DataRow
tone="accent"
eyebrow="Momentum"
title={`${trendingMovies().length} titles trending`}
description="High-interest films currently crossing discovery rails."
trailing={<Flame class="h-4 w-4" />}
/>
<DataRow
tone="neutral"
eyebrow="Quality"
title={`${averageRating()}/10 average top-rated score`}
description="Ranking quality based on current top-rated snapshot."
trailing={<Star class="h-4 w-4" />}
/>
<DataRow
tone="secondary"
eyebrow="Pipeline"
title={`${upcomingMovies().length} upcoming releases`}
description="Near-term movie launches currently visible in calendar feed."
trailing={<Clock3 class="h-4 w-4" />}
/>
</div>
</CardContent>
</Card>
</div>
<Card>
<CardContent class="space-y-5 pt-6">
<SectionHeading
title="Upcoming releases"
description="Movies arriving soon."
badge={<Badge variant="neutral">{upcomingMovies().length} upcoming</Badge>}
/>
<Show
when={upcomingMovies().length > 0}
fallback={<EmptyState title="No upcoming movies" description="Upcoming movie release feed is empty." />}
>
<div class="space-y-3">
<For each={upcomingMovies()}>
{(item) => (
<DataRow
tone={mediaBadgeVariant(item.type)}
eyebrow={mediaTypeLabel(item.type)}
title={item.title}
description={item.genres.slice(0, 2).join(' • ')}
meta={formatDate(item.releaseDate)}
badges={<Badge variant={mediaBadgeVariant(item.type)}>{mediaTypeLabel(item.type)}</Badge>}
trailing={<Clapperboard class="h-4 w-4" />}
/>
)}
</For>
</div>
</Show>
</CardContent>
</Card>
<Show when={discoverData.error}>
<div class="rounded-[1.5rem] border border-secondary/24 bg-secondary/10 px-4 py-4 text-sm text-fg">
{discoverData.error instanceof Error ? discoverData.error.message : 'Failed to load movies'}
</div>
</Show>
</section>
)
}