Skip to content

Commit c00a5a2

Browse files
authored
feat(blog): add navigation arrows to lightbox (#60)
1 parent 569ce94 commit c00a5a2

2 files changed

Lines changed: 125 additions & 38 deletions

File tree

src/layouts/PostLayout.astro

Lines changed: 82 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -100,19 +100,89 @@ const year = date.getFullYear();
100100
<span class="lightbox-title">Full-size image</span>
101101
<span class="lightbox-hint">Click outside or press Esc to close</span>
102102
</div>
103+
<button class="lightbox-nav lightbox-prev" aria-label="Previous image">&#8249;</button>
103104
<div class="lightbox-container">
104105
<img src="" alt="" />
105106
</div>
107+
<button class="lightbox-nav lightbox-next" aria-label="Next image">&#8250;</button>
106108
<button class="lightbox-close" aria-label="Close">&times;</button>
107109
<div class="lightbox-footer">
108-
<span>Click anywhere outside the image to close</span>
110+
<span class="lightbox-counter"></span>
109111
</div>
110112
`;
111113
document.body.appendChild(lightbox);
112114

113115
const lightboxImg = lightbox.querySelector('.lightbox-container img');
114116
const closeBtn = lightbox.querySelector('.lightbox-close');
115117
const container = lightbox.querySelector('.lightbox-container');
118+
const prevBtn = lightbox.querySelector('.lightbox-prev');
119+
const nextBtn = lightbox.querySelector('.lightbox-next');
120+
const counter = lightbox.querySelector('.lightbox-counter');
121+
122+
// Get year and slug from data attributes
123+
const article = document.querySelector('article[data-year][data-slug]');
124+
const year = article?.dataset.year;
125+
const slug = article?.dataset.slug;
126+
127+
// Collect all prose images (excluding those wrapped in links)
128+
const proseImages = Array.from(document.querySelectorAll('.prose img')).filter(
129+
(img) => img.parentElement?.tagName !== 'A'
130+
);
131+
let currentIndex = 0;
132+
133+
// Function to get original image path
134+
function getOriginalPath(src, baseName) {
135+
if (year && slug && baseName) {
136+
return `/originals/${year}/${slug}/${baseName}.png`;
137+
}
138+
return src;
139+
}
140+
141+
// Function to load image with fallbacks
142+
function loadImage(src, alt, baseName) {
143+
lightboxImg.src = getOriginalPath(src, baseName);
144+
lightboxImg.alt = alt;
145+
146+
if (baseName && year && slug) {
147+
lightboxImg.onerror = () => {
148+
lightboxImg.src = `/originals/${year}/${slug}/${baseName}.jpg`;
149+
lightboxImg.onerror = () => {
150+
lightboxImg.src = src;
151+
};
152+
};
153+
}
154+
}
155+
156+
// Function to update navigation state
157+
function updateNavigation() {
158+
prevBtn.classList.toggle('disabled', currentIndex === 0);
159+
nextBtn.classList.toggle('disabled', currentIndex === proseImages.length - 1);
160+
counter.textContent = `${currentIndex + 1} / ${proseImages.length}`;
161+
}
162+
163+
// Function to show image at index
164+
function showImage(index) {
165+
if (index < 0 || index >= proseImages.length) return;
166+
currentIndex = index;
167+
const img = proseImages[index];
168+
const src = img.getAttribute('src') || '';
169+
const alt = img.getAttribute('alt') || '';
170+
const match = src.match(/\/_astro\/([^.]+)\./);
171+
const baseName = match ? match[1] : null;
172+
loadImage(src, alt, baseName);
173+
updateNavigation();
174+
}
175+
176+
// Navigation handlers
177+
prevBtn.addEventListener('click', (e) => {
178+
e.stopPropagation();
179+
if (currentIndex > 0) showImage(currentIndex - 1);
180+
});
181+
182+
nextBtn.addEventListener('click', (e) => {
183+
e.stopPropagation();
184+
if (currentIndex < proseImages.length - 1) showImage(currentIndex + 1);
185+
});
116186

117187
// Close lightbox on overlay click (but not container), close button, or Escape key
118188
lightbox.addEventListener('click', (e) => {
@@ -122,52 +192,26 @@ const year = date.getFullYear();
122192
});
123193
// Prevent closing when clicking the container
124194
container.addEventListener('click', (e) => e.stopPropagation());
195+
196+
// Keyboard navigation
125197
document.addEventListener('keydown', (e) => {
198+
if (!lightbox.classList.contains('active')) return;
126199
if (e.key === 'Escape') lightbox.classList.remove('active');
200+
if (e.key === 'ArrowLeft' && currentIndex > 0) showImage(currentIndex - 1);
201+
if (e.key === 'ArrowRight' && currentIndex < proseImages.length - 1) showImage(currentIndex + 1);
127202
});
128203

129-
// Get year and slug from data attributes
130-
const article = document.querySelector('article[data-year][data-slug]');
131-
const year = article?.dataset.year;
132-
const slug = article?.dataset.slug;
133-
134204
// Make prose images clickable to open lightbox with original
135-
document.querySelectorAll('.prose img').forEach((img) => {
136-
// Skip if already wrapped in a link
137-
if (img.parentElement?.tagName === 'A') return;
138-
205+
proseImages.forEach((img, index) => {
139206
img.addEventListener('click', () => {
140-
// Extract the filename from the optimized src
207+
currentIndex = index;
141208
const src = img.getAttribute('src') || '';
142209
const alt = img.getAttribute('alt') || '';
143-
144-
// Try to find the original image
145-
// The optimized path is like /_astro/filename.hash.webp
146-
// We need to map it back to /originals/YEAR/SLUG/filename.png
147210
const match = src.match(/\/_astro\/([^.]+)\./);
148-
if (match && year && slug) {
149-
const baseName = match[1];
150-
// Try to load the original
151-
const originalPath = `/originals/${year}/${slug}/${baseName}.png`;
152-
lightboxImg.src = originalPath;
153-
lightboxImg.alt = alt;
154-
lightbox.classList.add('active');
155-
156-
// Fallback to optimized if original fails
157-
lightboxImg.onerror = () => {
158-
// Try jpg
159-
lightboxImg.src = `/originals/${year}/${slug}/${baseName}.jpg`;
160-
lightboxImg.onerror = () => {
161-
// Fall back to optimized version
162-
lightboxImg.src = src;
163-
};
164-
};
165-
} else {
166-
// Use the src as-is if we can't parse it
167-
lightboxImg.src = src;
168-
lightboxImg.alt = alt;
169-
lightbox.classList.add('active');
170-
}
211+
const baseName = match ? match[1] : null;
212+
loadImage(src, alt, baseName);
213+
updateNavigation();
214+
lightbox.classList.add('active');
171215
});
172216
});
173217
</script>

src/styles/global.css

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,49 @@ code {
219219
font-size: 0.75rem;
220220
}
221221

222+
/* Lightbox navigation arrows */
223+
.lightbox-nav {
224+
position: absolute;
225+
top: 50%;
226+
transform: translateY(-50%);
227+
background: var(--color-primary);
228+
border: none;
229+
color: white;
230+
font-size: 2rem;
231+
width: 3rem;
232+
height: 3rem;
233+
border-radius: 50%;
234+
cursor: pointer;
235+
display: flex;
236+
align-items: center;
237+
justify-content: center;
238+
transition: background 0.2s ease, transform 0.2s ease, opacity 0.2s ease;
239+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
240+
z-index: 10;
241+
}
242+
243+
.lightbox-nav:hover:not(.disabled) {
244+
background: var(--color-primary-hover);
245+
transform: translateY(-50%) scale(1.1);
246+
}
247+
248+
.lightbox-nav.disabled {
249+
opacity: 0.3;
250+
cursor: not-allowed;
251+
}
252+
253+
.lightbox-prev {
254+
left: 1rem;
255+
}
256+
257+
.lightbox-next {
258+
right: 4.5rem;
259+
}
260+
261+
.lightbox-counter {
262+
font-variant-numeric: tabular-nums;
263+
}
264+
222265
.prose h2 {
223266
margin-top: 2em;
224267
margin-bottom: 1em;

0 commit comments

Comments
 (0)