mirror of
https://github.com/Dvorinka/SEEN.git
synced 2026-06-05 04:53:01 +00:00
small fix, don't worry about it
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
import { Calendar, Clock, Eye, Film, Gamepad2, Plus, Star, Tv, X } from 'lucide-solid'
|
||||
import { Dialog as ArkDialog } from '@ark-ui/solid/dialog'
|
||||
import { Portal } from 'solid-js/web'
|
||||
import { Show, splitProps, type ParentComponent } from 'solid-js'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import type { MediaItem } from '@/types/domain'
|
||||
import { cn } from '@/utils/cn'
|
||||
import { formatRating, formatRuntime } from '@/utils/format'
|
||||
import { mediaBadgeVariant, mediaMeta, mediaMonogram, mediaTypeLabel, mediaYear } from '@/utils/media'
|
||||
|
||||
export interface MediaDetailDialogProps {
|
||||
item: MediaItem
|
||||
progressPercent?: number
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
onAddToWatchlist?: () => void
|
||||
onMarkWatched?: () => void
|
||||
onContinueWatching?: () => void
|
||||
}
|
||||
|
||||
const MediaTypeIcon = (props: { type: MediaItem['type']; class?: string }) => {
|
||||
const icons = {
|
||||
movie: Film,
|
||||
show: Tv,
|
||||
game: Gamepad2,
|
||||
}
|
||||
const Icon = icons[props.type]
|
||||
return <Icon class={cn('h-4 w-4', props.class)} />
|
||||
}
|
||||
|
||||
export const MediaDetailDialog: ParentComponent<MediaDetailDialogProps> = (props) => {
|
||||
const [local] = splitProps(props, [
|
||||
'item',
|
||||
'progressPercent',
|
||||
'open',
|
||||
'onOpenChange',
|
||||
'onAddToWatchlist',
|
||||
'onMarkWatched',
|
||||
'onContinueWatching',
|
||||
'children',
|
||||
])
|
||||
|
||||
const tone = () => mediaBadgeVariant(local.item.type)
|
||||
|
||||
return (
|
||||
<ArkDialog.Root open={local.open} onOpenChange={(details) => local.onOpenChange?.(details.open)}>
|
||||
<ArkDialog.Trigger class="w-full text-left">
|
||||
<Show when={local.children} fallback={<MediaDetailTrigger item={local.item} progressPercent={local.progressPercent} />}>
|
||||
{local.children}
|
||||
</Show>
|
||||
</ArkDialog.Trigger>
|
||||
|
||||
<Portal>
|
||||
<ArkDialog.Backdrop
|
||||
class={cn(
|
||||
'fixed inset-0 z-50 bg-black/70 backdrop-blur-sm',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||
'data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0',
|
||||
'data-[state=open]:duration-200 data-[state=closed]:duration-150',
|
||||
)}
|
||||
/>
|
||||
<ArkDialog.Positioner class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<ArkDialog.Content
|
||||
class={cn(
|
||||
'relative w-full max-w-2xl max-h-[85vh] overflow-hidden',
|
||||
'rounded-2xl bg-surface-container shadow-panel',
|
||||
'border border-outline-variant/15',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||
'data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0',
|
||||
'data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95',
|
||||
'data-[state=open]:duration-200 data-[state=closed]:duration-150',
|
||||
'focus:outline-none',
|
||||
)}
|
||||
>
|
||||
{/* Hero Section */}
|
||||
<div class="relative h-48 overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-primary/20 via-surface-container to-secondary/10" />
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-surface-container via-transparent to-transparent" />
|
||||
|
||||
{/* Monogram */}
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<p class="font-display text-8xl font-bold text-fg/5">
|
||||
{mediaMonogram(local.item.title)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Close button */}
|
||||
<ArkDialog.CloseTrigger
|
||||
class={cn(
|
||||
'absolute right-4 top-4 z-10',
|
||||
'flex h-8 w-8 items-center justify-center rounded-lg',
|
||||
'bg-surface-high text-muted-fg',
|
||||
'hover:bg-surface-highest hover:text-fg',
|
||||
'transition-all duration-200',
|
||||
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30',
|
||||
)}
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
</ArkDialog.CloseTrigger>
|
||||
|
||||
{/* Type badge */}
|
||||
<div class="absolute left-6 bottom-6 flex items-center gap-3">
|
||||
<Badge variant={tone()} class="flex items-center gap-1.5">
|
||||
<MediaTypeIcon type={local.item.type} />
|
||||
{mediaTypeLabel(local.item.type)}
|
||||
</Badge>
|
||||
<span class="text-xs font-semibold uppercase tracking-widest text-muted-fg">
|
||||
{mediaYear(local.item.releaseDate)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div class="flex flex-col space-y-1.5 px-6 pt-6 pb-0">
|
||||
<ArkDialog.Title class="text-2xl font-semibold text-fg pr-8">
|
||||
{local.item.title}
|
||||
</ArkDialog.Title>
|
||||
<div class="flex items-center gap-4 text-sm text-muted-fg">
|
||||
<Show when={local.item.type !== 'game'}>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<Clock class="h-3.5 w-3.5" />
|
||||
{formatRuntime(local.item.runtimeMinutes, local.item.type)}
|
||||
</span>
|
||||
</Show>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<Calendar class="h-3.5 w-3.5" />
|
||||
{new Date(local.item.releaseDate).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5 text-tertiary">
|
||||
<Star class="h-3.5 w-3.5 fill-current" />
|
||||
{formatRating(local.item.rating)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div class="flex-1 overflow-y-auto px-6 py-4 space-y-6">
|
||||
{/* Progress */}
|
||||
<Show when={typeof local.progressPercent === 'number'}>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-fg">Your progress</span>
|
||||
<span class="font-semibold text-fg tabular-nums">{local.progressPercent}%</span>
|
||||
</div>
|
||||
<Progress value={local.progressPercent} variant="accent" />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Genres */}
|
||||
<Show when={local.item.genres.length > 0}>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{local.item.genres.map((genre) => (
|
||||
<span class="rounded-lg bg-surface-high px-3 py-1 text-xs font-medium text-muted-fg">
|
||||
{genre}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Overview */}
|
||||
<div class="space-y-2">
|
||||
<h4 class="text-sm font-semibold text-fg">Overview</h4>
|
||||
<p class="text-sm leading-relaxed text-muted-fg">{local.item.overview}</p>
|
||||
</div>
|
||||
|
||||
{/* Platforms (for games) */}
|
||||
<Show when={local.item.type === 'game' && local.item.platforms.length > 0}>
|
||||
<div class="space-y-2">
|
||||
<h4 class="text-sm font-semibold text-fg">Platforms</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{local.item.platforms.map((platform) => (
|
||||
<span class="rounded-lg bg-secondary/10 px-3 py-1 text-xs font-medium text-secondary">
|
||||
{platform}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Meta info */}
|
||||
<div class="flex items-center gap-4 text-xs text-muted-fg">
|
||||
<span>Provider: {local.item.provider.toUpperCase()}</span>
|
||||
<span>ID: {local.item.providerId}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div class="flex items-center justify-end gap-3 border-t border-outline-variant/15 px-6 py-4">
|
||||
<Show when={local.onAddToWatchlist}>
|
||||
{(onAdd) => (
|
||||
<Button variant="ghost" onClick={onAdd()}>
|
||||
<Plus class="h-4 w-4" />
|
||||
Watchlist
|
||||
</Button>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={local.onMarkWatched}>
|
||||
{(onMark) => (
|
||||
<Button variant="secondary" onClick={onMark()}>
|
||||
<Eye class="h-4 w-4" />
|
||||
Mark Watched
|
||||
</Button>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={local.onContinueWatching}>
|
||||
{(onContinue) => (
|
||||
<Button variant="primary" onClick={onContinue()}>
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</ArkDialog.Content>
|
||||
</ArkDialog.Positioner>
|
||||
</Portal>
|
||||
</ArkDialog.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const MediaDetailTrigger = (props: { item: MediaItem; progressPercent?: number }) => {
|
||||
return (
|
||||
<article class="group rounded-2xl bg-surface-container overflow-hidden transition-all duration-300 hover:bg-surface-high hover:scale-[1.02] hover:shadow-ambient">
|
||||
<div class="relative h-40 overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-surface-low via-surface-container to-surface-high" />
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<p class="font-display text-5xl font-semibold text-fg/10">
|
||||
{mediaMonogram(props.item.title)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-primary/50 to-transparent" />
|
||||
<div class="absolute right-3 top-3 flex items-center gap-2">
|
||||
<Badge variant={mediaBadgeVariant(props.item.type)}>
|
||||
{mediaTypeLabel(props.item.type)}
|
||||
</Badge>
|
||||
<span class="text-[10px] font-semibold uppercase tracking-widest text-muted-fg">
|
||||
{mediaYear(props.item.releaseDate)}
|
||||
</span>
|
||||
</div>
|
||||
<Show when={typeof props.progressPercent === 'number'}>
|
||||
<div class="absolute bottom-0 left-0 right-0 h-1 bg-surface-highest">
|
||||
<div
|
||||
class="h-full bg-primary transition-all duration-300"
|
||||
style={{ width: `${Math.min(100, Math.max(0, props.progressPercent ?? 0))}%` }}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="p-4 space-y-2">
|
||||
<h4 class="line-clamp-1 text-sm font-semibold text-fg">{props.item.title}</h4>
|
||||
<p class="line-clamp-2 min-h-[2.25rem] text-xs text-muted-fg leading-relaxed">
|
||||
{props.item.overview}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center justify-between text-xs text-muted-fg pt-1">
|
||||
<span>{mediaMeta(props.item) || formatRuntime(props.item.runtimeMinutes, props.item.type)}</span>
|
||||
<span class="font-semibold text-tertiary">{formatRating(props.item.rating)} ★</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user