Skip to content

Commit 2f394a7

Browse files
fix: remove top-level return and implement Framer Motion lightbox
- Fix top-level return error in Astro script by wrapping logic in if block - Implement Framer Motion for smooth lightbox animations - Preserve aspect ratio for vertical and horizontal images - Add fade and scale transitions matching reference design - Add body scroll lock when lightbox is open
1 parent 3caa8b1 commit 2f394a7

File tree

4 files changed

+114
-89
lines changed

4 files changed

+114
-89
lines changed

bun.lockb

1.29 KB
Binary file not shown.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@types/react": "^18.3.3",
1818
"@types/react-dom": "^18.3.0",
1919
"astro": "5.15.2",
20+
"framer-motion": "^12.23.24",
2021
"lucide-react": "^0.548.0",
2122
"mdast-util-to-string": "^4.0.0",
2223
"react": "^18.3.1",
Lines changed: 82 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useState, useEffect } from 'react';
2+
import { motion, AnimatePresence } from 'framer-motion';
23

34
interface ImageLightboxProps {
45
images: { src: string; alt?: string }[];
@@ -28,7 +29,11 @@ export default function ImageLightbox({
2829

2930
if (isOpen) {
3031
document.addEventListener('keydown', handleKeyDown);
31-
return () => document.removeEventListener('keydown', handleKeyDown);
32+
document.body.style.overflow = 'hidden';
33+
return () => {
34+
document.removeEventListener('keydown', handleKeyDown);
35+
document.body.style.overflow = 'unset';
36+
};
3237
}
3338
}, [isOpen, currentIndex]);
3439

@@ -44,69 +49,87 @@ export default function ImageLightbox({
4449
};
4550

4651
return (
47-
<div
48-
className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center"
49-
onClick={onClose}
50-
role="dialog"
51-
aria-modal="true"
52-
aria-label="Image gallery"
53-
>
54-
<button
55-
onClick={handleClose}
56-
className="absolute top-4 right-4 text-white text-2xl hover:text-gray-300 z-10"
57-
aria-label="Close gallery"
58-
>
59-
×
60-
</button>
52+
<AnimatePresence>
53+
{isOpen && (
54+
<motion.div
55+
initial={{ opacity: 0 }}
56+
animate={{ opacity: 1 }}
57+
exit={{ opacity: 0 }}
58+
transition={{ duration: 0.2 }}
59+
className="fixed inset-0 z-50 bg-black/95 flex items-center justify-center"
60+
onClick={onClose}
61+
role="dialog"
62+
aria-modal="true"
63+
aria-label="Image gallery"
64+
>
65+
<button
66+
onClick={handleClose}
67+
className="absolute top-4 right-4 text-white/80 hover:text-white text-3xl w-10 h-10 flex items-center justify-center rounded-full hover:bg-white/10 transition-colors z-10"
68+
aria-label="Close gallery"
69+
>
70+
×
71+
</button>
6172

62-
<div className="relative w-full h-full flex items-center justify-center p-8">
63-
<img
64-
src={images[currentIndex].src}
65-
alt={images[currentIndex].alt || ''}
66-
className="max-w-full max-h-full object-contain"
67-
onClick={(e) => e.stopPropagation()}
68-
/>
73+
<div className="relative w-full h-full flex items-center justify-center p-4 md:p-8">
74+
<AnimatePresence mode="wait" custom={currentIndex}>
75+
<motion.img
76+
key={currentIndex}
77+
src={images[currentIndex].src}
78+
alt={images[currentIndex].alt || ''}
79+
initial={{ opacity: 0, scale: 0.9 }}
80+
animate={{ opacity: 1, scale: 1 }}
81+
exit={{ opacity: 0, scale: 0.9 }}
82+
transition={{ duration: 0.3, ease: 'easeOut' }}
83+
className="max-w-full max-h-full w-auto h-auto object-contain rounded-lg"
84+
onClick={(e) => e.stopPropagation()}
85+
style={{ maxWidth: '90vw', maxHeight: '90vh' }}
86+
/>
87+
</AnimatePresence>
6988

70-
{images.length > 1 && (
71-
<>
72-
<button
73-
onClick={(e) => {
74-
e.stopPropagation();
75-
goToPrev();
76-
}}
77-
className="absolute left-4 text-white text-4xl hover:text-gray-300"
78-
aria-label="Previous image"
79-
>
80-
81-
</button>
82-
<button
83-
onClick={(e) => {
84-
e.stopPropagation();
85-
goToNext();
86-
}}
87-
className="absolute right-4 text-white text-4xl hover:text-gray-300"
88-
aria-label="Next image"
89-
>
90-
91-
</button>
92-
<div className="absolute bottom-4 flex gap-2">
93-
{images.map((_, idx) => (
89+
{images.length > 1 && (
90+
<>
9491
<button
95-
key={images[idx].src}
9692
onClick={(e) => {
9793
e.stopPropagation();
98-
setCurrentIndex(idx);
94+
goToPrev();
9995
}}
100-
className={`w-2 h-2 rounded-full ${
101-
idx === currentIndex ? 'bg-white' : 'bg-white/50'
102-
}`}
103-
aria-label={`Go to image ${idx + 1}`}
104-
/>
105-
))}
106-
</div>
107-
</>
108-
)}
109-
</div>
110-
</div>
96+
className="absolute left-4 text-white/80 hover:text-white text-5xl w-12 h-12 flex items-center justify-center rounded-full hover:bg-white/10 transition-colors"
97+
aria-label="Previous image"
98+
>
99+
100+
</button>
101+
<button
102+
onClick={(e) => {
103+
e.stopPropagation();
104+
goToNext();
105+
}}
106+
className="absolute right-4 text-white/80 hover:text-white text-5xl w-12 h-12 flex items-center justify-center rounded-full hover:bg-white/10 transition-colors"
107+
aria-label="Next image"
108+
>
109+
110+
</button>
111+
<div className="absolute bottom-6 flex gap-2">
112+
{images.map((_, idx) => (
113+
<button
114+
key={images[idx].src}
115+
onClick={(e) => {
116+
e.stopPropagation();
117+
setCurrentIndex(idx);
118+
}}
119+
className={`w-2 h-2 rounded-full transition-all ${
120+
idx === currentIndex
121+
? 'bg-white w-6'
122+
: 'bg-white/50 hover:bg-white/70'
123+
}`}
124+
aria-label={`Go to image ${idx + 1}`}
125+
/>
126+
))}
127+
</div>
128+
</>
129+
)}
130+
</div>
131+
</motion.div>
132+
)}
133+
</AnimatePresence>
111134
);
112135
}

src/components/ui/WorkExperience.astro

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -57,37 +57,38 @@ const images = entry.data.images
5757
import { createElement } from 'react';
5858

5959
const container = document.querySelector('.flex.gap-2.flex-wrap.mt-2');
60-
if (!container) return;
6160

62-
const clickHandler = (event: Event) => {
63-
const thumb = (event.target as HTMLElement).closest('.work-image-thumb');
64-
if (!thumb || !container.contains(thumb)) return;
65-
66-
const images = JSON.parse(thumb.getAttribute('data-images') || '[]');
67-
const index = parseInt(thumb.getAttribute('data-index') || '0');
68-
69-
const lightboxContainer = document.createElement('div');
70-
document.body.appendChild(lightboxContainer);
71-
const root = createRoot(lightboxContainer);
72-
73-
const closeHandler = () => {
74-
root.unmount();
75-
try {
76-
document.body.removeChild(lightboxContainer);
77-
} catch (e) {
78-
// Container may have already been removed; ignore error
79-
}
61+
if (container) {
62+
const clickHandler = (event: Event) => {
63+
const thumb = (event.target as HTMLElement).closest('.work-image-thumb');
64+
if (!thumb || !container.contains(thumb)) return;
65+
66+
const images = JSON.parse(thumb.getAttribute('data-images') || '[]');
67+
const index = parseInt(thumb.getAttribute('data-index') || '0');
68+
69+
const lightboxContainer = document.createElement('div');
70+
document.body.appendChild(lightboxContainer);
71+
const root = createRoot(lightboxContainer);
72+
73+
const closeHandler = () => {
74+
root.unmount();
75+
try {
76+
document.body.removeChild(lightboxContainer);
77+
} catch (e) {
78+
// Container may have already been removed; ignore error
79+
}
80+
};
81+
82+
root.render(
83+
createElement(ImageLightbox, {
84+
images,
85+
isOpen: true,
86+
initialIndex: index,
87+
onClose: closeHandler,
88+
})
89+
);
8090
};
8191

82-
root.render(
83-
createElement(ImageLightbox, {
84-
images,
85-
isOpen: true,
86-
initialIndex: index,
87-
onClose: closeHandler,
88-
})
89-
);
90-
};
91-
92-
container.addEventListener('click', clickHandler);
92+
container.addEventListener('click', clickHandler);
93+
}
9394
</script>

0 commit comments

Comments
 (0)