Skip to content

Commit 61a3e92

Browse files
feat: ensure countdown clock disappears and resumes slideshow
Add onComplete callback to CountdownPieClock and handle completion state. #VERCEL_SKIP Co-authored-by: Darcy Liu <34801810+codejedi-ai@users.noreply.github.com>
1 parent e358a6f commit 61a3e92

File tree

2 files changed

+219
-99
lines changed

2 files changed

+219
-99
lines changed

app/components/Hero.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function Hero() {
1717
<div className="text-center">
1818

1919
<h2 className="text-4xl md:text-6xl font-bold text-white mb-4 glow-text">
20-
Hello, my Name is
20+
Hello, my name is
2121
<br />
2222
<span className="flex items-center justify-center gap-2 mt-2">
2323
Darcy <Heart className="text-primary-pink animate-pulse" size={32} /> Liu

app/components/WhoAmI.tsx

Lines changed: 218 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,132 @@
22

33
import Image from "next/image"
44
import { 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

3187
export 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

Comments
 (0)