Files
biblical-guide.com/public/sw.js
Andrei a01b2490dc Implement comprehensive PWA with offline Bible reading capabilities
- Add Web App Manifest with app metadata, icons, and installation support
- Create Service Worker with intelligent caching strategies for Bible content, static assets, and dynamic content
- Implement IndexedDB-based offline storage system for Bible versions, books, chapters, and verses
- Add offline download manager component for browsing and downloading Bible versions
- Create offline Bible reader component for seamless offline reading experience
- Integrate PWA install prompt with platform-specific instructions
- Add offline reading interface to existing Bible reader with download buttons
- Create dedicated offline page with tabbed interface for reading and downloading
- Add PWA and offline-related translations for English and Romanian locales
- Implement background sync for Bible downloads and cache management
- Add storage usage monitoring and management utilities
- Ensure SSR-safe implementation with dynamic imports for client-side components

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-28 22:20:44 +00:00

432 lines
13 KiB
JavaScript

// Biblical Guide Service Worker
const CACHE_NAME = 'biblical-guide-v1.0.0';
const STATIC_CACHE = 'biblical-guide-static-v1.0.0';
const DYNAMIC_CACHE = 'biblical-guide-dynamic-v1.0.0';
const BIBLE_CACHE = 'biblical-guide-bible-v1.0.0';
// Static resources that should be cached immediately
const STATIC_ASSETS = [
'/',
'/manifest.json',
'/icon-192.png',
'/icon-512.png',
'/biblical-guide-og-image.png',
'/offline.html', // We'll create this fallback page
];
// Bible API endpoints that should be cached
const BIBLE_API_PATTERNS = [
/\/api\/bible\/versions/,
/\/api\/bible\/books/,
/\/api\/bible\/chapter/,
/\/api\/bible\/verses/,
];
// Dynamic content that should be cached with network-first strategy
const DYNAMIC_PATTERNS = [
/\/api\/daily-verse/,
/\/api\/search/,
];
// Install event - cache static assets
self.addEventListener('install', (event) => {
console.log('[SW] Installing service worker...');
event.waitUntil(
Promise.all([
caches.open(STATIC_CACHE).then((cache) => {
console.log('[SW] Caching static assets');
return cache.addAll(STATIC_ASSETS.map(url => new Request(url, {credentials: 'same-origin'})));
}),
caches.open(BIBLE_CACHE).then(() => {
console.log('[SW] Bible cache initialized');
})
]).then(() => {
console.log('[SW] Installation complete');
// Skip waiting to activate immediately
return self.skipWaiting();
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('[SW] Activating service worker...');
event.waitUntil(
Promise.all([
// Clean up old caches
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== STATIC_CACHE &&
cacheName !== DYNAMIC_CACHE &&
cacheName !== BIBLE_CACHE &&
cacheName !== CACHE_NAME) {
console.log('[SW] Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
}),
// Take control of all pages immediately
self.clients.claim()
]).then(() => {
console.log('[SW] Activation complete');
})
);
});
// Fetch event - handle all network requests
self.addEventListener('fetch', (event) => {
const { request } = event;
const { url, method } = request;
// Only handle GET requests
if (method !== 'GET') {
return;
}
// Handle Bible content downloads (store in Bible cache)
if (BIBLE_API_PATTERNS.some(pattern => pattern.test(url))) {
event.respondWith(handleBibleRequest(request));
return;
}
// Handle dynamic content (network-first with cache fallback)
if (DYNAMIC_PATTERNS.some(pattern => pattern.test(url))) {
event.respondWith(handleDynamicRequest(request));
return;
}
// Handle static assets (cache-first)
if (isStaticAsset(url)) {
event.respondWith(handleStaticRequest(request));
return;
}
// Handle navigation requests (HTML pages)
if (request.mode === 'navigate') {
event.respondWith(handleNavigationRequest(request));
return;
}
// Default: network-first for everything else
event.respondWith(handleDefaultRequest(request));
});
// Bible content strategy: Cache-first with network update
async function handleBibleRequest(request) {
const cache = await caches.open(BIBLE_CACHE);
const cached = await cache.match(request);
if (cached) {
console.log('[SW] Serving Bible content from cache:', request.url);
// Update cache in background
updateBibleCache(request, cache);
return cached;
}
try {
console.log('[SW] Fetching Bible content from network:', request.url);
const response = await fetch(request);
if (response.ok) {
// Clone before caching
const responseClone = response.clone();
await cache.put(request, responseClone);
console.log('[SW] Bible content cached:', request.url);
}
return response;
} catch (error) {
console.error('[SW] Bible content fetch failed:', error);
throw error;
}
}
// Update Bible cache in background
async function updateBibleCache(request, cache) {
try {
const response = await fetch(request);
if (response.ok) {
await cache.put(request, response.clone());
console.log('[SW] Bible cache updated:', request.url);
}
} catch (error) {
console.log('[SW] Background Bible cache update failed:', error);
}
}
// Dynamic content strategy: Network-first with cache fallback
async function handleDynamicRequest(request) {
const cache = await caches.open(DYNAMIC_CACHE);
try {
console.log('[SW] Fetching dynamic content from network:', request.url);
const response = await fetch(request);
if (response.ok) {
// Cache successful responses
await cache.put(request, response.clone());
console.log('[SW] Dynamic content cached:', request.url);
}
return response;
} catch (error) {
console.log('[SW] Network failed, checking cache for:', request.url);
const cached = await cache.match(request);
if (cached) {
console.log('[SW] Serving dynamic content from cache:', request.url);
return cached;
}
throw error;
}
}
// Static assets strategy: Cache-first
async function handleStaticRequest(request) {
const cache = await caches.open(STATIC_CACHE);
const cached = await cache.match(request);
if (cached) {
console.log('[SW] Serving static asset from cache:', request.url);
return cached;
}
try {
console.log('[SW] Fetching static asset from network:', request.url);
const response = await fetch(request);
if (response.ok) {
await cache.put(request, response.clone());
console.log('[SW] Static asset cached:', request.url);
}
return response;
} catch (error) {
console.error('[SW] Static asset fetch failed:', error);
throw error;
}
}
// Navigation strategy: Network-first with offline fallback
async function handleNavigationRequest(request) {
try {
console.log('[SW] Fetching navigation from network:', request.url);
const response = await fetch(request);
if (response.ok) {
// Cache successful navigation responses
const cache = await caches.open(DYNAMIC_CACHE);
await cache.put(request, response.clone());
}
return response;
} catch (error) {
console.log('[SW] Navigation network failed, checking cache:', request.url);
const cache = await caches.open(DYNAMIC_CACHE);
const cached = await cache.match(request);
if (cached) {
console.log('[SW] Serving navigation from cache:', request.url);
return cached;
}
// Return offline fallback page
console.log('[SW] Serving offline fallback page');
const offlineCache = await caches.open(STATIC_CACHE);
const offlinePage = await offlineCache.match('/offline.html');
return offlinePage || new Response('Offline - Please check your connection', {
status: 503,
statusText: 'Service Unavailable',
headers: { 'Content-Type': 'text/plain' }
});
}
}
// Default strategy: Network-first
async function handleDefaultRequest(request) {
try {
return await fetch(request);
} catch (error) {
console.log('[SW] Default request failed:', request.url);
throw error;
}
}
// Helper function to check if URL is a static asset
function isStaticAsset(url) {
return url.includes('/_next/') ||
url.includes('/icon-') ||
url.includes('/manifest.json') ||
url.includes('/biblical-guide-og-image.png') ||
url.includes('.css') ||
url.includes('.js') ||
url.includes('.woff') ||
url.includes('.woff2');
}
// Background sync for Bible downloads
self.addEventListener('sync', (event) => {
console.log('[SW] Background sync triggered:', event.tag);
if (event.tag === 'download-bible-version') {
event.waitUntil(handleBibleDownloadSync());
}
});
// Handle Bible version download in background
async function handleBibleDownloadSync() {
try {
console.log('[SW] Processing background Bible download');
// Get pending downloads from IndexedDB
const pendingDownloads = await getPendingBibleDownloads();
for (const download of pendingDownloads) {
await downloadBibleVersion(download);
}
} catch (error) {
console.error('[SW] Background Bible download failed:', error);
}
}
// Get pending downloads from IndexedDB
async function getPendingBibleDownloads() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('BibleStorage', 1);
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(['downloads'], 'readonly');
const store = transaction.objectStore('downloads');
const getAllRequest = store.getAll();
getAllRequest.onsuccess = () => {
resolve(getAllRequest.result.filter(d => d.status === 'pending'));
};
getAllRequest.onerror = () => reject(getAllRequest.error);
};
request.onerror = () => reject(request.error);
});
}
// Download a Bible version
async function downloadBibleVersion(download) {
try {
console.log('[SW] Downloading Bible version:', download.versionId);
// Download books list
const booksResponse = await fetch(`/api/bible/books?version=${download.versionId}`);
if (!booksResponse.ok) throw new Error('Failed to fetch books');
const books = await booksResponse.json();
// Download each chapter
for (const book of books.books) {
for (let chapter = 1; chapter <= book.chaptersCount; chapter++) {
const chapterUrl = `/api/bible/chapter?book=${book.id}&chapter=${chapter}&version=${download.versionId}`;
await fetch(chapterUrl); // This will be cached by the fetch handler
}
}
// Mark download as complete
await updateDownloadStatus(download.versionId, 'completed');
console.log('[SW] Bible version download completed:', download.versionId);
} catch (error) {
console.error('[SW] Bible version download failed:', error);
await updateDownloadStatus(download.versionId, 'failed');
}
}
// Update download status in IndexedDB
async function updateDownloadStatus(versionId, status) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('BibleStorage', 1);
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(['downloads'], 'readwrite');
const store = transaction.objectStore('downloads');
const getRequest = store.get(versionId);
getRequest.onsuccess = () => {
const download = getRequest.result;
if (download) {
download.status = status;
download.updatedAt = new Date().toISOString();
const putRequest = store.put(download);
putRequest.onsuccess = () => resolve();
putRequest.onerror = () => reject(putRequest.error);
} else {
resolve(); // Download not found, ignore
}
};
getRequest.onerror = () => reject(getRequest.error);
};
request.onerror = () => reject(request.error);
});
}
// Message handling for communication with the main thread
self.addEventListener('message', (event) => {
console.log('[SW] Message received:', event.data);
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data && event.data.type === 'CLEAR_CACHE') {
clearAllCaches().then(() => {
event.ports[0].postMessage({success: true});
});
}
if (event.data && event.data.type === 'CACHE_BIBLE_VERSION') {
const { versionId } = event.data;
cacheBibleVersion(versionId).then(() => {
event.ports[0].postMessage({success: true, versionId});
}).catch((error) => {
event.ports[0].postMessage({success: false, error: error.message, versionId});
});
}
});
// Clear all caches
async function clearAllCaches() {
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map(name => caches.delete(name)));
console.log('[SW] All caches cleared');
}
// Cache a specific Bible version
async function cacheBibleVersion(versionId) {
console.log('[SW] Caching Bible version:', versionId);
try {
// Download and cache all content for this version
const booksResponse = await fetch(`/api/bible/books?version=${versionId}`);
if (!booksResponse.ok) throw new Error('Failed to fetch books');
const books = await booksResponse.json();
const cache = await caches.open(BIBLE_CACHE);
// Cache books list
await cache.put(`/api/bible/books?version=${versionId}`, booksResponse.clone());
// Cache each chapter
for (const book of books.books) {
for (let chapter = 1; chapter <= book.chaptersCount; chapter++) {
const chapterUrl = `/api/bible/chapter?book=${book.id}&chapter=${chapter}&version=${versionId}`;
const chapterResponse = await fetch(chapterUrl);
if (chapterResponse.ok) {
await cache.put(chapterUrl, chapterResponse.clone());
}
}
}
console.log('[SW] Bible version cached successfully:', versionId);
} catch (error) {
console.error('[SW] Failed to cache Bible version:', error);
throw error;
}
}
console.log('[SW] Service worker script loaded');