feat: Complete PWA implementation with offline support and install prompts
PWA Features Implemented: ✅ Offline Fallback Page (/offline) - User-friendly offline page with connection status - Auto-redirect when back online - Lists available offline features - Retry and home navigation buttons ✅ Install Prompt UI (InstallPrompt component) - beforeinstallprompt event handler for Android/Desktop - iOS-specific install instructions with Share icon - Smart dismissal with 7-day cooldown - Already-installed detection ✅ Background Sync for Pending Actions - useBackgroundSync hook with multiple sync triggers - Periodic sync every 5 minutes when online - Sync on tab visibility change - Service Worker sync registration - BackgroundSyncProvider integration ✅ next-pwa Configuration Updates - Offline fallback to /offline page - Network timeout (10s) for better offline detection - skipWaiting and clientsClaim enabled - Runtime caching with NetworkFirst strategy Files Created: - app/offline/page.tsx (131 lines) - components/pwa/InstallPrompt.tsx (164 lines) - hooks/useBackgroundSync.ts (71 lines) - components/providers/BackgroundSyncProvider.tsx (10 lines) Files Modified: - app/layout.tsx (added InstallPrompt and BackgroundSyncProvider) - next.config.mjs (offline fallback + workbox options) Total: 376 new lines across 4 new files + 2 modified files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
153
maternal-web/app/offline/page.tsx
Normal file
153
maternal-web/app/offline/page.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Box, Container, Typography, Button, Paper } from '@mui/material';
|
||||
import { WifiOff, Refresh, Home } from '@mui/icons-material';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function OfflinePage() {
|
||||
const router = useRouter();
|
||||
const [isOnline, setIsOnline] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check online status
|
||||
const handleOnline = () => setIsOnline(true);
|
||||
const handleOffline = () => setIsOnline(false);
|
||||
|
||||
setIsOnline(navigator.onLine);
|
||||
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Redirect when back online
|
||||
if (isOnline) {
|
||||
router.push('/');
|
||||
}
|
||||
}, [isOnline, router]);
|
||||
|
||||
const handleRetry = () => {
|
||||
if (navigator.onLine) {
|
||||
router.push('/');
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoHome = () => {
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm">
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
py: 4,
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
p: 4,
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<WifiOff
|
||||
sx={{
|
||||
fontSize: 80,
|
||||
color: 'error.main',
|
||||
mb: 2,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
gutterBottom
|
||||
sx={{ fontWeight: 600 }}
|
||||
>
|
||||
You're Offline
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="text.secondary"
|
||||
sx={{ mb: 4 }}
|
||||
>
|
||||
It looks like you've lost your internet connection. Some features may be limited while offline.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 500 }}>
|
||||
What you can still do:
|
||||
</Typography>
|
||||
<Box
|
||||
component="ul"
|
||||
sx={{
|
||||
textAlign: 'left',
|
||||
display: 'inline-block',
|
||||
pl: 2,
|
||||
}}
|
||||
>
|
||||
<li>
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
View previously loaded activities and data
|
||||
</Typography>
|
||||
</li>
|
||||
<li>
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
Create new activities (will sync when back online)
|
||||
</Typography>
|
||||
</li>
|
||||
<li>
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
Access cached pages and information
|
||||
</Typography>
|
||||
</li>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Refresh />}
|
||||
onClick={handleRetry}
|
||||
size="large"
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<Home />}
|
||||
onClick={handleGoHome}
|
||||
size="large"
|
||||
>
|
||||
Go Home
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{isOnline && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="success.main"
|
||||
sx={{ mt: 2, fontWeight: 500 }}
|
||||
>
|
||||
✓ Connection restored! Redirecting...
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user