Files
beStream/src/components/tv/EpisodeCard.tsx
2025-12-14 12:57:37 +01:00

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>
);
}