diff --git a/components/PWAInstallBanner.vue b/components/PWAInstallBanner.vue new file mode 100644 index 0000000..fef147c --- /dev/null +++ b/components/PWAInstallBanner.vue @@ -0,0 +1,195 @@ + + + + + + + + + + + Install Port Nimara Portal + + + Get faster access and work offline + + + + + + + + + + + mdi-download + Install App + + + + + + + + + + + + + Install Port Nimara Portal + + + + + + + + To install this app on your iPhone/iPad: + + + + + Tap the Share button at the bottom + + + + + Scroll down and tap "Add to Home Screen" + + + + + Tap "Add" to confirm + + + + + + Make sure you're using Safari browser for this to work. + + + + + + + Your browser doesn't support automatic installation. + + + Try using Chrome or another modern browser for the best experience. + + + + + + + + + + Close + + + + + + + diff --git a/composables/usePWA.ts b/composables/usePWA.ts new file mode 100644 index 0000000..1f46695 --- /dev/null +++ b/composables/usePWA.ts @@ -0,0 +1,101 @@ +export const usePWA = () => { + const isInstalled = ref(false); + const isInstallable = ref(false); + const deferredPrompt = ref(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 + }; +}; diff --git a/docs/pwa-implementation.md b/docs/pwa-implementation.md new file mode 100644 index 0000000..fa3c15b --- /dev/null +++ b/docs/pwa-implementation.md @@ -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 diff --git a/nuxt.config.ts b/nuxt.config.ts index 7f6f3ca..560d8c1 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -5,18 +5,106 @@ export default defineNuxtConfig({ modules: ["nuxt-directus", "vuetify-nuxt-module", "@vite-pwa/nuxt"], app: { head: { - titleTemplate: "%s • Port Nimara Client Portal", - title: "Port Nimara Client Portal", + titleTemplate: "%s • Port Nimara Portal", + title: "Port Nimara Portal", meta: [ - { property: "og:title", content: "Port Nimara Client Portal" }, + { property: "og:title", content: "Port Nimara Portal" }, { property: "og:image", content: "/og-image.png" }, { 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: { 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: { nocodb: { url: "", diff --git a/pages/login.vue b/pages/login.vue index 64dc2c5..d0e6112 100644 --- a/pages/login.vue +++ b/pages/login.vue @@ -3,71 +3,76 @@ - - - - - - - - - + + + + + + - - - - - - - - - - - + + - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/pwa.client.ts b/plugins/pwa.client.ts new file mode 100644 index 0000000..0388ddb --- /dev/null +++ b/plugins/pwa.client.ts @@ -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 + }); + } + } +});