Compare commits
1 Commits
7a01aecb34
...
feature/is
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b98b3bf222 |
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<NuxtLayout>
|
<div>
|
||||||
<NuxtPage />
|
<OfflineBanner />
|
||||||
</NuxtLayout>
|
<NuxtLayout>
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
58
app/components/OfflineBanner.vue
Normal file
58
app/components/OfflineBanner.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition ease-out duration-300"
|
||||||
|
enter-from-class="opacity-0 -translate-y-2"
|
||||||
|
enter-to-class="opacity-100 translate-y-0"
|
||||||
|
leave-active-class="transition ease-in duration-200"
|
||||||
|
leave-from-class="opacity-100 translate-y-0"
|
||||||
|
leave-to-class="opacity-0 -translate-y-2"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!isOnline"
|
||||||
|
class="fixed top-0 left-0 right-0 z-50 bg-amber-500 text-white px-4 py-2 text-center shadow-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-wifi-slash" class="w-5 h-5" />
|
||||||
|
<span class="font-medium">
|
||||||
|
You're currently offline. Changes will sync when connection is restored.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition ease-out duration-300"
|
||||||
|
enter-from-class="opacity-0 -translate-y-2"
|
||||||
|
enter-to-class="opacity-100 translate-y-0"
|
||||||
|
leave-active-class="transition ease-in duration-200"
|
||||||
|
leave-from-class="opacity-100 translate-y-0"
|
||||||
|
leave-to-class="opacity-0 -translate-y-2"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isOnline && wasOffline && showReconnected"
|
||||||
|
class="fixed top-0 left-0 right-0 z-50 bg-emerald-500 text-white px-4 py-2 text-center shadow-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-wifi" class="w-5 h-5" />
|
||||||
|
<span class="font-medium">
|
||||||
|
Back online! Syncing your changes...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { isOnline, wasOffline } = useOnlineStatus()
|
||||||
|
const showReconnected = ref(false)
|
||||||
|
|
||||||
|
// Show "back online" message for 3 seconds
|
||||||
|
watch(isOnline, (online) => {
|
||||||
|
if (online && wasOffline.value) {
|
||||||
|
showReconnected.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
showReconnected.value = false
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
47
app/composables/useOnlineStatus.ts
Normal file
47
app/composables/useOnlineStatus.ts
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,37 +77,101 @@ export default defineNuxtConfig({
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
workbox: {
|
workbox: {
|
||||||
navigateFallback: '/',
|
navigateFallback: '/offline',
|
||||||
globPatterns: ['**/*.{js,css,html,png,svg,ico}'],
|
navigateFallbackDenylist: [/^\/api\//],
|
||||||
|
globPatterns: ['**/*.{js,css,html,png,svg,ico,woff,woff2}'],
|
||||||
cleanupOutdatedCaches: true,
|
cleanupOutdatedCaches: true,
|
||||||
|
skipWaiting: true,
|
||||||
|
clientsClaim: true,
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
|
// Supabase API - Network first with fallback
|
||||||
{
|
{
|
||||||
urlPattern: /^https:\/\/api\.supabase\.co\/.*/i,
|
urlPattern: /^https:\/\/.*\.supabase\.co\/rest\/.*/i,
|
||||||
handler: 'NetworkFirst',
|
handler: 'NetworkFirst',
|
||||||
options: {
|
options: {
|
||||||
cacheName: 'supabase-api',
|
cacheName: 'supabase-rest-api',
|
||||||
|
networkTimeoutSeconds: 10,
|
||||||
expiration: {
|
expiration: {
|
||||||
maxEntries: 100,
|
maxEntries: 50,
|
||||||
maxAgeSeconds: 60 * 60 * 24 // 24 hours
|
maxAgeSeconds: 60 * 60 // 1 hour
|
||||||
},
|
},
|
||||||
cacheableResponse: {
|
cacheableResponse: {
|
||||||
statuses: [0, 200]
|
statuses: [0, 200]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Supabase Auth - Network only (don't cache auth)
|
||||||
{
|
{
|
||||||
urlPattern: /^https:\/\/.*\.supabase\.co\/.*/i,
|
urlPattern: /^https:\/\/.*\.supabase\.co\/auth\/.*/i,
|
||||||
handler: 'NetworkFirst',
|
handler: 'NetworkOnly'
|
||||||
|
},
|
||||||
|
// Supabase Storage - Cache first for images
|
||||||
|
{
|
||||||
|
urlPattern: /^https:\/\/.*\.supabase\.co\/storage\/.*/i,
|
||||||
|
handler: 'CacheFirst',
|
||||||
options: {
|
options: {
|
||||||
cacheName: 'supabase-data',
|
cacheName: 'supabase-storage',
|
||||||
expiration: {
|
expiration: {
|
||||||
maxEntries: 100,
|
maxEntries: 100,
|
||||||
maxAgeSeconds: 60 * 60 * 24 // 24 hours
|
maxAgeSeconds: 60 * 60 * 24 * 7 // 1 week
|
||||||
},
|
},
|
||||||
cacheableResponse: {
|
cacheableResponse: {
|
||||||
statuses: [0, 200]
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
69
app/pages/offline.vue
Normal file
69
app/pages/offline.vue
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-screen p-8 text-center">
|
||||||
|
<UIcon name="i-heroicons-wifi-slash" class="w-24 h-24 text-gray-400 mb-6" />
|
||||||
|
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 mb-4">
|
||||||
|
You're Offline
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p class="text-gray-600 mb-8 max-w-md">
|
||||||
|
No internet connection detected. Some features may be limited, but you can still:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-3 mb-8 text-left max-w-md">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<UIcon name="i-heroicons-check-circle" class="w-6 h-6 text-emerald-500 mt-0.5" />
|
||||||
|
<span class="text-gray-700">View cached inventory items</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<UIcon name="i-heroicons-check-circle" class="w-6 h-6 text-emerald-500 mt-0.5" />
|
||||||
|
<span class="text-gray-700">Scan barcodes (will sync when online)</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<UIcon name="i-heroicons-check-circle" class="w-6 h-6 text-emerald-500 mt-0.5" />
|
||||||
|
<span class="text-gray-700">Browse previously loaded data</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
color="emerald"
|
||||||
|
size="lg"
|
||||||
|
@click="retry"
|
||||||
|
:loading="retrying"
|
||||||
|
>
|
||||||
|
<template #leading>
|
||||||
|
<UIcon name="i-heroicons-arrow-path" />
|
||||||
|
</template>
|
||||||
|
Try Again
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const retrying = ref(false)
|
||||||
|
|
||||||
|
async function retry() {
|
||||||
|
retrying.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test if we're back online
|
||||||
|
const response = await fetch('/api/health', { method: 'HEAD' })
|
||||||
|
if (response.ok) {
|
||||||
|
// We're online! Go back
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Still offline
|
||||||
|
setTimeout(() => {
|
||||||
|
retrying.value = false
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-retry when online event fires
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('online', () => {
|
||||||
|
window.location.reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user