mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 14:11:13 -05:00
191 lines
5.9 KiB
TypeScript
191 lines
5.9 KiB
TypeScript
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
|
import { AlertCircle, ArrowLeft, 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 EntityErrorBoundaryProps {
|
|
children: ReactNode;
|
|
entityType: 'park' | 'ride' | 'manufacturer' | 'designer' | 'operator' | 'owner';
|
|
entitySlug?: string;
|
|
fallback?: ReactNode;
|
|
}
|
|
|
|
interface EntityErrorBoundaryState {
|
|
hasError: boolean;
|
|
error: Error | null;
|
|
errorInfo: ErrorInfo | null;
|
|
}
|
|
|
|
/**
|
|
* Entity Error Boundary Component (P0 #5)
|
|
*
|
|
* Specialized error boundary for entity detail pages.
|
|
* Prevents entity rendering errors from crashing the app.
|
|
*
|
|
* Usage:
|
|
* ```tsx
|
|
* <EntityErrorBoundary entityType="park" entitySlug={slug}>
|
|
* <ParkDetail />
|
|
* </EntityErrorBoundary>
|
|
* ```
|
|
*/
|
|
export class EntityErrorBoundary extends Component<EntityErrorBoundaryProps, EntityErrorBoundaryState> {
|
|
constructor(props: EntityErrorBoundaryProps) {
|
|
super(props);
|
|
this.state = {
|
|
hasError: false,
|
|
error: null,
|
|
errorInfo: null,
|
|
};
|
|
}
|
|
|
|
static getDerivedStateFromError(error: Error): Partial<EntityErrorBoundaryState> {
|
|
return {
|
|
hasError: true,
|
|
error,
|
|
};
|
|
}
|
|
|
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
logger.error('Entity page error caught by boundary', {
|
|
entityType: this.props.entityType,
|
|
entitySlug: this.props.entitySlug,
|
|
error: error.message,
|
|
stack: error.stack,
|
|
componentStack: errorInfo.componentStack,
|
|
});
|
|
|
|
this.setState({ errorInfo });
|
|
}
|
|
|
|
handleRetry = () => {
|
|
this.setState({
|
|
hasError: false,
|
|
error: null,
|
|
errorInfo: null,
|
|
});
|
|
};
|
|
|
|
handleBackToList = () => {
|
|
const listPages: Record<string, string> = {
|
|
park: '/parks',
|
|
ride: '/rides',
|
|
manufacturer: '/manufacturers',
|
|
designer: '/designers',
|
|
operator: '/operators',
|
|
owner: '/owners',
|
|
};
|
|
window.location.href = listPages[this.props.entityType] || '/';
|
|
};
|
|
|
|
handleGoHome = () => {
|
|
window.location.href = '/';
|
|
};
|
|
|
|
getEntityLabel() {
|
|
const labels: Record<string, string> = {
|
|
park: 'Park',
|
|
ride: 'Ride',
|
|
manufacturer: 'Manufacturer',
|
|
designer: 'Designer',
|
|
operator: 'Operator',
|
|
owner: 'Property Owner',
|
|
};
|
|
return labels[this.props.entityType] || 'Entity';
|
|
}
|
|
|
|
render() {
|
|
if (this.state.hasError) {
|
|
if (this.props.fallback) {
|
|
return this.props.fallback;
|
|
}
|
|
|
|
const entityLabel = this.getEntityLabel();
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-12">
|
|
<Card className="max-w-2xl mx-auto border-destructive/50">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-destructive">
|
|
<AlertCircle className="w-5 h-5" />
|
|
Failed to Load {entityLabel}
|
|
</CardTitle>
|
|
<CardDescription>
|
|
{this.props.entitySlug
|
|
? `Unable to display ${entityLabel.toLowerCase()}: ${this.props.entitySlug}`
|
|
: `Unable to display this ${entityLabel.toLowerCase()}`}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<Alert variant="destructive">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertTitle>Error Details</AlertTitle>
|
|
<AlertDescription>
|
|
<div className="mt-2 space-y-2">
|
|
<p className="text-sm">
|
|
{this.state.error?.message || `An unexpected error occurred while loading this ${entityLabel.toLowerCase()}`}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
This might be due to:
|
|
</p>
|
|
<ul className="text-xs text-muted-foreground list-disc list-inside space-y-1">
|
|
<li>The {entityLabel.toLowerCase()} no longer exists</li>
|
|
<li>Temporary data loading issues</li>
|
|
<li>Network connectivity problems</li>
|
|
</ul>
|
|
</div>
|
|
</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.handleBackToList}
|
|
className="gap-2"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
Back to {entityLabel}s
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={this.handleGoHome}
|
|
className="gap-2"
|
|
>
|
|
<Home className="w-4 h-4" />
|
|
Home
|
|
</Button>
|
|
</div>
|
|
|
|
{import.meta.env.DEV && this.state.errorInfo && (
|
|
<details className="text-xs">
|
|
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
|
|
Show Debug Info (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;
|
|
}
|
|
}
|