diff --git a/src/layouts/PostLayout.astro b/src/layouts/PostLayout.astro
index 96b7cd6..82f306e 100644
--- a/src/layouts/PostLayout.astro
+++ b/src/layouts/PostLayout.astro
@@ -100,12 +100,14 @@ const year = date.getFullYear();
Full-size image
Click outside or press Esc to close
+
+
`;
document.body.appendChild(lightbox);
@@ -113,6 +115,74 @@ const year = date.getFullYear();
const lightboxImg = lightbox.querySelector('.lightbox-container img');
const closeBtn = lightbox.querySelector('.lightbox-close');
const container = lightbox.querySelector('.lightbox-container');
+ const prevBtn = lightbox.querySelector('.lightbox-prev');
+ const nextBtn = lightbox.querySelector('.lightbox-next');
+ const counter = lightbox.querySelector('.lightbox-counter');
+
+ // Get year and slug from data attributes
+ const article = document.querySelector('article[data-year][data-slug]');
+ const year = article?.dataset.year;
+ const slug = article?.dataset.slug;
+
+ // Collect all prose images (excluding those wrapped in links)
+ const proseImages = Array.from(document.querySelectorAll('.prose img')).filter(
+ (img) => img.parentElement?.tagName !== 'A'
+ );
+ let currentIndex = 0;
+
+ // Function to get original image path
+ function getOriginalPath(src, baseName) {
+ if (year && slug && baseName) {
+ return `/originals/${year}/${slug}/${baseName}.png`;
+ }
+ return src;
+ }
+
+ // Function to load image with fallbacks
+ function loadImage(src, alt, baseName) {
+ lightboxImg.src = getOriginalPath(src, baseName);
+ lightboxImg.alt = alt;
+
+ if (baseName && year && slug) {
+ lightboxImg.onerror = () => {
+ lightboxImg.src = `/originals/${year}/${slug}/${baseName}.jpg`;
+ lightboxImg.onerror = () => {
+ lightboxImg.src = src;
+ };
+ };
+ }
+ }
+
+ // Function to update navigation state
+ function updateNavigation() {
+ prevBtn.classList.toggle('disabled', currentIndex === 0);
+ nextBtn.classList.toggle('disabled', currentIndex === proseImages.length - 1);
+ counter.textContent = `${currentIndex + 1} / ${proseImages.length}`;
+ }
+
+ // Function to show image at index
+ function showImage(index) {
+ if (index < 0 || index >= proseImages.length) return;
+ currentIndex = index;
+ const img = proseImages[index];
+ const src = img.getAttribute('src') || '';
+ const alt = img.getAttribute('alt') || '';
+ const match = src.match(/\/_astro\/([^.]+)\./);
+ const baseName = match ? match[1] : null;
+ loadImage(src, alt, baseName);
+ updateNavigation();
+ }
+
+ // Navigation handlers
+ prevBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ if (currentIndex > 0) showImage(currentIndex - 1);
+ });
+
+ nextBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ if (currentIndex < proseImages.length - 1) showImage(currentIndex + 1);
+ });
// Close lightbox on overlay click (but not container), close button, or Escape key
lightbox.addEventListener('click', (e) => {
@@ -122,52 +192,26 @@ const year = date.getFullYear();
});
// Prevent closing when clicking the container
container.addEventListener('click', (e) => e.stopPropagation());
+
+ // Keyboard navigation
document.addEventListener('keydown', (e) => {
+ if (!lightbox.classList.contains('active')) return;
if (e.key === 'Escape') lightbox.classList.remove('active');
+ if (e.key === 'ArrowLeft' && currentIndex > 0) showImage(currentIndex - 1);
+ if (e.key === 'ArrowRight' && currentIndex < proseImages.length - 1) showImage(currentIndex + 1);
});
- // Get year and slug from data attributes
- const article = document.querySelector('article[data-year][data-slug]');
- const year = article?.dataset.year;
- const slug = article?.dataset.slug;
-
// Make prose images clickable to open lightbox with original
- document.querySelectorAll('.prose img').forEach((img) => {
- // Skip if already wrapped in a link
- if (img.parentElement?.tagName === 'A') return;
-
+ proseImages.forEach((img, index) => {
img.addEventListener('click', () => {
- // Extract the filename from the optimized src
+ currentIndex = index;
const src = img.getAttribute('src') || '';
const alt = img.getAttribute('alt') || '';
-
- // Try to find the original image
- // The optimized path is like /_astro/filename.hash.webp
- // We need to map it back to /originals/YEAR/SLUG/filename.png
const match = src.match(/\/_astro\/([^.]+)\./);
- if (match && year && slug) {
- const baseName = match[1];
- // Try to load the original
- const originalPath = `/originals/${year}/${slug}/${baseName}.png`;
- lightboxImg.src = originalPath;
- lightboxImg.alt = alt;
- lightbox.classList.add('active');
-
- // Fallback to optimized if original fails
- lightboxImg.onerror = () => {
- // Try jpg
- lightboxImg.src = `/originals/${year}/${slug}/${baseName}.jpg`;
- lightboxImg.onerror = () => {
- // Fall back to optimized version
- lightboxImg.src = src;
- };
- };
- } else {
- // Use the src as-is if we can't parse it
- lightboxImg.src = src;
- lightboxImg.alt = alt;
- lightbox.classList.add('active');
- }
+ const baseName = match ? match[1] : null;
+ loadImage(src, alt, baseName);
+ updateNavigation();
+ lightbox.classList.add('active');
});
});
diff --git a/src/styles/global.css b/src/styles/global.css
index 7267f77..a4210b8 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -219,6 +219,49 @@ code {
font-size: 0.75rem;
}
+/* Lightbox navigation arrows */
+.lightbox-nav {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ background: var(--color-primary);
+ border: none;
+ color: white;
+ font-size: 2rem;
+ width: 3rem;
+ height: 3rem;
+ border-radius: 50%;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.2s ease, transform 0.2s ease, opacity 0.2s ease;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+ z-index: 10;
+}
+
+.lightbox-nav:hover:not(.disabled) {
+ background: var(--color-primary-hover);
+ transform: translateY(-50%) scale(1.1);
+}
+
+.lightbox-nav.disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+}
+
+.lightbox-prev {
+ left: 1rem;
+}
+
+.lightbox-next {
+ right: 4.5rem;
+}
+
+.lightbox-counter {
+ font-variant-numeric: tabular-nums;
+}
+
.prose h2 {
margin-top: 2em;
margin-bottom: 1em;