Files
thrilltrack-explorer/src-old/components/error/ModerationErrorBoundary.tsx

168 lines
5.1 KiB
TypeScript

import React, { Component, ErrorInfo, ReactNode } from 'react';
import { AlertCircle, RefreshCw } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { handleError } from '@/lib/errorHandler';
interface ModerationErrorBoundaryProps {
children: ReactNode;
submissionId?: string;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
interface ModerationErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
type ErrorWithId = Error & { errorId: string };
/**
* Error Boundary for Moderation Queue Components
*
* Prevents individual queue item render errors from crashing the entire queue.
* Shows user-friendly error UI with retry functionality.
*
* Usage:
* ```tsx
* <ModerationErrorBoundary submissionId={item.id}>
* <QueueItem item={item} />
* </ModerationErrorBoundary>
* ```
*/
export class ModerationErrorBoundary extends Component<
ModerationErrorBoundaryProps,
ModerationErrorBoundaryState
> {
constructor(props: ModerationErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error: Error): Partial<ModerationErrorBoundaryState> {
return {
hasError: true,
error,
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Log to database and get error ID for user reference
const errorId = handleError(error, {
action: 'Moderation queue item render error',
metadata: {
submissionId: this.props.submissionId,
componentStack: errorInfo.componentStack,
},
});
// Update state with error info
this.setState({
error: { ...error, errorId } as ErrorWithId,
errorInfo,
});
// Call optional error handler
this.props.onError?.(error, errorInfo);
}
handleRetry = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
});
};
render() {
if (this.state.hasError) {
// Custom fallback if provided
if (this.props.fallback) {
return this.props.fallback;
}
// Default error UI
return (
<Card className="border-red-200 dark:border-red-800 bg-red-50/50 dark:bg-red-900/10">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-red-700 dark:text-red-300">
<AlertCircle className="w-5 h-5" />
Queue Item Error
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Failed to render submission</AlertTitle>
<AlertDescription>
<div className="mt-2 space-y-2">
<p className="text-sm">
{this.state.error?.message || 'An unexpected error occurred'}
</p>
{(this.state.error as ErrorWithId)?.errorId && (
<p className="text-xs font-mono bg-destructive/10 px-2 py-1 rounded">
Reference ID: {(this.state.error as ErrorWithId).errorId.slice(0, 8)}
</p>
)}
{this.props.submissionId && (
<p className="text-xs text-muted-foreground font-mono">
Submission ID: {this.props.submissionId}
</p>
)}
</div>
</AlertDescription>
</Alert>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={this.handleRetry}
className="gap-2"
>
<RefreshCw className="w-4 h-4" />
Retry
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
navigator.clipboard.writeText(
JSON.stringify({
error: this.state.error?.message,
stack: this.state.error?.stack,
submissionId: this.props.submissionId,
}, null, 2)
);
}}
>
Copy Error Details
</Button>
</div>
{process.env.NODE_ENV === 'development' && this.state.errorInfo && (
<details className="text-xs">
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
Show Component Stack
</summary>
<pre className="mt-2 overflow-auto p-2 bg-muted rounded text-xs">
{this.state.errorInfo.componentStack}
</pre>
</details>
)}
</CardContent>
</Card>
);
}
return this.props.children;
}
}