:lightning: lazy load archive
This commit is contained in:
71
app.py
71
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"""
|
||||
|
||||
221
static/js/archive-lazy-load.js
Normal file
221
static/js/archive-lazy-load.js
Normal file
@@ -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 = '<p>Loading more comics...</p>';
|
||||
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 = '<p>End of archive</p>';
|
||||
setTimeout(() => {
|
||||
loadingIndicator.style.display = 'none';
|
||||
}, 2000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading more comics:', error);
|
||||
loadingIndicator.innerHTML = '<p>Error loading comics. Please try again.</p>';
|
||||
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 = `<h2>${sectionTitle}</h2>`;
|
||||
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);
|
||||
|
||||
})();
|
||||
@@ -7,10 +7,12 @@
|
||||
|
||||
<div class="page-header{% if archive_full_width %} page-header-fullwidth{% endif %}">
|
||||
<h1>Comic Archive</h1>
|
||||
<p>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</p>
|
||||
<p>Browse all {{ total_comics }} comics</p>
|
||||
</div>
|
||||
|
||||
<section class="archive-content{% if archive_full_width %} archive-content-fullwidth{% endif %}">
|
||||
<section class="archive-content{% if archive_full_width %} archive-content-fullwidth{% endif %}"
|
||||
data-total-comics="{{ total_comics }}"
|
||||
data-initial-batch="{{ initial_batch }}">
|
||||
{% for section_title, section_comics in sections %}
|
||||
{% if section_title and sections_enabled %}
|
||||
<div class="section-header">
|
||||
@@ -43,3 +45,7 @@
|
||||
<div class="container"> {# Reopen container for footer #}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{{ url_for('static', filename='js/archive-lazy-load.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user