const CACHE_NAME = 'tomsjavajive-v1'; const STATIC_CACHE = 'tomsjavajive-static-v1'; const DYNAMIC_CACHE = 'tomsjavajive-dynamic-v1'; // Static assets to cache immediately const STATIC_ASSETS = [ '/', '/shop.php', '/cart.php', '/assets/css/style.css', '/assets/js/main.js', '/assets/images/logo.png', '/manifest.json', '/offline.html' ]; // 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.map(url => { return new Request(url, { cache: 'no-cache' }); })).catch(err => { console.log('[Service Worker] Some assets failed to cache:', err); return Promise.resolve(); }); }) .then(() => self.skipWaiting()) ); }); // 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 .filter((cacheName) => { return cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE && cacheName.startsWith('tomsjavajive-'); }) .map((cacheName) => { console.log('[Service Worker] Deleting old cache:', cacheName); return caches.delete(cacheName); }) ); }) .then(() => self.clients.claim()) ); }); // Fetch event - serve from cache or network self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Skip non-GET requests if (request.method !== 'GET') { return; } // Skip admin panel if (url.pathname.startsWith('/admin')) { return; } // Skip API requests (always network) if (url.pathname.startsWith('/api')) { return; } // Handle navigation requests if (request.mode === 'navigate') { event.respondWith( fetch(request) .then((response) => { // Clone and cache the response const responseClone = response.clone(); caches.open(DYNAMIC_CACHE).then((cache) => { cache.put(request, responseClone); }); return response; }) .catch(() => { // Try to serve from cache return caches.match(request) .then((cachedResponse) => { if (cachedResponse) { return cachedResponse; } // Serve offline page return caches.match('/offline.html'); }); }) ); return; } // Handle static assets (cache-first strategy) if (isStaticAsset(url.pathname)) { event.respondWith( caches.match(request) .then((cachedResponse) => { if (cachedResponse) { // Fetch in background to update cache fetch(request).then((response) => { if (response.ok) { caches.open(STATIC_CACHE).then((cache) => { cache.put(request, response); }); } }).catch(() => {}); return cachedResponse; } return fetch(request).then((response) => { if (response.ok) { const responseClone = response.clone(); caches.open(STATIC_CACHE).then((cache) => { cache.put(request, responseClone); }); } return response; }); }) ); return; } // Handle images (cache-first with network fallback) if (isImageRequest(request)) { event.respondWith( caches.match(request) .then((cachedResponse) => { if (cachedResponse) { return cachedResponse; } return fetch(request) .then((response) => { if (response.ok) { const responseClone = response.clone(); caches.open(DYNAMIC_CACHE).then((cache) => { cache.put(request, responseClone); }); } return response; }) .catch(() => { // Return placeholder image return caches.match('/assets/images/placeholder-product.svg'); }); }) ); return; } // Default: network-first with cache fallback event.respondWith( fetch(request) .then((response) => { if (response.ok) { const responseClone = response.clone(); caches.open(DYNAMIC_CACHE).then((cache) => { cache.put(request, responseClone); }); } return response; }) .catch(() => { return caches.match(request); }) ); }); // Helper functions function isStaticAsset(pathname) { return pathname.match(/\.(css|js|woff|woff2|ttf|eot)$/i); } function isImageRequest(request) { return request.destination === 'image' || request.url.match(/\.(png|jpg|jpeg|gif|svg|webp|ico)$/i); } // Push notification handling self.addEventListener('push', (event) => { console.log('[Service Worker] Push received'); let data = { title: "Tom's Java Jive", body: 'You have a new notification!' }; if (event.data) { try { data = event.data.json(); } catch (e) { data.body = event.data.text(); } } const options = { body: data.body, icon: '/assets/icons/icon-192.png', badge: '/assets/icons/badge-72.png', vibrate: [100, 50, 100], data: { url: data.url || '/' }, actions: [ { action: 'view', title: 'View' }, { action: 'close', title: 'Close' } ] }; event.waitUntil( self.registration.showNotification(data.title, options) ); }); // Notification click handling self.addEventListener('notificationclick', (event) => { console.log('[Service Worker] Notification clicked'); event.notification.close(); if (event.action === 'close') { return; } const url = event.notification.data?.url || '/'; event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }) .then((clientList) => { // Focus existing window if available for (const client of clientList) { if (client.url === url && 'focus' in client) { return client.focus(); } } // Open new window if (clients.openWindow) { return clients.openWindow(url); } }) ); }); // Background sync for cart/orders self.addEventListener('sync', (event) => { console.log('[Service Worker] Background sync:', event.tag); if (event.tag === 'sync-cart') { event.waitUntil(syncCart()); } }); async function syncCart() { // Get pending cart actions from IndexedDB // and sync with server console.log('[Service Worker] Syncing cart...'); }