:lightning: client-side nav
This commit is contained in:
@@ -5,7 +5,10 @@ A Flask-based webcomic website with server-side rendering using Jinja2 templates
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Comic viewer with navigation (First, Previous, Next, Latest)
|
- Comic viewer with navigation (First, Previous, Next, Latest)
|
||||||
|
- Client-side navigation using JSON API (no page reloads)
|
||||||
- Archive page with thumbnail grid
|
- Archive page with thumbnail grid
|
||||||
|
- RSS feed support
|
||||||
|
- JSON API for programmatic access
|
||||||
- Responsive design
|
- Responsive design
|
||||||
- Server-side rendering with Jinja2
|
- Server-side rendering with Jinja2
|
||||||
- Clean, comic-focused layout
|
- Clean, comic-focused layout
|
||||||
@@ -33,6 +36,8 @@ sunday/
|
|||||||
└── static/ # Static files
|
└── static/ # Static files
|
||||||
├── css/
|
├── css/
|
||||||
│ └── style.css # Main stylesheet
|
│ └── style.css # Main stylesheet
|
||||||
|
├── js/
|
||||||
|
│ └── comic-nav.js # Client-side navigation
|
||||||
├── images/ # Comic images
|
├── images/ # Comic images
|
||||||
│ └── thumbs/ # Thumbnail images for archive
|
│ └── thumbs/ # Thumbnail images for archive
|
||||||
└── feed.rss # RSS feed (generated)
|
└── feed.rss # RSS feed (generated)
|
||||||
|
|||||||
151
static/js/comic-nav.js
Normal file
151
static/js/comic-nav.js
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
// Client-side comic navigation using the API
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
let totalComics = 0;
|
||||||
|
|
||||||
|
// Fetch and display a comic
|
||||||
|
async function loadComic(comicId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/comics/${comicId}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
window.location.href = '/404';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error('Failed to load comic');
|
||||||
|
}
|
||||||
|
|
||||||
|
const comic = response.json ? await response.json() : comic;
|
||||||
|
displayComic(comic);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading comic:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the page with comic data
|
||||||
|
function displayComic(comic) {
|
||||||
|
// Update title
|
||||||
|
const title = comic.title || `#${comic.number}`;
|
||||||
|
document.querySelector('.comic-header h1').textContent = title;
|
||||||
|
|
||||||
|
// Update date
|
||||||
|
document.querySelector('.comic-date').textContent = comic.date;
|
||||||
|
|
||||||
|
// Update image
|
||||||
|
const img = document.querySelector('.comic-image img');
|
||||||
|
img.src = `/static/images/${comic.filename}`;
|
||||||
|
img.alt = title;
|
||||||
|
img.title = comic.alt_text;
|
||||||
|
|
||||||
|
// Update author note
|
||||||
|
const transcriptDiv = document.querySelector('.comic-transcript');
|
||||||
|
if (comic.author_note) {
|
||||||
|
if (!transcriptDiv) {
|
||||||
|
const container = document.querySelector('.comic-container');
|
||||||
|
const newDiv = document.createElement('div');
|
||||||
|
newDiv.className = 'comic-transcript';
|
||||||
|
newDiv.innerHTML = '<h3>Author Note</h3><p></p>';
|
||||||
|
container.appendChild(newDiv);
|
||||||
|
}
|
||||||
|
document.querySelector('.comic-transcript p').textContent = comic.author_note;
|
||||||
|
document.querySelector('.comic-transcript').style.display = 'block';
|
||||||
|
} else if (transcriptDiv) {
|
||||||
|
transcriptDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update navigation buttons
|
||||||
|
updateNavButtons(comic.number);
|
||||||
|
|
||||||
|
// Update page title
|
||||||
|
document.title = `${title} - Sunday Comics`;
|
||||||
|
|
||||||
|
// Update URL without reload
|
||||||
|
history.pushState({ comicId: comic.number }, '', `/comic/${comic.number}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update navigation button states
|
||||||
|
function updateNavButtons(currentNumber) {
|
||||||
|
const navButtons = document.querySelector('.nav-buttons');
|
||||||
|
|
||||||
|
// First button
|
||||||
|
const firstBtn = navButtons.children[0];
|
||||||
|
if (currentNumber > 1) {
|
||||||
|
firstBtn.className = 'btn btn-nav';
|
||||||
|
firstBtn.onclick = (e) => { e.preventDefault(); loadComic(1); };
|
||||||
|
} else {
|
||||||
|
firstBtn.className = 'btn btn-nav btn-disabled';
|
||||||
|
firstBtn.onclick = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Previous button
|
||||||
|
const prevBtn = navButtons.children[1];
|
||||||
|
if (currentNumber > 1) {
|
||||||
|
prevBtn.className = 'btn btn-nav';
|
||||||
|
prevBtn.onclick = (e) => { e.preventDefault(); loadComic(currentNumber - 1); };
|
||||||
|
} else {
|
||||||
|
prevBtn.className = 'btn btn-nav btn-disabled';
|
||||||
|
prevBtn.onclick = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comic number display
|
||||||
|
navButtons.children[2].textContent = `Comic #${currentNumber}`;
|
||||||
|
|
||||||
|
// Next button
|
||||||
|
const nextBtn = navButtons.children[3];
|
||||||
|
if (currentNumber < totalComics) {
|
||||||
|
nextBtn.className = 'btn btn-nav';
|
||||||
|
nextBtn.onclick = (e) => { e.preventDefault(); loadComic(currentNumber + 1); };
|
||||||
|
} else {
|
||||||
|
nextBtn.className = 'btn btn-nav btn-disabled';
|
||||||
|
nextBtn.onclick = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Latest button
|
||||||
|
const latestBtn = navButtons.children[4];
|
||||||
|
if (currentNumber < totalComics) {
|
||||||
|
latestBtn.className = 'btn btn-nav';
|
||||||
|
latestBtn.onclick = (e) => { e.preventDefault(); loadComic(totalComics); };
|
||||||
|
} else {
|
||||||
|
latestBtn.className = 'btn btn-nav btn-disabled';
|
||||||
|
latestBtn.onclick = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
async function init() {
|
||||||
|
// Only run on comic pages
|
||||||
|
if (!document.querySelector('.comic-container')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total comics count from the page
|
||||||
|
totalComics = parseInt(document.querySelector('.comic-container').dataset.totalComics || 0);
|
||||||
|
|
||||||
|
// Get current comic number
|
||||||
|
const currentNumber = parseInt(document.querySelector('.comic-container').dataset.comicNumber || 0);
|
||||||
|
|
||||||
|
if (currentNumber && totalComics) {
|
||||||
|
updateNavButtons(currentNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle browser back/forward
|
||||||
|
window.addEventListener('popstate', (event) => {
|
||||||
|
if (event.state && event.state.comicId) {
|
||||||
|
loadComic(event.state.comicId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set initial state
|
||||||
|
history.replaceState({ comicId: currentNumber }, '', window.location.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -61,6 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/comic-nav.js') }}"></script>
|
||||||
{% block extra_js %}{% endblock %}
|
{% block extra_js %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="comic-container">
|
<div class="comic-container" data-comic-number="{{ comic.number }}" data-total-comics="{{ total_comics }}">
|
||||||
<div class="comic-header">
|
<div class="comic-header">
|
||||||
<h1>{{ comic.title if comic.title else '#' ~ comic.number }}</h1>
|
<h1>{{ comic.title if comic.title else '#' ~ comic.number }}</h1>
|
||||||
<p class="comic-date">{{ comic.date }}</p>
|
<p class="comic-date">{{ comic.date }}</p>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="comic-container">
|
<div class="comic-container" data-comic-number="{{ comic.number }}" data-total-comics="{{ total_comics }}">
|
||||||
<div class="comic-header">
|
<div class="comic-header">
|
||||||
<h1>{{ comic.title if comic.title else '#' ~ comic.number }}</h1>
|
<h1>{{ comic.title if comic.title else '#' ~ comic.number }}</h1>
|
||||||
<p class="comic-date">{{ comic.date }}</p>
|
<p class="comic-date">{{ comic.date }}</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user