1+ import React , { useEffect , useState , useCallback , useRef } from 'react' ;
2+ import PropTypes from 'prop-types' ;
3+
4+ const Lightbox = ( { photos, currentIndex, onClose } ) => {
5+ const [ activeIndex , setActiveIndex ] = useState ( currentIndex ) ;
6+ const [ transitionClass , setTransitionClass ] = useState ( '' ) ;
7+ const [ preloadedImages , setPreloadedImages ] = useState ( { } ) ;
8+ const lightboxRef = useRef ( null ) ;
9+
10+ // Handle remote image URLs
11+ const getOptimizedImageUrl = ( src ) => {
12+ if ( src . startsWith ( 'http' ) && src . includes ( 'unsplash.com' ) ) {
13+ return `${ src } ?w=1200&q=90&auto=format` ; // High quality version for fullscreen viewing
14+ }
15+ return src ;
16+ } ;
17+
18+ // Preload adjacent images
19+ const preloadAdjacentImages = useCallback ( ( ) => {
20+ const indicesToLoad = [
21+ activeIndex - 1 ,
22+ activeIndex + 1
23+ ] . filter ( idx => idx >= 0 && idx < photos . length ) ;
24+
25+ indicesToLoad . forEach ( idx => {
26+ if ( ! preloadedImages [ idx ] ) {
27+ const img = new Image ( ) ;
28+ img . src = getOptimizedImageUrl ( photos [ idx ] . src ) ;
29+ setPreloadedImages ( prev => ( {
30+ ...prev ,
31+ [ idx ] : true
32+ } ) ) ;
33+ }
34+ } ) ;
35+ } , [ activeIndex , photos , preloadedImages ] ) ;
36+
37+ // Handle keyboard navigation
38+ useEffect ( ( ) => {
39+ const handleKeyDown = ( e ) => {
40+ switch ( e . key ) {
41+ case 'ArrowLeft' :
42+ navigateImage ( 'prev' ) ;
43+ break ;
44+ case 'ArrowRight' :
45+ navigateImage ( 'next' ) ;
46+ break ;
47+ case 'Escape' :
48+ onClose ( ) ;
49+ break ;
50+ default :
51+ break ;
52+ }
53+ } ;
54+
55+ window . addEventListener ( 'keydown' , handleKeyDown ) ;
56+ return ( ) => window . removeEventListener ( 'keydown' , handleKeyDown ) ;
57+ } , [ activeIndex , photos . length , onClose ] ) ;
58+
59+ // Preload adjacent images
60+ useEffect ( ( ) => {
61+ preloadAdjacentImages ( ) ;
62+ } , [ activeIndex , preloadAdjacentImages ] ) ;
63+
64+ // Handle image navigation
65+ const navigateImage = ( direction ) => {
66+ if ( direction === 'prev' && activeIndex > 0 ) {
67+ setTransitionClass ( 'transition-prev' ) ;
68+ setTimeout ( ( ) => setActiveIndex ( activeIndex - 1 ) , 50 ) ;
69+ } else if ( direction === 'next' && activeIndex < photos . length - 1 ) {
70+ setTransitionClass ( 'transition-next' ) ;
71+ setTimeout ( ( ) => setActiveIndex ( activeIndex + 1 ) , 50 ) ;
72+ }
73+ } ;
74+
75+ // Handle touch events
76+ const [ touchStart , setTouchStart ] = useState ( null ) ;
77+ const handleTouchStart = ( e ) => {
78+ setTouchStart ( e . touches [ 0 ] . clientX ) ;
79+ } ;
80+
81+ const handleTouchEnd = ( e ) => {
82+ if ( ! touchStart ) return ;
83+
84+ const touchEnd = e . changedTouches [ 0 ] . clientX ;
85+ const diff = touchStart - touchEnd ;
86+ const threshold = 50 ;
87+
88+ if ( diff > threshold ) {
89+ navigateImage ( 'next' ) ;
90+ } else if ( diff < - threshold ) {
91+ navigateImage ( 'prev' ) ;
92+ }
93+
94+ setTouchStart ( null ) ;
95+ } ;
96+
97+ // Handle click to close
98+ const handleOverlayClick = ( e ) => {
99+ if ( e . target === lightboxRef . current ) {
100+ onClose ( ) ;
101+ }
102+ } ;
103+
104+ return (
105+ < div
106+ className = { `lightbox-overlay active` }
107+ ref = { lightboxRef }
108+ onClick = { handleOverlayClick }
109+ onTouchStart = { handleTouchStart }
110+ onTouchEnd = { handleTouchEnd }
111+ >
112+ < div className = "lightbox-container" >
113+ { photos . map ( ( photo , index ) => (
114+ < img
115+ key = { `lightbox-${ index } ` }
116+ src = { getOptimizedImageUrl ( photo . src ) }
117+ alt = { photo . alt || 'Image' }
118+ className = { `lightbox-img ${ index === activeIndex ? 'active ' + transitionClass : '' } ` }
119+ style = { { display : index === activeIndex ? 'block' : 'none' } }
120+ referrerPolicy = { photo . src . startsWith ( 'http' ) ? "no-referrer" : "" }
121+ loading = { Math . abs ( index - activeIndex ) <= 1 ? "eager" : "lazy" }
122+ />
123+ ) ) }
124+
125+ { photos [ activeIndex ] ?. caption && (
126+ < div className = { `lightbox-caption ${ activeIndex === currentIndex ? '' : 'active' } ` } >
127+ { photos [ activeIndex ] . caption }
128+ </ div >
129+ ) }
130+ </ div >
131+
132+ < div className = "lightbox-close" onClick = { onClose } > </ div >
133+
134+ < div className = "lightbox-navigation" >
135+ < button
136+ className = { `lightbox-nav-btn lightbox-prev ${ activeIndex === 0 ? 'disabled' : '' } ` }
137+ onClick = { ( ) => navigateImage ( 'prev' ) }
138+ disabled = { activeIndex === 0 }
139+ aria-label = "Previous image"
140+ > </ button >
141+ < button
142+ className = { `lightbox-nav-btn lightbox-next ${ activeIndex === photos . length - 1 ? 'disabled' : '' } ` }
143+ onClick = { ( ) => navigateImage ( 'next' ) }
144+ disabled = { activeIndex === photos . length - 1 }
145+ aria-label = "Next image"
146+ > </ button >
147+ </ div >
148+ </ div >
149+ ) ;
150+ } ;
151+
152+ Lightbox . propTypes = {
153+ photos : PropTypes . array . isRequired ,
154+ currentIndex : PropTypes . number . isRequired ,
155+ onClose : PropTypes . func . isRequired
156+ } ;
157+
158+ export default Lightbox ;
0 commit comments