diff --git a/app.py b/app.py index 7baf3bb..9f96d91 100644 --- a/app.py +++ b/app.py @@ -262,14 +262,22 @@ def group_comics_by_section(comics_list): @app.route('/archive') def archive(): """Archive page showing all comics""" + # Initial batch size for server-side rendering + initial_batch = 24 + # Reverse order to show newest first - comics = [enrich_comic(comic) for comic in reversed(COMICS)] + all_comics = [enrich_comic(comic) for comic in reversed(COMICS)] + + # Only take the first batch for initial render + initial_comics = all_comics[:initial_batch] # Group by section if enabled - sections = group_comics_by_section(comics) + sections = group_comics_by_section(initial_comics) return render_template('archive.html', title='Archive', - sections=sections) + sections=sections, + total_comics=len(COMICS), + initial_batch=initial_batch) @app.route('/about') @@ -323,6 +331,63 @@ def api_comic(comic_id): return jsonify(comic) +@app.route('/api/archive') +def api_archive(): + """API endpoint - returns paginated archive data""" + page = request.args.get('page', 1, type=int) + per_page = request.args.get('per_page', 24, type=int) + + # Limit per_page to reasonable values + per_page = min(max(per_page, 1), 100) + + # Reverse order to show newest first + all_comics = [enrich_comic(comic) for comic in reversed(COMICS)] + + # Group by section if enabled + sections = group_comics_by_section(all_comics) + + # Calculate pagination + total_comics = len(all_comics) + start_idx = (page - 1) * per_page + end_idx = start_idx + per_page + + # Handle section-aware pagination + result_sections = [] + current_idx = 0 + + for section_title, section_comics in sections: + section_start = current_idx + section_end = current_idx + len(section_comics) + + # Check if this section overlaps with our requested page + if section_end > start_idx and section_start < end_idx: + # Calculate which comics from this section to include + comics_start = max(0, start_idx - section_start) + comics_end = min(len(section_comics), end_idx - section_start) + + paginated_comics = section_comics[comics_start:comics_end] + + if paginated_comics: + result_sections.append({ + 'section_title': section_title, + 'comics': paginated_comics + }) + + current_idx = section_end + + # Stop if we've gone past the requested range + if current_idx >= end_idx: + break + + return jsonify({ + 'sections': result_sections, + 'page': page, + 'per_page': per_page, + 'total_comics': total_comics, + 'has_more': end_idx < total_comics + }) + + @app.route('/sitemap.xml') def sitemap(): """Serve the static sitemap.xml file""" diff --git a/static/js/archive-lazy-load.js b/static/js/archive-lazy-load.js new file mode 100644 index 0000000..7d3420c --- /dev/null +++ b/static/js/archive-lazy-load.js @@ -0,0 +1,221 @@ +/** + * 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/archive?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); + +})(); diff --git a/templates/archive.html b/templates/archive.html index f14d5f5..4a344fd 100644 --- a/templates/archive.html +++ b/templates/archive.html @@ -7,10 +7,12 @@

Comic Archive

-

Browse all {% set total = namespace(count=0) %}{% for section_title, section_comics in sections %}{% set total.count = total.count + section_comics|length %}{% endfor %}{{ total.count }} comics

+

Browse all {{ total_comics }} comics

-
+
{% for section_title, section_comics in sections %} {% if section_title and sections_enabled %}
@@ -43,3 +45,7 @@
{# Reopen container for footer #} {% endif %} {% endblock %} + +{% block extra_js %} + +{% endblock %}