Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, useRef } from 'react'; | |
| import ImageFlex from './ImageFlex.jsx'; | |
| export default function ImageSlider({ images, projectTitle }) { | |
| const [currentIndex, setCurrentIndex] = useState(0); | |
| const [isFullscreen, setIsFullscreen] = useState(false); | |
| const intervalRef = useRef(null); | |
| // Auto-advance every 4 seconds | |
| useEffect(() => { | |
| if (images.length <= 1) return; | |
| if (intervalRef.current) clearInterval(intervalRef.current); | |
| intervalRef.current = setInterval(() => { | |
| setCurrentIndex((prev) => (prev + 1) % images.length); | |
| }, 4000); | |
| return () => { | |
| if (intervalRef.current) clearInterval(intervalRef.current); | |
| }; | |
| }, [images.length]); | |
| // Pause on hover/focus | |
| const pauseAutoPlay = () => { | |
| if (intervalRef.current) clearInterval(intervalRef.current); | |
| }; | |
| const resumeAutoPlay = () => { | |
| if (images.length <= 1) return; | |
| if (intervalRef.current) clearInterval(intervalRef.current); | |
| intervalRef.current = setInterval(() => { | |
| setCurrentIndex((prev) => (prev + 1) % images.length); | |
| }, 4000); | |
| }; | |
| const goToNext = () => { | |
| pauseAutoPlay(); | |
| setCurrentIndex((prev) => (prev + 1) % images.length); | |
| resumeAutoPlay(); | |
| }; | |
| const goToPrev = () => { | |
| pauseAutoPlay(); | |
| setCurrentIndex((prev) => (prev - 1 + images.length) % images.length); | |
| resumeAutoPlay(); | |
| }; | |
| const goToSlide = (index) => { | |
| pauseAutoPlay(); | |
| setCurrentIndex(index); | |
| resumeAutoPlay(); | |
| }; | |
| const openFullscreen = () => { | |
| setIsFullscreen(true); | |
| pauseAutoPlay(); | |
| }; | |
| const closeFullscreen = () => { | |
| setIsFullscreen(false); | |
| resumeAutoPlay(); | |
| }; | |
| // Keyboard navigation in fullscreen (auto-play stays paused in fullscreen) | |
| useEffect(() => { | |
| if (!isFullscreen) return; | |
| const handleKeyDown = (e) => { | |
| if (e.key === 'Escape') { | |
| setIsFullscreen(false); | |
| resumeAutoPlay(); | |
| } else if (e.key === 'ArrowLeft') { | |
| setCurrentIndex((prev) => (prev - 1 + images.length) % images.length); | |
| } else if (e.key === 'ArrowRight') { | |
| setCurrentIndex((prev) => (prev + 1) % images.length); | |
| } | |
| }; | |
| window.addEventListener('keydown', handleKeyDown); | |
| return () => window.removeEventListener('keydown', handleKeyDown); | |
| }, [isFullscreen, images.length]); | |
| if (!images || images.length === 0) return null; | |
| const currentImage = images[currentIndex]; | |
| return ( | |
| <> | |
| <div | |
| className="relative group overflow-hidden rounded-xl bg-slate-100" | |
| onMouseEnter={pauseAutoPlay} | |
| onMouseLeave={resumeAutoPlay} | |
| > | |
| <div className="aspect-video relative"> | |
| <ImageFlex | |
| base={currentImage} | |
| alt={`${projectTitle} - Image ${currentIndex + 1}`} | |
| className="w-full h-full object-cover" | |
| /> | |
| {/* Click-to-fullscreen overlay */} | |
| <button | |
| type="button" | |
| onClick={openFullscreen} | |
| aria-label="View fullscreen" | |
| className="absolute inset-0 cursor-zoom-in focus:outline-none" | |
| /> | |
| {/* Navigation arrows */} | |
| {images.length > 1 && ( | |
| <> | |
| <button | |
| type="button" | |
| onClick={goToPrev} | |
| className="absolute left-4 top-1/2 -translate-y-1/2 bg-white/80 hover:bg-white rounded-full p-2 | |
| transition opacity-0 group-hover:opacity-100 focus:opacity-100" | |
| aria-label="Previous image" | |
| > | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> | |
| <path d="M15 18l-6-6 6-6" /> | |
| </svg> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={goToNext} | |
| className="absolute right-4 top-1/2 -translate-y-1/2 bg-white/80 hover:bg-white rounded-full p-2 | |
| transition opacity-0 group-hover:opacity-100 focus:opacity-100" | |
| aria-label="Next image" | |
| > | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> | |
| <path d="M9 18l6-6-6-6" /> | |
| </svg> | |
| </button> | |
| </> | |
| )} | |
| {/* Fullscreen button */} | |
| <button | |
| type="button" | |
| onClick={openFullscreen} | |
| className="absolute bottom-4 right-4 bg-white/80 hover:bg-white rounded-full p-2 | |
| transition opacity-0 group-hover:opacity-100 focus:opacity-100" | |
| aria-label="View fullscreen" | |
| > | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> | |
| <path d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3" /> | |
| </svg> | |
| </button> | |
| {/* Slide indicators */} | |
| {images.length > 1 && ( | |
| <div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2"> | |
| {images.map((_, i) => ( | |
| <button | |
| key={i} | |
| type="button" | |
| onClick={() => goToSlide(i)} | |
| className={`h-2 rounded-full transition ${ | |
| i === currentIndex ? 'w-8 bg-white' : 'w-2 bg-white/50 hover:bg-white/75' | |
| }`} | |
| aria-label={`Go to slide ${i + 1}`} | |
| /> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* Fullscreen modal */} | |
| {isFullscreen && ( | |
| <div | |
| className="fixed inset-0 z-[100] bg-black/95 flex items-center justify-center p-4" | |
| onClick={closeFullscreen} | |
| > | |
| <button | |
| type="button" | |
| onClick={closeFullscreen} | |
| className="absolute top-4 right-4 text-white hover:text-gray-300 p-2" | |
| aria-label="Close fullscreen" | |
| > | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> | |
| <path d="M18 6L6 18M6 6l12 12" /> | |
| </svg> | |
| </button> | |
| <div className="relative max-w-7xl w-full h-full flex items-center" onClick={(e) => e.stopPropagation()}> | |
| <button | |
| type="button" | |
| onClick={goToPrev} | |
| className="absolute left-4 bg-white/20 hover:bg-white/30 text-white rounded-full p-3 z-10" | |
| aria-label="Previous image" | |
| > | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> | |
| <path d="M15 18l-6-6 6-6" /> | |
| </svg> | |
| </button> | |
| <div className="w-full h-full flex items-center justify-center"> | |
| <ImageFlex | |
| base={currentImage} | |
| alt={`${projectTitle} - Image ${currentIndex + 1}`} | |
| className="max-w-full max-h-full object-contain" | |
| /> | |
| </div> | |
| <button | |
| type="button" | |
| onClick={goToNext} | |
| className="absolute right-4 bg-white/20 hover:bg-white/30 text-white rounded-full p-3 z-10" | |
| aria-label="Next image" | |
| > | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> | |
| <path d="M9 18l6-6-6-6" /> | |
| </svg> | |
| </button> | |
| {/* Fullscreen indicators */} | |
| {images.length > 1 && ( | |
| <div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex gap-2"> | |
| {images.map((_, i) => ( | |
| <button | |
| key={i} | |
| type="button" | |
| onClick={() => goToSlide(i)} | |
| className={`h-2 rounded-full transition ${ | |
| i === currentIndex ? 'w-8 bg-white' : 'w-2 bg-white/50 hover:bg-white/75' | |
| }`} | |
| aria-label={`Go to slide ${i + 1}`} | |
| /> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </> | |
| ); | |
| } | |