- 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
92 lines
2.8 KiB
TypeScript
92 lines
2.8 KiB
TypeScript
'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>
|
|
)
|
|
}
|