287 lines
14 KiB
TypeScript
287 lines
14 KiB
TypeScript
import { Media, Staff } from '@/types';
|
|
import { useNavigate } from 'react-router-dom';
|
|
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;
|
|
onPersonClick: (person: Staff) => void;
|
|
}
|
|
|
|
export default function DetailView({ media, onPersonClick }: DetailViewProps) {
|
|
const navigate = useNavigate();
|
|
return (
|
|
<div className="min-h-screen bg-background">
|
|
{/* 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-background via-background/40 to-transparent" />
|
|
|
|
<button
|
|
onClick={() => navigate(-1)}
|
|
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-6">
|
|
{/* Left Column: Poster + Metadata */}
|
|
<div className="w-full md:w-[300px] shrink-0">
|
|
<motion.div
|
|
layoutId={`media-${media.id}`}
|
|
className={`rounded-xl overflow-hidden shadow-2xl bg-card ${
|
|
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>
|
|
|
|
{/* Compact metadata under poster */}
|
|
<div className="mt-4 space-y-2">
|
|
{media.studios && media.studios.length > 0 && (
|
|
<p className="text-xs font-bold text-muted-foreground">
|
|
<span className="text-muted-foreground/70 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-muted-foreground uppercase tracking-widest">Developers:</span>
|
|
{media.developers.map(dev => (
|
|
<Badge key={dev} variant="secondary" className="bg-muted text-foreground hover:bg-muted/80 border-none px-2 py-0.5 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-muted-foreground uppercase tracking-widest">Platforms:</span>
|
|
{media.platforms.map(platform => (
|
|
<Badge key={platform} variant="secondary" className="bg-muted text-foreground hover:bg-muted/80 border-none px-2 py-0.5 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-muted-foreground uppercase tracking-widest">Categories:</span>
|
|
{media.categories.map(category => (
|
|
<Badge key={category} variant="secondary" className="bg-muted text-foreground hover:bg-muted/80 border-none px-2 py-0.5 font-bold text-[10px]">
|
|
{category}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
)}
|
|
{media.completionStatus && (
|
|
<p className="text-xs font-bold text-muted-foreground">
|
|
<span className="text-muted-foreground/70 uppercase tracking-widest mr-2">Completion:</span>
|
|
{media.completionStatus}
|
|
</p>
|
|
)}
|
|
{media.source && (
|
|
<p className="text-xs font-bold text-muted-foreground">
|
|
<span className="text-muted-foreground/70 uppercase tracking-widest mr-2">Source:</span>
|
|
{media.source}
|
|
</p>
|
|
)}
|
|
{media.playCount !== undefined && media.playCount !== null && (
|
|
<p className="text-xs font-bold text-muted-foreground">
|
|
<span className="text-muted-foreground/70 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-muted-foreground">
|
|
<span className="text-muted-foreground/70 uppercase tracking-widest mr-2">Playtime:</span>
|
|
{media.playtime}h
|
|
</p>
|
|
)}
|
|
{media.lastActivity && (
|
|
<p className="text-xs font-bold text-muted-foreground">
|
|
<span className="text-muted-foreground/70 uppercase tracking-widest mr-2">Last Activity:</span>
|
|
{media.lastActivity}
|
|
</p>
|
|
)}
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-xs font-bold text-muted-foreground 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>
|
|
|
|
{/* Right Column: Info */}
|
|
<div className="flex-1 pt-4 md:pt-8">
|
|
<div className="flex flex-wrap items-end justify-between gap-4 mb-6">
|
|
<div>
|
|
<h1 className="text-4xl font-black text-foreground mb-2">
|
|
{media.title} <span className="text-muted-foreground 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-border">
|
|
<Bookmark size={20} />
|
|
</Button>
|
|
<Button size="icon" variant="outline" className="rounded-full border-border">
|
|
<MoreHorizontal size={20} />
|
|
</Button>
|
|
</div>
|
|
<div className="flex items-center gap-1 text-foreground 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-foreground hover:text-[#6d28d9] cursor-pointer transition-colors">
|
|
• {genre}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className="text-foreground leading-relaxed mb-6 max-w-3xl prose prose-sm dark:prose-invert"
|
|
dangerouslySetInnerHTML={{ __html: media.description || '' }}
|
|
/>
|
|
|
|
{/* Tags */}
|
|
<div className="flex flex-wrap gap-2 mb-4">
|
|
{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>
|
|
</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-foreground">Cast & Crew</h2>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" size="icon" className="rounded-full border-border">
|
|
<ChevronLeft size={18} />
|
|
</Button>
|
|
<Button variant="outline" size="icon" className="rounded-full border-border">
|
|
<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-card p-3 rounded-xl shadow-sm border border-border 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-foreground truncate group-hover:text-[#6d28d9] transition-colors">{person.name}</h4>
|
|
<p className="text-xs text-muted-foreground truncate">{person.role}</p>
|
|
</div>
|
|
<div className="w-16 h-20 rounded-lg overflow-hidden shrink-0 bg-muted">
|
|
<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-muted-foreground" size={16} />
|
|
<Input placeholder="Search" className="pl-10 w-[200px] bg-muted border-none rounded-full h-9 text-sm" />
|
|
</div>
|
|
<Button variant="ghost" size="icon" className="text-muted-foreground">
|
|
<MoreHorizontal size={20} />
|
|
</Button>
|
|
<Button variant="ghost" size="icon" className="text-muted-foreground">
|
|
<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-foreground group-hover:text-[#6d28d9] transition-colors">
|
|
S1:E{episode.number} • {episode.title}
|
|
</h3>
|
|
<span className="text-xs font-bold text-muted-foreground">{episode.date} • {episode.duration}</span>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground leading-relaxed line-clamp-3">
|
|
{episode.description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Separator className="mt-6 bg-border" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|