/** * Sunday Comics - Archive Lazy Loading * Implements infinite scroll for the archive page */ (function() { 'use strict'; let currentPage = 1; let isLoading = false; let hasMore = true; const perPage = 24; // Get elements const archiveContent = document.querySelector('.archive-content'); if (!archiveContent) return; // Not on archive page const totalComics = parseInt(archiveContent.dataset.totalComics || '0'); const initialBatch = parseInt(archiveContent.dataset.initialBatch || '24'); // Calculate if there are more comics to load hasMore = totalComics > initialBatch; // Create loading indicator const loadingIndicator = document.createElement('div'); loadingIndicator.className = 'archive-loading'; loadingIndicator.innerHTML = '

Loading more comics...

'; loadingIndicator.style.display = 'none'; loadingIndicator.style.textAlign = 'center'; loadingIndicator.style.padding = '2rem'; archiveContent.parentNode.insertBefore(loadingIndicator, archiveContent.nextSibling); /** * Load more comics from the API */ async function loadMoreComics() { if (isLoading || !hasMore) return; isLoading = true; loadingIndicator.style.display = 'block'; try { currentPage++; const response = await fetch(`/api/comics?page=${currentPage}&per_page=${perPage}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // Add new comics to the DOM appendComics(data.sections); // Update state hasMore = data.has_more; if (!hasMore) { loadingIndicator.innerHTML = '

End of archive

'; setTimeout(() => { loadingIndicator.style.display = 'none'; }, 2000); } } catch (error) { console.error('Error loading more comics:', error); loadingIndicator.innerHTML = '

Error loading comics. Please try again.

'; setTimeout(() => { loadingIndicator.style.display = 'none'; isLoading = false; }, 3000); return; } isLoading = false; loadingIndicator.style.display = 'none'; } /** * Append comics to the archive * @param {Array} sections - Array of section objects with title and comics */ function appendComics(sections) { const archiveFullWidth = document.querySelector('.archive-content-fullwidth') !== null; const sectionsEnabled = document.querySelector('.section-header') !== null; sections.forEach(section => { const sectionTitle = section.section_title; const comics = section.comics; // Check if we need to create a new section or append to existing let targetGrid; if (sectionsEnabled && sectionTitle) { // Check if section already exists const existingSection = findSectionByTitle(sectionTitle); if (existingSection) { // Append to existing section grid targetGrid = existingSection.querySelector('.archive-grid'); } else { // Create new section const sectionHeader = document.createElement('div'); sectionHeader.className = 'section-header'; sectionHeader.innerHTML = `

${sectionTitle}

`; archiveContent.appendChild(sectionHeader); targetGrid = document.createElement('div'); targetGrid.className = 'archive-grid' + (archiveFullWidth ? ' archive-grid-fullwidth' : ''); archiveContent.appendChild(targetGrid); } } else { // No sections or no title - use the last grid or create one targetGrid = archiveContent.querySelector('.archive-grid:last-of-type'); if (!targetGrid) { targetGrid = document.createElement('div'); targetGrid.className = 'archive-grid' + (archiveFullWidth ? ' archive-grid-fullwidth' : ''); archiveContent.appendChild(targetGrid); } } // Add each comic to the grid comics.forEach(comic => { const item = createArchiveItem(comic, archiveFullWidth); targetGrid.appendChild(item); }); }); } /** * Find an existing section by title * @param {string} title - Section title to find * @returns {Element|null} - The section element or null */ function findSectionByTitle(title) { const sectionHeaders = archiveContent.querySelectorAll('.section-header h2'); for (const header of sectionHeaders) { if (header.textContent.trim() === title) { // Return the grid following this header let nextEl = header.parentElement.nextElementSibling; while (nextEl && !nextEl.classList.contains('archive-grid')) { nextEl = nextEl.nextElementSibling; } return nextEl ? nextEl.parentElement : null; } } return null; } /** * Create an archive item element * @param {Object} comic - Comic data * @param {boolean} fullWidth - Whether using full width layout * @returns {Element} - The archive item element */ function createArchiveItem(comic, fullWidth) { const item = document.createElement('div'); item.className = 'archive-item' + (fullWidth ? ' archive-item-fullwidth' : ''); const link = document.createElement('a'); link.href = `/comic/${comic.number}`; const img = document.createElement('img'); img.src = `/static/images/thumbs/${comic.filename}`; img.alt = comic.title || `#${comic.number}`; img.loading = 'lazy'; img.onerror = function() { this.onerror = null; this.src = '/static/images/thumbs/default.jpg'; }; const info = document.createElement('div'); info.className = 'archive-info'; if (!fullWidth) { const title = document.createElement('h3'); title.textContent = `#${comic.number}${comic.title ? ': ' + comic.title : ''}`; info.appendChild(title); } const date = document.createElement('p'); date.className = 'archive-date'; date.textContent = comic.date; info.appendChild(date); link.appendChild(img); link.appendChild(info); item.appendChild(link); return item; } /** * Check if user has scrolled near the bottom */ function checkScrollPosition() { if (isLoading || !hasMore) return; const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const windowHeight = window.innerHeight; const documentHeight = document.documentElement.scrollHeight; // Trigger when user is within 1000px of the bottom if (scrollTop + windowHeight >= documentHeight - 1000) { loadMoreComics(); } } // Set up scroll listener let scrollTimeout; window.addEventListener('scroll', function() { if (scrollTimeout) { clearTimeout(scrollTimeout); } scrollTimeout = setTimeout(checkScrollPosition, 100); }); // Check initial scroll position (in case page is short) setTimeout(checkScrollPosition, 500); })();