// Miracles in Motion - Service Worker // Version 1.0.0 const CACHE_NAME = 'miracles-in-motion-v1' const STATIC_CACHE = 'static-v1' const DYNAMIC_CACHE = 'dynamic-v1' // Assets to cache immediately const STATIC_ASSETS = [ '/', '/index.html', '/favicon.svg', '/robots.txt', '/site.webmanifest' ] // Assets to cache on demand const CACHE_STRATEGIES = { // Cache first for static assets CACHE_FIRST: ['.js', '.css', '.woff', '.woff2', '.ttf', '.eot'], // Network first for API calls NETWORK_FIRST: ['/api/', '/analytics/'], // Stale while revalidate for images STALE_WHILE_REVALIDATE: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'] } // Install event - cache static assets self.addEventListener('install', event => { console.log('Service Worker: Installing...') event.waitUntil( caches.open(STATIC_CACHE) .then(cache => { console.log('Service Worker: Caching static assets') return cache.addAll(STATIC_ASSETS) }) .then(() => { console.log('Service Worker: Installation complete') return self.skipWaiting() }) .catch(error => { console.error('Service Worker: Installation failed', error) }) ) }) // Activate event - clean up old caches self.addEventListener('activate', event => { console.log('Service Worker: Activating...') event.waitUntil( caches.keys() .then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (cacheName !== CACHE_NAME && cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) { console.log('Service Worker: Deleting old cache', cacheName) return caches.delete(cacheName) } }) ) }) .then(() => { console.log('Service Worker: Activation complete') return self.clients.claim() }) ) }) // Fetch event - handle requests with appropriate caching strategy self.addEventListener('fetch', event => { const { request } = event const url = new URL(request.url) // Skip non-GET requests if (request.method !== 'GET') return // Skip cross-origin requests if (url.origin !== location.origin) return event.respondWith(handleFetch(request)) }) // Determine caching strategy based on file type function getCachingStrategy(url) { const pathname = new URL(url).pathname.toLowerCase() // Check for cache-first assets if (CACHE_STRATEGIES.CACHE_FIRST.some(ext => pathname.includes(ext))) { return 'cache-first' } // Check for network-first assets if (CACHE_STRATEGIES.NETWORK_FIRST.some(path => pathname.includes(path))) { return 'network-first' } // Check for stale-while-revalidate assets if (CACHE_STRATEGIES.STALE_WHILE_REVALIDATE.some(ext => pathname.includes(ext))) { return 'stale-while-revalidate' } // Default to network-first return 'network-first' } // Handle fetch requests with appropriate strategy async function handleFetch(request) { const strategy = getCachingStrategy(request.url) switch (strategy) { case 'cache-first': return cacheFirst(request) case 'network-first': return networkFirst(request) case 'stale-while-revalidate': return staleWhileRevalidate(request) default: return networkFirst(request) } } // Cache-first strategy async function cacheFirst(request) { try { const cache = await caches.open(STATIC_CACHE) const cachedResponse = await cache.match(request) if (cachedResponse) { return cachedResponse } const networkResponse = await fetch(request) if (networkResponse.ok) { cache.put(request, networkResponse.clone()) } return networkResponse } catch (error) { console.error('Cache-first strategy failed:', error) return new Response('Offline content unavailable', { status: 503 }) } } // Network-first strategy async function networkFirst(request) { try { const networkResponse = await fetch(request) if (networkResponse.ok) { const cache = await caches.open(DYNAMIC_CACHE) cache.put(request, networkResponse.clone()) } return networkResponse } catch (error) { console.log('Network failed, trying cache:', error) const cache = await caches.open(DYNAMIC_CACHE) const cachedResponse = await cache.match(request) if (cachedResponse) { return cachedResponse } return new Response('Content unavailable offline', { status: 503, headers: { 'Content-Type': 'text/plain' } }) } } // Stale-while-revalidate strategy async function staleWhileRevalidate(request) { const cache = await caches.open(DYNAMIC_CACHE) const cachedResponse = await cache.match(request) const networkUpdate = fetch(request).then(response => { if (response.ok) { cache.put(request, response.clone()) } return response }) return cachedResponse || networkUpdate } // Background sync for offline form submissions self.addEventListener('sync', event => { if (event.tag === 'donation-submission') { event.waitUntil(syncDonations()) } if (event.tag === 'assistance-request') { event.waitUntil(syncAssistanceRequests()) } }) // Sync offline donations async function syncDonations() { try { const donations = await getOfflineData('pending-donations') for (const donation of donations) { await fetch('/api/donations', { method: 'POST', body: JSON.stringify(donation), headers: { 'Content-Type': 'application/json' } }) } await clearOfflineData('pending-donations') console.log('Offline donations synced successfully') } catch (error) { console.error('Failed to sync donations:', error) } } // Sync offline assistance requests async function syncAssistanceRequests() { try { const requests = await getOfflineData('pending-requests') for (const request of requests) { await fetch('/api/assistance-requests', { method: 'POST', body: JSON.stringify(request), headers: { 'Content-Type': 'application/json' } }) } await clearOfflineData('pending-requests') console.log('Offline assistance requests synced successfully') } catch (error) { console.error('Failed to sync assistance requests:', error) } } // Helper functions for offline data management function getOfflineData(key) { return new Promise((resolve, reject) => { const request = indexedDB.open('MiraclesInMotion', 1) request.onsuccess = () => { const db = request.result const transaction = db.transaction(['offlineData'], 'readonly') const store = transaction.objectStore('offlineData') const getRequest = store.get(key) getRequest.onsuccess = () => { resolve(getRequest.result?.data || []) } getRequest.onerror = () => reject(getRequest.error) } request.onerror = () => reject(request.error) }) } function clearOfflineData(key) { return new Promise((resolve, reject) => { const request = indexedDB.open('MiraclesInMotion', 1) request.onsuccess = () => { const db = request.result const transaction = db.transaction(['offlineData'], 'readwrite') const store = transaction.objectStore('offlineData') const deleteRequest = store.delete(key) deleteRequest.onsuccess = () => resolve() deleteRequest.onerror = () => reject(deleteRequest.error) } request.onerror = () => reject(request.error) }) } // Push notification handling self.addEventListener('push', event => { const options = { body: event.data?.text() || 'New update available', icon: '/favicon.svg', badge: '/favicon.svg', vibrate: [200, 100, 200], data: { timestamp: Date.now(), url: '/' }, actions: [ { action: 'view', title: 'View', icon: '/favicon.svg' }, { action: 'close', title: 'Close' } ] } event.waitUntil( self.registration.showNotification('Miracles in Motion', options) ) }) // Notification click handling self.addEventListener('notificationclick', event => { event.notification.close() if (event.action === 'view') { event.waitUntil( clients.openWindow(event.notification.data.url) ) } })