mirror of
https://github.com/Dvorinka/SEEN.git
synced 2026-06-05 04:53:01 +00:00
167 lines
7.2 KiB
TypeScript
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>
|
|
)
|
|
}
|