veza/apps/web/src/components/OfflineQueueManager.tsx
senke 6974c12a25 aesthetic-improvements: align spacing to 8px grid (Action 11.2.1.3)
- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid
- Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px)
- Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation
- Modified files across all components to ensure consistent 8px grid alignment
- Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
2026-01-16 11:50:46 +01:00

219 lines
7 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';
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-kodo-red border-kodo-red/30';
case 'normal':
return 'bg-kodo-steel/20 text-kodo-steel border-kodo-steel/30';
case 'low':
return 'bg-kodo-steel/30 text-kodo-secondary border-kodo-steel/50';
default:
return 'bg-kodo-steel/30 text-kodo-secondary border-kodo-steel/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) {
console.error('Failed to remove request:', error);
} finally {
setIsRemoving(null);
}
};
const handleClearQueue = async () => {
setIsClearing(true);
try {
await offlineQueue.clearQueue();
setQueue([]);
onClose();
} catch (error) {
console.error('Failed to clear queue:', error);
} 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-kodo-steel">
<div className="flex items-center gap-2">
<Clock className="w-5 h-5 text-kodo-steel" />
<span className="text-sm text-kodo-secondary">
{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-kodo-secondary">
<CheckCircle2 className="w-12 h-12 mx-auto mb-4 text-kodo-cyan/50" />
<p className="text-sm">All requests have been processed</p>
</div>
) : (
<div className="space-y-2 max-h-[400px] overflow-y-auto custom-scrollbar">
{queue.map((request) => (
<div
key={request.id}
className="p-4 bg-kodo-ink/30 rounded-lg border border-kodo-steel hover:border-kodo-steel/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-kodo-secondary">
{/* 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-kodo-red">
<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-kodo-steel/10 border border-kodo-steel/20 rounded-lg text-xs text-kodo-secondary">
<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>
);
}