From 9a3b35b428fdb785bdf361582e3e6f94e324227f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Tue, 17 Feb 2026 19:07:11 +0100 Subject: [PATCH] feat: Implement infinite scroll with preloader --- assets/js/main.js | 119 +++++++++++++++++++++++++++++++++++++++++++--- default.hbs | 2 + index.hbs | 8 +++- 3 files changed, 120 insertions(+), 9 deletions(-) diff --git a/assets/js/main.js b/assets/js/main.js index 616d9b9..52dcdea 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -6,9 +6,9 @@ document.addEventListener('DOMContentLoaded', () => { // Remove 'hidden' class from html to prevent FOUC htmlElement.classList.remove('hidden'); - // Staggered fade-in animation for post grid - const gridItems = document.querySelectorAll('.post-grid-item'); - gridItems.forEach((item, index) => { + // Staggered fade-in animation for post grid (initial load) + const initialGridItems = document.querySelectorAll('.post-grid-item'); + initialGridItems.forEach((item, index) => { item.style.animationDelay = `${index * 100}ms`; item.classList.add('animate-fadeInUp'); }); @@ -19,16 +19,18 @@ document.addEventListener('DOMContentLoaded', () => { const lightboxImage = document.getElementById('lightbox-image'); const lightboxClose = document.getElementById('lightbox-close'); - const images = document.querySelectorAll('.kg-image-card img, .post-content img'); - - images.forEach(image => { + const setupLightboxImage = (image) => { image.style.cursor = 'pointer'; image.addEventListener('click', () => { lightbox.classList.remove('hidden'); lightbox.classList.add('show'); lightboxImage.src = image.src; }); - }); + }; + + // Initial setup for existing images + document.querySelectorAll('.kg-image-card img, .post-content img').forEach(setupLightboxImage); + lightboxClose.addEventListener('click', () => { lightbox.classList.add('hidden'); @@ -90,4 +92,107 @@ document.addEventListener('DOMContentLoaded', () => { }); }); } + + // Infinite Scroll + const postsContainer = document.getElementById('posts-container'); + const loadingSpinner = document.getElementById('loading-spinner'); + + if (postsContainer && loadingSpinner) { + let currentPage = 1; + let isLoading = false; + let hasMorePosts = true; + + const ghostApiKeyMeta = document.querySelector('meta[name="ghost-api-key"]'); + const ghostApiUrlMeta = document.querySelector('meta[name="ghost-api-url"]'); + + if (!ghostApiKeyMeta || !ghostApiUrlMeta) { + console.error('Ghost Content API Key or URL meta tag not found. Infinite scroll will not work.'); + return; + } + + const GHOST_API_KEY = ghostApiKeyMeta.content; + const GHOST_API_URL = ghostApiUrlMeta.content; + + const fetchPosts = async () => { + if (isLoading || !hasMorePosts) return; + + isLoading = true; + loadingSpinner.classList.remove('hidden'); + + currentPage++; + const postsPerPage = 12; // Matches posts_per_page in package.json config + const url = `${GHOST_API_URL}/ghost/api/content/posts/?key=${GHOST_API_KEY}&limit=${postsPerPage}&page=${currentPage}&include=tags,authors`; + + try { + const response = await fetch(url); + const data = await response.json(); + + const newPosts = data.posts; + const pagination = data.meta.pagination; + + if (newPosts.length > 0) { + newPosts.forEach((post, index) => { + const article = document.createElement('article'); + article.className = 'post-grid-item opacity-0 relative bg-[var(--bg-secondary)] rounded-lg shadow-lg overflow-hidden group'; + + // Basic rendering of a post for infinite scroll. + // This should ideally match the structure in index.hbs + // For simplicity, I'm reconstructing it here. + article.innerHTML = ` + + ${post.feature_image ? ` + ${post.title} + ` : ` +
No Image
+ `} +
+

${post.title}

+
+
+ `; + postsContainer.appendChild(article); + + // Apply staggered animation to newly added posts + // Calculate delay based on total items + const totalItems = postsContainer.children.length; + article.style.animationDelay = `${(totalItems - newPosts.length + index) * 100}ms`; + article.classList.add('animate-fadeInUp'); + + // If lightbox is present, set up new images for lightbox + if (lightbox) { + article.querySelectorAll('.kg-image-card img, .post-content img').forEach(setupLightboxImage); + } + }); + } else { + hasMorePosts = false; + } + + if (pagination && pagination.next === null) { + hasMorePosts = false; + } + + } catch (error) { + console.error('Error fetching posts:', error); + hasMorePosts = false; // Stop trying to fetch if there's an error + } finally { + isLoading = false; + loadingSpinner.classList.add('hidden'); + } + }; + + const observer = new IntersectionObserver(entries => { + entries.forEach(entry => { + if (entry.isIntersecting && hasMorePosts && !isLoading) { + fetchPosts(); + } + }); + }, { threshold: 0.1 }); // Trigger when 10% of the spinner is visible + + observer.observe(loadingSpinner); + } }); diff --git a/default.hbs b/default.hbs index edbfcd9..40f1041 100644 --- a/default.hbs +++ b/default.hbs @@ -6,6 +6,8 @@ {{meta_title}} + + {{ghost_head}} diff --git a/index.hbs b/index.hbs index 5298d6b..4735e23 100644 --- a/index.hbs +++ b/index.hbs @@ -7,7 +7,7 @@
{{#if posts}} -