Add filmography sorting and role-count UI
Introduce filmography sorting controls and role-count indicators across cast list and detail views. In CastDetailView: add sort state (sortBy/sortOrder), compute sortedFilmography, add UI for choosing sort key and toggling order, and show a filmography count badge; import new icons and useState. In CastView: add a new 'roleCount' sort option (default sort is now roleCount desc), persist/restore it from localStorage, adjust reset/default filter logic and hasActiveFilters check, render role-count badges for each person, and tweak layout (flex-1) for better truncation. These changes make it easier to surface prolific cast members and sort filmography entries by year, title, or role.
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
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 } from 'lucide-react';
|
||||
import { ArrowLeft, Calendar, MapPin, Briefcase, Film, User, Ruler, Palette, Eye, ChevronDown, ListFilter } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface CastDetailViewProps {
|
||||
person: Staff;
|
||||
@@ -12,10 +13,24 @@ interface CastDetailViewProps {
|
||||
|
||||
export default function CastDetailView({ person, relatedMedia }: CastDetailViewProps) {
|
||||
const navigate = useNavigate();
|
||||
const [sortBy, setSortBy] = useState<'year' | 'title' | 'role'>('role');
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||
|
||||
const handleMediaClick = (mediaId: string) => {
|
||||
navigate(`/media/${mediaId}`);
|
||||
};
|
||||
|
||||
const sortedFilmography = [...(person.filmography || [])].sort((a, b) => {
|
||||
let comparison = 0;
|
||||
if (sortBy === 'year') {
|
||||
comparison = (a.year || 0) - (b.year || 0);
|
||||
} else if (sortBy === 'title') {
|
||||
comparison = (a.title || '').localeCompare(b.title || '');
|
||||
} else if (sortBy === 'role') {
|
||||
comparison = (a.role || '').localeCompare(b.role || '');
|
||||
}
|
||||
return sortOrder === 'asc' ? comparison : -comparison;
|
||||
});
|
||||
return (
|
||||
<div className="min-h-screen bg-background pb-20">
|
||||
{/* Hero Section */}
|
||||
@@ -33,7 +48,7 @@ export default function CastDetailView({ person, relatedMedia }: CastDetailViewP
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="w-48 h-48 md:w-64 md:h-64 rounded-2xl overflow-hidden border-4 border-background shadow-2xl shrink-0"
|
||||
className="h-48 md:h-64 rounded-2xl overflow-hidden border-4 border-background shadow-2xl shrink-0"
|
||||
>
|
||||
<img
|
||||
src={person.photo}
|
||||
@@ -58,6 +73,11 @@ export default function CastDetailView({ person, relatedMedia }: CastDetailViewP
|
||||
{occ}
|
||||
</Badge>
|
||||
))}
|
||||
{person.filmography && person.filmography.length > 0 && (
|
||||
<Badge variant="outline" className="border-[#6d28d9]/30 text-[#6d28d9] font-bold px-4 py-1">
|
||||
{person.filmography.length} Role{person.filmography.length !== 1 ? 's' : ''}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
@@ -279,12 +299,33 @@ export default function CastDetailView({ person, relatedMedia }: CastDetailViewP
|
||||
|
||||
{person.filmography && person.filmography.length > 0 && (
|
||||
<section>
|
||||
<h2 className="text-2xl font-black text-foreground mb-6 flex items-center gap-3">
|
||||
<Film className="text-[#6d28d9]" />
|
||||
Filmography
|
||||
</h2>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl 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-full border-border"
|
||||
>
|
||||
<ListFilter size={16} />
|
||||
</Button>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as 'year' | 'title' | 'role')}
|
||||
className="bg-muted border border-border rounded-full px-3 py-1.5 text-sm font-bold text-foreground focus:outline-none focus:ring-2 focus:ring-[#6d28d9]"
|
||||
>
|
||||
<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">
|
||||
{person.filmography.map(item => (
|
||||
{sortedFilmography.map(item => (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => handleMediaClick(item.id.toString())}
|
||||
|
||||
Reference in New Issue
Block a user