Spaces:
Sleeping
Sleeping
| 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; | |