Skip to content

Commit dfb3d19

Browse files
committed
Merge branch 'main' into feat/improvements
2 parents 051e543 + bb2f3b5 commit dfb3d19

File tree

11 files changed

+220
-8
lines changed

11 files changed

+220
-8
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: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { useState, useEffect } from 'react';
2+
import { motion, AnimatePresence } from 'framer-motion';
3+
4+
interface ImageLightboxProps {
5+
images: { src: string; alt?: string }[];
6+
isOpen: boolean;
7+
initialIndex: number;
8+
onClose: () => void;
9+
}
10+
11+
export default function ImageLightbox({
12+
images,
13+
isOpen,
14+
initialIndex,
15+
onClose,
16+
}: ImageLightboxProps) {
17+
const [currentIndex, setCurrentIndex] = useState(initialIndex);
18+
19+
useEffect(() => {
20+
const handleKeyDown = (e: KeyboardEvent) => {
21+
if (e.key === 'Escape') {
22+
onClose();
23+
} else if (e.key === 'ArrowRight') {
24+
goToNext();
25+
} else if (e.key === 'ArrowLeft') {
26+
goToPrev();
27+
}
28+
};
29+
30+
if (isOpen) {
31+
document.addEventListener('keydown', handleKeyDown);
32+
document.body.style.overflow = 'hidden';
33+
return () => {
34+
document.removeEventListener('keydown', handleKeyDown);
35+
document.body.style.overflow = 'unset';
36+
};
37+
}
38+
}, [isOpen, currentIndex]);
39+
40+
if (!isOpen) return null;
41+
42+
const goToNext = () => setCurrentIndex((currentIndex + 1) % images.length);
43+
const goToPrev = () =>
44+
setCurrentIndex((currentIndex - 1 + images.length) % images.length);
45+
46+
const handleClose = (e: React.MouseEvent) => {
47+
e.stopPropagation();
48+
onClose();
49+
};
50+
51+
return (
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>
72+
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>
88+
89+
{images.length > 1 && (
90+
<>
91+
<button
92+
onClick={(e) => {
93+
e.stopPropagation();
94+
goToPrev();
95+
}}
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>
134+
);
135+
}

src/components/ui/WorkExperience.astro

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ interface Props {
77
88
const { entry } = Astro.props;
99
const { Content } = await render(entry);
10+
11+
const images = entry.data.images
12+
? entry.data.images.map((img) => ({
13+
src: img.src,
14+
alt: entry.data.title,
15+
}))
16+
: [];
1017
---
1118

1219
<li class="py-0.5">
@@ -24,6 +31,68 @@ const { Content } = await render(entry);
2431
<div class="prose dark:prose-invert prose-sm">
2532
<Content />
2633
</div>
34+
35+
{
36+
images.length > 0 && (
37+
<div class="flex gap-2 flex-wrap mt-2">
38+
{images.map((image, idx) => (
39+
<button
40+
class="work-image-thumb overflow-hidden rounded-lg border border-border hover:opacity-80 transition-opacity"
41+
data-images={JSON.stringify(images)}
42+
data-index={idx}
43+
>
44+
<img
45+
src={image.src}
46+
alt={image.alt}
47+
class="w-20 h-20 object-cover"
48+
/>
49+
</button>
50+
))}
51+
</div>
52+
)
53+
}
2754
</div>
2855
</div>
2956
</li>
57+
58+
<script>
59+
import ImageLightbox from './ImageLightbox';
60+
import { createRoot } from 'react-dom/client';
61+
import { createElement } from 'react';
62+
63+
const container = document.querySelector('.flex.gap-2.flex-wrap.mt-2');
64+
65+
if (container) {
66+
const clickHandler = (event: Event) => {
67+
const thumb = (event.target as HTMLElement).closest('.work-image-thumb');
68+
if (!thumb || !container.contains(thumb)) return;
69+
70+
const images = JSON.parse(thumb.getAttribute('data-images') || '[]');
71+
const index = parseInt(thumb.getAttribute('data-index') || '0');
72+
73+
const lightboxContainer = document.createElement('div');
74+
document.body.appendChild(lightboxContainer);
75+
const root = createRoot(lightboxContainer);
76+
77+
const closeHandler = () => {
78+
root.unmount();
79+
try {
80+
document.body.removeChild(lightboxContainer);
81+
} catch (e) {
82+
// Container may have already been removed; ignore error
83+
}
84+
};
85+
86+
root.render(
87+
createElement(ImageLightbox, {
88+
images,
89+
isOpen: true,
90+
initialIndex: index,
91+
onClose: closeHandler,
92+
}),
93+
);
94+
};
95+
96+
container.addEventListener('click', clickHandler);
97+
}
98+
</script>

src/content.config.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,16 @@ const linkCollection = defineCollection({
4343

4444
const jobCollection = defineCollection({
4545
loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: './src/content/jobs' }),
46-
schema: z.object({
47-
title: z.string(),
48-
company: z.string(),
49-
location: z.string(),
50-
from: z.number(),
51-
to: z.number().optional(), // optional if currently working
52-
url: z.string(),
53-
}),
46+
schema: ({ image }) =>
47+
z.object({
48+
title: z.string(),
49+
company: z.string(),
50+
location: z.string(),
51+
from: z.number(),
52+
to: z.number().optional(), // optional if currently working
53+
url: z.string(),
54+
images: z.array(image()).optional(),
55+
}),
5456
});
5557

5658
const talkCollection = defineCollection({
1.72 MB
Loading
1.92 MB
Loading
1.61 MB
Loading
1.82 MB
Loading

src/content/jobs/reboot-studio.mdx renamed to src/content/jobs/reboot-studio/index.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ location: Mexico City
55
from: 2022
66
to: 2022
77
url: https://reboot.studio
8+
images:
9+
- ./image-1.jpg
10+
- ./image-2.jpg
11+
- ./image-3.jpg
12+
- ./image-4.jpg
813
---
914

1015
I developed a new feature to display related products in Freshis blog posts. I also designed and launched an operational dashboard to manage delivery routes and customer orders in real-time, improving the Freshis delivery process.

0 commit comments

Comments
 (0)