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:
195
components/PWAInstallBanner.vue
Normal file
195
components/PWAInstallBanner.vue
Normal file
@@ -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>
|
||||
Reference in New Issue
Block a user