/* eslint-disable no-restricted-globals */ // Service Worker for PWA support and offline functionality const CACHE_VERSION = 'v1.0.0'; const CACHE_NAME = `fotbal-club-cache-${CACHE_VERSION}`; // Assets to cache on install const STATIC_ASSETS = [ '/', '/index.html', '/static/css/main.css', '/static/js/main.js', '/manifest.json', '/favicon.ico', '/logo192.png', '/logo512.png', ]; // API endpoints to cache const API_CACHE_ENDPOINTS = [ '/api/v1/settings/public', '/api/v1/seo', ]; // Install event - cache static assets self.addEventListener('install', (event) => { console.log('[SW] Installing service worker...'); event.waitUntil( caches.open(CACHE_NAME).then((cache) => { console.log('[SW] Caching static assets'); return cache.addAll(STATIC_ASSETS.map(url => new Request(url, { cache: 'reload' }))); }).catch((error) => { console.error('[SW] Failed to cache static assets:', error); }) ); // Activate immediately self.skipWaiting(); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { console.log('[SW] Activating service worker...'); event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames .filter((name) => name !== CACHE_NAME) .map((name) => { console.log('[SW] Deleting old cache:', name); return caches.delete(name); }) ); }) ); // Take control immediately return self.clients.claim(); }); // Fetch event - serve from cache, fall back to network self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Skip non-GET requests if (request.method !== 'GET') { return; } // Skip Chrome extensions and non-http(s) requests if (!url.protocol.startsWith('http')) { return; } // Skip admin routes if (url.pathname.startsWith('/admin')) { return; } // Handle API requests if (url.pathname.startsWith('/api/')) { event.respondWith(handleAPIRequest(request)); return; } // Handle static assets and pages event.respondWith(handleStaticRequest(request)); }); // Handle static requests - Cache First strategy async function handleStaticRequest(request) { try { // Try cache first const cachedResponse = await caches.match(request); if (cachedResponse) { // Return cached response and update in background fetchAndUpdateCache(request); return cachedResponse; } // Not in cache - fetch from network const networkResponse = await fetch(request); // Cache successful responses if (networkResponse.ok) { const cache = await caches.open(CACHE_NAME); cache.put(request, networkResponse.clone()); } return networkResponse; } catch (error) { console.error('[SW] Fetch failed:', error); // Return offline page if available const cachedOffline = await caches.match('/offline.html'); if (cachedOffline) { return cachedOffline; } // Return basic offline response return new Response('Offline - Please check your connection', { status: 503, statusText: 'Service Unavailable', headers: { 'Content-Type': 'text/plain' }, }); } } // Handle API requests - Network First strategy with cache fallback async function handleAPIRequest(request) { try { // Try network first for fresh data const networkResponse = await fetch(request); // Cache successful responses if (networkResponse.ok) { const cache = await caches.open(CACHE_NAME); cache.put(request, networkResponse.clone()); } return networkResponse; } catch (error) { console.log('[SW] Network failed, trying cache:', request.url); // Fall back to cache const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Return error response return new Response( JSON.stringify({ error: 'Offline - cached data not available' }), { status: 503, statusText: 'Service Unavailable', headers: { 'Content-Type': 'application/json' }, } ); } } // Update cache in background async function fetchAndUpdateCache(request) { try { const response = await fetch(request); if (response.ok) { const cache = await caches.open(CACHE_NAME); cache.put(request, response); } } catch (error) { // Silent fail - we already returned cached version } } // Handle background sync for offline actions self.addEventListener('sync', (event) => { console.log('[SW] Background sync:', event.tag); if (event.tag === 'sync-data') { event.waitUntil(syncOfflineData()); } }); async function syncOfflineData() { // Implement offline data sync logic here // For example, sync form submissions, votes, etc. console.log('[SW] Syncing offline data...'); } // Handle push notifications self.addEventListener('push', (event) => { const data = event.data ? event.data.json() : {}; const title = data.title || 'Fotbal Club'; const options = { body: data.body || 'Nová notifikace', icon: '/logo192.png', badge: '/logo192.png', data: data.url || '/', }; event.waitUntil( self.registration.showNotification(title, options) ); }); // Handle notification clicks self.addEventListener('notificationclick', (event) => { event.notification.close(); const urlToOpen = event.notification.data || '/'; event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => { // Check if there's already a window open for (const client of clientList) { if (client.url === urlToOpen && 'focus' in client) { return client.focus(); } } // Open new window if (clients.openWindow) { return clients.openWindow(urlToOpen); } }) ); }); // Message handler for manual cache updates self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } if (event.data && event.data.type === 'CLEAR_CACHE') { event.waitUntil( caches.delete(CACHE_NAME).then(() => { return caches.open(CACHE_NAME); }) ); } }); console.log('[SW] Service Worker loaded successfully');