Add background pre-caching for full offline support

- Service worker now pre-caches ~160 pages in background after install
- Includes all resource pages, topics, study guides, stories, book pages
- Caches in batches of 5 with 1-second delays to avoid overwhelming server
- Progress indicator shows "Caching for offline: X%" in bottom-left
- Shows "Ready for offline! (X pages)" when complete, then fades out
- Bumped cache version to v2 to trigger re-cache

Combined with the Bible JSON (~4.7MB), the entire site is now
available offline after first visit.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-30 01:09:35 -05:00
parent 87926fa83c
commit bc8149ceb7
2 changed files with 277 additions and 2 deletions
+238 -2
View File
@@ -1,5 +1,5 @@
// KJV Study Service Worker - Offline Bible Access
const CACHE_VERSION = 'v1';
const CACHE_VERSION = 'v2';
const STATIC_CACHE = 'kjvstudy-static-' + CACHE_VERSION;
const BIBLE_CACHE = 'kjvstudy-bible-' + CACHE_VERSION;
const PAGE_CACHE = 'kjvstudy-pages-' + CACHE_VERSION;
@@ -16,6 +16,169 @@ const STATIC_ASSETS = [
'/books'
];
// All pages to pre-cache in background (185 pages)
const PAGES_TO_CACHE = [
"/angels",
"/apostles",
"/biblical-timeline",
"/book/1 Chronicles",
"/book/1 Corinthians",
"/book/1 John",
"/book/1 Kings",
"/book/1 Peter",
"/book/1 Samuel",
"/book/1 Thessalonians",
"/book/1 Timothy",
"/book/2 Chronicles",
"/book/2 Corinthians",
"/book/2 John",
"/book/2 Kings",
"/book/2 Peter",
"/book/2 Samuel",
"/book/2 Thessalonians",
"/book/2 Timothy",
"/book/3 John",
"/book/Acts",
"/book/Amos",
"/book/Colossians",
"/book/Daniel",
"/book/Deuteronomy",
"/book/Ecclesiastes",
"/book/Ephesians",
"/book/Esther",
"/book/Exodus",
"/book/Ezekiel",
"/book/Ezra",
"/book/Galatians",
"/book/Genesis",
"/book/Habakkuk",
"/book/Haggai",
"/book/Hebrews",
"/book/Hosea",
"/book/Isaiah",
"/book/James",
"/book/Jeremiah",
"/book/Job",
"/book/Joel",
"/book/John",
"/book/Jonah",
"/book/Joshua",
"/book/Jude",
"/book/Judges",
"/book/Lamentations",
"/book/Leviticus",
"/book/Luke",
"/book/Malachi",
"/book/Mark",
"/book/Matthew",
"/book/Micah",
"/book/Nahum",
"/book/Nehemiah",
"/book/Numbers",
"/book/Obadiah",
"/book/Philemon",
"/book/Philippians",
"/book/Proverbs",
"/book/Psalms",
"/book/Revelation",
"/book/Romans",
"/book/Ruth",
"/book/Song of Solomon",
"/book/Titus",
"/book/Zechariah",
"/book/Zephaniah",
"/covenants",
"/family-tree",
"/festivals",
"/fruits-of-spirit",
"/interlinear",
"/names-of-god",
"/parables",
"/prophets",
"/reading-plans",
"/reading-plans/chronological",
"/reading-plans/gospels-acts-30",
"/reading-plans/nt-90-days",
"/reading-plans/one-year",
"/reading-plans/paul-epistles-30",
"/reading-plans/psalms-proverbs",
"/resources",
"/search",
"/stories",
"/stories/kids",
"/strongs",
"/strongs/greek",
"/strongs/hebrew",
"/study-guides",
"/study-guides/attributes-of-god",
"/study-guides/biblical-marriage",
"/study-guides/christian-living",
"/study-guides/covenant-theology",
"/study-guides/doctrine-of-scripture",
"/study-guides/faith-and-works",
"/study-guides/fruits-spirit",
"/study-guides/gods-love",
"/study-guides/gospel",
"/study-guides/gospel-in-ot",
"/study-guides/heaven-eternity",
"/study-guides/hope-comfort",
"/study-guides/law-and-christian",
"/study-guides/money-stewardship",
"/study-guides/new-believer",
"/study-guides/prayer-faith",
"/study-guides/problem-of-evil",
"/study-guides/raising-children",
"/study-guides/resurrection",
"/study-guides/salvation",
"/study-guides/scarlet-thread",
"/study-guides/sovereignty-of-god",
"/study-guides/spirits-demons",
"/study-guides/trinity",
"/study-guides/wisdom-guidance",
"/topics",
"/topics/anxiety",
"/topics/baptism",
"/topics/communion",
"/topics/contentment",
"/topics/faith",
"/topics/fasting",
"/topics/forgiveness",
"/topics/generosity",
"/topics/grace",
"/topics/heaven",
"/topics/holiness",
"/topics/holy-spirit",
"/topics/hope",
"/topics/humility",
"/topics/joy",
"/topics/judgment",
"/topics/love",
"/topics/marriage",
"/topics/mental-health",
"/topics/obedience",
"/topics/parenting",
"/topics/patience",
"/topics/peace",
"/topics/prayer",
"/topics/repentance",
"/topics/rest",
"/topics/salvation",
"/topics/service",
"/topics/spiritual-warfare",
"/topics/stewardship",
"/topics/suffering",
"/topics/temptation",
"/topics/the-church",
"/topics/wisdom",
"/topics/work",
"/topics/worship",
"/twelve-apostles",
"/verse-of-the-day",
"/women",
"/christology",
"/blood-in-scripture"
];
// Install event - cache static assets
self.addEventListener('install', (event) => {
console.log('[SW] Installing service worker...');
@@ -35,7 +198,7 @@ self.addEventListener('install', (event) => {
);
});
// Activate event - clean up old caches
// Activate event - clean up old caches and start background caching
self.addEventListener('activate', (event) => {
console.log('[SW] Activating service worker...');
event.waitUntil(
@@ -58,9 +221,82 @@ self.addEventListener('activate', (event) => {
console.log('[SW] Service worker activated');
return self.clients.claim();
})
.then(() => {
// Start background pre-caching after activation
startBackgroundCaching();
})
);
});
// Background pre-caching - cache all pages gradually
let cachingInProgress = false;
let cachedCount = 0;
async function startBackgroundCaching() {
if (cachingInProgress) return;
cachingInProgress = true;
console.log('[SW] Starting background pre-cache of', PAGES_TO_CACHE.length, 'pages...');
const cache = await caches.open(PAGE_CACHE);
// Check which pages are already cached
const uncachedPages = [];
for (const url of PAGES_TO_CACHE) {
const cached = await cache.match(url);
if (!cached) {
uncachedPages.push(url);
}
}
console.log('[SW] Need to cache', uncachedPages.length, 'pages');
// Cache pages in batches with delay to avoid overwhelming the server
const BATCH_SIZE = 5;
const BATCH_DELAY = 1000; // 1 second between batches
for (let i = 0; i < uncachedPages.length; i += BATCH_SIZE) {
const batch = uncachedPages.slice(i, i + BATCH_SIZE);
await Promise.all(
batch.map(async (url) => {
try {
const response = await fetch(url);
if (response.ok) {
await cache.put(url, response);
cachedCount++;
// Notify clients of progress
notifyClients({
type: 'CACHE_PROGRESS',
cached: cachedCount,
total: uncachedPages.length
});
}
} catch (err) {
console.log('[SW] Failed to cache:', url);
}
})
);
// Wait between batches
if (i + BATCH_SIZE < uncachedPages.length) {
await new Promise(resolve => setTimeout(resolve, BATCH_DELAY));
}
}
console.log('[SW] Background caching complete!', cachedCount, 'pages cached');
cachingInProgress = false;
// Notify clients that caching is complete
notifyClients({ type: 'CACHE_COMPLETE', total: cachedCount });
}
// Notify all clients of caching progress
async function notifyClients(message) {
const clients = await self.clients.matchAll();
clients.forEach(client => client.postMessage(message));
}
// Fetch event - serve from cache, fallback to network
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
+39
View File
@@ -2264,6 +2264,45 @@
console.log('[App] Service Worker registration failed:', error);
});
});
// Listen for cache progress messages from service worker
navigator.serviceWorker.addEventListener('message', function(event) {
var data = event.data;
if (data.type === 'CACHE_PROGRESS') {
showCacheProgress(data.cached, data.total);
} else if (data.type === 'CACHE_COMPLETE') {
showCacheComplete(data.total);
}
});
}
// Cache progress indicator
var cacheIndicator = null;
function showCacheProgress(cached, total) {
if (!cacheIndicator) {
cacheIndicator = document.createElement('div');
cacheIndicator.id = 'cache-indicator';
cacheIndicator.style.cssText = 'position:fixed;bottom:1rem;left:1rem;padding:0.5rem 1rem;background:#4a7c59;color:white;border-radius:4px;font-size:0.85rem;z-index:9999;box-shadow:0 2px 8px rgba(0,0,0,0.2);transition:opacity 0.3s;';
document.body.appendChild(cacheIndicator);
}
var pct = Math.round((cached / total) * 100);
cacheIndicator.innerHTML = 'Caching for offline: ' + pct + '%';
cacheIndicator.style.opacity = '1';
}
function showCacheComplete(total) {
if (cacheIndicator) {
cacheIndicator.innerHTML = 'Ready for offline! (' + total + ' pages)';
setTimeout(function() {
cacheIndicator.style.opacity = '0';
setTimeout(function() {
if (cacheIndicator && cacheIndicator.parentNode) {
cacheIndicator.parentNode.removeChild(cacheIndicator);
cacheIndicator = null;
}
}, 300);
}, 3000);
}
}
// Offline/Online indicator