Add a ThemeContext and provider, wrap the app with ThemeProvider, and sync user settings' theme into the context. Replace hardcoded color classes with design token classes (background, muted, foreground, border, card, etc.) across multiple UI components to centralize theming and enable consistent light/dark styling. Files updated include App.tsx (useTheme, setTheme, ThemeProvider, footer/background tokens), several views and components (AddMediaView, BrowseView, CastDetailView, CastView, MediaCard, MediaListItem, SettingsView, ImporterView) to use tokenized classes, and add new src/contexts/ThemeContext.tsx.
102 lines
3.4 KiB
TypeScript
102 lines
3.4 KiB
TypeScript
import { Media } from '@/types';
|
|
import { cn } from '@/lib/utils';
|
|
import { motion } from 'motion/react';
|
|
import { Star, Play, Bookmark } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
interface MediaListItemProps {
|
|
key?: string;
|
|
media: Media;
|
|
onClick: (media: Media) => void;
|
|
}
|
|
|
|
export default function MediaListItem({ media, onClick }: MediaListItemProps) {
|
|
const statusColors = {
|
|
watching: 'bg-blue-500',
|
|
completed: 'bg-green-500',
|
|
planned: 'bg-gray-500',
|
|
dropped: 'bg-red-500',
|
|
reading: 'bg-amber-500',
|
|
listening: 'bg-purple-500',
|
|
playing: 'bg-indigo-500',
|
|
'on-hold': 'bg-orange-500',
|
|
};
|
|
|
|
const getAspectRatio = () => {
|
|
if (media.aspectRatio) return media.aspectRatio;
|
|
switch (media.category) {
|
|
case 'Music': return '1/1';
|
|
case 'Games':
|
|
case 'Adult': return '16/9';
|
|
default: return '2/3';
|
|
}
|
|
};
|
|
|
|
const aspectRatioClass = {
|
|
'2/3': 'w-24 h-32',
|
|
'16/9': 'w-48 h-27', // 16:9 ratio for w-48 is approx h-27
|
|
'1/1': 'w-24 h-24',
|
|
}[getAspectRatio()];
|
|
|
|
return (
|
|
<motion.div
|
|
layout
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -10 }}
|
|
className="group flex items-center gap-6 p-4 rounded-xl hover:bg-muted/50 transition-colors cursor-pointer border border-transparent hover:border-border"
|
|
onClick={() => onClick(media)}
|
|
>
|
|
<div className={cn(
|
|
"relative rounded-lg overflow-hidden shrink-0 shadow-md bg-card transition-all duration-300",
|
|
aspectRatioClass
|
|
)}>
|
|
<img
|
|
src={media.poster}
|
|
alt={media.title}
|
|
className="w-full h-full object-cover"
|
|
referrerPolicy="no-referrer"
|
|
/>
|
|
{media.status && (
|
|
<div className={cn(
|
|
"absolute top-2 left-2 w-3 h-3 rounded-full border border-white/20 shadow-sm",
|
|
statusColors[media.status]
|
|
)} />
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-3 mb-1">
|
|
<h3 className="text-lg font-black text-foreground truncate group-hover:text-[#6d28d9] transition-colors">
|
|
{media.title}
|
|
</h3>
|
|
<span className="text-sm font-bold text-muted-foreground">({media.year})</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4 mb-3">
|
|
<div className="flex items-center gap-1 text-xs font-bold text-muted-foreground">
|
|
<Star size={14} className="text-yellow-500" fill="currentColor" />
|
|
{media.rating || 'N/A'}
|
|
</div>
|
|
<div className="text-xs font-bold text-muted-foreground uppercase tracking-wider">
|
|
{media.genres?.slice(0, 3).join(' • ') || 'Anime'}
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-sm text-muted-foreground line-clamp-2 max-w-2xl">
|
|
{media.description || "No description available for this title."}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="hidden md:flex items-center gap-2">
|
|
<Button size="icon" variant="ghost" className="rounded-full text-muted-foreground hover:text-[#6d28d9] hover:bg-[#6d28d9]/10">
|
|
<Play size={18} fill="currentColor" />
|
|
</Button>
|
|
<Button size="icon" variant="ghost" className="rounded-full text-muted-foreground hover:text-[#6d28d9] hover:bg-[#6d28d9]/10">
|
|
<Bookmark size={18} />
|
|
</Button>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
}
|