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:
Lars Behrends
2026-04-11 00:47:04 +02:00
parent b36b72b8e0
commit 52d272c701
2 changed files with 65 additions and 14 deletions
+48 -7
View File
@@ -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())}