284 lines
13 KiB
TypeScript
284 lines
13 KiB
TypeScript
import { Media, Staff } from '@/types';
|
|
import {
|
|
Play,
|
|
Bookmark,
|
|
MoreHorizontal,
|
|
Star,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
Search,
|
|
ListFilter
|
|
} from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { motion } from 'motion/react';
|
|
|
|
interface DetailViewProps {
|
|
media: Media;
|
|
onBack: () => void;
|
|
onPersonClick: (person: Staff) => void;
|
|
}
|
|
|
|
export default function DetailView({ media, onBack, onPersonClick }: DetailViewProps) {
|
|
return (
|
|
<div className="min-h-screen bg-zinc-50">
|
|
{/* Banner */}
|
|
<div className="relative h-[400px] w-full overflow-hidden">
|
|
<img
|
|
src={media.banner || media.poster}
|
|
alt={media.title}
|
|
className="w-full h-full object-cover"
|
|
referrerPolicy="no-referrer"
|
|
/>
|
|
<div className="absolute inset-0 bg-gradient-to-t from-zinc-50 via-zinc-50/40 to-transparent" />
|
|
|
|
<button
|
|
onClick={onBack}
|
|
className="absolute top-24 left-6 p-2 bg-black/20 hover:bg-black/40 text-white rounded-full transition-colors z-10"
|
|
>
|
|
<ChevronLeft size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="max-w-[1400px] mx-auto px-6 -mt-32 relative z-10 pb-24">
|
|
<div className="flex flex-col md:flex-row gap-8">
|
|
{/* Left Column: Poster */}
|
|
<div className="w-full md:w-[300px] shrink-0">
|
|
<motion.div
|
|
layoutId={`media-${media.id}`}
|
|
className={`rounded-xl overflow-hidden shadow-2xl bg-zinc-800 ${
|
|
media.aspectRatio === '16/9' ? 'aspect-video' :
|
|
media.aspectRatio === '1/1' ? 'aspect-square' :
|
|
'aspect-[2/3]'
|
|
}`}
|
|
>
|
|
<img
|
|
src={media.poster}
|
|
alt={media.title}
|
|
className="w-full h-full object-cover"
|
|
referrerPolicy="no-referrer"
|
|
/>
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Right Column: Info */}
|
|
<div className="flex-1 pt-32 md:pt-40">
|
|
<div className="flex flex-wrap items-end justify-between gap-4 mb-6">
|
|
<div>
|
|
<h1 className="text-4xl font-black text-zinc-900 mb-2">
|
|
{media.title} <span className="text-zinc-400 font-medium">({media.year})</span>
|
|
</h1>
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<Button size="icon" className="rounded-full bg-[#6d28d9] hover:bg-[#5b21b6]">
|
|
<Play size={20} fill="currentColor" />
|
|
</Button>
|
|
<Button size="icon" variant="outline" className="rounded-full border-zinc-300">
|
|
<Bookmark size={20} />
|
|
</Button>
|
|
<Button size="icon" variant="outline" className="rounded-full border-zinc-300">
|
|
<MoreHorizontal size={20} />
|
|
</Button>
|
|
</div>
|
|
<div className="flex items-center gap-1 text-zinc-600 font-bold">
|
|
<Star size={18} className="text-yellow-500" fill="currentColor" />
|
|
{media.rating} / 10
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="hidden lg:block text-right">
|
|
<h3 className="text-xs font-black text-[#6d28d9] uppercase tracking-wider mb-2">Genres</h3>
|
|
<div className="flex flex-col items-end gap-1">
|
|
{media.genres?.map(genre => (
|
|
<span key={genre} className="text-sm font-bold text-zinc-600 hover:text-[#6d28d9] cursor-pointer transition-colors">
|
|
• {genre}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-zinc-600 leading-relaxed mb-8 max-w-3xl">
|
|
{media.description}
|
|
</p>
|
|
|
|
{/* Tags */}
|
|
<div className="flex flex-wrap gap-2 mb-8">
|
|
{media.tags?.map(tag => (
|
|
<Badge key={tag} variant="secondary" className="bg-[#6d28d9]/10 text-[#6d28d9] hover:bg-[#6d28d9]/20 border-none px-3 py-1 font-bold text-[10px] uppercase tracking-wider">
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{media.studios && media.studios.length > 0 && (
|
|
<p className="text-xs font-bold text-zinc-500">
|
|
<span className="text-zinc-400 uppercase tracking-widest mr-2">Studios:</span>
|
|
{media.studios.join(', ')}
|
|
</p>
|
|
)}
|
|
{media.developers && media.developers.length > 0 && (
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<span className="text-xs font-bold text-zinc-400 uppercase tracking-widest">Developers:</span>
|
|
{media.developers.map(dev => (
|
|
<Badge key={dev} variant="secondary" className="bg-zinc-100 text-zinc-700 hover:bg-zinc-200 border-none px-3 py-1 font-bold text-[10px]">
|
|
{dev}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
)}
|
|
{media.platforms && media.platforms.length > 0 && (
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<span className="text-xs font-bold text-zinc-400 uppercase tracking-widest">Platforms:</span>
|
|
{media.platforms.map(platform => (
|
|
<Badge key={platform} variant="secondary" className="bg-zinc-100 text-zinc-700 hover:bg-zinc-200 border-none px-3 py-1 font-bold text-[10px]">
|
|
{platform}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
)}
|
|
{media.categories && media.categories.length > 0 && (
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<span className="text-xs font-bold text-zinc-400 uppercase tracking-widest">Categories:</span>
|
|
{media.categories.map(category => (
|
|
<Badge key={category} variant="secondary" className="bg-zinc-100 text-zinc-700 hover:bg-zinc-200 border-none px-3 py-1 font-bold text-[10px]">
|
|
{category}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
)}
|
|
{media.completionStatus && (
|
|
<p className="text-xs font-bold text-zinc-500">
|
|
<span className="text-zinc-400 uppercase tracking-widest mr-2">Completion:</span>
|
|
{media.completionStatus}
|
|
</p>
|
|
)}
|
|
{media.source && (
|
|
<p className="text-xs font-bold text-zinc-500">
|
|
<span className="text-zinc-400 uppercase tracking-widest mr-2">Source:</span>
|
|
{media.source}
|
|
</p>
|
|
)}
|
|
{media.playCount !== undefined && media.playCount !== null && (
|
|
<p className="text-xs font-bold text-zinc-500">
|
|
<span className="text-zinc-400 uppercase tracking-widest mr-2">Play Count:</span>
|
|
{media.playCount}
|
|
</p>
|
|
)}
|
|
{media.playtime !== undefined && media.playtime !== null && media.playtime > 0 && (
|
|
<p className="text-xs font-bold text-zinc-500">
|
|
<span className="text-zinc-400 uppercase tracking-widest mr-2">Playtime:</span>
|
|
{media.playtime}h
|
|
</p>
|
|
)}
|
|
{media.lastActivity && (
|
|
<p className="text-xs font-bold text-zinc-500">
|
|
<span className="text-zinc-400 uppercase tracking-widest mr-2">Last Activity:</span>
|
|
{media.lastActivity}
|
|
</p>
|
|
)}
|
|
<div className="flex items-center gap-4">
|
|
<span className="text-xs font-bold text-zinc-400 uppercase tracking-widest">Links:</span>
|
|
<Button variant="link" className="p-0 h-auto text-[#6d28d9] font-bold text-xs">Tvdb</Button>
|
|
<Button variant="link" className="p-0 h-auto text-[#6d28d9] font-bold text-xs">AniDb</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Staff Section - Only show if staff data exists */}
|
|
{media.staff && media.staff.length > 0 && (
|
|
<section className="mt-20">
|
|
<div className="flex items-center justify-between mb-8">
|
|
<h2 className="text-2xl font-black text-zinc-900">Cast & Crew</h2>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" size="icon" className="rounded-full border-zinc-200">
|
|
<ChevronLeft size={18} />
|
|
</Button>
|
|
<Button variant="outline" size="icon" className="rounded-full border-zinc-200">
|
|
<ChevronRight size={18} />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{media.staff.map(person => (
|
|
<div
|
|
key={person.id}
|
|
className="flex items-center gap-4 bg-white p-3 rounded-xl shadow-sm border border-zinc-100 hover:shadow-md transition-shadow cursor-pointer group"
|
|
onClick={() => onPersonClick(person)}
|
|
>
|
|
<div className="w-16 h-20 rounded-lg overflow-hidden shrink-0">
|
|
<img src={person.photo} alt={person.name} className="w-full h-full object-cover group-hover:scale-105 transition-transform" referrerPolicy="no-referrer" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h4 className="font-bold text-zinc-900 truncate group-hover:text-[#6d28d9] transition-colors">{person.name}</h4>
|
|
<p className="text-xs text-zinc-500 truncate">{person.role}</p>
|
|
</div>
|
|
<div className="w-16 h-20 rounded-lg overflow-hidden shrink-0 bg-zinc-50">
|
|
<img src={person.characterImage} alt={person.characterName} className="w-full h-full object-contain" referrerPolicy="no-referrer" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Episodes Section - Only show if episodes data exists */}
|
|
{media.episodes && media.episodes.length > 0 && (
|
|
<section className="mt-20">
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
|
|
<div className="flex items-center gap-6">
|
|
<div className="flex items-center gap-2 text-[#6d28d9] font-black text-xl">
|
|
<span className="opacity-40">{media.episodes.length}</span> Episode{media.episodes.length !== 1 ? 's' : ''}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" size={16} />
|
|
<Input placeholder="Search" className="pl-10 w-[200px] bg-zinc-100 border-none rounded-full h-9 text-sm" />
|
|
</div>
|
|
<Button variant="ghost" size="icon" className="text-zinc-400">
|
|
<MoreHorizontal size={20} />
|
|
</Button>
|
|
<Button variant="ghost" size="icon" className="text-zinc-400">
|
|
<ListFilter size={20} />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
{media.episodes.map(episode => (
|
|
<div key={episode.id} className="group cursor-pointer">
|
|
<div className="flex flex-col md:flex-row gap-6">
|
|
<div className="w-full md:w-[240px] shrink-0 aspect-video rounded-xl overflow-hidden shadow-sm relative">
|
|
<img src={episode.thumbnail} alt={episode.title} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" referrerPolicy="no-referrer" />
|
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
|
|
</div>
|
|
<div className="flex-1 py-1">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h3 className="font-black text-zinc-900 group-hover:text-[#6d28d9] transition-colors">
|
|
S1:E{episode.number} • {episode.title}
|
|
</h3>
|
|
<span className="text-xs font-bold text-zinc-400">{episode.date} • {episode.duration}</span>
|
|
</div>
|
|
<p className="text-sm text-zinc-500 leading-relaxed line-clamp-3">
|
|
{episode.description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Separator className="mt-6 bg-zinc-200" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|