mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 10:51:12 -05:00
feat: Implement timeline manager
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { memo, useCallback } from 'react';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import {
|
||||
AlertCircle, Edit, Info, ExternalLink, ChevronDown, ListTree, Calendar, Crown, Unlock
|
||||
@@ -14,6 +14,7 @@ import { UserAvatar } from '@/components/ui/user-avatar';
|
||||
import { format } from 'date-fns';
|
||||
import type { ModerationItem } from '@/types/moderation';
|
||||
import { sanitizeURL, sanitizePlainText } from '@/lib/sanitize';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
|
||||
interface QueueItemActionsProps {
|
||||
item: ModerationItem;
|
||||
@@ -64,30 +65,50 @@ export const QueueItemActions = memo(({
|
||||
onClaim,
|
||||
onSuperuserReleaseLock
|
||||
}: QueueItemActionsProps) => {
|
||||
// Error state for retry functionality
|
||||
const [actionError, setActionError] = useState<{
|
||||
message: string;
|
||||
errorId?: string;
|
||||
action: 'approve' | 'reject';
|
||||
} | null>(null);
|
||||
|
||||
// Memoize all handlers to prevent re-renders
|
||||
const handleNoteChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
onNoteChange(item.id, e.target.value);
|
||||
}, [onNoteChange, item.id]);
|
||||
|
||||
// Debounced handlers to prevent duplicate submissions
|
||||
// Debounced handlers with error tracking
|
||||
const handleApprove = useDebouncedCallback(
|
||||
() => {
|
||||
// Extra guard against race conditions
|
||||
if (actionLoading === item.id) {
|
||||
return;
|
||||
async () => {
|
||||
if (actionLoading === item.id) return;
|
||||
try {
|
||||
setActionError(null);
|
||||
await onApprove(item, 'approved', notes[item.id]);
|
||||
} catch (error: any) {
|
||||
setActionError({
|
||||
message: getErrorMessage(error),
|
||||
errorId: error.errorId,
|
||||
action: 'approve',
|
||||
});
|
||||
}
|
||||
onApprove(item, 'approved', notes[item.id]);
|
||||
},
|
||||
300, // 300ms debounce
|
||||
{ leading: true, trailing: false } // Only fire on first click
|
||||
300,
|
||||
{ leading: true, trailing: false }
|
||||
);
|
||||
|
||||
const handleReject = useDebouncedCallback(
|
||||
() => {
|
||||
if (actionLoading === item.id) {
|
||||
return;
|
||||
async () => {
|
||||
if (actionLoading === item.id) return;
|
||||
try {
|
||||
setActionError(null);
|
||||
await onApprove(item, 'rejected', notes[item.id]);
|
||||
} catch (error: any) {
|
||||
setActionError({
|
||||
message: getErrorMessage(error),
|
||||
errorId: error.errorId,
|
||||
action: 'reject',
|
||||
});
|
||||
}
|
||||
onApprove(item, 'rejected', notes[item.id]);
|
||||
},
|
||||
300,
|
||||
{ leading: true, trailing: false }
|
||||
@@ -149,6 +170,40 @@ export const QueueItemActions = memo(({
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Error Display with Retry */}
|
||||
{actionError && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Action Failed: {actionError.action}</AlertTitle>
|
||||
<AlertDescription>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">{actionError.message}</p>
|
||||
{actionError.errorId && (
|
||||
<p className="text-xs font-mono bg-destructive/10 px-2 py-1 rounded">
|
||||
Reference ID: {actionError.errorId.slice(0, 8)}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex gap-2 mt-3">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setActionError(null);
|
||||
if (actionError.action === 'approve') handleApprove();
|
||||
else if (actionError.action === 'reject') handleReject();
|
||||
}}
|
||||
>
|
||||
Retry {actionError.action}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => setActionError(null)}>
|
||||
Dismiss
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Action buttons based on status */}
|
||||
{(item.status === 'pending' || item.status === 'flagged') && (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user