feat: implement offline synchronization with mutation queue and background sync

- Created useOfflineSync hook for connectivity detection and sync management
- Implemented localStorage-based mutation queue for offline operations
- Added background sync support in service worker
- Created OfflineIndicator component to show connection status
- Added offline-aware action wrappers for seamless offline/online transitions
- Implemented conflict handling with retry logic (up to 3 retries)
- Auto-sync when connection is restored
- Validates Requirements 4.5
This commit is contained in:
2025-12-01 20:34:21 +00:00
parent ef6a0bf5af
commit 9726f3a9e1
8 changed files with 716 additions and 1 deletions

View File

@@ -0,0 +1,91 @@
'use client'
import { useOfflineSync } from '@/hooks'
import { WifiOff, Wifi, RefreshCw, AlertCircle } from 'lucide-react'
/**
* Offline indicator component
* Shows connection status and sync progress
* Validates: Requirements 4.5
*/
export default function OfflineIndicator() {
const { isOnline, isSyncing, syncError, queueLength, triggerSync } = useOfflineSync()
// Don't show anything if online and no pending syncs
if (isOnline && queueLength === 0 && !syncError) {
return null
}
return (
<div className="fixed bottom-4 right-4 z-50">
<div
className={`
flex items-center gap-2 px-4 py-3 rounded-lg shadow-lg
${!isOnline ? 'bg-gray-800 text-white' : ''}
${isOnline && queueLength > 0 ? 'bg-blue-600 text-white' : ''}
${syncError ? 'bg-red-600 text-white' : ''}
`}
>
{/* Icon */}
{!isOnline && <WifiOff className="w-5 h-5" />}
{isOnline && !syncError && <Wifi className="w-5 h-5" />}
{syncError && <AlertCircle className="w-5 h-5" />}
{/* Status text */}
<div className="flex flex-col">
{!isOnline && (
<>
<span className="text-sm font-medium">Offline</span>
{queueLength > 0 && (
<span className="text-xs opacity-90">
{queueLength} change{queueLength !== 1 ? 's' : ''} pending
</span>
)}
</>
)}
{isOnline && isSyncing && (
<>
<span className="text-sm font-medium">Syncing...</span>
<span className="text-xs opacity-90">
{queueLength} change{queueLength !== 1 ? 's' : ''} remaining
</span>
</>
)}
{isOnline && !isSyncing && queueLength > 0 && !syncError && (
<>
<span className="text-sm font-medium">Ready to sync</span>
<span className="text-xs opacity-90">
{queueLength} change{queueLength !== 1 ? 's' : ''} pending
</span>
</>
)}
{syncError && (
<>
<span className="text-sm font-medium">Sync failed</span>
<span className="text-xs opacity-90">{syncError.message}</span>
</>
)}
</div>
{/* Sync button */}
{isOnline && queueLength > 0 && !isSyncing && (
<button
onClick={triggerSync}
className="ml-2 p-1 hover:bg-white/20 rounded transition-colors"
aria-label="Sync now"
>
<RefreshCw className="w-4 h-4" />
</button>
)}
{/* Syncing spinner */}
{isSyncing && (
<RefreshCw className="w-4 h-4 animate-spin ml-2" />
)}
</div>
</div>
)
}