|
|
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-netflix-black via-netflix-black/60 to-transparent" /> |
|
|
<div className="absolute inset-0 bg-gradient-to-r from-netflix-black/80 via-netflix-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-theme-primary text-white font-semibold hover:bg-theme-primary-hover 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-theme-primary-light w-5' |
|
|
: 'bg-gray-500/50 hover:bg-gray-400/70' |
|
|
}`} |
|
|
aria-label={`Go to slide ${index + 1}`} |
|
|
/> |
|
|
))} |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
interface HeroSectionProps { |
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
|
const recentItems = await getRecentItems(5); |
|
|
|
|
|
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, |
|
|
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; |
|
|
|