Files
kjvstudy.org/kjvstudy_org/templates/offline.html
T
kennethreitz 8f89fe9bfb Improve /offline page design and fix import error
Design improvements:
- Added Quick Access section with links to Books, Stories, Topics, etc.
- Moved technical debug info to collapsible "Technical Details" section
- Added styled quick link grid with hover effects
- Cleaner layout focusing on user-friendly features

Bug fix:
- Fixed import error: use load_all_stories() instead of get_all_stories()

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 01:43:36 -05:00

803 lines
32 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Offline Mode - KJV Study</title>
<link rel="stylesheet" href="/static/tufte.css"/>
<link rel="manifest" href="/static/manifest.json"/>
<style>
:root {
--bg-color: #fffff8;
--text-color: #111;
--text-secondary: #666;
--border-color: #ddd;
--link-color: #333;
--success-color: #4a7c59;
--error-color: #c41e3a;
}
[data-theme="dark"] {
--bg-color: #1a1a1a;
--text-color: #e0e0e0;
--text-secondary: #b0b0b0;
--border-color: #444;
--link-color: #d0d0d0;
}
body {
background: var(--bg-color);
color: var(--text-color);
max-width: 900px;
margin: 0 auto;
padding: 2rem;
}
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
h2 { font-size: 1.4rem; margin-top: 2rem; border-bottom: 1px solid var(--border-color); padding-bottom: 0.5rem; }
h3 { font-size: 1.1rem; margin-top: 1.5rem; color: var(--text-secondary); }
.subtitle { color: var(--text-secondary); font-style: italic; margin-bottom: 2rem; }
.badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
margin-left: 0.5rem;
vertical-align: middle;
}
.badge-offline { background: var(--error-color); color: white; }
.badge-online { background: var(--success-color); color: white; }
.debug-section {
background: var(--border-color);
padding: 1rem;
border-radius: 4px;
margin: 1rem 0;
font-family: monospace;
font-size: 0.85rem;
}
.debug-section dt { font-weight: bold; margin-top: 0.5rem; }
.debug-section dd { margin-left: 1rem; color: var(--text-secondary); }
.status-ok { color: var(--success-color); }
.status-error { color: var(--error-color); }
.cached-links {
column-count: 3;
column-gap: 2rem;
margin: 1rem 0;
}
@media (max-width: 768px) {
.cached-links { column-count: 2; }
}
@media (max-width: 480px) {
.cached-links { column-count: 1; }
}
.cached-links a {
display: block;
padding: 0.25rem 0;
color: var(--link-color);
text-decoration: none;
font-size: 0.9rem;
}
.cached-links a:hover {
text-decoration: underline;
}
.category {
break-inside: avoid;
margin-bottom: 1rem;
}
.category-title {
font-weight: bold;
font-size: 0.85rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.25rem;
}
.nav-section {
display: flex;
gap: 1rem;
margin: 2rem 0;
flex-wrap: wrap;
align-items: center;
}
select {
padding: 0.5rem;
font-size: 1rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-color);
color: var(--text-color);
font-family: inherit;
}
.chapter-nav {
display: flex;
gap: 0.5rem;
}
.nav-btn {
padding: 0.5rem 1rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-color);
color: var(--text-color);
cursor: pointer;
font-size: 1rem;
}
.nav-btn:hover { background: var(--border-color); }
.nav-btn:disabled { opacity: 0.5; cursor: not-allowed; }
#chapter-content {
line-height: 2;
font-size: 1.2rem;
display: none;
}
#chapter-content.active {
display: block;
}
#chapter-content p {
margin: 1rem 0;
}
.verse-num {
font-size: 0.9rem;
vertical-align: super;
margin-right: 0.25rem;
color: var(--text-secondary);
}
.loading {
text-align: center;
padding: 2rem;
color: var(--text-secondary);
}
#keyboard-hint {
font-size: 0.85rem;
color: var(--text-secondary);
margin-top: 1rem;
font-style: italic;
}
.back-link {
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
}
.back-link a {
color: var(--link-color);
}
#cached-count {
font-size: 1.5rem;
font-weight: bold;
color: var(--success-color);
}
.cache-controls {
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
padding: 1.5rem;
border-radius: 8px;
margin: 1.5rem 0;
border: 1px solid #bae6fd;
}
[data-theme="dark"] .cache-controls {
background: linear-gradient(135deg, #1e3a5f 0%, #1e293b 100%);
border-color: #334155;
}
.cache-controls h3 {
margin: 0 0 0.5rem 0;
color: var(--text-color);
}
.cache-controls p {
margin: 0 0 1rem 0;
color: var(--text-secondary);
font-size: 0.95rem;
}
.cache-btn {
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
border: none;
border-radius: 6px;
background: var(--success-color);
color: white;
cursor: pointer;
transition: all 0.2s;
}
.cache-btn:hover {
filter: brightness(1.1);
transform: translateY(-1px);
}
.cache-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.cache-progress {
margin-top: 1rem;
display: none;
}
.cache-progress.active {
display: block;
}
.progress-bar {
width: 100%;
height: 8px;
background: var(--border-color);
border-radius: 4px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.progress-fill {
height: 100%;
background: var(--success-color);
width: 0%;
transition: width 0.3s ease;
}
.progress-text {
font-size: 0.85rem;
color: var(--text-secondary);
}
.cache-complete {
color: var(--success-color);
font-weight: 600;
}
.quick-links {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin: 1.5rem 0;
}
.quick-link {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 1rem;
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 8px;
text-decoration: none;
color: var(--text-color);
font-weight: 500;
transition: all 0.2s;
}
.quick-link:hover {
border-color: var(--success-color);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.debug-details {
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid var(--border-color);
}
.debug-details summary {
cursor: pointer;
color: var(--text-secondary);
font-size: 0.9rem;
padding: 0.5rem 0;
}
.debug-details summary:hover {
color: var(--text-color);
}
.debug-details h3 {
margin-top: 1.5rem;
font-size: 1rem;
color: var(--text-secondary);
}
</style>
</head>
<body>
<h1>Offline Mode <span class="badge" id="connection-badge">checking...</span></h1>
<p class="subtitle">KJV Study - Available without internet</p>
<div class="cache-controls">
<h3>Enable Offline Access</h3>
<p>Download the entire site for offline use: all Bible chapters, individual verse pages with commentary, stories, study guides, and resources. The sitemap will be parsed to find all available pages.</p>
<button class="cache-btn" id="start-cache-btn">Download for Offline Use</button>
<div class="cache-progress" id="cache-progress">
<div class="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div class="progress-text" id="progress-text">Preparing...</div>
</div>
</div>
<h2>Quick Access</h2>
<p>Jump directly to commonly used sections:</p>
<div class="quick-links">
<a href="/books" class="quick-link">📖 All Books</a>
<a href="/stories" class="quick-link">📚 Bible Stories</a>
<a href="/topics" class="quick-link">🏷️ Topics</a>
<a href="/reading-plans" class="quick-link">📅 Reading Plans</a>
<a href="/study-guides" class="quick-link">📝 Study Guides</a>
<a href="/resources" class="quick-link">📂 Resources</a>
</div>
<h2>Bible Reader</h2>
<p>Read any chapter using cached Bible data (works even without any downloads):</p>
<div class="nav-section">
<select id="book-select">
<option value="">Select a book...</option>
</select>
<select id="chapter-select" disabled>
<option value="">Chapter</option>
</select>
<div class="chapter-nav">
<button class="nav-btn" id="prev-btn" disabled>← Prev</button>
<button class="nav-btn" id="next-btn" disabled>Next →</button>
</div>
</div>
<div id="chapter-content">
<p class="loading">Select a book and chapter above...</p>
</div>
<p id="keyboard-hint">Tip: Use ← → arrow keys to navigate chapters</p>
<div class="back-link">
<a href="/">← Back to KJV Study</a>
</div>
<details class="debug-details">
<summary>Technical Details</summary>
<div class="debug-section">
<dl>
<dt>Service Worker</dt>
<dd id="sw-status">Checking...</dd>
<dt>Cache Storage</dt>
<dd id="cache-status">Checking...</dd>
<dt>Bible Data</dt>
<dd id="bible-status">Checking...</dd>
<dt>Pages Cached</dt>
<dd id="pages-cached">Checking...</dd>
</dl>
</div>
<h3>Cached Pages <span id="cached-count">0</span></h3>
<div id="cached-links" class="cached-links">
<p class="loading">Scanning cache...</p>
</div>
</details>
<script>
(function() {
// Check theme preference
const theme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', theme);
// Debug elements
const connectionBadge = document.getElementById('connection-badge');
const swStatus = document.getElementById('sw-status');
const cacheStatus = document.getElementById('cache-status');
const bibleStatus = document.getElementById('bible-status');
const pagesCached = document.getElementById('pages-cached');
const cachedLinks = document.getElementById('cached-links');
const cachedCount = document.getElementById('cached-count');
// Update connection status
function updateConnectionStatus() {
if (navigator.onLine) {
connectionBadge.textContent = 'Online';
connectionBadge.className = 'badge badge-online';
} else {
connectionBadge.textContent = 'Offline';
connectionBadge.className = 'badge badge-offline';
}
}
updateConnectionStatus();
window.addEventListener('online', updateConnectionStatus);
window.addEventListener('offline', updateConnectionStatus);
// Check service worker status
async function checkServiceWorker() {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.getRegistration();
if (registration) {
const state = registration.active ? 'Active' :
registration.waiting ? 'Waiting' :
registration.installing ? 'Installing' : 'Unknown';
swStatus.innerHTML = '<span class="status-ok">✓ Registered (' + state + ')</span>';
swStatus.innerHTML += '<br>Scope: ' + registration.scope;
} else {
swStatus.innerHTML = '<span class="status-error">✗ Not registered</span>';
swStatus.innerHTML += '<br>Try refreshing the page or check if content blockers are blocking it.';
}
} catch (err) {
swStatus.innerHTML = '<span class="status-error">✗ Error: ' + err.message + '</span>';
}
} else {
swStatus.innerHTML = '<span class="status-error">✗ Not supported in this browser</span>';
}
}
// Check cache status and list cached pages
async function checkCaches() {
if ('caches' in window) {
try {
const cacheNames = await caches.keys();
if (cacheNames.length > 0) {
cacheStatus.innerHTML = '<span class="status-ok">✓ Available</span>';
cacheStatus.innerHTML += '<br>Caches: ' + cacheNames.join(', ');
} else {
cacheStatus.innerHTML = '<span class="status-error">✗ No caches found</span>';
}
// List all cached URLs
const allUrls = [];
for (const cacheName of cacheNames) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
keys.forEach(req => {
const url = new URL(req.url);
if (url.pathname && !url.pathname.startsWith('/static/')) {
allUrls.push(url.pathname);
}
});
}
// Remove duplicates and sort
const uniqueUrls = [...new Set(allUrls)].sort();
pagesCached.innerHTML = '<span class="status-ok">' + uniqueUrls.length + ' pages</span>';
cachedCount.textContent = uniqueUrls.length;
// Group by category
const categories = {
'Main': [],
'Books': [],
'Topics': [],
'Stories': [],
'Study Guides': [],
'Reading Plans': [],
'Parables': [],
'Resources': [],
'Other': []
};
uniqueUrls.forEach(url => {
if (url === '/' || url === '/books' || url === '/offline' || url === '/search') {
categories['Main'].push(url);
} else if (url.startsWith('/book/')) {
categories['Books'].push(url);
} else if (url.startsWith('/topics/')) {
categories['Topics'].push(url);
} else if (url.startsWith('/stories/')) {
categories['Stories'].push(url);
} else if (url.startsWith('/study-guides/')) {
categories['Study Guides'].push(url);
} else if (url.startsWith('/reading-plans/')) {
categories['Reading Plans'].push(url);
} else if (url.startsWith('/parables/')) {
categories['Parables'].push(url);
} else if (url.match(/^\/(christology|names-of-god|biblical-|fruits-|messianic-|miracles-|prayers-|spirits-|types-|women-|ten-|the-twelve|trinity|worship|grace|eschatology|ecclesiology|soteriology|pneumatology|hamartiology|anthropology|bibliology|theology-proper|justification|sanctification|providence|kingdom-of-god|law-and-gospel|armor-of-god|beatitudes|i-am-statements|names-of-christ|personifications|tetragrammaton|blood-in-scripture)/)) {
categories['Resources'].push(url);
} else {
categories['Other'].push(url);
}
});
// Render links
let html = '';
for (const [category, urls] of Object.entries(categories)) {
if (urls.length > 0) {
html += '<div class="category">';
html += '<div class="category-title">' + category + ' (' + urls.length + ')</div>';
urls.forEach(url => {
const label = url === '/' ? 'Home' : url.replace(/^\//, '').replace(/-/g, ' ');
html += '<a href="' + url + '">' + label + '</a>';
});
html += '</div>';
}
}
cachedLinks.innerHTML = html || '<p>No pages cached yet. Browse the site while online to cache pages.</p>';
} catch (err) {
cacheStatus.innerHTML = '<span class="status-error">✗ Error: ' + err.message + '</span>';
cachedLinks.innerHTML = '<p class="status-error">Could not read cache.</p>';
}
} else {
cacheStatus.innerHTML = '<span class="status-error">✗ Not supported</span>';
}
}
// Check Bible data
async function checkBibleData() {
try {
// Try cache first
let found = false;
const cacheNames = await caches.keys();
for (const cacheName of cacheNames) {
const cache = await caches.open(cacheName);
const response = await cache.match('/static/verses-1769.json');
if (response) {
const data = await response.json();
const verseCount = Object.keys(data).length;
bibleStatus.innerHTML = '<span class="status-ok">✓ Cached (' + verseCount.toLocaleString() + ' verses)</span>';
found = true;
break;
}
}
if (!found) {
bibleStatus.innerHTML = '<span class="status-error">✗ Not cached</span>';
bibleStatus.innerHTML += '<br>Visit the site while online to cache Bible data.';
}
} catch (err) {
bibleStatus.innerHTML = '<span class="status-error">✗ Error: ' + err.message + '</span>';
}
}
// Run all checks
checkServiceWorker();
checkCaches();
checkBibleData();
// Cache controls
const startCacheBtn = document.getElementById('start-cache-btn');
const cacheProgress = document.getElementById('cache-progress');
const progressFill = document.getElementById('progress-fill');
const progressText = document.getElementById('progress-text');
startCacheBtn.addEventListener('click', async function() {
if (!('serviceWorker' in navigator)) {
alert('Service workers are not supported in this browser.');
return;
}
const registration = await navigator.serviceWorker.getRegistration();
if (!registration || !registration.active) {
alert('Service worker is not active. Please refresh the page and try again.');
return;
}
// Disable button and show progress
startCacheBtn.disabled = true;
startCacheBtn.textContent = 'Downloading...';
cacheProgress.classList.add('active');
progressText.textContent = 'Starting download...';
// Send message to service worker to start caching
registration.active.postMessage({ type: 'START_CACHING' });
});
// Listen for progress messages from service worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', function(event) {
const data = event.data;
if (data.type === 'CACHE_STATUS') {
cacheProgress.classList.add('active');
progressText.textContent = data.status;
} else if (data.type === 'CACHE_PROGRESS') {
// Show progress bar if caching is in progress
cacheProgress.classList.add('active');
startCacheBtn.disabled = true;
startCacheBtn.textContent = 'Downloading...';
const pct = Math.round((data.cached / data.total) * 100);
progressFill.style.width = pct + '%';
progressText.textContent = 'Downloading: ' + data.cached.toLocaleString() + ' / ' + data.total.toLocaleString() + ' pages (' + pct + '%)';
} else if (data.type === 'CACHE_COMPLETE') {
cacheProgress.classList.add('active');
progressFill.style.width = '100%';
progressText.innerHTML = '<span class="cache-complete">✓ Download complete! ' + data.total.toLocaleString() + ' pages available offline.</span>';
startCacheBtn.textContent = 'Downloaded!';
startCacheBtn.disabled = true;
// Refresh the cache list
setTimeout(checkCaches, 500);
} else if (data.type === 'CACHE_ERROR') {
cacheProgress.classList.add('active');
progressText.innerHTML = '<span class="status-error">Error: ' + data.error + '</span>';
startCacheBtn.disabled = false;
startCacheBtn.textContent = 'Try Again';
}
});
// Check if caching is already in progress when page loads
navigator.serviceWorker.ready.then(function(registration) {
if (registration.active) {
registration.active.postMessage({ type: 'GET_CACHE_STATUS' });
}
});
}
// Bible Reader functionality
let bibleData = null;
let bookStructure = {};
let currentBook = '';
let currentChapter = 1;
const BOOKS = [
'Genesis', 'Exodus', 'Leviticus', 'Numbers', 'Deuteronomy',
'Joshua', 'Judges', 'Ruth', '1 Samuel', '2 Samuel',
'1 Kings', '2 Kings', '1 Chronicles', '2 Chronicles',
'Ezra', 'Nehemiah', 'Esther', 'Job', 'Psalms', 'Proverbs',
'Ecclesiastes', 'Song of Solomon', 'Isaiah', 'Jeremiah',
'Lamentations', 'Ezekiel', 'Daniel', 'Hosea', 'Joel', 'Amos',
'Obadiah', 'Jonah', 'Micah', 'Nahum', 'Habakkuk', 'Zephaniah',
'Haggai', 'Zechariah', 'Malachi',
'Matthew', 'Mark', 'Luke', 'John', 'Acts',
'Romans', '1 Corinthians', '2 Corinthians', 'Galatians',
'Ephesians', 'Philippians', 'Colossians',
'1 Thessalonians', '2 Thessalonians',
'1 Timothy', '2 Timothy', 'Titus', 'Philemon',
'Hebrews', 'James', '1 Peter', '2 Peter',
'1 John', '2 John', '3 John', 'Jude', 'Revelation'
];
const bookSelect = document.getElementById('book-select');
const chapterSelect = document.getElementById('chapter-select');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const content = document.getElementById('chapter-content');
async function loadBibleData() {
try {
let response = null;
const cacheNames = await caches.keys();
for (const cacheName of cacheNames) {
const cache = await caches.open(cacheName);
response = await cache.match('/static/verses-1769.json');
if (response) break;
}
if (!response) {
response = await fetch('/static/verses-1769.json');
}
if (!response || !response.ok) {
throw new Error('Could not load Bible data');
}
bibleData = await response.json();
buildBookStructure();
populateBookSelect();
parseUrlAndNavigate();
} catch (err) {
console.error('Failed to load Bible:', err);
}
}
function buildBookStructure() {
bookStructure = {};
for (const ref in bibleData) {
const match = ref.match(/^(.+) (\d+):(\d+)$/);
if (match) {
const [, book, chapter, verse] = match;
if (!bookStructure[book]) bookStructure[book] = {};
if (!bookStructure[book][chapter]) bookStructure[book][chapter] = [];
bookStructure[book][chapter].push(parseInt(verse));
}
}
for (const book in bookStructure) {
for (const chapter in bookStructure[book]) {
bookStructure[book][chapter].sort((a, b) => a - b);
}
}
}
function populateBookSelect() {
BOOKS.forEach(book => {
if (bookStructure[book]) {
const opt = document.createElement('option');
opt.value = book;
opt.textContent = book;
bookSelect.appendChild(opt);
}
});
}
function populateChapterSelect(book) {
chapterSelect.innerHTML = '<option value="">Chapter</option>';
if (!bookStructure[book]) return;
const chapters = Object.keys(bookStructure[book]).map(Number).sort((a, b) => a - b);
chapters.forEach(ch => {
const opt = document.createElement('option');
opt.value = ch;
opt.textContent = ch;
chapterSelect.appendChild(opt);
});
chapterSelect.disabled = false;
}
function renderChapter(book, chapter) {
if (!bookStructure[book] || !bookStructure[book][chapter]) {
return;
}
currentBook = book;
currentChapter = parseInt(chapter);
content.classList.add('active');
const verses = bookStructure[book][chapter];
let html = '<h3>' + book + ' ' + chapter + '</h3>';
verses.forEach(verseNum => {
const ref = book + ' ' + chapter + ':' + verseNum;
const text = bibleData[ref] || '';
const cleanText = text.replace(/^#\s*/, '');
html += '<p><span class="verse-num">' + verseNum + '</span> ' + cleanText + '</p>';
});
content.innerHTML = html;
updateNavButtons();
history.replaceState(null, '', '/offline?book=' + encodeURIComponent(book) + '&chapter=' + chapter);
}
function updateNavButtons() {
const chapters = Object.keys(bookStructure[currentBook] || {}).map(Number).sort((a, b) => a - b);
const bookIndex = BOOKS.indexOf(currentBook);
prevBtn.disabled = !(currentChapter > chapters[0] || bookIndex > 0);
nextBtn.disabled = !(currentChapter < chapters[chapters.length - 1] || bookIndex < BOOKS.length - 1);
}
function goPrev() {
const chapters = Object.keys(bookStructure[currentBook] || {}).map(Number).sort((a, b) => a - b);
const chapterIndex = chapters.indexOf(currentChapter);
if (chapterIndex > 0) {
chapterSelect.value = chapters[chapterIndex - 1];
renderChapter(currentBook, chapters[chapterIndex - 1]);
} else {
const bookIndex = BOOKS.indexOf(currentBook);
if (bookIndex > 0) {
const prevBook = BOOKS[bookIndex - 1];
const prevChapters = Object.keys(bookStructure[prevBook] || {}).map(Number).sort((a, b) => a - b);
bookSelect.value = prevBook;
populateChapterSelect(prevBook);
chapterSelect.value = prevChapters[prevChapters.length - 1];
renderChapter(prevBook, prevChapters[prevChapters.length - 1]);
}
}
}
function goNext() {
const chapters = Object.keys(bookStructure[currentBook] || {}).map(Number).sort((a, b) => a - b);
const chapterIndex = chapters.indexOf(currentChapter);
if (chapterIndex < chapters.length - 1) {
chapterSelect.value = chapters[chapterIndex + 1];
renderChapter(currentBook, chapters[chapterIndex + 1]);
} else {
const bookIndex = BOOKS.indexOf(currentBook);
if (bookIndex < BOOKS.length - 1) {
const nextBook = BOOKS[bookIndex + 1];
const nextChapters = Object.keys(bookStructure[nextBook] || {}).map(Number).sort((a, b) => a - b);
bookSelect.value = nextBook;
populateChapterSelect(nextBook);
chapterSelect.value = nextChapters[0];
renderChapter(nextBook, nextChapters[0]);
}
}
}
function parseUrlAndNavigate() {
const params = new URLSearchParams(window.location.search);
const book = params.get('book');
const chapter = params.get('chapter');
if (book && bookStructure[book]) {
bookSelect.value = book;
populateChapterSelect(book);
if (chapter && bookStructure[book][chapter]) {
chapterSelect.value = chapter;
renderChapter(book, chapter);
}
}
}
bookSelect.addEventListener('change', function() {
const book = this.value;
if (book) {
populateChapterSelect(book);
const chapters = Object.keys(bookStructure[book]).map(Number).sort((a, b) => a - b);
if (chapters.length > 0) {
chapterSelect.value = chapters[0];
renderChapter(book, chapters[0]);
}
}
});
chapterSelect.addEventListener('change', function() {
if (this.value && currentBook) {
renderChapter(currentBook, this.value);
}
});
prevBtn.addEventListener('click', goPrev);
nextBtn.addEventListener('click', goNext);
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'SELECT') return;
if (e.key === 'ArrowLeft' || e.key === 'h') {
e.preventDefault();
if (!prevBtn.disabled) goPrev();
} else if (e.key === 'ArrowRight' || e.key === 'l') {
e.preventDefault();
if (!nextBtn.disabled) goNext();
}
});
loadBibleData();
})();
</script>
</body>
</html>