diff --git a/app/app/app.vue b/app/app/app.vue index f8eacfa..bac3d53 100644 --- a/app/app/app.vue +++ b/app/app/app.vue @@ -1,5 +1,8 @@ diff --git a/app/components/OfflineBanner.vue b/app/components/OfflineBanner.vue new file mode 100644 index 0000000..201ec86 --- /dev/null +++ b/app/components/OfflineBanner.vue @@ -0,0 +1,58 @@ + + + diff --git a/app/composables/useOnlineStatus.ts b/app/composables/useOnlineStatus.ts new file mode 100644 index 0000000..328d8a7 --- /dev/null +++ b/app/composables/useOnlineStatus.ts @@ -0,0 +1,47 @@ +/** + * Composable to track online/offline status + * + * Usage: + * const { isOnline, wasOffline } = useOnlineStatus() + * + * watch(isOnline, (online) => { + * if (online && wasOffline.value) { + * // User came back online, sync data + * } + * }) + */ +export function useOnlineStatus() { + const isOnline = ref(true) + const wasOffline = ref(false) + + if (process.client) { + // Initial state + isOnline.value = navigator.onLine + + // Listen for online/offline events + const updateOnlineStatus = () => { + const online = navigator.onLine + + if (!online && isOnline.value) { + // Just went offline + wasOffline.value = true + } + + isOnline.value = online + } + + window.addEventListener('online', updateOnlineStatus) + window.addEventListener('offline', updateOnlineStatus) + + // Cleanup on unmount + onUnmounted(() => { + window.removeEventListener('online', updateOnlineStatus) + window.removeEventListener('offline', updateOnlineStatus) + }) + } + + return { + isOnline: readonly(isOnline), + wasOffline: readonly(wasOffline) + } +} diff --git a/app/nuxt.config.ts b/app/nuxt.config.ts index 69e9019..07add8a 100644 --- a/app/nuxt.config.ts +++ b/app/nuxt.config.ts @@ -77,37 +77,101 @@ export default defineNuxtConfig({ ] }, workbox: { - navigateFallback: '/', - globPatterns: ['**/*.{js,css,html,png,svg,ico}'], + navigateFallback: '/offline', + navigateFallbackDenylist: [/^\/api\//], + globPatterns: ['**/*.{js,css,html,png,svg,ico,woff,woff2}'], cleanupOutdatedCaches: true, + skipWaiting: true, + clientsClaim: true, runtimeCaching: [ + // Supabase API - Network first with fallback { - urlPattern: /^https:\/\/api\.supabase\.co\/.*/i, + urlPattern: /^https:\/\/.*\.supabase\.co\/rest\/.*/i, handler: 'NetworkFirst', options: { - cacheName: 'supabase-api', + cacheName: 'supabase-rest-api', + networkTimeoutSeconds: 10, expiration: { - maxEntries: 100, - maxAgeSeconds: 60 * 60 * 24 // 24 hours + maxEntries: 50, + maxAgeSeconds: 60 * 60 // 1 hour }, cacheableResponse: { statuses: [0, 200] } } }, + // Supabase Auth - Network only (don't cache auth) { - urlPattern: /^https:\/\/.*\.supabase\.co\/.*/i, - handler: 'NetworkFirst', + urlPattern: /^https:\/\/.*\.supabase\.co\/auth\/.*/i, + handler: 'NetworkOnly' + }, + // Supabase Storage - Cache first for images + { + urlPattern: /^https:\/\/.*\.supabase\.co\/storage\/.*/i, + handler: 'CacheFirst', options: { - cacheName: 'supabase-data', + cacheName: 'supabase-storage', expiration: { maxEntries: 100, - maxAgeSeconds: 60 * 60 * 24 // 24 hours + maxAgeSeconds: 60 * 60 * 24 * 7 // 1 week }, cacheableResponse: { statuses: [0, 200] } } + }, + // Open Food Facts API - Cache first with network fallback + { + urlPattern: /^https:\/\/world\.openfoodfacts\.org\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'openfoodfacts-api', + expiration: { + maxEntries: 200, + maxAgeSeconds: 60 * 60 * 24 * 30 // 30 days + }, + cacheableResponse: { + statuses: [0, 200] + } + } + }, + // External images - Cache first + { + urlPattern: /^https:\/\/images\.openfoodfacts\.org\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'product-images', + expiration: { + maxEntries: 100, + maxAgeSeconds: 60 * 60 * 24 * 30 // 30 days + }, + cacheableResponse: { + statuses: [0, 200] + } + } + }, + // Google Fonts - Cache first + { + urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'google-fonts-stylesheets', + expiration: { + maxEntries: 20, + maxAgeSeconds: 60 * 60 * 24 * 365 // 1 year + } + } + }, + { + urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'google-fonts-webfonts', + expiration: { + maxEntries: 30, + maxAgeSeconds: 60 * 60 * 24 * 365 // 1 year + } + } } ] }, diff --git a/app/pages/offline.vue b/app/pages/offline.vue new file mode 100644 index 0000000..88cdd23 --- /dev/null +++ b/app/pages/offline.vue @@ -0,0 +1,69 @@ + + +