Files
mystuff_frontend/src/components/MediaTable.tsx
T
Lars Behrends d61472f069 ui2
2026-05-23 00:54:01 +02:00

265 lines
9.6 KiB
TypeScript

import React, { useState, useMemo } from 'react';
import { Media, MediaCategory } from '@/types';
import { cn } from '@/lib/utils';
import { motion } from 'motion/react';
import {
Star,
Heart,
Gamepad2,
Film,
Tv,
Eye,
Music,
BookOpen,
Monitor,
ArrowUpDown,
ArrowUp,
ArrowDown
} from 'lucide-react';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
interface MediaTableProps {
mediaList: Media[];
onMediaClick: (media: Media) => void;
onFavoriteToggle?: (media: Media) => void;
favoriteIds?: Set<string>;
}
type SortField = 'title' | 'category' | 'genre' | 'rating' | 'year' | 'plays';
type SortDirection = 'asc' | 'desc';
const categoryConfig: Record<MediaCategory, {
label: string;
color: string;
bgColor: string;
icon: React.ElementType | null;
}> = {
'Anime': { label: 'ANIME', color: 'text-purple-400', bgColor: 'bg-purple-500/20', icon: null },
'Movies': { label: 'MOVIE', color: 'text-blue-400', bgColor: 'bg-blue-500/20', icon: Film },
'TV Series': { label: 'SERIES', color: 'text-green-400', bgColor: 'bg-green-500/20', icon: Tv },
'Music': { label: 'MUSIC', color: 'text-pink-400', bgColor: 'bg-pink-500/20', icon: Music },
'Books': { label: 'BOOK', color: 'text-yellow-400', bgColor: 'bg-yellow-500/20', icon: BookOpen },
'Games': { label: 'GAME', color: 'text-indigo-400', bgColor: 'bg-indigo-500/20', icon: Gamepad2 },
'Consoles': { label: 'CONSOLE', color: 'text-orange-400', bgColor: 'bg-orange-500/20', icon: Monitor },
'Adult': { label: 'ADULT', color: 'text-rose-400', bgColor: 'bg-rose-500/20', icon: Eye },
};
export default function MediaTable({
mediaList,
onMediaClick,
onFavoriteToggle,
favoriteIds = new Set()
}: MediaTableProps) {
const [sortField, setSortField] = useState<SortField>('title');
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
const handleSort = (field: SortField) => {
if (sortField === field) {
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection('asc');
}
};
const sortedMedia = useMemo(() => {
const sorted = [...mediaList];
sorted.sort((a, b) => {
let comparison = 0;
switch (sortField) {
case 'title':
comparison = a.title.localeCompare(b.title);
break;
case 'category':
comparison = a.category.localeCompare(b.category);
break;
case 'genre':
const genreA = a.genres?.[0] || '';
const genreB = b.genres?.[0] || '';
comparison = genreA.localeCompare(genreB);
break;
case 'rating':
comparison = (b.rating || 0) - (a.rating || 0);
break;
case 'year':
comparison = b.year.localeCompare(a.year);
break;
case 'plays':
comparison = (b.playCount || 0) - (a.playCount || 0);
break;
}
return sortDirection === 'asc' ? comparison : -comparison;
});
return sorted;
}, [mediaList, sortField, sortDirection]);
const SortIcon = ({ field }: { field: SortField }) => {
if (sortField !== field) {
return <ArrowUpDown size={14} className="text-muted-foreground/40 ml-1 opacity-0 group-hover:opacity-100 transition-opacity" />;
}
return sortDirection === 'asc'
? <ArrowUp size={14} className="text-[#e8466c] ml-1" />
: <ArrowDown size={14} className="text-[#e8466c] ml-1" />;
};
const handleFavoriteClick = (e: React.MouseEvent, media: Media) => {
e.stopPropagation();
onFavoriteToggle?.(media);
};
return (
<Table className="w-full">
<TableHeader>
<TableRow className="border-b border-border/20 hover:bg-transparent">
<TableHead
className="text-xs font-semibold text-muted-foreground uppercase tracking-wider cursor-pointer hover:text-foreground/80 transition-colors group w-[45%]"
onClick={() => handleSort('title')}
>
<div className="flex items-center">
Title <SortIcon field="title" />
</div>
</TableHead>
<TableHead
className="text-xs font-semibold text-muted-foreground uppercase tracking-wider cursor-pointer hover:text-foreground/80 transition-colors group w-[80px]"
onClick={() => handleSort('category')}
>
<div className="flex items-center">
Type <SortIcon field="category" />
</div>
</TableHead>
<TableHead
className="text-xs font-semibold text-muted-foreground uppercase tracking-wider cursor-pointer hover:text-foreground/80 transition-colors group w-[18%]"
onClick={() => handleSort('genre')}
>
<div className="flex items-center">
Genre <SortIcon field="genre" />
</div>
</TableHead>
<TableHead
className="text-xs font-semibold text-muted-foreground uppercase tracking-wider cursor-pointer hover:text-foreground/80 transition-colors group w-[70px] text-center"
onClick={() => handleSort('rating')}
>
<div className="flex items-center justify-center">
Rating <SortIcon field="rating" />
</div>
</TableHead>
<TableHead
className="text-xs font-semibold text-muted-foreground uppercase tracking-wider cursor-pointer hover:text-foreground/80 transition-colors group w-[60px] text-center"
onClick={() => handleSort('year')}
>
<div className="flex items-center justify-center">
Year <SortIcon field="year" />
</div>
</TableHead>
<TableHead
className="text-xs font-semibold text-muted-foreground uppercase tracking-wider cursor-pointer hover:text-foreground/80 transition-colors group w-[60px] text-right"
onClick={() => handleSort('plays')}
>
<div className="flex items-center justify-end">
Plays <SortIcon field="plays" />
</div>
</TableHead>
<TableHead className="w-[40px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedMedia.map((media) => {
const categoryInfo = categoryConfig[media.category];
const CategoryIcon = categoryInfo?.icon;
const isFavorite = favoriteIds.has(media.id);
return (
<TableRow
key={media.id}
className="border-b border-border/20 hover:bg-muted/30 transition-colors cursor-pointer group"
onClick={() => onMediaClick(media)}
>
{/* Title Cell with Poster */}
<TableCell className="py-2">
<div className="flex items-center gap-3">
<div className="relative w-10 h-14 rounded overflow-hidden shrink-0 bg-muted">
<img
src={media.poster}
alt={media.title}
className="w-full h-full object-cover"
referrerPolicy="no-referrer"
/>
</div>
<div className="min-w-0">
<div className="text-sm font-medium text-foreground truncate group-hover:text-[#e8466c] transition-colors">
{media.title}
</div>
</div>
</div>
</TableCell>
{/* Type Badge */}
<TableCell>
<span className={cn(
"inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-bold uppercase tracking-wide",
categoryInfo.bgColor,
categoryInfo.color
)}>
{CategoryIcon && <CategoryIcon size={9} />}
{categoryInfo.label}
</span>
</TableCell>
{/* Genre */}
<TableCell>
<span className="text-sm text-muted-foreground truncate block">
{media.genres?.join(', ') || '-'}
</span>
</TableCell>
{/* Rating */}
<TableCell className="text-center">
<div className="flex items-center justify-center gap-1">
<Star size={12} className="text-[#e8466c] fill-[#e8466c]" />
<span className="text-sm font-medium text-foreground/80">
{media.rating?.toFixed(1) || '-'}
</span>
</div>
</TableCell>
{/* Year */}
<TableCell className="text-center">
<span className="text-sm text-muted-foreground/80">{media.year}</span>
</TableCell>
{/* Plays */}
<TableCell className="text-right">
<span className="text-sm text-muted-foreground/80">{media.playCount || 0}</span>
</TableCell>
{/* Favorite */}
<TableCell>
<button
onClick={(e) => handleFavoriteClick(e, media)}
className={cn(
"p-1 rounded transition-colors",
isFavorite
? "text-[#e8466c]"
: "text-muted-foreground/40 hover:text-muted-foreground/60"
)}
>
<Heart size={14} className={cn(isFavorite && "fill-current")} />
</button>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
}