veza/apps/web/src/components/OfflineQueueManager.tsx
senke dd6333d540 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

227 lines
7.3 KiB
TypeScript

/**
* Offline Queue Manager Component
* Action 2.5.1.4: Add UI for offline queue management
*
* Displays queued requests and allows users to view details, remove requests, or clear the queue
*/
import { useEffect, useState } from 'react';
import { offlineQueue, type QueuedRequest } from '@/services/offlineQueue';
import { Dialog } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Trash2, X, Clock, AlertCircle, CheckCircle2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { logger } from '@/utils/logger';
interface OfflineQueueManagerProps {
open: boolean;
onClose: () => void;
}
/**
* Format timestamp to readable date/time
*/
function formatTimestamp(timestamp: number): string {
const date = new Date(timestamp);
return date.toLocaleString();
}
/**
* Format request method and URL for display
*/
function formatRequest(request: QueuedRequest): string {
const method = request.config.method?.toUpperCase() || 'UNKNOWN';
const url = request.config.url || 'Unknown URL';
return `${method} ${url}`;
}
/**
* Get priority badge color
*/
function getPriorityColor(priority: QueuedRequest['priority']): string {
switch (priority) {
case 'high':
return 'bg-kodo-red/20 text-destructive border-kodo-red/30';
case 'normal':
return 'bg-muted/20 text-kodo-steel border-border/30';
case 'low':
return 'bg-muted/30 text-muted-foreground border-border/50';
default:
return 'bg-muted/30 text-muted-foreground border-border/50';
}
}
export function OfflineQueueManager({
open,
onClose,
}: OfflineQueueManagerProps) {
const [queue, setQueue] = useState<QueuedRequest[]>([]);
const [isRemoving, setIsRemoving] = useState<string | null>(null);
const [isClearing, setIsClearing] = useState(false);
// Update queue when dialog opens or periodically
useEffect(() => {
if (!open) return;
const updateQueue = () => {
setQueue(offlineQueue.getQueue());
};
// Update immediately
updateQueue();
// Update every second while dialog is open
const interval = setInterval(updateQueue, 1000);
return () => clearInterval(interval);
}, [open]);
const handleRemoveRequest = async (requestId: string) => {
setIsRemoving(requestId);
try {
await offlineQueue.removeRequest(requestId);
setQueue(offlineQueue.getQueue());
} catch (error) {
logger.error('Failed to remove request', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
requestId,
});
} finally {
setIsRemoving(null);
}
};
const handleClearQueue = async () => {
setIsClearing(true);
try {
await offlineQueue.clearQueue();
setQueue([]);
onClose();
} catch (error) {
logger.error('Failed to clear queue', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
} finally {
setIsClearing(false);
}
};
return (
<Dialog
open={open}
onClose={onClose}
title="Offline Queue Manager"
size="lg"
variant="info"
>
<div className="space-y-4">
{/* Queue Summary */}
<div className="flex items-center justify-between p-4 bg-kodo-ink/50 rounded-lg border border-border">
<div className="flex items-center gap-2">
<Clock className="w-5 h-5 text-kodo-steel" />
<span className="text-sm text-muted-foreground">
{queue.length === 0
? 'No queued requests'
: `${queue.length} ${queue.length === 1 ? 'request' : 'requests'} queued`}
</span>
</div>
{queue.length > 0 && (
<Button
variant="destructive"
size="sm"
onClick={handleClearQueue}
disabled={isClearing}
>
<Trash2 className="w-4 h-4 mr-2" />
Clear All
</Button>
)}
</div>
{/* Queue List */}
{queue.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<CheckCircle2 className="w-12 h-12 mx-auto mb-4 text-primary/50" />
<p className="text-sm">All requests have been processed</p>
</div>
) : (
<div className="space-y-2 max-h-layout-list overflow-y-auto custom-scrollbar">
{queue.map((request) => (
<div
key={request.id}
className="p-4 bg-kodo-ink/30 rounded-lg border border-border hover:border-border/50 transition-colors"
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
{/* Request Method and URL */}
<div className="flex items-center gap-2 mb-2">
<span className="font-mono text-sm font-semibold text-white truncate">
{formatRequest(request)}
</span>
</div>
{/* Metadata */}
<div className="flex items-center gap-4 flex-wrap text-xs text-muted-foreground">
{/* Priority Badge */}
<span
className={cn(
'px-2 py-0.5 rounded border',
getPriorityColor(request.priority),
)}
>
{request.priority}
</span>
{/* Timestamp */}
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{formatTimestamp(request.timestamp)}
</span>
{/* Retry Count */}
{request.retryCount > 0 && (
<span className="flex items-center gap-1 text-destructive">
<AlertCircle className="w-3 h-3" />
{request.retryCount} retry
{request.retryCount > 1 ? 'ies' : ''}
</span>
)}
</div>
</div>
{/* Remove Button */}
<Button
variant="ghost"
size="icon"
onClick={() => handleRemoveRequest(request.id)}
disabled={isRemoving === request.id}
className="shrink-0"
>
{isRemoving === request.id ? (
<Clock className="w-4 h-4 animate-spin" />
) : (
<X className="w-4 h-4" />
)}
</Button>
</div>
</div>
))}
</div>
)}
{/* Info Message */}
{queue.length > 0 && (
<div className="p-4 bg-muted/10 border border-border/20 rounded-lg text-xs text-muted-foreground">
<p>
Queued requests will be automatically processed when you're back
online. You can remove individual requests or clear the entire
queue.
</p>
</div>
)}
</div>
</Dialog>
);
}