Files
thrilltrack-explorer/src/components/error/RouteErrorBoundary.tsx
2025-11-04 21:31:37 +00:00

148 lines
5.0 KiB
TypeScript

import React, { Component, ErrorInfo, ReactNode } from 'react';
import { AlertTriangle, Home, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { handleError } from '@/lib/errorHandler';
interface RouteErrorBoundaryProps {
children: ReactNode;
}
interface RouteErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
type ErrorWithId = Error & { errorId: string };
export class RouteErrorBoundary extends Component<RouteErrorBoundaryProps, RouteErrorBoundaryState> {
constructor(props: RouteErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
};
}
static getDerivedStateFromError(error: Error): Partial<RouteErrorBoundaryState> {
return {
hasError: true,
error,
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Detect chunk load failures (deployment cache issue)
const isChunkLoadError =
error.message.includes('Failed to fetch dynamically imported module') ||
error.message.includes('Loading chunk') ||
error.message.includes('ChunkLoadError');
if (isChunkLoadError) {
// Check if we've already tried reloading
const hasReloaded = sessionStorage.getItem('chunk-load-reload');
if (!hasReloaded) {
// Mark as reloaded and reload once
sessionStorage.setItem('chunk-load-reload', 'true');
window.location.reload();
return; // Don't log error yet
} else {
// Second failure - clear flag and show error
sessionStorage.removeItem('chunk-load-reload');
}
}
// Log to database and get error ID for user reference
const errorId = handleError(error, {
action: 'Route-level component crash',
metadata: {
componentStack: errorInfo.componentStack,
url: window.location.href,
severity: isChunkLoadError ? 'medium' : 'critical',
isChunkLoadError,
},
});
this.setState({ error: { ...error, errorId } as ErrorWithId });
}
handleReload = () => {
window.location.reload();
};
handleGoHome = () => {
window.location.href = '/';
};
render() {
if (this.state.hasError) {
const isChunkError =
this.state.error?.message.includes('Failed to fetch dynamically imported module') ||
this.state.error?.message.includes('Loading chunk') ||
this.state.error?.message.includes('ChunkLoadError');
return (
<div className="min-h-screen flex items-center justify-center p-4 bg-background">
<Card className="max-w-lg w-full shadow-lg">
<CardHeader className="text-center pb-4">
<div className="mx-auto w-16 h-16 bg-destructive/10 rounded-full flex items-center justify-center mb-4">
<AlertTriangle className="w-8 h-8 text-destructive" />
</div>
<CardTitle className="text-2xl">
{isChunkError ? 'New Version Available' : 'Something Went Wrong'}
</CardTitle>
<CardDescription className="mt-2">
{isChunkError
? "The app has been updated. Please reload the page to get the latest version."
: "We encountered an unexpected error. This has been logged and we'll look into it."}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{this.state.error && (
<div className="p-3 bg-muted rounded-lg space-y-2">
{import.meta.env.DEV && (
<p className="text-xs font-mono text-muted-foreground">
{this.state.error.message}
</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>
)}
</div>
)}
<div className="flex flex-col sm:flex-row gap-2">
<Button
variant="default"
onClick={this.handleReload}
className="flex-1 gap-2"
>
<RefreshCw className="w-4 h-4" />
Reload Page
</Button>
<Button
variant="outline"
onClick={this.handleGoHome}
className="flex-1 gap-2"
>
<Home className="w-4 h-4" />
Go Home
</Button>
</div>
<p className="text-xs text-center text-muted-foreground">
If this problem persists, please contact support
</p>
</CardContent>
</Card>
</div>
);
}
return this.props.children;
}
}