Skip to content

Commit 47dfd66

Browse files
clegouesclaude
andcommitted
feat: add photo carousel to team page
Replace static photo grid with auto-advancing carousel. Shows 2 photos at a time on desktop, 1 on mobile. Auto-advances every 5 seconds with prev/next controls. Photos loaded from src/data/photos.yaml for easy maintenance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2b08be5 commit 47dfd66

2 files changed

Lines changed: 89 additions & 17 deletions

File tree

src/data/photos.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Lab group photos. Add new photos to public/img/ and add an entry here.
2+
- src: /img/GroupBowlingPhoto.JPG
3+
alt: squaresLab after a friendly but competitive bowling excursion
4+
- src: /img/GroupEscapeRoomPhoto.jpg
5+
alt: "squaresLab after successfully solving Carnegie'$ Million$ at Escape Room Pittsburgh"
6+
- src: /img/GroupAxePhoto.jpg
7+
alt: squaresLab in our squaresLab t-shirts after an axe throwing social event
8+
- src: /img/GroupApples.jpg
9+
alt: squaresLab goes apple picking
10+
- src: /img/GroupTopgolfPhoto.jpg
11+
alt: squaresLab goes to Topgolf
12+
- src: /img/GroupBowling2.jpg
13+
alt: squaresLab the next generation goes bowling, too

src/pages/team.astro

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
import BaseLayout from '../layouts/BaseLayout.astro';
33
import TeamMember from '../components/TeamMember.astro';
44
import { getCollection } from 'astro:content';
5+
import { promises as fs } from 'node:fs';
6+
import * as path from 'node:path';
7+
import yaml from 'js-yaml';
58
69
const allMembers = await getCollection('team');
710
@@ -33,15 +36,9 @@ const sortedAlumni = alumni.sort((a, b) => {
3336
return a.data.name.localeCompare(b.data.name);
3437
});
3538
36-
37-
const photos = [
38-
{ src: '/img/GroupBowlingPhoto.JPG', alt: 'squaresLab after a friendly but competitive bowling excursion' },
39-
{ src: '/img/GroupEscapeRoomPhoto.jpg', alt: "squaresLab after successfully solving Carnegie'$ Million$ at Escape Room Pittsburgh" },
40-
{ src: '/img/GroupAxePhoto.jpg', alt: 'squaresLab in our squaresLab t-shirts after an axe throwing social event' },
41-
{ src: '/img/GroupApples.jpg', alt: 'squaresLab goes apple picking' },
42-
{ src: '/img/GroupTopgolfPhoto.jpg', alt: 'squaresLab goes to Topgolf' },
43-
{ src: '/img/GroupBowling2.jpg', alt: 'squaresLab the next generation goes bowling, too' },
44-
];
39+
// Load photos from data file
40+
const photosRaw = await fs.readFile(path.resolve('src/data/photos.yaml'), 'utf-8');
41+
const photos = (yaml.load(photosRaw) as { src: string; alt: string }[]) || [];
4542
---
4643

4744
<BaseLayout title="Team">
@@ -52,15 +49,36 @@ const photos = [
5249
</div>
5350
</section>
5451

55-
{/* Photo Gallery */}
52+
{/* Photo Carousel */}
5653
<section class="max-w-content mx-auto px-8 py-8">
57-
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
58-
{photos.map((photo) => (
59-
<figure class="m-0">
60-
<img src={photo.src} alt={photo.alt} class="w-full rounded-lg" loading="lazy" />
61-
<figcaption class="text-xs text-gray-secondary mt-1 italic">{photo.alt}</figcaption>
62-
</figure>
63-
))}
54+
<div id="photo-carousel" class="relative">
55+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4" id="carousel-grid">
56+
{photos.map((photo, i) => (
57+
<figure
58+
class="m-0 carousel-slide"
59+
data-index={i}
60+
style={i >= 2 ? 'display:none' : ''}
61+
>
62+
<img src={photo.src} alt={photo.alt} class="w-full rounded-lg" loading="lazy" />
63+
<figcaption class="text-xs text-gray-secondary mt-1 italic">{photo.alt}</figcaption>
64+
</figure>
65+
))}
66+
</div>
67+
{photos.length > 2 && (
68+
<div class="flex items-center justify-center gap-4 mt-4">
69+
<button id="carousel-prev" class="text-gray-secondary hover:text-red-cmu transition-colors p-1" aria-label="Previous photos">
70+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
71+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
72+
</svg>
73+
</button>
74+
<span id="carousel-indicator" class="text-xs text-gray-secondary"></span>
75+
<button id="carousel-next" class="text-gray-secondary hover:text-red-cmu transition-colors p-1" aria-label="Next photos">
76+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
77+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
78+
</svg>
79+
</button>
80+
</div>
81+
)}
6482
</div>
6583
</section>
6684

@@ -179,4 +197,45 @@ const photos = [
179197
details[open] .details-arrow {
180198
transform: rotate(90deg);
181199
}
200+
.carousel-slide {
201+
transition: opacity 0.4s ease;
202+
}
182203
</style>
204+
205+
<script>
206+
const slides = document.querySelectorAll<HTMLElement>('.carousel-slide');
207+
const total = slides.length;
208+
if (total > 2) {
209+
const perPage = window.matchMedia('(min-width: 768px)').matches ? 2 : 1;
210+
let offset = 0;
211+
const indicator = document.getElementById('carousel-indicator');
212+
213+
function show() {
214+
slides.forEach((s, i) => {
215+
s.style.display = (i >= offset && i < offset + perPage) ? '' : 'none';
216+
});
217+
if (indicator) {
218+
const page = Math.floor(offset / perPage) + 1;
219+
const pages = Math.ceil(total / perPage);
220+
indicator.textContent = `${page} / ${pages}`;
221+
}
222+
}
223+
224+
function advance(dir: number) {
225+
const perPage = window.matchMedia('(min-width: 768px)').matches ? 2 : 1;
226+
offset += dir * perPage;
227+
if (offset >= total) offset = 0;
228+
if (offset < 0) offset = Math.max(0, total - perPage);
229+
show();
230+
}
231+
232+
document.getElementById('carousel-prev')?.addEventListener('click', () => {
233+
advance(-1);
234+
});
235+
document.getElementById('carousel-next')?.addEventListener('click', () => {
236+
advance(1);
237+
});
238+
239+
show();
240+
}
241+
</script>

0 commit comments

Comments
 (0)