Integrate shadcn UI & add UI primitives

Integrates the shadcn/ui design system across the app and adds a collection of reusable UI primitives and layout components. Adds new UI atoms/molecules (avatar, card, collapsible, progress, select, sheet, sidebar, skeleton, table, tabs, toggles, tooltip), app sidebar, media filters, MediaTable, and a mobile hook; updates many views/components to use the new UI. Updates AGENTS.md with styling, layout, accessibility and design standards (Tailwind/shadcn guidance) and adds a registry entry to components.json. Also updates dependencies/lockfile to align shadcn and related packages.
This commit is contained in:
Lars Behrends
2026-04-26 02:18:01 +02:00
parent 9a72ba3064
commit 073c8a6c5d
37 changed files with 6306 additions and 1593 deletions
+395 -273
View File
@@ -1,10 +1,25 @@
import { Staff, Media } from '@/types';
import { useNavigate } from 'react-router-dom';
import { motion } from 'motion/react';
import { ArrowLeft, Calendar, MapPin, Briefcase, Film, User, Ruler, Palette, Eye, ChevronDown, ListFilter } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import {
ArrowLeft, Calendar, MapPin, Briefcase, Film, User, Ruler, Palette, Eye,
BookOpen, Theater, ArrowUpAZ, ArrowDownAZ, ArrowUpDown, Star
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from '@/components/ui/dropdown-menu';
import { Separator } from '@/components/ui/separator';
import { useState } from 'react';
import { cn } from '@/lib/utils';
interface CastDetailViewProps {
person: Staff;
@@ -31,51 +46,64 @@ export default function CastDetailView({ person, relatedMedia }: CastDetailViewP
}
return sortOrder === 'asc' ? comparison : -comparison;
});
// Sort options
const sortOptions = [
{ value: 'year', label: 'Year', icon: Calendar },
{ value: 'title', label: 'Title', icon: ArrowUpAZ },
{ value: 'role', label: 'Role', icon: Briefcase },
] as const;
return (
<div className="min-h-screen bg-background pb-20">
{/* Hero Section */}
<div className="relative h-[50vh] md:h-[60vh] overflow-hidden bg-zinc-900">
<img
src={person.photo}
<div className="min-h-screen bg-background pb-16">
{/* Compact Hero Section */}
<div className="relative h-[35vh] md:h-[40vh] overflow-hidden bg-zinc-900">
<img
src={person.photo}
alt={person.name}
className="w-full h-full object-cover opacity-40 blur-xl scale-110"
className="w-full h-full object-cover opacity-30 blur-xl scale-110"
referrerPolicy="no-referrer"
/>
<div className="absolute inset-0 bg-gradient-to-t from-background via-transparent to-transparent" />
<div className="absolute inset-0 flex items-end px-6 pb-12">
<div className="max-w-[1920px] mx-auto w-full flex flex-col md:flex-row items-center md:items-end gap-8">
<motion.div
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/50 to-transparent" />
<div className="absolute inset-0 flex items-end px-4 sm:px-6 pb-8">
<div className="max-w-[1920px] mx-auto w-full flex flex-col md:flex-row items-center md:items-end gap-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="h-48 md:h-72 rounded-2xl overflow-hidden border-4 border-background shadow-2xl shrink-0"
className="shrink-0"
>
<img
src={person.photo}
alt={person.name}
className="w-full h-full object-cover"
referrerPolicy="no-referrer"
/>
<Avatar className="h-32 md:h-40 w-auto aspect-[3/4] rounded-none border-3 border-background shadow-2xl">
<AvatarImage
src={person.photo}
alt={person.name}
className="object-cover"
referrerPolicy="no-referrer"
/>
<AvatarFallback className="rounded-none text-3xl">
<User className="h-12 w-12" />
</AvatarFallback>
</Avatar>
</motion.div>
<div className="flex-1 text-center md:text-left pb-4">
<div className="flex-1 text-center md:text-left pb-2">
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 }}
>
<h1 className="text-5xl md:text-7xl font-black text-foreground mb-4 drop-shadow-sm">
<h1 className="text-3xl md:text-5xl font-bold text-foreground mb-3 tracking-tight">
{person.name}
</h1>
<div className="flex flex-wrap justify-center md:justify-start gap-3">
<div className="flex flex-wrap justify-center md:justify-start gap-2">
{person.occupations?.map(occ => (
<Badge key={occ} variant="secondary" className="bg-[#6d28d9]/10 text-[#6d28d9] border-[#6d28d9]/20 font-bold px-4 py-1.5 backdrop-blur-sm">
<Badge key={occ} variant="secondary" className="bg-[#6d28d9]/10 text-[#6d28d9] border-[#6d28d9]/20 font-medium px-3 py-1 text-xs">
{occ}
</Badge>
))}
{person.filmography && person.filmography.length > 0 && (
<Badge variant="outline" className="border-[#6d28d9]/30 text-[#6d28d9] font-bold px-4 py-1.5">
{person.filmography.length} Role{person.filmography.length !== 1 ? 's' : ''}
<Badge variant="outline" className="border-[#6d28d9]/30 text-[#6d28d9] font-medium px-3 py-1 text-xs">
<Star className="w-3 h-3 mr-1" />
{person.filmography.length}
</Badge>
)}
</div>
@@ -84,289 +112,383 @@ export default function CastDetailView({ person, relatedMedia }: CastDetailViewP
</div>
</div>
<Button
variant="ghost"
size="icon"
<Button
variant="ghost"
size="icon"
onClick={() => navigate(-1)}
className="absolute top-24 left-6 bg-white/30 hover:bg-white/50 text-white rounded-2xl backdrop-blur-md transition-all duration-300 hover:scale-110 border border-white/20"
className="absolute top-20 left-4 sm:left-6 bg-white/20 hover:bg-white/40 text-white rounded-xl backdrop-blur-md transition-all duration-300 hover:scale-105 border border-white/20 h-10 w-10"
>
<ArrowLeft size={24} />
<ArrowLeft size={20} />
</Button>
</div>
{/* Content Section */}
<div className="max-w-[1920px] mx-auto px-6 mt-12 grid grid-cols-1 lg:grid-cols-3 gap-12">
{/* Sidebar Info */}
<div className="space-y-8">
<div className="bg-muted/50 backdrop-blur-sm rounded-3xl p-8 space-y-6 border border-border/50">
<h3 className="text-2xl font-black text-foreground">Personal Info</h3>
<div className="space-y-4">
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border/50">
<Calendar size={20} />
<div className="max-w-[1920px] mx-auto px-4 sm:px-6 mt-8 grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar Info - Modern shadcn Design */}
<div className="space-y-4 lg:col-span-1">
{/* Personal Info Card */}
<Card className="border-border/60 overflow-hidden">
<CardHeader className="py-3 px-4 border-b border-border/40">
<CardTitle className="text-xs font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-2">
<div className="w-5 h-5 rounded bg-[#6d28d9]/10 flex items-center justify-center">
<User size={12} className="text-[#6d28d9]" />
</div>
<div>
<p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Birth Date</p>
<p className="font-bold text-foreground">{person.birthDate || 'Unknown'}</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border/50">
<MapPin size={20} />
</div>
<div>
<p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Birth Place</p>
<p className="font-bold text-foreground">{person.birthPlace || 'Unknown'}</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border/50">
<Briefcase size={20} />
</div>
<div>
<p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Known For</p>
<p className="font-bold text-foreground">{person.role}</p>
Personal Info
</CardTitle>
</CardHeader>
<CardContent className="p-0">
{/* Birth Date */}
<div className="flex items-center justify-between px-4 py-3 hover:bg-muted/30 transition-colors">
<div className="flex items-center gap-2.5">
<div className="w-7 h-7 rounded-md bg-[#6d28d9]/10 flex items-center justify-center text-[#6d28d9]">
<Calendar size={14} />
</div>
<span className="text-xs text-muted-foreground">Born</span>
</div>
<span className="text-sm font-medium">{person.birthDate || '—'}</span>
</div>
<Separator />
{/* Birth Place */}
<div className="flex items-center justify-between px-4 py-3 hover:bg-muted/30 transition-colors">
<div className="flex items-center gap-2.5">
<div className="w-7 h-7 rounded-md bg-[#6d28d9]/10 flex items-center justify-center text-[#6d28d9]">
<MapPin size={14} />
</div>
<span className="text-xs text-muted-foreground">Origin</span>
</div>
<span className="text-sm font-medium truncate max-w-[140px]" title={person.birthPlace || undefined}>
{person.birthPlace || '—'}
</span>
</div>
<Separator />
{/* Known For */}
<div className="flex items-center justify-between px-4 py-3 hover:bg-muted/30 transition-colors">
<div className="flex items-center gap-2.5">
<div className="w-7 h-7 rounded-md bg-[#6d28d9]/10 flex items-center justify-center text-[#6d28d9]">
<Briefcase size={14} />
</div>
<span className="text-xs text-muted-foreground">Role</span>
</div>
<Badge variant="secondary" className="text-xs font-normal bg-[#6d28d9]/10 text-[#6d28d9] border-none">
{person.role}
</Badge>
</div>
{/* Ethnicity - only if present */}
{(person.ethnicity || person.adult_specifics?.ethnicity) && (
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border/50">
<User size={20} />
<>
<Separator />
<div className="flex items-center justify-between px-4 py-3 hover:bg-muted/30 transition-colors">
<div className="flex items-center gap-2.5">
<div className="w-7 h-7 rounded-md bg-[#6d28d9]/10 flex items-center justify-center text-[#6d28d9]">
<User size={14} />
</div>
<span className="text-xs text-muted-foreground">Ethnicity</span>
</div>
<span className="text-sm font-medium truncate max-w-[140px]">
{person.adult_specifics?.ethnicity || person.ethnicity}
</span>
</div>
<div>
<p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Ethnicity</p>
<p className="font-bold text-foreground">{person.adult_specifics?.ethnicity || person.ethnicity}</p>
</div>
</div>
</>
)}
</div>
</div>
</CardContent>
</Card>
<div className="bg-muted/50 backdrop-blur-sm rounded-3xl p-8 space-y-6 border border-border/50">
<h3 className="text-2xl font-black text-foreground">Measurements</h3>
<div className="space-y-4">
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border/50">
<Ruler size={20} />
</div>
<div>
<p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Height</p>
<p className="font-bold text-foreground">{person.adult_specifics?.height || person.height} cm</p>
</div>
{/* Measurements Card - Only if data exists */}
{(person.adult_specifics?.height || person.height || person.adult_specifics?.weight || person.weight ||
person.adult_specifics?.measurements || person.bust_size || person.hair_color || person.adult_specifics?.hair_color) && (
<Card className="border-border/60 overflow-hidden">
<CardHeader className="py-3 px-4 border-b border-border/40">
<CardTitle className="text-xs font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-2">
<div className="w-5 h-5 rounded bg-[#6d28d9]/10 flex items-center justify-center">
<Ruler size={12} className="text-[#6d28d9]" />
</div>
{(person.weight || person.adult_specifics?.weight) && (
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border/50">
<Ruler size={20} />
Measurements
</CardTitle>
</CardHeader>
<CardContent className="p-0">
{/* Height & Weight Grid */}
{(person.adult_specifics?.height || person.height || person.adult_specifics?.weight || person.weight) && (
<>
<div className="grid grid-cols-2 divide-x divide-border">
{(person.adult_specifics?.height || person.height) && (
<div className="px-4 py-3 hover:bg-muted/30 transition-colors text-center">
<p className="text-[10px] text-muted-foreground uppercase tracking-wide mb-0.5">Height</p>
<p className="text-lg font-semibold text-foreground">
{person.adult_specifics?.height || person.height}
<span className="text-xs font-normal text-muted-foreground ml-0.5">cm</span>
</p>
</div>
)}
{(person.adult_specifics?.weight || person.weight) && (
<div className="px-4 py-3 hover:bg-muted/30 transition-colors text-center">
<p className="text-[10px] text-muted-foreground uppercase tracking-wide mb-0.5">Weight</p>
<p className="text-lg font-semibold text-foreground">
{person.adult_specifics?.weight || person.weight}
<span className="text-xs font-normal text-muted-foreground ml-0.5">kg</span>
</p>
</div>
)}
</div>
<div>
<p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Weight</p>
<p className="font-bold text-foreground">{person.adult_specifics?.weight || person.weight} kg</p>
</div>
</div>
<Separator />
</>
)}
{/* Measurements (Bust-Waist-Hip) */}
{(person.adult_specifics?.measurements || person.bust_size || person.cup_size || person.waist_size || person.hip_size) && (
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border/50">
<Ruler size={20} />
</div>
<div>
<p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Measurements</p>
<p className="font-bold text-foreground">
<>
<div className="px-4 py-3 hover:bg-muted/30 transition-colors">
<p className="text-[10px] text-muted-foreground uppercase tracking-wide mb-1.5">Figure</p>
<p className="text-sm font-medium font-mono tracking-wide">
{person.adult_specifics?.measurements || (
<>
{person.bust_size && `${person.bust_size}`}
{person.cup_size && person.cup_size}
{person.bust_size || person.cup_size ? '-' : ''}
{person.waist_size && `${person.waist_size}`}
{person.waist_size ? '-' : ''}
{person.hip_size && `${person.hip_size}`}
{person.bust_size && <span className="inline-flex items-center gap-0.5">{person.bust_size}{person.cup_size && <span className="text-xs text-muted-foreground">{person.cup_size}</span>}</span>}
{(person.bust_size || person.cup_size) && person.waist_size && <span className="text-muted-foreground mx-1"></span>}
{person.waist_size && <span>{person.waist_size}</span>}
{person.hip_size && <span className="text-muted-foreground mx-1"></span>}
{person.hip_size && <span>{person.hip_size}</span>}
</>
)}
</p>
</div>
</div>
<Separator />
</>
)}
{(person.hair_color || person.adult_specifics?.hair_color) && (
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border/50">
<Palette size={20} />
{/* Hair & Eyes Grid */}
<div className="grid grid-cols-2 divide-x divide-border">
{(person.hair_color || person.adult_specifics?.hair_color) && (
<div className="px-4 py-3 hover:bg-muted/30 transition-colors">
<div className="flex items-center gap-2 mb-1">
<Palette size={12} className="text-[#6d28d9]" />
<span className="text-[10px] text-muted-foreground uppercase tracking-wide">Hair</span>
</div>
<p className="text-sm font-medium truncate">
{person.adult_specifics?.hair_color || person.hair_color}
</p>
</div>
<div>
<p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Hair Color</p>
<p className="font-bold text-foreground">{person.adult_specifics?.hair_color || person.hair_color}</p>
)}
{(person.eye_color || person.adult_specifics?.eye_color) && (
<div className="px-4 py-3 hover:bg-muted/30 transition-colors">
<div className="flex items-center gap-2 mb-1">
<Eye size={12} className="text-[#6d28d9]" />
<span className="text-[10px] text-muted-foreground uppercase tracking-wide">Eyes</span>
</div>
<p className="text-sm font-medium truncate">
{person.adult_specifics?.eye_color || person.eye_color}
</p>
</div>
</div>
)}
)}
</div>
{(person.eye_color || person.adult_specifics?.eye_color) && (
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border/50">
<Eye size={20} />
</div>
<div>
<p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Eye Color</p>
<p className="font-bold text-foreground">{person.adult_specifics?.eye_color || person.eye_color}</p>
</div>
</div>
)}
{person.adult_specifics?.tattoos && (
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border/50">
<Palette size={20} />
</div>
<div>
<p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Tattoos</p>
<p className="font-bold text-foreground">{person.adult_specifics.tattoos}</p>
</div>
</div>
)}
{person.adult_specifics?.piercings && (
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border/50">
<Palette size={20} />
</div>
<div>
<p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Piercings</p>
<p className="font-bold text-foreground">{person.adult_specifics.piercings}</p>
</div>
</div>
)}
</div>
</div>
</div>
{/* Main Bio & Roles */}
<div className="lg:col-span-2 space-y-12">
{person.bio && (
<section>
<h2 className="text-3xl font-black text-foreground mb-6 flex items-center gap-3">
Biography
</h2>
<p className="text-foreground leading-relaxed text-lg">
{person.bio}
</p>
</section>
)}
{person.filmography && person.filmography.length > 0 && (
<section>
<h2 className="text-3xl font-black text-foreground mb-6 flex items-center gap-3">
<User className="text-[#6d28d9]" />
Characters
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
{person.filmography.map(item => (
<div
key={`${item.id}-char`}
className="flex items-center gap-4 p-5 rounded-2xl bg-muted/50 border border-border/50 hover:border-[#6d28d9]/30 hover:shadow-lg transition-all duration-300"
>
<div className="w-20 h-20 rounded-xl overflow-hidden shrink-0 shadow-sm border-2 border-background">
<img
src={item.poster || person.photo}
alt={item.title}
className="w-full h-full object-cover"
referrerPolicy="no-referrer"
/>
</div>
<div className="min-w-0 flex-1">
<p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest mb-1">Character</p>
<h4 className="font-black text-foreground truncate">{item.characterName || item.role}</h4>
<button
onClick={() => handleMediaClick(item.id.toString())}
className="text-xs font-bold text-[#6d28d9] hover:underline mt-1 text-left transition-colors"
>
in {item.title}
</button>
{item.category && (
<Badge variant="secondary" className="text-[10px] font-bold mt-2 bg-muted text-muted-foreground border-none">
{item.category}
</Badge>
{/* Tattoos & Piercings */}
{(person.adult_specifics?.tattoos || person.adult_specifics?.piercings) && (
<>
<Separator />
<div className="grid grid-cols-2 divide-x divide-border">
{person.adult_specifics?.tattoos && (
<div className="px-4 py-3 hover:bg-muted/30 transition-colors">
<p className="text-[10px] text-muted-foreground uppercase tracking-wide mb-1">Tattoos</p>
<p className="text-xs font-medium text-foreground line-clamp-2">{person.adult_specifics.tattoos}</p>
</div>
)}
{person.adult_specifics?.piercings && (
<div className="px-4 py-3 hover:bg-muted/30 transition-colors">
<p className="text-[10px] text-muted-foreground uppercase tracking-wide mb-1">Piercings</p>
<p className="text-xs font-medium text-foreground line-clamp-2">{person.adult_specifics.piercings}</p>
</div>
)}
</div>
</div>
))}
</div>
</section>
</>
)}
</CardContent>
</Card>
)}
</div>
{person.filmography && person.filmography.length > 0 && (
<section>
<div className="flex items-center justify-between mb-6">
<h2 className="text-3xl font-black text-foreground flex items-center gap-3">
<Film className="text-[#6d28d9]" />
Filmography
</h2>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
className="rounded-xl border-border hover:border-[#6d28d9]/50 transition-all duration-300"
>
<ListFilter size={16} />
</Button>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as 'year' | 'title' | 'role')}
className="bg-muted/50 backdrop-blur-sm border border-border/50 rounded-xl px-4 py-2 text-sm font-bold text-foreground focus:outline-none focus:ring-2 focus:ring-[#6d28d9]/50"
>
<option value="year">Year</option>
<option value="title">Title</option>
<option value="role">Role</option>
</select>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{sortedFilmography.map(item => (
<div
key={item.id}
onClick={() => handleMediaClick(item.id.toString())}
className="group flex items-center gap-4 p-4 rounded-2xl bg-card border border-border/50 hover:border-[#6d28d9]/30 hover:shadow-lg hover:shadow-[#6d28d9]/10 transition-all duration-300 cursor-pointer"
>
<div className="w-16 h-20 rounded-xl overflow-hidden shrink-0 shadow-sm border border-border/30">
<img
src={item.poster || person.photo}
alt={item.title}
className="w-full h-full object-cover"
referrerPolicy="no-referrer"
/>
</div>
<div className="min-w-0">
<h4 className="font-black text-foreground truncate group-hover:text-[#6d28d9] transition-colors duration-300">
{item.title}
</h4>
<p className="text-xs font-bold text-muted-foreground uppercase tracking-wider mb-1">
{item.year || 'Unknown'}
</p>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-[10px] font-bold py-0 h-5 border-border/50">
{item.role}
</Badge>
{item.category && (
<Badge variant="secondary" className="text-[10px] font-bold py-0 h-5 bg-muted text-muted-foreground border-none">
{item.category}
</Badge>
)}
</div>
{/* Main Bio & Roles - Wider */}
<div className="lg:col-span-3">
<Tabs defaultValue={person.bio ? 'bio' : 'filmography'} className="w-full">
<TabsList className="mb-4 w-full justify-start bg-muted/50 p-1 rounded-lg h-auto">
{person.bio && (
<TabsTrigger value="bio" className="gap-1.5 text-xs rounded-md px-3 py-1.5 data-[state=active]:bg-background data-[state=active]:shadow-sm">
<BookOpen size={14} />
Biography
</TabsTrigger>
)}
{person.filmography && person.filmography.length > 0 && (
<>
<TabsTrigger value="characters" className="gap-1.5 text-xs rounded-md px-3 py-1.5 data-[state=active]:bg-background data-[state=active]:shadow-sm">
<Theater size={14} />
Characters
</TabsTrigger>
<TabsTrigger value="filmography" className="gap-1.5 text-xs rounded-md px-3 py-1.5 data-[state=active]:bg-background data-[state=active]:shadow-sm">
<Film size={14} />
Filmography
</TabsTrigger>
</>
)}
</TabsList>
{person.bio && (
<TabsContent value="bio" className="mt-0">
<Card className="border-border/60">
<CardHeader className="pb-3">
<CardTitle className="text-base font-semibold">Biography</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<p className="text-foreground leading-relaxed text-sm">
{person.bio}
</p>
</CardContent>
</Card>
</TabsContent>
)}
{person.filmography && person.filmography.length > 0 && (
<>
<TabsContent value="characters" className="mt-0">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<AnimatePresence mode="popLayout">
{person.filmography.map((item, index) => (
<motion.div
key={`${item.id}-char`}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.03 }}
>
<Card
className="hover:border-[#6d28d9]/30 hover:shadow-md transition-all duration-200 cursor-pointer group border-border/60"
onClick={() => handleMediaClick(item.id.toString())}
>
<CardContent className="p-3 flex items-center gap-3">
<div className="w-14 h-14 rounded-none overflow-hidden shrink-0 bg-muted border border-border/40">
<img
src={item.poster || person.photo}
alt={item.title}
className="w-full h-full object-cover"
referrerPolicy="no-referrer"
/>
</div>
<div className="min-w-0 flex-1">
<p className="text-[10px] text-muted-foreground uppercase tracking-wide">Character</p>
<h4 className="font-semibold text-foreground truncate text-sm group-hover:text-[#6d28d9] transition-colors">
{item.characterName || item.role}
</h4>
<p className="text-xs text-[#6d28d9] truncate">{item.title}</p>
</div>
</CardContent>
</Card>
</motion.div>
))}
</AnimatePresence>
</div>
</TabsContent>
<TabsContent value="filmography" className="mt-0">
{/* Sort Toolbar */}
<div className="flex items-center justify-between mb-4">
<p className="text-sm text-muted-foreground">
{person.filmography.length} {person.filmography.length === 1 ? 'title' : 'titles'}
</p>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-8 px-2.5 rounded-lg text-xs border-border/60"
>
<ArrowUpDown size={14} className="mr-1.5" />
{sortOrder === 'asc' ? <ArrowUpAZ size={14} className="mr-1.5" /> : <ArrowDownAZ size={14} className="mr-1.5" />}
{sortOptions.find(o => o.value === sortBy)?.label}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem disabled className="text-xs font-semibold text-muted-foreground">
Sort by
</DropdownMenuItem>
<DropdownMenuSeparator />
{sortOptions.map(option => (
<DropdownMenuItem
key={option.value}
onClick={() => {
if (sortBy === option.value) {
setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
} else {
setSortBy(option.value);
setSortOrder('asc');
}
}}
className="flex items-center justify-between text-xs"
>
<span className="flex items-center gap-2">
<option.icon size={14} />
{option.label}
</span>
{sortBy === option.value && (
sortOrder === 'asc' ? <ArrowUpAZ size={14} className="text-[#6d28d9]" /> : <ArrowDownAZ size={14} className="text-[#6d28d9]" />
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
))}
</div>
</section>
)}
{/* Filmography Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
<AnimatePresence mode="popLayout">
{sortedFilmography.map((item, index) => (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.03 }}
>
<Card
onClick={() => handleMediaClick(item.id.toString())}
className="group cursor-pointer hover:border-[#6d28d9]/30 hover:shadow-md transition-all duration-200 border-border/60"
>
<CardContent className="p-3 flex items-center gap-3">
<div className="w-12 h-16 rounded-none overflow-hidden shrink-0 bg-muted border border-border/40">
<img
src={item.poster || person.photo}
alt={item.title}
className="w-full h-full object-cover"
referrerPolicy="no-referrer"
/>
</div>
<div className="min-w-0 flex-1">
<h4 className="font-semibold text-foreground truncate text-sm group-hover:text-[#6d28d9] transition-colors">
{item.title}
</h4>
<p className="text-xs text-muted-foreground mb-1">
{item.year || 'Unknown'}
</p>
<div className="flex items-center gap-1.5">
<Badge variant="outline" className="text-[10px] px-1.5 py-0 h-4 border-border/50 font-normal">
{item.role}
</Badge>
{item.category && (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-4 bg-muted font-normal">
{item.category}
</Badge>
)}
</div>
</div>
</CardContent>
</Card>
</motion.div>
))}
</AnimatePresence>
</div>
</TabsContent>
</>
)}
</Tabs>
</div>
</div>
</div>