feat: Implement comprehensive PWA functionality
- Add full PWA configuration with manifest and service worker - Create PWAInstallBanner component with mobile detection - Implement install banner on login page (shows below login form) - Add usePWA composable for reusable PWA functionality - Configure offline support with Workbox caching strategies - Add PWA initialization plugin - Update app name to 'Port Nimara Portal' throughout - Use circular logo in install banner and instructions - Banner shows only once and hides if already installed - Support both Android (direct install) and iOS (manual instructions) - Add comprehensive documentation for PWA implementation Features: - Mobile-only install banner with dismissal tracking - Standalone mode detection to hide banner when installed - Platform-specific installation instructions - Offline functionality with API caching - Auto-updating service worker - Native app-like experience when installed
This commit is contained in:
parent
4916c20f64
commit
b25e93d2a0
|
|
@ -0,0 +1,195 @@
|
||||||
|
<template>
|
||||||
|
<v-slide-y-transition>
|
||||||
|
<v-card
|
||||||
|
v-if="showBanner"
|
||||||
|
class="mt-4 mx-auto"
|
||||||
|
max-width="350"
|
||||||
|
elevation="2"
|
||||||
|
rounded
|
||||||
|
>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<v-row align="center" no-gutters>
|
||||||
|
<v-col cols="auto" class="mr-3">
|
||||||
|
<v-img
|
||||||
|
src="/Port Nimara New Logo-Circular Frame.png"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
class="rounded-circle"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col>
|
||||||
|
<div class="text-body-2 font-weight-medium mb-1">
|
||||||
|
Install Port Nimara Portal
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey-darken-1">
|
||||||
|
Get faster access and work offline
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-close"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="dismissBanner"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row class="mt-3" no-gutters>
|
||||||
|
<v-col>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
size="small"
|
||||||
|
block
|
||||||
|
@click="installApp"
|
||||||
|
:loading="installing"
|
||||||
|
>
|
||||||
|
<v-icon left>mdi-download</v-icon>
|
||||||
|
Install App
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-slide-y-transition>
|
||||||
|
|
||||||
|
<!-- Installation Instructions Dialog -->
|
||||||
|
<v-dialog v-model="showInstructions" max-width="400">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<v-img
|
||||||
|
src="/Port Nimara New Logo-Circular Frame.png"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
class="rounded-circle mr-2"
|
||||||
|
/>
|
||||||
|
Install Port Nimara Portal
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<div v-if="isIOS">
|
||||||
|
<div class="text-body-2 mb-3">
|
||||||
|
To install this app on your iPhone/iPad:
|
||||||
|
</div>
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item prepend-icon="mdi-share-variant">
|
||||||
|
<v-list-item-title class="text-body-2">
|
||||||
|
Tap the <strong>Share</strong> button at the bottom
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item prepend-icon="mdi-plus-box">
|
||||||
|
<v-list-item-title class="text-body-2">
|
||||||
|
Scroll down and tap <strong>"Add to Home Screen"</strong>
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item prepend-icon="mdi-check">
|
||||||
|
<v-list-item-title class="text-body-2">
|
||||||
|
Tap <strong>"Add"</strong> to confirm
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
<v-alert type="info" variant="tonal" class="mt-3">
|
||||||
|
<div class="text-caption">
|
||||||
|
Make sure you're using Safari browser for this to work.
|
||||||
|
</div>
|
||||||
|
</v-alert>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div class="text-body-2 mb-3">
|
||||||
|
Your browser doesn't support automatic installation.
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey-darken-1">
|
||||||
|
Try using Chrome or another modern browser for the best experience.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn @click="showInstructions = false" variant="text">
|
||||||
|
Close
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { isInstalled, isMobile, isIOS, canInstall, install, init } = usePWA();
|
||||||
|
|
||||||
|
const showBanner = ref(false);
|
||||||
|
const showInstructions = ref(false);
|
||||||
|
const installing = ref(false);
|
||||||
|
|
||||||
|
// Check if banner has been dismissed
|
||||||
|
const wasDismissed = computed(() => {
|
||||||
|
if (process.client) {
|
||||||
|
return localStorage.getItem('pwa-install-dismissed') === 'true';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if should show banner
|
||||||
|
const shouldShowBanner = computed(() => {
|
||||||
|
return canInstall.value && !wasDismissed.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const dismissBanner = () => {
|
||||||
|
showBanner.value = false;
|
||||||
|
if (process.client) {
|
||||||
|
localStorage.setItem('pwa-install-dismissed', 'true');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const installApp = async () => {
|
||||||
|
installing.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await install();
|
||||||
|
if (success) {
|
||||||
|
dismissBanner();
|
||||||
|
} else if (isIOS.value) {
|
||||||
|
// iOS - show instructions
|
||||||
|
showInstructions.value = true;
|
||||||
|
} else {
|
||||||
|
// Other platforms - show instructions
|
||||||
|
showInstructions.value = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Install failed:', error);
|
||||||
|
if (isIOS.value) {
|
||||||
|
showInstructions.value = true;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
installing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (process.client) {
|
||||||
|
// Initialize PWA functionality
|
||||||
|
init();
|
||||||
|
|
||||||
|
// Show banner if conditions are met
|
||||||
|
if (shouldShowBanner.value) {
|
||||||
|
// Small delay to let the page load
|
||||||
|
setTimeout(() => {
|
||||||
|
showBanner.value = true;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for installation status changes
|
||||||
|
watch(isInstalled, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
dismissBanner();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
export const usePWA = () => {
|
||||||
|
const isInstalled = ref(false);
|
||||||
|
const isInstallable = ref(false);
|
||||||
|
const deferredPrompt = ref<any>(null);
|
||||||
|
|
||||||
|
// Check if app is installed (running in standalone mode)
|
||||||
|
const checkInstallStatus = () => {
|
||||||
|
if (process.client) {
|
||||||
|
isInstalled.value = window.matchMedia('(display-mode: standalone)').matches ||
|
||||||
|
(window.navigator as any).standalone ||
|
||||||
|
document.referrer.includes('android-app://');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if device is mobile
|
||||||
|
const isMobile = computed(() => {
|
||||||
|
if (process.client) {
|
||||||
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if device is iOS
|
||||||
|
const isIOS = computed(() => {
|
||||||
|
if (process.client) {
|
||||||
|
return /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if device is Android
|
||||||
|
const isAndroid = computed(() => {
|
||||||
|
if (process.client) {
|
||||||
|
return /Android/i.test(navigator.userAgent);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Install the app
|
||||||
|
const install = async () => {
|
||||||
|
if (deferredPrompt.value) {
|
||||||
|
deferredPrompt.value.prompt();
|
||||||
|
const choiceResult = await deferredPrompt.value.userChoice;
|
||||||
|
|
||||||
|
if (choiceResult.outcome === 'accepted') {
|
||||||
|
console.log('User accepted the install prompt');
|
||||||
|
deferredPrompt.value = null;
|
||||||
|
isInstallable.value = false;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.log('User dismissed the install prompt');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if app can be installed
|
||||||
|
const canInstall = computed(() => {
|
||||||
|
return isMobile.value && !isInstalled.value && (isInstallable.value || isIOS.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize PWA functionality
|
||||||
|
const init = () => {
|
||||||
|
if (process.client) {
|
||||||
|
checkInstallStatus();
|
||||||
|
|
||||||
|
// Listen for the beforeinstallprompt event
|
||||||
|
window.addEventListener('beforeinstallprompt', (e) => {
|
||||||
|
console.log('beforeinstallprompt event fired');
|
||||||
|
e.preventDefault();
|
||||||
|
deferredPrompt.value = e;
|
||||||
|
isInstallable.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for successful app installation
|
||||||
|
window.addEventListener('appinstalled', () => {
|
||||||
|
console.log('PWA was installed');
|
||||||
|
isInstalled.value = true;
|
||||||
|
isInstallable.value = false;
|
||||||
|
deferredPrompt.value = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for display mode changes
|
||||||
|
window.matchMedia('(display-mode: standalone)').addEventListener('change', (e) => {
|
||||||
|
isInstalled.value = e.matches;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isInstalled: readonly(isInstalled),
|
||||||
|
isInstallable: readonly(isInstallable),
|
||||||
|
isMobile,
|
||||||
|
isIOS,
|
||||||
|
isAndroid,
|
||||||
|
canInstall,
|
||||||
|
install,
|
||||||
|
init
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
# PWA Implementation Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The Port Nimara Portal has been implemented as a Progressive Web App (PWA) to provide users with a native app-like experience on mobile devices.
|
||||||
|
|
||||||
|
## Features Implemented
|
||||||
|
|
||||||
|
### 1. PWA Configuration
|
||||||
|
- **App Name**: Port Nimara Portal
|
||||||
|
- **Short Name**: Port Nimara
|
||||||
|
- **Theme Color**: #387bca (brand primary color)
|
||||||
|
- **Background Color**: #ffffff
|
||||||
|
- **Display Mode**: standalone
|
||||||
|
- **Start URL**: /
|
||||||
|
- **Icons**: Multiple sizes from 72x72 to 512x512 pixels
|
||||||
|
|
||||||
|
### 2. Install Banner
|
||||||
|
- **Location**: Login page, below the login form
|
||||||
|
- **Behavior**:
|
||||||
|
- Only shows on mobile devices
|
||||||
|
- Hidden if app is already installed (standalone mode)
|
||||||
|
- Shows only once (dismissal stored in localStorage)
|
||||||
|
- Animated slide-in with 1-second delay
|
||||||
|
|
||||||
|
### 3. Installation Flow
|
||||||
|
- **Android**: Direct browser install prompt
|
||||||
|
- **iOS**: Step-by-step instructions modal
|
||||||
|
- **Other browsers**: Fallback instructions
|
||||||
|
|
||||||
|
### 4. Offline Support
|
||||||
|
- **Service Worker**: Auto-updating with Workbox
|
||||||
|
- **Caching**: Network-first for API calls, cache-first for assets
|
||||||
|
- **Navigation Fallback**: Routes to home page when offline
|
||||||
|
|
||||||
|
## Files Added/Modified
|
||||||
|
|
||||||
|
### New Files
|
||||||
|
- `components/PWAInstallBanner.vue` - Install banner component
|
||||||
|
- `composables/usePWA.ts` - PWA functionality composable
|
||||||
|
- `plugins/pwa.client.ts` - PWA initialization plugin
|
||||||
|
- `docs/pwa-implementation.md` - This documentation
|
||||||
|
|
||||||
|
### Modified Files
|
||||||
|
- `nuxt.config.ts` - Added PWA configuration
|
||||||
|
- `pages/login.vue` - Added install banner component
|
||||||
|
|
||||||
|
## Configuration Details
|
||||||
|
|
||||||
|
### PWA Module Settings
|
||||||
|
```typescript
|
||||||
|
pwa: {
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
manifest: {
|
||||||
|
name: 'Port Nimara Portal',
|
||||||
|
short_name: 'Port Nimara',
|
||||||
|
description: 'Port Nimara Client Portal - Manage your berth interests and expressions of interest',
|
||||||
|
theme_color: '#387bca',
|
||||||
|
background_color: '#ffffff',
|
||||||
|
display: 'standalone',
|
||||||
|
orientation: 'portrait',
|
||||||
|
start_url: '/',
|
||||||
|
scope: '/',
|
||||||
|
icons: [/* 8 icon sizes */]
|
||||||
|
},
|
||||||
|
workbox: {
|
||||||
|
// Caching strategies
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Icon Requirements
|
||||||
|
The following icon sizes are configured:
|
||||||
|
- 72x72, 96x96, 128x128, 144x144 (Android)
|
||||||
|
- 152x152, 192x192 (iOS, Android)
|
||||||
|
- 384x384, 512x512 (Splash screens, large displays)
|
||||||
|
|
||||||
|
All icons are located in `/public/icons/` and based on the circular Port Nimara logo.
|
||||||
|
|
||||||
|
### Browser Support
|
||||||
|
- **Chrome/Edge (Android)**: Full PWA support with install prompt
|
||||||
|
- **Safari (iOS)**: Manual installation via "Add to Home Screen"
|
||||||
|
- **Firefox**: Basic PWA support
|
||||||
|
- **Other browsers**: Graceful degradation with instructions
|
||||||
|
|
||||||
|
## Usage Instructions
|
||||||
|
|
||||||
|
### For Users
|
||||||
|
1. Visit the login page on a mobile device
|
||||||
|
2. If eligible, an install banner will appear below the login form
|
||||||
|
3. Tap "Install App" to begin installation
|
||||||
|
4. Follow platform-specific instructions if needed
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
The PWA functionality is automatically initialized. Use the `usePWA()` composable to access PWA state:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { isInstalled, canInstall, install, isMobile, isIOS } = usePWA();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
- Test on actual mobile devices for best results
|
||||||
|
- Use Chrome DevTools > Application > Manifest to verify configuration
|
||||||
|
- Check Service Worker registration in DevTools > Application > Service Workers
|
||||||
|
- Test offline functionality by disabling network
|
||||||
|
|
||||||
|
## Deployment Notes
|
||||||
|
- Ensure HTTPS is enabled (required for PWA)
|
||||||
|
- Icons must be accessible via direct URLs
|
||||||
|
- Service worker will be auto-generated and registered
|
||||||
|
- Manifest will be auto-generated from nuxt.config.ts settings
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
- Push notifications
|
||||||
|
- Background sync
|
||||||
|
- App shortcuts
|
||||||
|
- Share target functionality
|
||||||
|
- Install banner in dashboard for non-mobile users
|
||||||
|
|
@ -5,18 +5,106 @@ export default defineNuxtConfig({
|
||||||
modules: ["nuxt-directus", "vuetify-nuxt-module", "@vite-pwa/nuxt"],
|
modules: ["nuxt-directus", "vuetify-nuxt-module", "@vite-pwa/nuxt"],
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
titleTemplate: "%s • Port Nimara Client Portal",
|
titleTemplate: "%s • Port Nimara Portal",
|
||||||
title: "Port Nimara Client Portal",
|
title: "Port Nimara Portal",
|
||||||
meta: [
|
meta: [
|
||||||
{ property: "og:title", content: "Port Nimara Client Portal" },
|
{ property: "og:title", content: "Port Nimara Portal" },
|
||||||
{ property: "og:image", content: "/og-image.png" },
|
{ property: "og:image", content: "/og-image.png" },
|
||||||
{ name: "twitter:card", content: "summary_large_image" },
|
{ name: "twitter:card", content: "summary_large_image" },
|
||||||
|
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
||||||
|
{ name: "apple-mobile-web-app-capable", content: "yes" },
|
||||||
|
{ name: "apple-mobile-web-app-status-bar-style", content: "default" },
|
||||||
|
{ name: "apple-mobile-web-app-title", content: "Port Nimara Portal" },
|
||||||
],
|
],
|
||||||
htmlAttrs: {
|
htmlAttrs: {
|
||||||
lang: "en",
|
lang: "en",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
pwa: {
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
manifest: {
|
||||||
|
name: 'Port Nimara Portal',
|
||||||
|
short_name: 'Port Nimara',
|
||||||
|
description: 'Port Nimara Client Portal - Manage your berth interests and expressions of interest',
|
||||||
|
theme_color: '#387bca',
|
||||||
|
background_color: '#ffffff',
|
||||||
|
display: 'standalone',
|
||||||
|
orientation: 'portrait',
|
||||||
|
start_url: '/',
|
||||||
|
scope: '/',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/icons/icon-72x72.png',
|
||||||
|
sizes: '72x72',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/icons/icon-96x96.png',
|
||||||
|
sizes: '96x96',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/icons/icon-128x128.png',
|
||||||
|
sizes: '128x128',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/icons/icon-144x144.png',
|
||||||
|
sizes: '144x144',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/icons/icon-152x152.png',
|
||||||
|
sizes: '152x152',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/icons/icon-192x192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/icons/icon-384x384.png',
|
||||||
|
sizes: '384x384',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/icons/icon-512x512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
workbox: {
|
||||||
|
navigateFallback: '/',
|
||||||
|
globPatterns: ['**/*.{js,css,html,png,jpg,jpeg,svg,ico}'],
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
urlPattern: /^https:\/\/cms\.portnimara\.dev\/.*/i,
|
||||||
|
handler: 'NetworkFirst',
|
||||||
|
options: {
|
||||||
|
cacheName: 'api-cache',
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 10,
|
||||||
|
maxAgeSeconds: 60 * 60 * 24 // 24 hours
|
||||||
|
},
|
||||||
|
cacheableResponse: {
|
||||||
|
statuses: [0, 200]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
installPrompt: true,
|
||||||
|
periodicSyncForUpdates: 20
|
||||||
|
},
|
||||||
|
devOptions: {
|
||||||
|
enabled: true,
|
||||||
|
type: 'module'
|
||||||
|
}
|
||||||
|
},
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
nocodb: {
|
nocodb: {
|
||||||
url: "",
|
url: "",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<v-main class="container">
|
<v-main class="container">
|
||||||
<v-container class="fill-height" fluid>
|
<v-container class="fill-height" fluid>
|
||||||
<v-row align="center" justify="center" class="fill-height">
|
<v-row align="center" justify="center" class="fill-height">
|
||||||
|
<v-col cols="12" class="d-flex flex-column align-center">
|
||||||
<v-card class="pa-6" rounded max-width="350" elevation="2">
|
<v-card class="pa-6" rounded max-width="350" elevation="2">
|
||||||
<v-form @submit.prevent="submit" v-model="valid">
|
<v-form @submit.prevent="submit" v-model="valid">
|
||||||
<v-row no-gutters>
|
<v-row no-gutters>
|
||||||
|
|
@ -68,6 +69,10 @@
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
<!-- PWA Install Banner -->
|
||||||
|
<PWAInstallBanner />
|
||||||
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
</v-main>
|
</v-main>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
const { init } = usePWA();
|
||||||
|
|
||||||
|
// Initialize PWA functionality when the app starts
|
||||||
|
if (process.client) {
|
||||||
|
init();
|
||||||
|
|
||||||
|
// Register service worker update handler
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||||
|
console.log('PWA updated successfully');
|
||||||
|
// You could show a toast notification here
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue