Spaces:
Running
Running
import React, { useState, useEffect } from 'react'; | |
import { getRecentItems } from '../lib/api'; | |
import { useNavigate } from 'react-router-dom'; | |
import { motion, AnimatePresence } from 'framer-motion'; | |
import { Play, Info, ChevronLeft, ChevronRight } from 'lucide-react'; | |
import { Link } from 'react-router-dom'; | |
interface SlideItem { | |
id: string; | |
type: 'movie' | 'tvshow'; | |
title: string; | |
description: string; | |
backdrop: string; | |
genre?: string[]; | |
year?: string | number; | |
} | |
interface DynamicHeroSlideshowProps { | |
slides: SlideItem[]; | |
autoplaySpeed?: number; | |
} | |
const DynamicHeroSlideshow: React.FC<DynamicHeroSlideshowProps> = ({ | |
slides, | |
autoplaySpeed = 6000 | |
}) => { | |
const [currentIndex, setCurrentIndex] = useState(0); | |
const [isAutoplay, setIsAutoplay] = useState(true); | |
const navigate = useNavigate(); | |
useEffect(() => { | |
if (!slides.length) return; | |
let interval: NodeJS.Timeout | null = null; | |
if (isAutoplay) { | |
interval = setInterval(() => { | |
setCurrentIndex((prevIndex) => (prevIndex + 1) % slides.length); | |
}, autoplaySpeed); | |
} | |
return () => { | |
if (interval) clearInterval(interval); | |
}; | |
}, [slides, isAutoplay, autoplaySpeed]); | |
const handleNext = () => { | |
setIsAutoplay(false); | |
setCurrentIndex((prevIndex) => (prevIndex + 1) % slides.length); | |
}; | |
const handlePrev = () => { | |
setIsAutoplay(false); | |
setCurrentIndex((prevIndex) => (prevIndex - 1 + slides.length) % slides.length); | |
}; | |
const handleDotClick = (index: number) => { | |
setIsAutoplay(false); | |
setCurrentIndex(index); | |
}; | |
if (!slides.length) return null; | |
const currentSlide = slides[currentIndex]; | |
const path = currentSlide.type === 'movie' | |
? `/movie/${encodeURIComponent(currentSlide.title)}` | |
: `/tv-show/${encodeURIComponent(currentSlide.title)}`; | |
return ( | |
<div className="relative w-full min-h-[450px] sm:min-h-[550px] md:min-h-[650px]"> | |
{/* Backdrop Slideshow */} | |
<AnimatePresence mode="wait"> | |
<motion.div | |
key={currentSlide.id} | |
className="absolute inset-0 w-full h-full" | |
initial={{ opacity: 0 }} | |
animate={{ opacity: 1 }} | |
exit={{ opacity: 0 }} | |
transition={{ duration: 0.8 }} | |
> | |
<img | |
src={currentSlide.backdrop} | |
alt={currentSlide.title} | |
className="w-full h-full object-cover object-top" | |
onError={(e) => { | |
const target = e.target as HTMLImageElement; | |
target.src = '/placeholder.svg'; | |
}} | |
/> | |
<div className="absolute inset-0 bg-gradient-to-t from-black via-black/60 to-transparent" /> | |
<div className="absolute inset-0 bg-gradient-to-r from-black/80 via-black/40 to-transparent" /> | |
</motion.div> | |
</AnimatePresence> | |
{/* Navigation arrows */} | |
<button | |
onClick={handlePrev} | |
className="absolute left-4 top-1/2 -translate-y-1/2 z-20 p-2 rounded-full bg-black/30 text-white backdrop-blur-sm hover:bg-indigo-800/40 transition-colors" | |
aria-label="Previous slide" | |
> | |
<ChevronLeft className="w-6 h-6" /> | |
</button> | |
<button | |
onClick={handleNext} | |
className="absolute right-4 top-1/2 -translate-y-1/2 z-20 p-2 rounded-full bg-black/30 text-white backdrop-blur-sm hover:bg-indigo-800/40 transition-colors" | |
aria-label="Next slide" | |
> | |
<ChevronRight className="w-6 h-6" /> | |
</button> | |
{/* Content */} | |
<AnimatePresence mode="wait"> | |
<motion.div | |
key={`content-${currentSlide.id}`} | |
initial={{ opacity: 0, y: 20 }} | |
animate={{ opacity: 1, y: 0 }} | |
exit={{ opacity: 0, y: -20 }} | |
transition={{ duration: 0.5, delay: 0.3 }} | |
className="absolute z-10 flex flex-col justify-end h-full bottom-0 pb-16 pt-24 px-4 sm:px-8 md:px-16 max-w-4xl" | |
> | |
<div className="animate-slide-up"> | |
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold mb-3 text-white">{currentSlide.title}</h1> | |
<div className="flex flex-wrap items-center text-sm text-gray-300 mb-4"> | |
{currentSlide.year && <span className="mr-3">{currentSlide.year}</span>} | |
{currentSlide.genre && currentSlide.genre.length > 0 && ( | |
<span className="mr-3">{currentSlide.genre.slice(0, 3).join(' • ')}</span> | |
)} | |
<span className="capitalize bg-indigo-500/40 px-2 py-0.5 rounded">{currentSlide.type}</span> | |
</div> | |
<p className="text-sm sm:text-base md:text-lg mb-6 line-clamp-3 sm:line-clamp-4 max-w-2xl text-gray-100"> | |
{currentSlide.description} | |
</p> | |
<div className="flex space-x-3"> | |
<Link | |
to={`${path}/watch`} | |
className="flex items-center px-6 py-2 rounded bg-indigo-600 text-white font-semibold hover:bg-indigo-700 transition" | |
> | |
<Play className="w-5 h-5 mr-2" /> Play | |
</Link> | |
<Link | |
to={path} | |
className="flex items-center px-6 py-2 rounded bg-gray-800/60 text-white font-semibold hover:bg-gray-700/80 transition" | |
> | |
<Info className="w-5 h-5 mr-2" /> More Info | |
</Link> | |
</div> | |
</div> | |
</motion.div> | |
</AnimatePresence> | |
{/* Dots navigation */} | |
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 flex space-x-2"> | |
{slides.map((_, index) => ( | |
<button | |
key={index} | |
onClick={() => handleDotClick(index)} | |
className={`w-2.5 h-2.5 rounded-full transition-all ${ | |
index === currentIndex | |
? 'bg-indigo-500 w-6' | |
: 'bg-gray-500/50 hover:bg-gray-400/70' | |
}`} | |
aria-label={`Go to slide ${index + 1}`} | |
/> | |
))} | |
</div> | |
</div> | |
); | |
}; | |
interface HeroSectionProps { | |
// These props are still available if you need to override the API data, | |
// but the API data will be used as the primary slides. | |
type?: 'movie' | 'tvshow'; | |
title?: string; | |
description?: string; | |
backdrop?: string; | |
genre?: string[]; | |
year?: string | number; | |
} | |
const HeroSection: React.FC<HeroSectionProps> = (props) => { | |
const [slides, setSlides] = useState<any[]>([]); | |
const [isLoaded, setIsLoaded] = useState(false); | |
useEffect(() => { | |
const fetchSlides = async () => { | |
try { | |
// Fetch recent items from the API (change the limit as needed) | |
const recentItems = await getRecentItems(5); | |
// Map recent items to the slide format expected by DynamicHeroSlideshow | |
const formattedSlides = recentItems.map((item: any, index: number) => ({ | |
id: item.id || index.toString(), | |
type: item.type, | |
title: item.title, | |
description: item.description, | |
backdrop: item.image, // assuming the API returns "image" to be used as backdrop | |
genre: item.genre || [], | |
year: item.year, | |
})); | |
setSlides(formattedSlides); | |
} catch (error) { | |
console.error('Error fetching recent items:', error); | |
} finally { | |
setIsLoaded(true); | |
} | |
}; | |
fetchSlides(); | |
}, []); | |
if (!isLoaded) { | |
return ( | |
<div className="relative w-full min-h-[450px] sm:min-h-[550px] md:min-h-[650px] animate-pulse bg-cinema-medium/30"></div> | |
); | |
} | |
return <DynamicHeroSlideshow slides={slides} autoplaySpeed={8000} />; | |
}; | |
export default HeroSection; | |