veza/apps/web/src/components/OfflineQueueManager.tsx
senke 73e8372b0e refactor: Phase 7 — Clean up legacy components and remove dead tokens
- Bulk replace text-white → text-foreground across 116 component files
  (preserving text-white/ opacity variants)
- Remove hover-glow-cyan, shadow-card-glow-cyan, shadow-button-primary-glow
  classes from all components
- Replace --duration-normal/--duration-immersive/--duration-slow with
  --sumi-duration-normal/--sumi-duration-slow across 130+ files
- Replace --ease-out/--ease-in-out with --sumi-ease-out/--sumi-ease-in-out
- Replace focus:ring-blue-500 → focus:ring-primary (4 files)
- Remove hover:scale-105/110 and hover:-translate-y-1/0.5 transforms
  (SUMI anti-pattern: no scale on hover)
- Clean up stale kodo- references in comments

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 02:09:29 +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-destructive/20 text-destructive border-destructive/30';
case 'normal':
return 'bg-muted/20 text-muted-foreground 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-card/50 rounded-lg border border-border">
<div className="flex items-center gap-2">
<Clock className="w-5 h-5 text-muted-foreground" />
<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-card/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-foreground 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>
);
}