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:
2025-06-12 16:36:32 +02:00
parent 4916c20f64
commit b25e93d2a0
6 changed files with 586 additions and 64 deletions

View File

@@ -3,71 +3,76 @@
<v-main class="container">
<v-container class="fill-height" fluid>
<v-row align="center" justify="center" class="fill-height">
<v-card class="pa-6" rounded max-width="350" elevation="2">
<v-form @submit.prevent="submit" v-model="valid">
<v-row no-gutters>
<v-col cols="12">
<v-img src="/logo.jpg" width="200" class="mb-3 mx-auto" />
</v-col>
<v-scroll-y-transition>
<v-col v-if="errorThrown" cols="12" class="my-3">
<v-alert
text="Invalid email address or password"
color="error"
variant="tonal"
/>
<v-col cols="12" class="d-flex flex-column align-center">
<v-card class="pa-6" rounded max-width="350" elevation="2">
<v-form @submit.prevent="submit" v-model="valid">
<v-row no-gutters>
<v-col cols="12">
<v-img src="/logo.jpg" width="200" class="mb-3 mx-auto" />
</v-col>
</v-scroll-y-transition>
<v-col cols="12">
<v-row dense>
<v-col cols="12" class="mt-4">
<v-text-field
v-model="emailAddress"
placeholder="Email address"
:disabled="loading"
:rules="[
(value) => !!value || 'Must not be empty',
(value) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ||
'Invalid email address',
]"
variant="outlined"
type="email"
autofocus
/>
</v-col>
<v-col cols="12">
<v-text-field
@click:append-inner="passwordVisible = !passwordVisible"
v-model="password"
placeholder="Password"
:disabled="loading"
:type="passwordVisible ? 'text' : 'password'"
:append-inner-icon="
passwordVisible ? 'mdi-eye' : 'mdi-eye-off'
"
:rules="[(value) => !!value || 'Must not be empty']"
autocomplete="current-password"
variant="outlined"
/>
</v-col>
<v-col cols="12">
<v-btn
text="Log in"
:disabled="!valid"
:loading="loading"
type="submit"
<v-scroll-y-transition>
<v-col v-if="errorThrown" cols="12" class="my-3">
<v-alert
text="Invalid email address or password"
color="error"
variant="tonal"
color="primary"
size="large"
block
/>
</v-col>
</v-row>
</v-col>
</v-row>
</v-form>
</v-card>
</v-scroll-y-transition>
<v-col cols="12">
<v-row dense>
<v-col cols="12" class="mt-4">
<v-text-field
v-model="emailAddress"
placeholder="Email address"
:disabled="loading"
:rules="[
(value) => !!value || 'Must not be empty',
(value) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ||
'Invalid email address',
]"
variant="outlined"
type="email"
autofocus
/>
</v-col>
<v-col cols="12">
<v-text-field
@click:append-inner="passwordVisible = !passwordVisible"
v-model="password"
placeholder="Password"
:disabled="loading"
:type="passwordVisible ? 'text' : 'password'"
:append-inner-icon="
passwordVisible ? 'mdi-eye' : 'mdi-eye-off'
"
:rules="[(value) => !!value || 'Must not be empty']"
autocomplete="current-password"
variant="outlined"
/>
</v-col>
<v-col cols="12">
<v-btn
text="Log in"
:disabled="!valid"
:loading="loading"
type="submit"
variant="tonal"
color="primary"
size="large"
block
/>
</v-col>
</v-row>
</v-col>
</v-row>
</v-form>
</v-card>
<!-- PWA Install Banner -->
<PWAInstallBanner />
</v-col>
</v-row>
</v-container>
</v-main>