generated from nhcarrigan/template
feat: transform library into Progressive Web App (PWA)
Implements issue #52 - adds complete PWA functionality with offline support, installability, and app-like experience. PWA Features: - Installable on mobile devices and desktop - Offline support with smart caching strategies - App-like experience with standalone display mode - Full-screen mode and app icon on home screen - Install prompt with beautiful banner UI Manifest Configuration: - Created manifest.json with app metadata - App name: "Naomi's Library" - Theme colour: #9d4edd (witchy purple!) - Background colour: #1a1a2e (dark theme) - Display mode: standalone (app-like) - Icons: Multiple sizes specified (72-512px, maskable variants) - Categories: entertainment, lifestyle Service Worker Implementation: - Cache-first strategy for static assets (JS, CSS, fonts, images) - Network-first strategy for API requests with cache fallback - Network-first strategy for HTML pages with cache fallback - Cache versioning: library-v1 (static, dynamic, images) - Automatic cache cleanup on activation - Update checking every 60 seconds - Message handling for cache clearing and skipWaiting Offline Support: - Created offline.html fallback page with beautiful UI - Auto-retry when network comes back online - Purple gradient matching app theme - Informative messaging about offline state Install Prompt: - Created PwaInstallComponent with banner UI - Detects installability and shows prompt - "Install" and "Not Now" actions - Responsive design for mobile and desktop - Session storage for dismissal state - Animated slide-up entrance Services: - PwaService: Manages service worker registration, install prompts, cache clearing - Automatic initialization on app bootstrap - Signals for reactivity: isInstallable, isInstalled, promptEvent - Update detection and notification HTML Updates: - Added PWA meta tags (theme-color, description) - Linked manifest.json - Apple-specific meta tags for iOS support - Apple touch icon specified Build Configuration: - Updated project.json to include PWA assets - manifest.json, service-worker.js, offline.html copied to dist root Integration: - Added PwaService to app bootstrap - Integrated PwaInstallComponent in main app template - Install banner appears at bottom of screen - Dismissible with session storage Technical Notes: - Service worker uses efficient caching strategies per resource type - Handles Chrome extensions and non-HTTP protocols gracefully - Supports message-based cache clearing - Includes skipWaiting for immediate updates - Compatible with iOS, Android, and desktop PWA standards The library is now a fully-functional Progressive Web App that users can install on any device! The app works offline with cached content and provides an app-like experience with no browser UI in standalone mode. Note: App icons still need to be generated/provided (placeholder paths specified in manifest). ✨ This feature was built by Hikari~ 🌸
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* @copyright 2026 NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Hikari
|
||||
*/
|
||||
|
||||
const CACHE_VERSION = 'library-v1';
|
||||
const STATIC_CACHE = `${CACHE_VERSION}-static`;
|
||||
const DYNAMIC_CACHE = `${CACHE_VERSION}-dynamic`;
|
||||
const IMAGE_CACHE = `${CACHE_VERSION}-images`;
|
||||
|
||||
// Static assets to cache on install
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/main.js',
|
||||
'/runtime.js',
|
||||
'/styles.css',
|
||||
'/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).catch((err) => {
|
||||
console.error('[Service Worker] Failed to cache static assets:', err);
|
||||
});
|
||||
}).then(() => {
|
||||
return 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.startsWith('library-') && cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE && cacheName !== IMAGE_CACHE;
|
||||
})
|
||||
.map((cacheName) => {
|
||||
console.log('[Service Worker] Deleting old cache:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
})
|
||||
);
|
||||
}).then(() => {
|
||||
return self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch event - implement caching strategies
|
||||
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 other protocols
|
||||
if (!url.protocol.startsWith('http')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API requests - Network first, cache fallback
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then((response) => {
|
||||
// Clone the response before caching
|
||||
const responseClone = response.clone();
|
||||
caches.open(DYNAMIC_CACHE).then((cache) => {
|
||||
cache.put(request, responseClone);
|
||||
});
|
||||
return response;
|
||||
})
|
||||
.catch(() => {
|
||||
return caches.match(request).then((cachedResponse) => {
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
// Return offline page for failed API requests
|
||||
return caches.match('/offline.html');
|
||||
});
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Images - Cache first, network fallback
|
||||
if (request.destination === 'image' || url.pathname.match(/\.(jpg|jpeg|png|gif|webp|svg)$/)) {
|
||||
event.respondWith(
|
||||
caches.match(request).then((cachedResponse) => {
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
return fetch(request).then((response) => {
|
||||
const responseClone = response.clone();
|
||||
caches.open(IMAGE_CACHE).then((cache) => {
|
||||
cache.put(request, responseClone);
|
||||
});
|
||||
return response;
|
||||
});
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Static assets - Cache first, network fallback
|
||||
if (url.pathname.match(/\.(js|css|woff|woff2|ttf|eot)$/)) {
|
||||
event.respondWith(
|
||||
caches.match(request).then((cachedResponse) => {
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
return fetch(request).then((response) => {
|
||||
const responseClone = response.clone();
|
||||
caches.open(STATIC_CACHE).then((cache) => {
|
||||
cache.put(request, responseClone);
|
||||
});
|
||||
return response;
|
||||
});
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// HTML pages - Network first, cache fallback
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then((response) => {
|
||||
const responseClone = response.clone();
|
||||
caches.open(DYNAMIC_CACHE).then((cache) => {
|
||||
cache.put(request, responseClone);
|
||||
});
|
||||
return response;
|
||||
})
|
||||
.catch(() => {
|
||||
return caches.match(request).then((cachedResponse) => {
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
// Return offline page as fallback
|
||||
return caches.match('/offline.html');
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Message event - handle cache clearing requests
|
||||
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.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => caches.delete(cacheName))
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user