Files
mystuff_frontend/src/components/DetailView.tsx
T
2026-04-11 00:39:36 +02:00

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>
);
}