Files
thrilltrack-explorer/src-old/components/submission/SubmissionQueueIndicator.tsx

229 lines
7.5 KiB
TypeScript

import { useState } from 'react';
import { Clock, RefreshCw, Trash2, CheckCircle2, XCircle, ChevronDown } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { cn } from '@/lib/utils';
import { formatDistanceToNow } from 'date-fns';
export interface QueuedSubmission {
id: string;
type: string;
entityName: string;
timestamp: Date;
status: 'pending' | 'retrying' | 'failed';
retryCount?: number;
error?: string;
}
interface SubmissionQueueIndicatorProps {
queuedItems: QueuedSubmission[];
lastSyncTime?: Date;
onRetryItem?: (id: string) => Promise<void>;
onRetryAll?: () => Promise<void>;
onClearQueue?: () => Promise<void>;
onRemoveItem?: (id: string) => void;
}
export function SubmissionQueueIndicator({
queuedItems,
lastSyncTime,
onRetryItem,
onRetryAll,
onClearQueue,
onRemoveItem,
}: SubmissionQueueIndicatorProps) {
const [isOpen, setIsOpen] = useState(false);
const [retryingIds, setRetryingIds] = useState<Set<string>>(new Set());
const handleRetryItem = async (id: string) => {
if (!onRetryItem) return;
setRetryingIds(prev => new Set(prev).add(id));
try {
await onRetryItem(id);
} finally {
setRetryingIds(prev => {
const next = new Set(prev);
next.delete(id);
return next;
});
}
};
const getStatusIcon = (status: QueuedSubmission['status']) => {
switch (status) {
case 'pending':
return <Clock className="h-3.5 w-3.5 text-muted-foreground" />;
case 'retrying':
return <RefreshCw className="h-3.5 w-3.5 text-primary animate-spin" />;
case 'failed':
return <XCircle className="h-3.5 w-3.5 text-destructive" />;
}
};
const getStatusColor = (status: QueuedSubmission['status']) => {
switch (status) {
case 'pending':
return 'bg-secondary text-secondary-foreground';
case 'retrying':
return 'bg-primary/10 text-primary';
case 'failed':
return 'bg-destructive/10 text-destructive';
}
};
if (queuedItems.length === 0) {
return null;
}
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="relative gap-2 h-9"
>
<Clock className="h-4 w-4" />
<span className="text-sm font-medium">
Queue
</span>
<Badge
variant="secondary"
className="h-5 min-w-[20px] px-1.5 bg-primary text-primary-foreground"
>
{queuedItems.length}
</Badge>
<ChevronDown className={cn(
"h-3.5 w-3.5 transition-transform",
isOpen && "rotate-180"
)} />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-96 p-0"
align="end"
sideOffset={8}
>
<div className="flex items-center justify-between p-4 border-b">
<div>
<h3 className="font-semibold text-sm">Submission Queue</h3>
<p className="text-xs text-muted-foreground mt-0.5">
{queuedItems.length} pending submission{queuedItems.length !== 1 ? 's' : ''}
</p>
{lastSyncTime && (
<p className="text-xs text-muted-foreground mt-0.5 flex items-center gap-1">
<CheckCircle2 className="h-3 w-3" />
Last sync {formatDistanceToNow(lastSyncTime, { addSuffix: true })}
</p>
)}
</div>
<div className="flex gap-1.5">
{onRetryAll && queuedItems.length > 0 && (
<Button
size="sm"
variant="outline"
onClick={onRetryAll}
className="h-8"
>
<RefreshCw className="h-3.5 w-3.5 mr-1.5" />
Retry All
</Button>
)}
</div>
</div>
<ScrollArea className="max-h-[400px]">
<div className="p-2 space-y-1">
{queuedItems.map((item) => (
<div
key={item.id}
className={cn(
"group rounded-md p-3 border transition-colors hover:bg-accent/50",
getStatusColor(item.status)
)}
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
{getStatusIcon(item.status)}
<span className="text-sm font-medium truncate">
{item.entityName}
</span>
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span className="capitalize">{item.type}</span>
<span></span>
<span>{formatDistanceToNow(item.timestamp, { addSuffix: true })}</span>
{item.retryCount && item.retryCount > 0 && (
<>
<span></span>
<span>{item.retryCount} {item.retryCount === 1 ? 'retry' : 'retries'}</span>
</>
)}
</div>
{item.error && (
<p className="text-xs text-destructive mt-1.5 truncate">
{item.error}
</p>
)}
</div>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
{onRetryItem && (
<Button
size="sm"
variant="ghost"
onClick={() => handleRetryItem(item.id)}
disabled={retryingIds.has(item.id)}
className="h-7 w-7 p-0"
>
<RefreshCw className={cn(
"h-3.5 w-3.5",
retryingIds.has(item.id) && "animate-spin"
)} />
<span className="sr-only">Retry</span>
</Button>
)}
{onRemoveItem && (
<Button
size="sm"
variant="ghost"
onClick={() => onRemoveItem(item.id)}
className="h-7 w-7 p-0 hover:bg-destructive/10 hover:text-destructive"
>
<Trash2 className="h-3.5 w-3.5" />
<span className="sr-only">Remove</span>
</Button>
)}
</div>
</div>
</div>
))}
</div>
</ScrollArea>
{onClearQueue && queuedItems.length > 0 && (
<div className="p-3 border-t">
<Button
size="sm"
variant="outline"
onClick={onClearQueue}
className="w-full h-8 text-destructive hover:bg-destructive/10"
>
<Trash2 className="h-3.5 w-3.5 mr-1.5" />
Clear Queue
</Button>
</div>
)}
</PopoverContent>
</Popover>
);
}