slideshare/public/sw.js
Michael Mainguy 7bd25e1a7a Add Progressive Web App support and content formatting standards
## 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>
2025-09-12 09:34:40 -05:00

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');