mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 16:51:12 -05:00
197 lines
6.9 KiB
TypeScript
197 lines
6.9 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();
|
|
};
|
|
|
|
handleClearCacheAndReload = async () => {
|
|
try {
|
|
// Clear all caches
|
|
if ('caches' in window) {
|
|
const cacheNames = await caches.keys();
|
|
await Promise.all(cacheNames.map(name => caches.delete(name)));
|
|
}
|
|
|
|
// Unregister service workers
|
|
if ('serviceWorker' in navigator) {
|
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
await Promise.all(registrations.map(reg => reg.unregister()));
|
|
}
|
|
|
|
// Clear session storage chunk reload flag
|
|
sessionStorage.removeItem('chunk-load-reload');
|
|
|
|
// Force reload bypassing cache
|
|
window.location.reload();
|
|
} catch (error) {
|
|
// Fallback to regular reload if cache clearing fails
|
|
console.error('Failed to clear cache:', error);
|
|
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 ? 'App Update Required' : 'Something Went Wrong'}
|
|
</CardTitle>
|
|
<CardDescription className="mt-2 space-y-2">
|
|
{isChunkError ? (
|
|
<>
|
|
<p>The app has been updated with new features and improvements.</p>
|
|
<p className="text-sm font-medium">
|
|
To continue, please clear your browser cache and reload:
|
|
</p>
|
|
<ul className="text-sm list-disc list-inside space-y-1 ml-2">
|
|
<li>Click "Clear Cache & Reload" below, or</li>
|
|
<li>Press <kbd className="px-1.5 py-0.5 text-xs font-semibold bg-muted rounded">Ctrl+Shift+R</kbd> (Windows/Linux) or <kbd className="px-1.5 py-0.5 text-xs font-semibold bg-muted rounded">⌘+Shift+R</kbd> (Mac)</li>
|
|
</ul>
|
|
</>
|
|
) : (
|
|
"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 gap-2">
|
|
{isChunkError && (
|
|
<Button
|
|
variant="default"
|
|
onClick={this.handleClearCacheAndReload}
|
|
className="w-full gap-2"
|
|
>
|
|
<RefreshCw className="w-4 h-4" />
|
|
Clear Cache & Reload
|
|
</Button>
|
|
)}
|
|
<div className="flex flex-col sm:flex-row gap-2">
|
|
<Button
|
|
variant={isChunkError ? "outline" : "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>
|
|
</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;
|
|
}
|
|
}
|