small fix, don't worry about it

This commit is contained in:
Tomas Dvorak
2026-04-10 12:06:24 +02:00
commit 5c500a72b0
243 changed files with 44176 additions and 0 deletions
@@ -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>
)
}