import React, { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { Play, Info, Plus, Check, Clock, Loader2 } from 'lucide-react'; import { getMovieCard, getTvShowCard } from '../lib/api'; import { isInMyList, addToMyList, removeFromMyList } from '../lib/storage'; import { useToast } from '@/hooks/use-toast'; // -- Common Trailer type -- export interface Trailer { id: number; name: string; url: string; language: string; runtime: number; } // -- TV Show types -- export interface TvShowPortrait { id: number; image: string; thumbnail: string; language: string; type: number; score: number; width: number; height: number; includesText: boolean; thumbnailWidth: number; thumbnailHeight: number; updatedAt: number; status: { id: number; name: string | null; }; tagOptions: any; } export interface TvShowBanner { id: number; image: string; thumbnail: string; language: string; type: number; score: number; width: number; height: number; includesText: boolean; thumbnailWidth: number; thumbnailHeight: number; updatedAt: number; status: { id: number; name: string | null; }; tagOptions: any; } export interface TvShowCardData { title: string; year: string; image: string; portrait: TvShowPortrait[]; banner: TvShowBanner[]; overview: string; trailers: Trailer[]; genres?: { name: string }[]; } // -- Movie types -- export interface MoviePortrait { id: number; image: string; thumbnail: string; language: string; type: number; score: number; width: number; height: number; includesText: boolean; } export interface MovieBanner { id: number; image: string; thumbnail: string; language: string | null; type: number; score: number; width: number; height: number; includesText: boolean; } export interface MovieCardData { title: string; year: string; image: string; portrait: MoviePortrait[]; banner: MovieBanner[]; overview: string; trailers: Trailer[]; genres?: { name: string }[]; } interface ContentCardProps { type: 'movie' | 'tvshow'; title: string; image?: string; description?: string; genre?: string[]; year?: number | string; prefetchData?: boolean; } interface PlaybackProgress { currentTime: number; duration: number; lastPlayed: string; completed: boolean; } const ContentCard: React.FC = ({ type, title, image, description: initialDescription, genre: initialGenre, year: initialYear, prefetchData = true }) => { const [isHovered, setIsHovered] = useState(false); const [progress, setProgress] = useState<{ percent: number, completed: boolean } | null>(null); const [loading, setLoading] = useState(prefetchData); const [cardData, setCardData] = useState(null); const [inMyList, setInMyList] = useState(false); const [addingToList, setAddingToList] = useState(false); const [selectedImage, setSelectedImage] = useState(null); const { toast } = useToast(); const fallbackImage = '/placeholder.svg'; const path = type === 'movie' ? `/movie/${encodeURIComponent(title)}` : `/tv-show/${encodeURIComponent(title)}`; // Derived data with fallbacks const description = cardData?.overview || initialDescription || ''; const genre = (cardData?.genres?.map((g: any) => g.name) || initialGenre || []); const year = cardData?.year || initialYear || ''; // Function to randomly select an image from available banners or portraits const selectRandomImage = (cardData: MovieCardData | TvShowCardData | null) => { if (!cardData) return null; // First try to get banner images (landscape) if (cardData.banner && cardData.banner.length > 0) { const randomIndex = Math.floor(Math.random() * cardData.banner.length); return cardData.banner[randomIndex].image; } // Fall back to portrait images if no banners if (cardData.portrait && cardData.portrait.length > 0) { const randomIndex = Math.floor(Math.random() * cardData.portrait.length); return cardData.portrait[randomIndex].image; } // Finally fall back to the default image return cardData.image || image || fallbackImage; }; // Check if item is in user's list useEffect(() => { const checkMyList = async () => { const isInList = await isInMyList(title, type); setInMyList(isInList); }; checkMyList(); }, [title, type]); // Toggle my list status const toggleMyList = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setAddingToList(true); try { if (inMyList) { await removeFromMyList(title, type); setInMyList(false); toast({ title: "Removed from My List", description: `${title} has been removed from your list` }); } else { await addToMyList({ type, title, addedAt: new Date().toISOString() }); setInMyList(true); toast({ title: "Added to My List", description: `${title} has been added to your list` }); } } catch (error) { console.error('Error updating My List:', error); toast({ title: "Error", description: "Failed to update your list", variant: "destructive" }); } finally { setAddingToList(false); } }; // Load content data useEffect(() => { if (!prefetchData) { setLoading(false); return; } const fetchData = async () => { try { setLoading(true); let data; if (type === 'movie') { data = await getMovieCard(title); } else { data = await getTvShowCard(title); // TV show data is nested in a data property data = data?.data || data; } if (data) { setCardData(data); const randomImage = selectRandomImage(data); setSelectedImage(randomImage); } } catch (error) { console.error(`Error fetching ${type} data:`, error); } finally { setLoading(false); } }; fetchData(); }, [type, title, prefetchData, image]); // Load playback progress on mount useEffect(() => { try { const progressKey = type === 'movie' ? `movie-progress-${title}` : `playback-${title}`; const storedProgress = localStorage.getItem(progressKey); if (storedProgress) { let maxProgress = 0; let isCompleted = false; if (type === 'movie') { const progressData = JSON.parse(storedProgress); maxProgress = Math.min(100, Math.floor((progressData.currentTime / progressData.duration) * 100)); isCompleted = progressData.completed; } // For TV shows, find the latest episode with progress else { const progressData = JSON.parse(storedProgress); let latestPlaybackTime = 0; Object.values(progressData).forEach((item: PlaybackProgress) => { if (new Date(item.lastPlayed).getTime() > latestPlaybackTime) { latestPlaybackTime = new Date(item.lastPlayed).getTime(); maxProgress = Math.min(100, Math.floor((item.currentTime / item.duration) * 100)); isCompleted = item.completed; } }); } if (maxProgress > 0 || isCompleted) { setProgress({ percent: maxProgress, completed: isCompleted }); } } } catch (error) { console.error("Failed to load playback progress:", error); } }, [title, type]); const displayImage = selectedImage || image || fallbackImage; return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} >
{/* Base card image */} {loading ? (
) : ( {title} { const target = e.target as HTMLImageElement; target.src = fallbackImage; }} /> )} {/* Progress indicator */} {progress && progress.percent > 0 && !progress.completed && (
)} {/* Title overlay (simple version when not hovered) */}

{title}

{progress?.completed && (
)}
{year && {year}} {genre && genre.length > 0 && • {genre[0]}}
{progress && !progress.completed && progress.percent > 0 && (
{progress.percent}%
)}
{/* Expanded hover overlay with detailed info and buttons */}
{/* Top section - title and info */}

{title}

{progress?.completed && (
)}
{year && {year}} {genre && genre.length > 0 && • {genre[0]}}
{description && (

{description}

)} {progress && !progress.completed && progress.percent > 0 && (

{progress.percent}% watched

)}
{/* Bottom section - action buttons */}
{progress && progress.percent > 0 && !progress.completed ? "Resume" : "Play"}
); }; export default ContentCard;