// Service Worker for SmartSolTech PWA const CACHE_NAME = 'smartsoltech-v1.0.0'; const STATIC_CACHE_NAME = 'smartsoltech-static-v1.0.0'; const DYNAMIC_CACHE_NAME = 'smartsoltech-dynamic-v1.0.0'; // Files to cache immediately const STATIC_FILES = [ '/', '/css/main.css', '/js/main.js', '/images/logo.png', '/images/icon-192x192.png', '/images/icon-512x512.png', '/manifest.json', 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap', 'https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css', 'https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.css', 'https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.js' ]; // Routes to cache dynamically const DYNAMIC_ROUTES = [ '/about', '/services', '/portfolio', '/calculator', '/contact' ]; // API endpoints to cache const API_CACHE_PATTERNS = [ /^\/api\/portfolio/, /^\/api\/services/, /^\/api\/calculator\/services/ ]; // Install event - cache static files self.addEventListener('install', event => { console.log('Service Worker: Installing...'); event.waitUntil( caches.open(STATIC_CACHE_NAME) .then(cache => { console.log('Service Worker: Caching static files'); return cache.addAll(STATIC_FILES); }) .then(() => { console.log('Service Worker: Static files cached'); return self.skipWaiting(); }) .catch(error => { console.error('Service Worker: Error caching static files', 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 !== STATIC_CACHE_NAME && cacheName !== DYNAMIC_CACHE_NAME) { console.log('Service Worker: Deleting old cache', cacheName); return caches.delete(cacheName); } }) ); }) .then(() => { console.log('Service Worker: Activated'); return self.clients.claim(); }) ); }); // Fetch event - serve cached files or fetch from network self.addEventListener('fetch', event => { const request = event.request; const url = new URL(request.url); // Skip non-GET requests if (request.method !== 'GET') { return; } // Skip Chrome extension requests if (url.protocol === 'chrome-extension:') { return; } // Handle different types of requests if (isStaticFile(request.url)) { event.respondWith(cacheFirst(request)); } else if (isAPIRequest(request.url)) { event.respondWith(networkFirst(request)); } else if (isDynamicRoute(request.url)) { event.respondWith(staleWhileRevalidate(request)); } else { event.respondWith(networkFirst(request)); } }); // Cache strategies async function cacheFirst(request) { try { const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } const networkResponse = await fetch(request); const cache = await caches.open(STATIC_CACHE_NAME); cache.put(request, networkResponse.clone()); return networkResponse; } catch (error) { console.error('Cache first strategy failed:', error); return new Response('Offline', { status: 503 }); } } async function networkFirst(request) { try { const networkResponse = await fetch(request); // Cache successful responses if (networkResponse.ok) { const cache = await caches.open(DYNAMIC_CACHE_NAME); cache.put(request, networkResponse.clone()); } return networkResponse; } catch (error) { console.log('Network first: Falling back to cache for', request.url); const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Return offline page for navigation requests if (request.mode === 'navigate') { return caches.match('/offline.html') || new Response('Offline', { status: 503, headers: { 'Content-Type': 'text/html' } }); } return new Response('Network Error', { status: 503 }); } } async function staleWhileRevalidate(request) { const cache = await caches.open(DYNAMIC_CACHE_NAME); const cachedResponse = await cache.match(request); const fetchPromise = fetch(request).then(networkResponse => { if (networkResponse.ok) { cache.put(request, networkResponse.clone()); } return networkResponse; }); return cachedResponse || fetchPromise; } // Helper functions function isStaticFile(url) { return url.includes('/css/') || url.includes('/js/') || url.includes('/images/') || url.includes('/fonts/') || url.includes('googleapis.com') || url.includes('cdnjs.cloudflare.com'); } function isAPIRequest(url) { return url.includes('/api/') || API_CACHE_PATTERNS.some(pattern => pattern.test(url)); } function isDynamicRoute(url) { const pathname = new URL(url).pathname; return DYNAMIC_ROUTES.includes(pathname) || pathname.startsWith('/portfolio/') || pathname.startsWith('/services/'); } // Background sync for form submissions self.addEventListener('sync', event => { console.log('Service Worker: Background sync triggered', event.tag); if (event.tag === 'contact-form-sync') { event.waitUntil(syncContactForms()); } }); async function syncContactForms() { try { const cache = await caches.open(DYNAMIC_CACHE_NAME); const requests = await cache.keys(); const contactRequests = requests.filter(request => request.url.includes('/api/contact/submit') ); for (const request of contactRequests) { try { await fetch(request); await cache.delete(request); console.log('Contact form synced successfully'); } catch (error) { console.error('Failed to sync contact form:', error); } } } catch (error) { console.error('Background sync failed:', error); } } // Push notification handling self.addEventListener('push', event => { console.log('Service Worker: Push received', event); let data = {}; if (event.data) { data = event.data.json(); } const title = data.title || 'SmartSolTech'; const options = { body: data.body || 'You have a new notification', icon: '/images/icon-192x192.png', badge: '/images/icon-72x72.png', tag: data.tag || 'default', data: data.url || '/', actions: [ { action: 'open', title: '열기', icon: '/images/icon-open.png' }, { action: 'close', title: '닫기', icon: '/images/icon-close.png' } ], requireInteraction: data.requireInteraction || false, silent: data.silent || false, vibrate: data.vibrate || [200, 100, 200] }; event.waitUntil( self.registration.showNotification(title, options) ); }); // Notification click handling self.addEventListener('notificationclick', event => { console.log('Service Worker: Notification clicked', event); event.notification.close(); if (event.action === 'close') { return; } const url = event.notification.data || '/'; event.waitUntil( clients.matchAll({ type: 'window' }).then(clientList => { // Check if window is already open 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); } }) ); }); // Handle messages from main thread self.addEventListener('message', event => { console.log('Service Worker: Message received', event.data); if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } if (event.data && event.data.type === 'CACHE_URLS') { cacheUrls(event.data.urls); } }); async function cacheUrls(urls) { try { const cache = await caches.open(DYNAMIC_CACHE_NAME); await cache.addAll(urls); console.log('URLs cached successfully:', urls); } catch (error) { console.error('Failed to cache URLs:', error); } } // Periodic background sync (if supported) self.addEventListener('periodicsync', event => { console.log('Service Worker: Periodic sync triggered', event.tag); if (event.tag === 'content-sync') { event.waitUntil(syncContent()); } }); async function syncContent() { try { // Fetch fresh portfolio and services data const portfolioResponse = await fetch('/api/portfolio?featured=true'); const servicesResponse = await fetch('/api/services?featured=true'); if (portfolioResponse.ok && servicesResponse.ok) { const cache = await caches.open(DYNAMIC_CACHE_NAME); cache.put('/api/portfolio?featured=true', portfolioResponse.clone()); cache.put('/api/services?featured=true', servicesResponse.clone()); console.log('Content synced successfully'); } } catch (error) { console.error('Content sync failed:', error); } } // Cache management utilities async function cleanupCaches() { const cacheNames = await caches.keys(); const currentCaches = [STATIC_CACHE_NAME, DYNAMIC_CACHE_NAME]; return Promise.all( cacheNames.map(cacheName => { if (!currentCaches.includes(cacheName)) { console.log('Deleting old cache:', cacheName); return caches.delete(cacheName); } }) ); } // Limit cache size async function limitCacheSize(cacheName, maxItems) { const cache = await caches.open(cacheName); const keys = await cache.keys(); if (keys.length > maxItems) { const keysToDelete = keys.slice(0, keys.length - maxItems); return Promise.all(keysToDelete.map(key => cache.delete(key))); } } // Performance monitoring self.addEventListener('fetch', event => { if (event.request.url.includes('/api/')) { const start = performance.now(); event.respondWith( fetch(event.request).then(response => { const duration = performance.now() - start; // Log slow API requests if (duration > 2000) { console.warn('Slow API request:', event.request.url, duration + 'ms'); } return response; }) ); } }); // Error tracking self.addEventListener('error', event => { console.error('Service Worker error:', event.error); // Could send to analytics service }); self.addEventListener('unhandledrejection', event => { console.error('Service Worker unhandled rejection:', event.reason); // Could send to analytics service });