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:
71
maternal-web/hooks/useBackgroundSync.ts
Normal file
71
maternal-web/hooks/useBackgroundSync.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setOnlineStatus } from '@/store/slices/networkSlice';
|
||||
|
||||
export function useBackgroundSync() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { isOnline } = useAppSelector((state) => state.network);
|
||||
const { pendingActions } = useAppSelector((state) => state.offline);
|
||||
const syncIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Register background sync if supported
|
||||
if ('serviceWorker' in navigator && 'sync' in ServiceWorkerRegistration.prototype) {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
// Register a sync event
|
||||
return (registration as any).sync.register('sync-pending-actions');
|
||||
}).catch((error) => {
|
||||
console.error('Background sync registration failed:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for sync events
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'BACKGROUND_SYNC') {
|
||||
// Trigger sync from service worker by simulating online status
|
||||
dispatch(setOnlineStatus(true));
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
// Periodic sync every 5 minutes when online and have pending actions
|
||||
if (isOnline && pendingActions.length > 0) {
|
||||
syncIntervalRef.current = setInterval(() => {
|
||||
console.log('Periodic sync check...');
|
||||
// Trigger sync by re-dispatching online status
|
||||
dispatch(setOnlineStatus(true));
|
||||
}, 5 * 60 * 1000); // 5 minutes
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (syncIntervalRef.current) {
|
||||
clearInterval(syncIntervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [isOnline, pendingActions.length, dispatch]);
|
||||
|
||||
// Sync on visibility change (when user returns to tab)
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible' && isOnline && pendingActions.length > 0) {
|
||||
console.log('Tab visible, syncing pending actions...');
|
||||
// Trigger sync by re-dispatching online status
|
||||
dispatch(setOnlineStatus(true));
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [isOnline, pendingActions.length, dispatch]);
|
||||
|
||||
return {
|
||||
pendingCount: pendingActions.length,
|
||||
isOnline,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user