mirror of
https://github.com/kennethreitz/kjvstudy.org.git
synced 2026-06-05 23:00:16 +00:00
8f89fe9bfb
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>
803 lines
32 KiB
HTML
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>
|