## PWA Implementation • Add comprehensive service worker for offline caching and app installation • Implement PWA manifest with app shortcuts and file handling • Create offline indicator component with update notifications • Add service worker utilities for cache management and updates • Update HTML with PWA meta tags and SEO optimization ## Content Standards Enhancement • Update presentation JSON generator prompt with strict Unicode prohibition • Add comprehensive content quality checklist for ASCII-only formatting • Create two example presentations demonstrating proper formatting • Fix build errors in OfflineIndicator component styling • Enforce consistent markdown formatting with plain dash bullets ## Features Added • Install as native app capability on all platforms • Complete offline functionality after first load • Automatic background updates with user notifications • Export/import JSON presentations with proper formatting • Real-time online/offline status indicators 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
253 lines
6.5 KiB
JavaScript
253 lines
6.5 KiB
JavaScript
// SlideShare Service Worker
|
|
// Enables offline functionality and app installation
|
|
|
|
const CACHE_NAME = 'slideshare-v1';
|
|
const STATIC_CACHE = `${CACHE_NAME}-static`;
|
|
const DYNAMIC_CACHE = `${CACHE_NAME}-dynamic`;
|
|
|
|
// Resources to cache immediately when service worker installs
|
|
const STATIC_ASSETS = [
|
|
'/',
|
|
'/index.html',
|
|
'/manifest.json',
|
|
// Core app files will be added by build process
|
|
];
|
|
|
|
// Resources that should always be fetched from network first
|
|
const NETWORK_FIRST = [
|
|
'/api/',
|
|
'.json'
|
|
];
|
|
|
|
// Resources that can be cached and served stale while revalidating
|
|
const STALE_WHILE_REVALIDATE = [
|
|
'/themes/',
|
|
'.css',
|
|
'.js',
|
|
'.woff2',
|
|
'.woff'
|
|
];
|
|
|
|
// Install event - cache core assets
|
|
self.addEventListener('install', event => {
|
|
console.log('[SW] Installing service worker...');
|
|
|
|
event.waitUntil(
|
|
caches.open(STATIC_CACHE)
|
|
.then(cache => {
|
|
console.log('[SW] Caching static assets');
|
|
return cache.addAll(STATIC_ASSETS);
|
|
})
|
|
.then(() => {
|
|
console.log('[SW] Service worker installed successfully');
|
|
// Skip waiting to activate immediately
|
|
return self.skipWaiting();
|
|
})
|
|
.catch(error => {
|
|
console.error('[SW] Failed to cache static assets:', error);
|
|
})
|
|
);
|
|
});
|
|
|
|
// Activate event - cleanup old caches
|
|
self.addEventListener('activate', event => {
|
|
console.log('[SW] Activating service worker...');
|
|
|
|
event.waitUntil(
|
|
caches.keys()
|
|
.then(cacheNames => {
|
|
return Promise.all(
|
|
cacheNames
|
|
.filter(cacheName =>
|
|
cacheName.startsWith('slideshare-') &&
|
|
!cacheName.includes(CACHE_NAME)
|
|
)
|
|
.map(cacheName => {
|
|
console.log('[SW] Deleting old cache:', cacheName);
|
|
return caches.delete(cacheName);
|
|
})
|
|
);
|
|
})
|
|
.then(() => {
|
|
console.log('[SW] Service worker activated');
|
|
// Take control of all clients immediately
|
|
return self.clients.claim();
|
|
})
|
|
);
|
|
});
|
|
|
|
// Fetch event - handle network requests with caching strategies
|
|
self.addEventListener('fetch', event => {
|
|
const { request } = event;
|
|
const url = new URL(request.url);
|
|
|
|
// Only handle same-origin requests
|
|
if (url.origin !== location.origin) {
|
|
return;
|
|
}
|
|
|
|
// Determine caching strategy based on request
|
|
if (shouldUseNetworkFirst(request)) {
|
|
event.respondWith(networkFirst(request));
|
|
} else if (shouldUseStaleWhileRevalidate(request)) {
|
|
event.respondWith(staleWhileRevalidate(request));
|
|
} else {
|
|
event.respondWith(cacheFirst(request));
|
|
}
|
|
});
|
|
|
|
// Network-first strategy (for dynamic content)
|
|
async function networkFirst(request) {
|
|
const cache = await caches.open(DYNAMIC_CACHE);
|
|
|
|
try {
|
|
const response = await fetch(request);
|
|
if (response.status === 200) {
|
|
cache.put(request, response.clone());
|
|
}
|
|
return response;
|
|
} catch (error) {
|
|
console.log('[SW] Network failed, serving from cache:', request.url);
|
|
const cachedResponse = await cache.match(request);
|
|
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
|
|
// Return offline page for navigation requests
|
|
if (request.mode === 'navigate') {
|
|
return caches.match('/');
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Stale-while-revalidate strategy (for assets that can be updated in background)
|
|
async function staleWhileRevalidate(request) {
|
|
const cache = await caches.open(STATIC_CACHE);
|
|
const cachedResponse = await cache.match(request);
|
|
|
|
const fetchPromise = fetch(request)
|
|
.then(response => {
|
|
if (response.status === 200) {
|
|
cache.put(request, response.clone());
|
|
}
|
|
return response;
|
|
})
|
|
.catch(() => cachedResponse); // Fallback to cache on network error
|
|
|
|
// Return cached version immediately if available, otherwise wait for network
|
|
return cachedResponse || fetchPromise;
|
|
}
|
|
|
|
// Cache-first strategy (for static assets)
|
|
async function cacheFirst(request) {
|
|
const cachedResponse = await caches.match(request);
|
|
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(request);
|
|
if (response.status === 200) {
|
|
const cache = await caches.open(STATIC_CACHE);
|
|
cache.put(request, response.clone());
|
|
}
|
|
return response;
|
|
} catch (error) {
|
|
console.log('[SW] Failed to fetch and cache:', request.url);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Helper functions to determine caching strategy
|
|
function shouldUseNetworkFirst(request) {
|
|
return NETWORK_FIRST.some(pattern =>
|
|
request.url.includes(pattern) ||
|
|
request.method !== 'GET'
|
|
);
|
|
}
|
|
|
|
function shouldUseStaleWhileRevalidate(request) {
|
|
return STALE_WHILE_REVALIDATE.some(pattern =>
|
|
request.url.includes(pattern)
|
|
);
|
|
}
|
|
|
|
// Handle messages from the main thread
|
|
self.addEventListener('message', event => {
|
|
if (event.data && event.data.type) {
|
|
switch (event.data.type) {
|
|
case 'SKIP_WAITING':
|
|
self.skipWaiting();
|
|
break;
|
|
case 'GET_VERSION':
|
|
event.ports[0].postMessage({ version: CACHE_NAME });
|
|
break;
|
|
case 'CLEAR_CACHE':
|
|
clearAllCaches().then(() => {
|
|
event.ports[0].postMessage({ success: true });
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Clear all caches (useful for development)
|
|
async function clearAllCaches() {
|
|
const cacheNames = await caches.keys();
|
|
await Promise.all(
|
|
cacheNames.map(cacheName => caches.delete(cacheName))
|
|
);
|
|
console.log('[SW] All caches cleared');
|
|
}
|
|
|
|
// Background sync for when connection is restored
|
|
self.addEventListener('sync', event => {
|
|
console.log('[SW] Background sync triggered:', event.tag);
|
|
|
|
if (event.tag === 'background-sync') {
|
|
event.waitUntil(
|
|
// Could implement background data sync here
|
|
Promise.resolve()
|
|
);
|
|
}
|
|
});
|
|
|
|
// Push notifications (for future use)
|
|
self.addEventListener('push', event => {
|
|
if (!event.data) return;
|
|
|
|
const data = event.data.json();
|
|
const options = {
|
|
body: data.body,
|
|
icon: '/icons/icon-192.png',
|
|
badge: '/icons/badge-72.png',
|
|
vibrate: [100, 50, 100],
|
|
data: data.data,
|
|
actions: data.actions
|
|
};
|
|
|
|
event.waitUntil(
|
|
self.registration.showNotification(data.title, options)
|
|
);
|
|
});
|
|
|
|
// Handle notification clicks
|
|
self.addEventListener('notificationclick', event => {
|
|
event.notification.close();
|
|
|
|
if (event.action) {
|
|
// Handle action button clicks
|
|
console.log('[SW] Notification action clicked:', event.action);
|
|
} else {
|
|
// Handle notification body click
|
|
event.waitUntil(
|
|
clients.openWindow('/')
|
|
);
|
|
}
|
|
});
|
|
|
|
console.log('[SW] Service worker script loaded'); |