feat: Add error boundaries

This commit is contained in:
gpt-engineer-app[bot]
2025-11-03 14:51:39 +00:00
parent 3ee65403ea
commit ee09e3652c
8 changed files with 1682 additions and 56 deletions

View File

@@ -0,0 +1,162 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { AlertCircle, Home, RefreshCw } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { logger } from '@/lib/logger';
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
context?: string; // e.g., "PhotoUpload", "ParkDetail"
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
/**
* Generic Error Boundary Component (P0 #5)
*
* Prevents component errors from crashing the entire application.
* Shows user-friendly error UI with recovery options.
*
* Usage:
* ```tsx
* <ErrorBoundary context="PhotoUpload">
* <PhotoUploadForm />
* </ErrorBoundary>
* ```
*/
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
return {
hasError: true,
error,
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Log error with context
logger.error('Component error caught by boundary', {
context: this.props.context || 'unknown',
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
});
this.setState({ errorInfo });
this.props.onError?.(error, errorInfo);
}
handleRetry = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
});
};
handleGoHome = () => {
window.location.href = '/';
};
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="min-h-[400px] flex items-center justify-center p-4">
<Card className="max-w-2xl w-full border-destructive/50">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-destructive">
<AlertCircle className="w-5 h-5" />
Something Went Wrong
</CardTitle>
<CardDescription>
{this.props.context
? `An error occurred in ${this.props.context}`
: 'An unexpected error occurred'}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error Details</AlertTitle>
<AlertDescription>
<p className="text-sm mt-2">
{this.state.error?.message || 'An unexpected error occurred'}
</p>
</AlertDescription>
</Alert>
<div className="flex items-center gap-2 flex-wrap">
<Button
variant="default"
size="sm"
onClick={this.handleRetry}
className="gap-2"
>
<RefreshCw className="w-4 h-4" />
Try Again
</Button>
<Button
variant="outline"
size="sm"
onClick={this.handleGoHome}
className="gap-2"
>
<Home className="w-4 h-4" />
Go Home
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
navigator.clipboard.writeText(
JSON.stringify({
context: this.props.context,
error: this.state.error?.message,
stack: this.state.error?.stack,
timestamp: new Date().toISOString(),
}, null, 2)
);
}}
>
Copy Error Details
</Button>
</div>
{import.meta.env.DEV && this.state.errorInfo && (
<details className="text-xs">
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
Show Component Stack (Development Only)
</summary>
<pre className="mt-2 overflow-auto p-3 bg-muted rounded text-xs max-h-[300px]">
{this.state.errorInfo.componentStack}
</pre>
</details>
)}
</CardContent>
</Card>
</div>
);
}
return this.props.children;
}
}