diff --git a/app/index.html b/app/index.html index bded0aa..76cad71 100644 --- a/app/index.html +++ b/app/index.html @@ -7,10 +7,16 @@ - + + + + Echo Sphere - + diff --git a/app/public/manifest.webmanifest b/app/public/manifest.webmanifest new file mode 100644 index 0000000..27ca864 --- /dev/null +++ b/app/public/manifest.webmanifest @@ -0,0 +1,17 @@ +{ + "name": "Echo Sphere", + "short_name": "EchoSphere", + "start_url": "/", + "display": "standalone", + "background_color": "#0f172a", + "theme_color": "#0f172a", + "description": "Explore the Echo Sphere demo application even when you are offline.", + "icons": [ + { + "src": "/pwa-icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any maskable" + } + ] +} diff --git a/app/public/pwa-icon.svg b/app/public/pwa-icon.svg new file mode 100644 index 0000000..b3a1c9a --- /dev/null +++ b/app/public/pwa-icon.svg @@ -0,0 +1,15 @@ + + Echo Sphere Icon + Monogram of the letters E and S inside a circle. + + + + + + + + + + + + diff --git a/app/public/sw.js b/app/public/sw.js new file mode 100644 index 0000000..75c5b81 --- /dev/null +++ b/app/public/sw.js @@ -0,0 +1,99 @@ +const CACHE_NAME = 'webframework-cache-v1' +const OFFLINE_URL = '/' + +// Assets to cache on install +const STATIC_ASSETS = [ + '/', + '/static/app/index.html', + '/static/app/manifest.webmanifest', + '/static/app/pwa-icon.svg', + '/static/app/vite-logo.svg', +] + +self.addEventListener('install', (event) => { + event.waitUntil( + caches + .open(CACHE_NAME) + .then((cache) => { + // Cache static assets + return cache.addAll(STATIC_ASSETS.map((url) => new Request(url, { cache: 'reload' }))) + }) + .catch(() => {}) + .then(() => self.skipWaiting()) + ) +}) + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => + Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))) + ) + .then(() => self.clients.claim()) + ) +}) + +self.addEventListener('fetch', (event) => { + const { request } = event + + if (request.method !== 'GET' || request.url.startsWith('chrome-extension')) { + return + } + + const requestURL = new URL(request.url) + + if (requestURL.origin !== self.location.origin) { + return + } + + event.respondWith( + caches.open(CACHE_NAME).then(async (cache) => { + try { + const response = await fetch(request) + if ( + response && + response.status === 200 && + response.type === 'basic' && + !request.url.includes('/__vite_ping') && + !request.url.includes('/api/') // Don't cache API responses + ) { + cache.put(request, response.clone()).catch(() => {}) + } + return response + } catch (error) { + // Try to serve from cache + const cachedResponse = await cache.match(request) + if (cachedResponse) { + return cachedResponse + } + + // For navigation requests, serve the offline page + if (request.mode === 'navigate') { + const offlineResponse = + (await cache.match('/static/app/index.html')) || (await cache.match(OFFLINE_URL)) + if (offlineResponse) { + return offlineResponse + } + } + + // For API requests, return a proper offline response + if (request.url.includes('/api/')) { + return new Response( + JSON.stringify({ + error: 'Offline', + message: 'This feature is not available offline', + }), + { + status: 503, + statusText: 'Service Unavailable', + headers: { 'Content-Type': 'application/json' }, + } + ) + } + + throw error + } + }) + ) +}) diff --git a/app/src/main.tsx b/app/src/main.tsx index 946a569..46ff5e4 100644 --- a/app/src/main.tsx +++ b/app/src/main.tsx @@ -1,5 +1,8 @@ import { createRoot } from 'react-dom/client' import App from './App.tsx' +import { registerServiceWorker } from './service-worker-registration.ts' // biome-ignore lint/style/noNonNullAssertion: expect root element to exist createRoot(document.getElementById('root')!).render() + +registerServiceWorker() diff --git a/app/src/service-worker-registration.ts b/app/src/service-worker-registration.ts new file mode 100644 index 0000000..f9efb48 --- /dev/null +++ b/app/src/service-worker-registration.ts @@ -0,0 +1,36 @@ +export function registerServiceWorker(): void { + if (!import.meta.env.PROD) { + return + } + + if (!('serviceWorker' in navigator)) { + return + } + + const register = () => { + // Use the correct path based on environment + const swPath = import.meta.env.PROD ? '/static/app/sw.js' : '/sw.js' + + navigator.serviceWorker.register(swPath).catch((error) => { + console.error('Service worker registration failed:', error) + }) + } + + if (document.readyState === 'complete') { + register() + } else { + window.addEventListener('load', register, { once: true }) + } +} + +export function unregisterServiceWorker(): void { + if (!('serviceWorker' in navigator)) { + return + } + + navigator.serviceWorker.ready + .then((registration) => registration.unregister()) + .catch(() => { + // Swallow errors silently to avoid crashing the app when service worker cannot be unregistered. + }) +} diff --git a/server/apps/website/templates/website/index.html b/server/apps/website/templates/website/index.html index 9c23f0a..0dadb01 100644 --- a/server/apps/website/templates/website/index.html +++ b/server/apps/website/templates/website/index.html @@ -10,8 +10,11 @@ {% else %} - - + + + + + {% endif %} Echo Sphere @@ -94,4 +97,4 @@ - \ No newline at end of file +