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;