veza/apps/web/src/components/OfflineIndicator.tsx
senke 1e897c95a0 ui(tokens): migrate kodo-cyan to primary (51 files, 88 instances)
Replace legacy text-kodo-cyan/border-kodo-cyan/bg-kodo-cyan with semantic
text-primary/border-primary/bg-primary across 51 components.

The brand primary color now uses the design system token, enabling proper
theme adaptation. Covers UI primitives, search, dashboard, chat, playlists,
settings, social, marketplace, and auth components.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 00:19:12 +01:00

190 lines
8.1 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useOnlineStatus } from '@/hooks/useOnlineStatus';
import { offlineQueue } from '@/services/offlineQueue';
import { WifiOff, Loader2, List } from 'lucide-react';
import { hasRecentNetworkError } from '@/utils/networkErrorTracker';
import { OfflineQueueManager } from './OfflineQueueManager';
/**
* Composant pour afficher un indicateur de mode hors ligne avec nombre de requêtes en attente
*/
export function OfflineIndicator() {
const isOnline = useOnlineStatus();
const [queueSize, setQueueSize] = useState(0);
const [isProcessing, setIsProcessing] = useState(false);
const [hasNetworkError, setHasNetworkError] = useState(false);
const [showQueueManager, setShowQueueManager] = useState(false);
const [shouldShowSyncBar, setShouldShowSyncBar] = useState(false);
// Mettre à jour la taille de la file d'attente
useEffect(() => {
const updateQueueSize = () => {
const size = offlineQueue.getQueueSize();
// #region agent log
// fetch('http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'OfflineIndicator.tsx:updateQueueSize',message:'Queue size updated',data:{queueSize:size,isOnline},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
// #endregion
setQueueSize(size);
};
// Mettre à jour immédiatement
updateQueueSize();
// Mettre à jour toutes les secondes
const interval = setInterval(updateQueueSize, 1000);
return () => clearInterval(interval);
}, []);
// Vérifier si la file est en cours de traitement
useEffect(() => {
if (isOnline && queueSize > 0) {
setIsProcessing(true);
// Vérifier périodiquement si le traitement est terminé
const checkProcessing = setInterval(() => {
const currentSize = offlineQueue.getQueueSize();
if (currentSize === 0) {
setIsProcessing(false);
clearInterval(checkProcessing);
}
}, 500);
return () => clearInterval(checkProcessing);
} else {
setIsProcessing(false);
return undefined;
}
}, [isOnline, queueSize]);
// Check for recent network errors
useEffect(() => {
const checkNetworkError = () => {
setHasNetworkError(hasRecentNetworkError());
};
// Check immediately
checkNetworkError();
// Check periodically (every 2 seconds) to update when error expires
const interval = setInterval(checkNetworkError, 2000);
return () => clearInterval(interval);
}, []);
// FIX: Délai avant d'afficher la barre de synchronisation pour éviter les flashs
useEffect(() => {
// #region agent log
// fetch('http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'OfflineIndicator.tsx:useEffect',message:'Sync bar effect triggered',data:{isProcessing,queueSize,isOnline,shouldShowSyncBar},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'B'})}).catch(()=>{});
// #endregion
if (isProcessing && queueSize > 0 && isOnline) {
const timer = setTimeout(() => {
// #region agent log
// fetch('http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'OfflineIndicator.tsx:setTimeout',message:'Setting shouldShowSyncBar to true',data:{queueSize,isProcessing},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'B'})}).catch(()=>{});
// #endregion
setShouldShowSyncBar(true);
}, 500);
return () => {
clearTimeout(timer);
setShouldShowSyncBar(false);
};
} else {
setShouldShowSyncBar(false);
return undefined;
}
}, [isProcessing, queueSize, isOnline]);
// Ne rien afficher si en ligne, aucune requête en attente, et pas d'erreur réseau récente
if (isOnline && queueSize === 0 && !isProcessing && !hasNetworkError) {
return null;
}
// Mode hors ligne ou erreur réseau récente
if (!isOnline || hasNetworkError) {
return (
<>
<div className="fixed top-0 left-0 right-0 bg-kodo-red/90 backdrop-blur-sm text-white px-4 py-2.5 text-sm z-50 flex items-center justify-center gap-2 shadow-lg border-b border-kodo-red">
<WifiOff className="w-4 h-4" />
<span>
Mode hors ligne
{queueSize > 0 && (
<span className="ml-2 font-semibold">
- {queueSize} {queueSize === 1 ? 'requête' : 'requêtes'} en
attente
</span>
)}
</span>
{queueSize > 0 && (
<button
onClick={() => setShowQueueManager(true)}
className="ml-3 px-2 py-1 bg-white/10 hover:bg-white/20 rounded border border-white/20 transition-colors flex items-center gap-1.5 text-xs font-medium"
title="View queued requests"
>
<List className="w-3.5 h-3.5" />
View Queue
</button>
)}
</div>
<OfflineQueueManager
open={showQueueManager}
onClose={() => setShowQueueManager(false)}
/>
</>
);
}
// En ligne mais traitement de la file en cours
// FIX: Ne pas afficher la barre si les requêtes sont rapidement traitées (moins de 500ms)
// Cela évite d'afficher la barre pour des requêtes qui sont déjà en cours de traitement
// #region agent log
if (isProcessing && queueSize > 0) {
// fetch('http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'OfflineIndicator.tsx:123',message:'Checking sync bar display',data:{isProcessing,queueSize,shouldShowSyncBar,willShow:shouldShowSyncBar},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'B'})}).catch(()=>{});
}
// #endregion
if (isProcessing && queueSize > 0 && shouldShowSyncBar) {
return (
<>
<div className="fixed top-0 left-0 right-0 bg-primary/90 backdrop-blur-sm text-kodo-void px-4 py-2.5 text-sm z-50 flex items-center justify-center gap-2 shadow-lg border-b border-border">
<Loader2 className="w-4 h-4 animate-spin" />
<span>
Synchronisation en cours
{queueSize > 0 && (
<span className="ml-2 font-semibold">
- {queueSize} {queueSize === 1 ? 'requête' : 'requêtes'} restante
{queueSize > 1 ? 's' : ''}
</span>
)}
</span>
{queueSize > 0 && (
<>
<button
onClick={async () => {
// #region agent log
// fetch('http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'OfflineIndicator.tsx:clearQueue',message:'User clicked clear queue',data:{queueSize},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
// #endregion
await offlineQueue.clearQueue();
setQueueSize(0);
}}
className="ml-2 px-2 py-1 bg-kodo-red/20 hover:bg-kodo-red/30 rounded border border-kodo-red/30 transition-colors flex items-center gap-1.5 text-xs font-medium"
title="Clear queued requests"
>
Clear Queue
</button>
<button
onClick={() => setShowQueueManager(true)}
className="ml-2 px-2 py-1 bg-kodo-void/20 hover:bg-kodo-void/30 rounded border border-kodo-void/30 transition-colors flex items-center gap-1.5 text-xs font-medium"
title="View queued requests"
>
<List className="w-3.5 h-3.5" />
View Queue
</button>
</>
)}
</div>
<OfflineQueueManager
open={showQueueManager}
onClose={() => setShowQueueManager(false)}
/>
</>
);
}
return null;
}