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:
91
components/OfflineIndicator.tsx
Normal file
91
components/OfflineIndicator.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user