Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | 'use client'; import { useEffect, useState, useCallback } from 'react'; import { cn } from '@/lib/core/utils'; import { hasPendingActions, syncPendingActions } from '@/lib/pwa'; import { Icon } from '@/components/ui/icons'; interface OfflineIndicatorProps { /** Additional CSS classes */ className?: string; /** Position of the indicator */ position?: 'top' | 'bottom'; /** Auto-hide after coming back online (ms) */ autoHideDelay?: number; } /** * OfflineIndicator Component * * Displays a banner when the user is offline, and shows a sync status * when coming back online. Provides feedback about pending actions. */ export function OfflineIndicator({ className, position = 'bottom', autoHideDelay = 3000, }: OfflineIndicatorProps) { const [isOnline, setIsOnline] = useState(true); const [showBanner, setShowBanner] = useState(false); const [isSyncing, setIsSyncing] = useState(false); const [pendingCount, setPendingCount] = useState(0); const [syncResult, setSyncResult] = useState<{ synced: number; failed: number; } | null>(null); // Check pending actions count const checkPendingActions = useCallback(async () => { try { const hasPending = await hasPendingActions(); if (hasPending) { const { getPendingActions } = await import('@/lib/pwa'); const actions = await getPendingActions(); setPendingCount(actions.length); } else { setPendingCount(0); } } catch { setPendingCount(0); } }, []); // Handle coming back online const handleOnline = useCallback(async () => { setIsOnline(true); setShowBanner(true); setIsSyncing(true); try { const result = await syncPendingActions(); setSyncResult(result); await checkPendingActions(); } catch { setSyncResult({ synced: 0, failed: 0 }); } finally { setIsSyncing(false); } // Auto-hide after delay setTimeout(() => { setShowBanner(false); setSyncResult(null); }, autoHideDelay); }, [autoHideDelay, checkPendingActions]); // Handle going offline const handleOffline = useCallback(() => { setIsOnline(false); setShowBanner(true); checkPendingActions(); }, [checkPendingActions]); useEffect(() => { // Set initial state setIsOnline(navigator.onLine); if (!navigator.onLine) { setShowBanner(true); checkPendingActions(); } window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, [handleOnline, handleOffline, checkPendingActions]); // Don't render if nothing to show if (!showBanner) return null; // Get banner content and styles based on state const getBannerContent = () => { if (isSyncing) { return { icon: 'sync' as const, message: 'Syncing your changes...', bgClass: 'bg-blue-500 dark:bg-blue-600', textClass: 'text-white', animate: true, }; } if (isOnline && syncResult) { if (syncResult.failed > 0) { return { icon: 'alert-circle' as const, message: `Synced ${syncResult.synced}, ${syncResult.failed} failed`, bgClass: 'bg-yellow-500 dark:bg-yellow-600', textClass: 'text-yellow-900 dark:text-white', animate: false, }; } return { icon: 'check-circle' as const, message: syncResult.synced > 0 ? `Back online! Synced ${syncResult.synced} changes` : 'Back online!', bgClass: 'bg-green-500 dark:bg-green-600', textClass: 'text-white', animate: false, }; } return { icon: 'alert-triangle' as const, message: pendingCount > 0 ? `You're offline. ${pendingCount} changes pending` : "You're offline. Some features may be limited.", bgClass: 'bg-yellow-500 dark:bg-yellow-600', textClass: 'text-yellow-900 dark:text-white', animate: false, }; }; const content = getBannerContent(); return ( <div role="alert" aria-live="polite" className={cn( 'fixed left-4 right-4 md:left-auto md:right-4 md:w-96', 'p-4 rounded-lg shadow-lg z-50', 'flex items-center gap-3', 'transform transition-all duration-300 ease-in-out', content.bgClass, content.textClass, position === 'top' ? 'top-4' : 'bottom-4', className )} > <Icon name={content.icon} className={cn('h-5 w-5 flex-shrink-0', content.animate && 'animate-spin')} /> <span className="text-sm font-medium">{content.message}</span> {!isOnline && ( <button onClick={() => setShowBanner(false)} className="ml-auto p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 transition-colors" aria-label="Dismiss" > <Icon name="x" className="h-4 w-4" /> </button> )} </div> ); } export default OfflineIndicator; |