Spaces:
Running
Running
File size: 7,784 Bytes
cc2caf9 beeb302 cc2caf9 beeb302 cc2caf9 beeb302 cc2caf9 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
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 {
// 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;
|