Skip to content

Commit b2e793b

Browse files
feat: add image gallery to work experience
Add support for displaying featured images in work experience entries with lightbox gallery view. Closes #8
1 parent a80974b commit b2e793b

File tree

9 files changed

+172
-11
lines changed

9 files changed

+172
-11
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { useState } from 'react';
2+
3+
interface ImageLightboxProps {
4+
images: { src: string; alt?: string }[];
5+
isOpen: boolean;
6+
initialIndex: number;
7+
onClose: () => void;
8+
}
9+
10+
export default function ImageLightbox({
11+
images,
12+
isOpen,
13+
initialIndex,
14+
onClose,
15+
}: ImageLightboxProps) {
16+
const [currentIndex, setCurrentIndex] = useState(initialIndex);
17+
18+
if (!isOpen) return null;
19+
20+
const goToNext = (e: React.MouseEvent) => {
21+
e.stopPropagation();
22+
setCurrentIndex((currentIndex + 1) % images.length);
23+
};
24+
25+
const goToPrev = (e: React.MouseEvent) => {
26+
e.stopPropagation();
27+
setCurrentIndex((currentIndex - 1 + images.length) % images.length);
28+
};
29+
30+
const handleClose = (e: React.MouseEvent) => {
31+
e.stopPropagation();
32+
onClose();
33+
};
34+
35+
return (
36+
<div
37+
className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center"
38+
onClick={onClose}
39+
>
40+
<button
41+
onClick={handleClose}
42+
className="absolute top-4 right-4 text-white text-2xl hover:text-gray-300 z-10"
43+
aria-label="Close"
44+
>
45+
×
46+
</button>
47+
48+
<div className="relative w-full h-full flex items-center justify-center p-8">
49+
<img
50+
src={images[currentIndex].src}
51+
alt={images[currentIndex].alt || ''}
52+
className="max-w-full max-h-full object-contain"
53+
onClick={(e) => e.stopPropagation()}
54+
/>
55+
56+
{images.length > 1 && (
57+
<>
58+
<button
59+
onClick={goToPrev}
60+
className="absolute left-4 text-white text-4xl hover:text-gray-300"
61+
aria-label="Previous"
62+
>
63+
64+
</button>
65+
<button
66+
onClick={goToNext}
67+
className="absolute right-4 text-white text-4xl hover:text-gray-300"
68+
aria-label="Next"
69+
>
70+
71+
</button>
72+
<div className="absolute bottom-4 flex gap-2">
73+
{images.map((_, idx) => (
74+
<button
75+
key={idx}
76+
onClick={(e) => {
77+
e.stopPropagation();
78+
setCurrentIndex(idx);
79+
}}
80+
className={`w-2 h-2 rounded-full ${
81+
idx === currentIndex ? 'bg-white' : 'bg-white/50'
82+
}`}
83+
aria-label={`Go to image ${idx + 1}`}
84+
/>
85+
))}
86+
</div>
87+
</>
88+
)}
89+
</div>
90+
</div>
91+
);
92+
}

src/components/ui/WorkExperience.astro

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ interface Props {
77
88
const { entry } = Astro.props;
99
const { Content } = await render(entry);
10+
11+
const images = entry.data.images
12+
? await Promise.all(
13+
entry.data.images.map(async (img) => ({
14+
src: img.src,
15+
alt: entry.data.title,
16+
}))
17+
)
18+
: [];
1019
---
1120

1221
<li class="py-0.5">
@@ -22,6 +31,59 @@ const { Content } = await render(entry);
2231
<div class="prose dark:prose-invert prose-sm">
2332
<Content />
2433
</div>
34+
35+
{images.length > 0 && (
36+
<div class="flex gap-2 flex-wrap mt-2">
37+
{images.map((image, idx) => (
38+
<button
39+
class="work-image-thumb overflow-hidden rounded-lg border border-border hover:opacity-80 transition-opacity"
40+
data-images={JSON.stringify(images)}
41+
data-index={idx}
42+
>
43+
<img
44+
src={image.src}
45+
alt={image.alt}
46+
class="w-20 h-20 object-cover"
47+
/>
48+
</button>
49+
))}
50+
</div>
51+
)}
2552
</div>
2653
</div>
27-
</li>
54+
</li>
55+
56+
<script>
57+
import ImageLightbox from './ImageLightbox';
58+
import { createRoot } from 'react-dom/client';
59+
import { createElement } from 'react';
60+
61+
document.addEventListener('DOMContentLoaded', () => {
62+
const thumbs = document.querySelectorAll('.work-image-thumb');
63+
64+
thumbs.forEach((thumb) => {
65+
thumb.addEventListener('click', () => {
66+
const images = JSON.parse(thumb.getAttribute('data-images') || '[]');
67+
const index = parseInt(thumb.getAttribute('data-index') || '0');
68+
69+
const container = document.createElement('div');
70+
document.body.appendChild(container);
71+
const root = createRoot(container);
72+
73+
const closeHandler = () => {
74+
root.unmount();
75+
document.body.removeChild(container);
76+
};
77+
78+
root.render(
79+
createElement(ImageLightbox, {
80+
images,
81+
isOpen: true,
82+
initialIndex: index,
83+
onClose: closeHandler,
84+
})
85+
);
86+
});
87+
});
88+
});
89+
</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().or(z.enum(['Now'])),
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().or(z.enum(['Now'])),
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
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@ location: Mexico City
55
from: 2022
66
to: 2022 # Use 'Now' if the job is still active
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.
1116

1217
Additionally, I redesigned the web mobile app used by pickers to optimize the placement process when they prepare orders and place products in the bag for delivery. I also assisted in testing and developing a new TO-DO extension for Raycast called Hypersonic, connected through Notion with OAuth.
1318

14-
Tools used: Node, React + Next.js, GraphQL, NestJS, Prisma, PostgreSQL, Strapi, Stitches, TypeScript.
19+
Tools used: Node, React + Next.js, GraphQL, NestJS, Prisma, PostgreSQL, Strapi, Stitches, TypeScript.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ Currently building a health insurance platform for Mexico. Responsibilities incl
1313
- Maintain the app and fix bugs.
1414
- Work closely with the product team to understand the needs of the business and the users.
1515

16-
Tools used: React, React Native, TypeScript
16+
Tools used: React, React Native, TypeScript

0 commit comments

Comments
 (0)