22
33import Image from "next/image"
44import { useState , useEffect , useRef , useCallback } from "react"
5- import { ChevronLeft , ChevronRight } from "lucide-react"
6-
7- // Define the slides directly in the component to ensure they're loaded
8- const slidesData = [
9- {
10- id : "about1" ,
11- src : "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/about1.jpg-TbfdbEe1niYCAR6Fqv7JYcqm2zeKO9.jpeg" ,
12- alt : "Kayaking with a Star Wars Rebel Alliance cap" ,
13- } ,
14- {
15- id : "about2" ,
16- src : "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/about2-X48rWZdpV4Q7RxVbbD5F7xRy5JhQdO.jpeg" ,
17- alt : "Sailing at the beach with life vest" ,
18- } ,
19- {
20- id : "about3" ,
21- src : "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/about3-9AFwiFVEdtKGJqM9LmWvBQWHcfyyC2.jpeg" ,
22- alt : "Building a sand castle on the beach" ,
23- } ,
24- {
25- id : "about4" ,
26- src : "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/about4-Vpsom9WTaJ93mBOvtKEjXoCSR1QzC5.jpeg" ,
27- alt : "Kayaking in a blue Hydro-Force inflatable kayak" ,
28- } ,
29- ]
5+ import { ChevronLeft , ChevronRight , Pause } from "lucide-react"
6+
7+ interface SlideData {
8+ id : string
9+ src : string
10+ alt : string
11+ }
12+
13+ // Countdown Pie Clock Component
14+ function CountdownPieClock ( { duration = 5000 , onComplete } : { duration ?: number ; onComplete ?: ( ) => void } ) {
15+ const [ progress , setProgress ] = useState ( 100 )
16+ const startTimeRef = useRef ( Date . now ( ) )
17+ const animationFrameRef = useRef < number | null > ( null )
18+
19+ useEffect ( ( ) => {
20+ startTimeRef . current = Date . now ( )
21+
22+ const updateProgress = ( ) => {
23+ const elapsed = Date . now ( ) - startTimeRef . current
24+ const remaining = Math . max ( 0 , duration - elapsed )
25+ const newProgress = ( remaining / duration ) * 100
26+
27+ setProgress ( newProgress )
28+
29+ if ( newProgress > 0 ) {
30+ animationFrameRef . current = requestAnimationFrame ( updateProgress )
31+ } else {
32+ // When countdown reaches zero, call the onComplete callback
33+ if ( onComplete ) {
34+ onComplete ( )
35+ }
36+ }
37+ }
38+
39+ animationFrameRef . current = requestAnimationFrame ( updateProgress )
40+
41+ return ( ) => {
42+ if ( animationFrameRef . current ) {
43+ cancelAnimationFrame ( animationFrameRef . current )
44+ }
45+ }
46+ } , [ duration , onComplete ] )
47+
48+ // Calculate SVG parameters for the pie/circle
49+ const size = 40
50+ const strokeWidth = 4
51+ const radius = ( size - strokeWidth ) / 2
52+ const circumference = radius * 2 * Math . PI
53+ const strokeDashoffset = circumference - ( progress / 100 ) * circumference
54+
55+ return (
56+ < div className = "relative flex items-center justify-center" >
57+ < svg width = { size } height = { size } viewBox = { `0 0 ${ size } ${ size } ` } className = "transform -rotate-90" >
58+ { /* Background circle */ }
59+ < circle
60+ cx = { size / 2 }
61+ cy = { size / 2 }
62+ r = { radius }
63+ fill = "transparent"
64+ stroke = "rgba(255,255,255,0.2)"
65+ strokeWidth = { strokeWidth }
66+ />
67+ { /* Progress circle */ }
68+ < circle
69+ cx = { size / 2 }
70+ cy = { size / 2 }
71+ r = { radius }
72+ fill = "transparent"
73+ stroke = "rgba(0,210,255,0.8)"
74+ strokeWidth = { strokeWidth }
75+ strokeDasharray = { circumference }
76+ strokeDashoffset = { strokeDashoffset }
77+ strokeLinecap = "round"
78+ />
79+ </ svg >
80+ < div className = "absolute inset-0 flex items-center justify-center" >
81+ < Pause className = "h-4 w-4 text-white" />
82+ </ div >
83+ </ div >
84+ )
85+ }
3086
3187export default function AboutMe ( ) {
88+ const [ slidesData , setSlidesData ] = useState < SlideData [ ] > ( [ ] )
89+ const [ isLoadingSlides , setIsLoadingSlides ] = useState ( true )
90+ const [ slidesError , setSlidesError ] = useState < string | null > ( null )
3291 const [ currentSlide , setCurrentSlide ] = useState ( 0 )
3392 const [ isPaused , setIsPaused ] = useState ( false )
3493 const pauseTimerRef = useRef < NodeJS . Timeout | null > ( null )
3594 const slideIntervalRef = useRef < NodeJS . Timeout | null > ( null )
95+ const pauseDuration = 5000 // 5 seconds pause duration
96+
97+ // Fetch slides data from API
98+ useEffect ( ( ) => {
99+ async function fetchAboutImages ( ) {
100+ try {
101+ const response = await fetch ( "/api/about-images" )
102+
103+ if ( ! response . ok ) {
104+ throw new Error ( "Failed to fetch about images" )
105+ }
106+
107+ const data = await response . json ( )
108+ setSlidesData ( data . aboutImages )
109+ } catch ( err ) {
110+ console . error ( "Error fetching about images:" , err )
111+ setSlidesError ( "Failed to load images. Please try again later." )
112+ } finally {
113+ setIsLoadingSlides ( false )
114+ }
115+ }
116+
117+ fetchAboutImages ( )
118+ } , [ ] )
36119
37120 // Function to advance to the next slide
38121 const nextSlide = useCallback ( ( ) => {
122+ if ( slidesData . length === 0 ) return
39123 setCurrentSlide ( ( prev ) => ( prev + 1 ) % slidesData . length )
40- } , [ ] )
124+ } , [ slidesData . length ] )
41125
42126 // Function to go to the previous slide
43127 const prevSlide = useCallback ( ( ) => {
128+ if ( slidesData . length === 0 ) return
44129 setCurrentSlide ( ( prev ) => ( prev - 1 + slidesData . length ) % slidesData . length )
45- } , [ ] )
130+ } , [ slidesData . length ] )
46131
47132 // Function to handle user interaction
48133 const handleUserInteraction = useCallback ( ( newSlideIndex ?: number ) => {
@@ -59,10 +144,10 @@ export default function AboutMe() {
59144 clearTimeout ( pauseTimerRef . current )
60145 }
61146
62- // Set a new pause timer to resume after 5 seconds
147+ // Set a new pause timer to resume after the pause duration
63148 pauseTimerRef . current = setTimeout ( ( ) => {
64149 setIsPaused ( false )
65- } , 5000 )
150+ } , pauseDuration )
66151 } , [ ] )
67152
68153 // Set up automatic slide cycling
@@ -72,8 +157,8 @@ export default function AboutMe() {
72157 clearInterval ( slideIntervalRef . current )
73158 }
74159
75- // Only set up interval if not paused
76- if ( ! isPaused ) {
160+ // Only set up interval if not paused and we have slides
161+ if ( ! isPaused && slidesData . length > 0 ) {
77162 slideIntervalRef . current = setInterval ( ( ) => {
78163 nextSlide ( )
79164 } , 3000 ) // Change slide every 3 seconds
@@ -88,7 +173,7 @@ export default function AboutMe() {
88173 clearTimeout ( pauseTimerRef . current )
89174 }
90175 }
91- } , [ isPaused , nextSlide ] )
176+ } , [ isPaused , nextSlide , slidesData . length ] )
92177
93178 const quote =
94179 '"Let each person examine his own work, and then he can take pride in himself alone, and not compare himself with someone else." -- Galatians 6:4'
@@ -97,7 +182,7 @@ export default function AboutMe() {
97182 < section id = "about" className = "py-20 text-white" >
98183 < div className = "container mx-auto px-4" >
99184 { /* Header */ }
100- < div className = "flex items-center justify-between mb-16 p-8 from-gray-900 to-gray-800 rounded-lg shadow-2xl " >
185+ < div className = "flex items-center justify-between mb-16 p-8 from-gray-900 to-gray-800 rounded-lg" >
101186 < div className = "max-w-2xl" >
102187 < h2 className = "text-5xl font-bold mb-4 text-white" > I believe ...</ h2 >
103188 < p className = "text-gray-300 italic text-lg" > { quote } </ p >
@@ -117,78 +202,113 @@ export default function AboutMe() {
117202 { /* Slideshow */ }
118203 < div className = "relative" >
119204 < div className = "aspect-w-16 aspect-h-9 relative h-[400px] group" >
120- { /* Fade transition for images */ }
121- { slidesData . map ( ( slide , index ) => (
122- < div
123- key = { slide . id }
124- className = { `absolute inset-0 transition-opacity duration-1000 ${
125- currentSlide === index ? "opacity-100" : "opacity-0"
126- } `}
127- >
128- < Image
129- src = { slide . src || "/placeholder.svg" }
130- alt = { slide . alt }
131- fill
132- className = "object-cover rounded-lg"
133- priority = { index === 0 }
134- sizes = "(max-width: 768px) 100vw, 50vw"
135- />
205+ { isLoadingSlides ? (
206+ < div className = "absolute inset-0 flex items-center justify-center bg-gray-800 rounded-lg" >
207+ < div className = "animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary-cyan" > </ div >
208+ </ div >
209+ ) : slidesError ? (
210+ < div className = "absolute inset-0 flex items-center justify-center bg-gray-800 rounded-lg" >
211+ < p className = "text-primary-pink" > { slidesError } </ p >
212+ </ div >
213+ ) : slidesData . length === 0 ? (
214+ < div className = "absolute inset-0 flex items-center justify-center bg-gray-800 rounded-lg" >
215+ < p className = "text-gray-400" > No images available</ p >
136216 </ div >
137- ) ) }
217+ ) : (
218+ < >
219+ { /* Fade transition for images */ }
220+ { slidesData . map ( ( slide , index ) => (
221+ < div
222+ key = { slide . id }
223+ className = { `absolute inset-0 transition-opacity duration-1000 ${
224+ currentSlide === index ? "opacity-100" : "opacity-0"
225+ } `}
226+ >
227+ < Image
228+ src = { slide . src || "/placeholder.svg" }
229+ alt = { slide . alt }
230+ fill
231+ className = "object-cover rounded-lg"
232+ priority = { index === 0 }
233+ sizes = "(max-width: 768px) 100vw, 50vw"
234+ />
235+ </ div >
236+ ) ) }
138237
139- { /* Pause indicator */ }
140- { isPaused && (
141- < div className = "absolute top-4 right-4 bg-black/50 text-white px-2 py-1 rounded text-sm" > Paused</ div >
142- ) }
238+ { /* Countdown Pie Clock instead of "Paused" text */ }
239+ { isPaused && (
240+ < div className = "absolute top-4 right-4 bg-black/50 backdrop-blur-sm p-2 rounded-full shadow-lg" >
241+ < CountdownPieClock
242+ duration = { pauseDuration }
243+ onComplete = { ( ) => {
244+ // Ensure the slideshow resumes when the countdown completes
245+ if ( pauseTimerRef . current ) {
246+ clearTimeout ( pauseTimerRef . current )
247+ pauseTimerRef . current = null
248+ }
249+ setIsPaused ( false )
250+ } }
251+ />
252+ </ div >
253+ ) }
143254
144- { /* Hover overlay with additional info */ }
145- < div className = "absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-end" >
146- < div className = "p-4 w-full" >
147- < p className = "text-white text-lg font-medium" > { slidesData [ currentSlide ] . alt } </ p >
148- </ div >
149- </ div >
255+ { /* Hover overlay with additional info */ }
256+ < div className = "absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-end" >
257+ < div className = "p-4 w-full" >
258+ < p className = "text-white text-lg font-medium" >
259+ { slidesData [ currentSlide ] ?. alt || "Image description" }
260+ </ p >
261+ </ div >
262+ </ div >
263+ </ >
264+ ) }
150265 </ div >
151266
152- { /* Navigation buttons with enhanced styling */ }
153- < button
154- onClick = { ( ) => {
155- prevSlide ( )
156- handleUserInteraction ( )
157- } }
158- className = "absolute left-2 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 p-3 rounded-full text-white transition-all duration-300 transform hover:scale-110"
159- aria-label = "Previous slide"
160- >
161- < ChevronLeft className = "h-6 w-6" />
162- </ button >
163- < button
164- onClick = { ( ) => {
165- nextSlide ( )
166- handleUserInteraction ( )
167- } }
168- className = "absolute right-2 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 p-3 rounded-full text-white transition-all duration-300 transform hover:scale-110"
169- aria-label = "Next slide"
170- >
171- < ChevronRight className = "h-6 w-6" />
172- </ button >
173-
174- { /* Enhanced indicator dots */ }
175- < div className = "flex justify-center space-x-3 mt-6" >
176- { slidesData . map ( ( _ , index ) => (
267+ { /* Only show navigation controls if we have slides */ }
268+ { ! isLoadingSlides && ! slidesError && slidesData . length > 0 && (
269+ < >
270+ { /* Navigation buttons with enhanced styling */ }
177271 < button
178- key = { index }
179- onClick = { ( ) => handleUserInteraction ( index ) }
180- className = { `w-4 h-4 rounded-full transition-all duration-300 ${
181- currentSlide === index ? "bg-blue-500 scale-110" : "bg-gray-400 hover:bg-gray-300"
182- } `}
183- aria-label = { `Go to slide ${ index + 1 } ` }
184- />
185- ) ) }
186- </ div >
272+ onClick = { ( ) => {
273+ prevSlide ( )
274+ handleUserInteraction ( )
275+ } }
276+ className = "absolute left-2 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 p-3 rounded-full text-white transition-all duration-300 transform hover:scale-110"
277+ aria-label = "Previous slide"
278+ >
279+ < ChevronLeft className = "h-6 w-6" />
280+ </ button >
281+ < button
282+ onClick = { ( ) => {
283+ nextSlide ( )
284+ handleUserInteraction ( )
285+ } }
286+ className = "absolute right-2 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 p-3 rounded-full text-white transition-all duration-300 transform hover:scale-110"
287+ aria-label = "Next slide"
288+ >
289+ < ChevronRight className = "h-6 w-6" />
290+ </ button >
187291
188- { /* Slide counter */ }
189- < div className = "absolute bottom-4 right-4 bg-black/50 text-white px-2 py-1 rounded text-sm" >
190- { currentSlide + 1 } / { slidesData . length }
191- </ div >
292+ { /* Enhanced indicator dots */ }
293+ < div className = "flex justify-center space-x-3 mt-6" >
294+ { slidesData . map ( ( _ , index ) => (
295+ < button
296+ key = { index }
297+ onClick = { ( ) => handleUserInteraction ( index ) }
298+ className = { `w-4 h-4 rounded-full transition-all duration-300 ${
299+ currentSlide === index ? "bg-blue-500 scale-110" : "bg-gray-400 hover:bg-gray-300"
300+ } `}
301+ aria-label = { `Go to slide ${ index + 1 } ` }
302+ />
303+ ) ) }
304+ </ div >
305+
306+ { /* Slide counter */ }
307+ < div className = "absolute bottom-4 right-4 bg-black/50 text-white px-2 py-1 rounded text-sm" >
308+ { currentSlide + 1 } / { slidesData . length }
309+ </ div >
310+ </ >
311+ ) }
192312 </ div >
193313
194314 { /* Info */ }
0 commit comments