web / frontend /src /components /HeroSection.tsx
Chandima Prabhath
Track bun.lockb with Git LFS
cc2caf9
raw
history blame
7.73 kB
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;