mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 18:11:12 -05:00
229 lines
7.5 KiB
TypeScript
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>
|
|
);
|
|
}
|