Files
Todo/components/OfflineIndicator.tsx
Paul Bailey 78901c8e2e Fix hydration error in OfflineIndicator component
- Add mounted state to prevent SSR/client mismatch
- Component now only renders after client-side mount
- Resolves React hydration error
2025-12-02 14:22:34 +00:00

103 lines
3.0 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
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 [mounted, setMounted] = useState(false)
const { isOnline, isSyncing, syncError, queueLength, triggerSync } = useOfflineSync()
useEffect(() => {
setMounted(true)
}, [])
// Don't render on server to avoid hydration mismatch
if (!mounted) {
return null
}
// 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>
)
}