Add PWA support with install banner and app icons
All checks were successful
Build And Push Image / docker (push) Successful in 2m56s
All checks were successful
Build And Push Image / docker (push) Successful in 2m56s
- Configure @vite-pwa/nuxt module with manifest and service worker - Add PWA install banner component to login page - Include app icons (192x192, 512x512) and favicon assets - Update admin dashboard layout and remove backup section - Add PWA-related API endpoints and utility scripts
This commit is contained in:
239
components/PWAInstallBanner.vue
Normal file
239
components/PWAInstallBanner.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<v-card
|
||||
v-if="showBanner"
|
||||
class="pwa-install-banner"
|
||||
elevation="8"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
>
|
||||
<v-card-text class="pa-4">
|
||||
<v-row align="center" no-gutters>
|
||||
<v-col cols="auto" class="mr-3">
|
||||
<v-avatar size="48" color="white">
|
||||
<v-img src="/icon-192x192.png" alt="MonacoUSA Portal" />
|
||||
</v-avatar>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<div class="text-white">
|
||||
<div class="text-subtitle-1 font-weight-bold mb-1">
|
||||
Install MonacoUSA Portal
|
||||
</div>
|
||||
<div class="text-body-2 text-grey-lighten-2">
|
||||
{{ installMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
v-if="canInstall"
|
||||
@click="installPWA"
|
||||
color="white"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
:loading="installing"
|
||||
>
|
||||
<v-icon start>mdi-download</v-icon>
|
||||
Install
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
@click="dismissBanner"
|
||||
color="white"
|
||||
variant="text"
|
||||
size="small"
|
||||
icon
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
prompt(): Promise<void>;
|
||||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
|
||||
}
|
||||
|
||||
// Reactive state
|
||||
const showBanner = ref(false);
|
||||
const canInstall = ref(false);
|
||||
const installing = ref(false);
|
||||
const installMessage = ref('Add to your home screen for quick access');
|
||||
let deferredPrompt: BeforeInstallPromptEvent | null = null;
|
||||
|
||||
// Device detection
|
||||
const isIOS = computed(() => {
|
||||
if (process.client) {
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const isAndroid = computed(() => {
|
||||
if (process.client) {
|
||||
return /Android/.test(navigator.userAgent);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const isStandalone = computed(() => {
|
||||
if (process.client) {
|
||||
return window.matchMedia('(display-mode: standalone)').matches ||
|
||||
(window.navigator as any).standalone === true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Install messages based on platform
|
||||
const getInstallMessage = () => {
|
||||
if (isIOS.value) {
|
||||
return 'Tap Share → Add to Home Screen to install';
|
||||
} else if (isAndroid.value) {
|
||||
return 'Add to your home screen for quick access';
|
||||
} else {
|
||||
return 'Install this app for a better experience';
|
||||
}
|
||||
};
|
||||
|
||||
// PWA installation logic
|
||||
const installPWA = async () => {
|
||||
if (!deferredPrompt) return;
|
||||
|
||||
installing.value = true;
|
||||
|
||||
try {
|
||||
// Show the install prompt
|
||||
await deferredPrompt.prompt();
|
||||
|
||||
// Wait for the user to respond to the prompt
|
||||
const { outcome } = await deferredPrompt.userChoice;
|
||||
|
||||
console.log(`PWA install prompt outcome: ${outcome}`);
|
||||
|
||||
if (outcome === 'accepted') {
|
||||
console.log('✅ PWA installation accepted');
|
||||
showBanner.value = false;
|
||||
localStorage.setItem('pwa-install-dismissed', 'true');
|
||||
}
|
||||
|
||||
// Clear the deferredPrompt
|
||||
deferredPrompt = null;
|
||||
canInstall.value = false;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ PWA installation error:', error);
|
||||
} finally {
|
||||
installing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const dismissBanner = () => {
|
||||
showBanner.value = false;
|
||||
localStorage.setItem('pwa-install-dismissed', 'true');
|
||||
localStorage.setItem('pwa-install-dismissed-date', new Date().toISOString());
|
||||
};
|
||||
|
||||
const shouldShowBanner = () => {
|
||||
// Don't show if already dismissed recently (within 7 days)
|
||||
const dismissedDate = localStorage.getItem('pwa-install-dismissed-date');
|
||||
if (dismissedDate) {
|
||||
const daysSinceDismissed = (Date.now() - new Date(dismissedDate).getTime()) / (1000 * 60 * 60 * 24);
|
||||
if (daysSinceDismissed < 7) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't show if permanently dismissed
|
||||
if (localStorage.getItem('pwa-install-dismissed') === 'true' && !dismissedDate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't show if already installed
|
||||
if (isStandalone.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Setup event listeners
|
||||
onMounted(() => {
|
||||
if (!process.client) return;
|
||||
|
||||
installMessage.value = getInstallMessage();
|
||||
|
||||
// Listen for the beforeinstallprompt event
|
||||
window.addEventListener('beforeinstallprompt', (e: Event) => {
|
||||
console.log('🔔 PWA install prompt available');
|
||||
|
||||
// Prevent the mini-infobar from appearing on mobile
|
||||
e.preventDefault();
|
||||
|
||||
// Save the event so it can be triggered later
|
||||
deferredPrompt = e as BeforeInstallPromptEvent;
|
||||
canInstall.value = true;
|
||||
|
||||
// Show banner if conditions are met
|
||||
if (shouldShowBanner()) {
|
||||
showBanner.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for successful installation
|
||||
window.addEventListener('appinstalled', () => {
|
||||
console.log('✅ PWA was installed successfully');
|
||||
showBanner.value = false;
|
||||
deferredPrompt = null;
|
||||
canInstall.value = false;
|
||||
});
|
||||
|
||||
// For iOS devices, show banner if not installed and not dismissed
|
||||
if (isIOS.value && shouldShowBanner()) {
|
||||
showBanner.value = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pwa-install-banner {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.pwa-install-banner {
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation */
|
||||
.pwa-install-banner {
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(100px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user