134 lines
4.2 KiB
TypeScript
134 lines
4.2 KiB
TypeScript
import { useState } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { Play, Download, Check, Clock, Calendar } from 'lucide-react';
|
|
import type { UnifiedEpisode } from '../../types/unified';
|
|
import Badge from '../ui/Badge';
|
|
|
|
interface EpisodeCardProps {
|
|
episode: UnifiedEpisode;
|
|
showSeriesTitle?: boolean;
|
|
onPlay?: (episode: UnifiedEpisode) => void;
|
|
onDownload?: (episode: UnifiedEpisode) => void;
|
|
className?: string;
|
|
}
|
|
|
|
export default function EpisodeCard({
|
|
episode,
|
|
showSeriesTitle = false,
|
|
onPlay,
|
|
onDownload,
|
|
className = '',
|
|
}: EpisodeCardProps) {
|
|
const [imageLoaded, setImageLoaded] = useState(false);
|
|
|
|
const airDate = episode.airDate ? new Date(episode.airDate) : null;
|
|
const isAired = airDate ? airDate <= new Date() : false;
|
|
const isFuture = airDate ? airDate > new Date() : false;
|
|
|
|
const thumbnailUrl = episode.poster || '/placeholder-episode.png';
|
|
|
|
return (
|
|
<motion.div
|
|
whileHover={{ scale: 1.02 }}
|
|
className={`group relative rounded-lg overflow-hidden bg-netflix-dark-gray ${className}`}
|
|
>
|
|
{/* Thumbnail */}
|
|
<div className="aspect-video relative">
|
|
{!imageLoaded && (
|
|
<div className="absolute inset-0 bg-netflix-medium-gray animate-pulse" />
|
|
)}
|
|
<img
|
|
src={thumbnailUrl}
|
|
alt={episode.title}
|
|
className={`w-full h-full object-cover transition-opacity duration-300 ${
|
|
imageLoaded ? 'opacity-100' : 'opacity-0'
|
|
}`}
|
|
loading="lazy"
|
|
onLoad={() => setImageLoaded(true)}
|
|
/>
|
|
|
|
{/* Play Overlay */}
|
|
{episode.hasFile && (
|
|
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
|
<motion.button
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.9 }}
|
|
onClick={() => onPlay?.(episode)}
|
|
className="w-12 h-12 rounded-full bg-white flex items-center justify-center"
|
|
>
|
|
<Play size={24} className="text-black ml-1" fill="black" />
|
|
</motion.button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Episode Number Badge */}
|
|
<div className="absolute top-2 left-2">
|
|
<Badge size="sm" className="bg-black/70">
|
|
E{episode.episodeNumber.toString().padStart(2, '0')}
|
|
</Badge>
|
|
</div>
|
|
|
|
{/* Status Badge */}
|
|
<div className="absolute top-2 right-2">
|
|
{episode.hasFile ? (
|
|
<Badge size="sm" variant="success">
|
|
<Check size={10} />
|
|
</Badge>
|
|
) : isFuture ? (
|
|
<Badge size="sm" variant="info">
|
|
<Clock size={10} />
|
|
</Badge>
|
|
) : isAired ? (
|
|
<Badge size="sm" variant="warning">Missing</Badge>
|
|
) : null}
|
|
</div>
|
|
|
|
{/* Duration */}
|
|
{episode.runtime && (
|
|
<div className="absolute bottom-2 right-2">
|
|
<Badge size="sm" className="bg-black/70">
|
|
{episode.runtime}m
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Info */}
|
|
<div className="p-3">
|
|
{showSeriesTitle && episode.seriesTitle && (
|
|
<p className="text-xs text-netflix-red mb-1 line-clamp-1">
|
|
{episode.seriesTitle}
|
|
</p>
|
|
)}
|
|
<h4 className="font-medium text-white line-clamp-1">
|
|
{episode.title}
|
|
</h4>
|
|
{episode.overview && (
|
|
<p className="text-sm text-gray-400 mt-1 line-clamp-2">
|
|
{episode.overview}
|
|
</p>
|
|
)}
|
|
<div className="flex items-center justify-between mt-2 text-xs text-gray-500">
|
|
{airDate && (
|
|
<span className="flex items-center gap-1">
|
|
<Calendar size={10} />
|
|
{airDate.toLocaleDateString()}
|
|
</span>
|
|
)}
|
|
{!episode.hasFile && isAired && onDownload && (
|
|
<motion.button
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.9 }}
|
|
onClick={() => onDownload(episode)}
|
|
className="p-1 hover:bg-white/10 rounded transition-colors"
|
|
>
|
|
<Download size={14} />
|
|
</motion.button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|