395 lines
13 KiB
TypeScript
395 lines
13 KiB
TypeScript
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
|
import {
|
|
Box,
|
|
Button,
|
|
Typography,
|
|
Paper,
|
|
Container,
|
|
Stack,
|
|
Alert,
|
|
Collapse,
|
|
Divider
|
|
} from '@mui/material';
|
|
import {
|
|
ErrorOutline as ErrorIcon,
|
|
Refresh as RefreshIcon,
|
|
Home as HomeIcon,
|
|
ExpandMore as ExpandMoreIcon,
|
|
BugReport as BugReportIcon
|
|
} from '@mui/icons-material';
|
|
|
|
interface ErrorBoundaryProps {
|
|
children: ReactNode;
|
|
fallback?: ReactNode;
|
|
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
showDetails?: boolean;
|
|
context?: string; // Context for better error messages (e.g., "Onboarding Wizard")
|
|
}
|
|
|
|
interface ErrorBoundaryState {
|
|
hasError: boolean;
|
|
error: Error | null;
|
|
errorInfo: ErrorInfo | null;
|
|
showDetails: boolean;
|
|
}
|
|
|
|
/**
|
|
* ErrorBoundary Component
|
|
*
|
|
* Catches JavaScript errors anywhere in the child component tree,
|
|
* logs those errors, and displays a fallback UI instead of blank screen.
|
|
*
|
|
* Usage:
|
|
* <ErrorBoundary context="Dashboard">
|
|
* <YourComponent />
|
|
* </ErrorBoundary>
|
|
*/
|
|
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
constructor(props: ErrorBoundaryProps) {
|
|
super(props);
|
|
this.state = {
|
|
hasError: false,
|
|
error: null,
|
|
errorInfo: null,
|
|
showDetails: false,
|
|
};
|
|
}
|
|
|
|
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
|
|
// Update state so the next render will show the fallback UI
|
|
return {
|
|
hasError: true,
|
|
error,
|
|
};
|
|
}
|
|
|
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
// Log error details
|
|
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|
|
|
// Update state with error info
|
|
this.setState({
|
|
error,
|
|
errorInfo,
|
|
});
|
|
|
|
// Call custom error handler if provided
|
|
if (this.props.onError) {
|
|
this.props.onError(error, errorInfo);
|
|
}
|
|
|
|
// Send to error tracking service (Sentry, LogRocket, etc.)
|
|
this.logErrorToService(error, errorInfo);
|
|
}
|
|
|
|
logErrorToService(error: Error, errorInfo: ErrorInfo) {
|
|
try {
|
|
// Import error reporting utility
|
|
import('../../utils/errorReporting').then(({ reportError }) => {
|
|
reportError({
|
|
error,
|
|
context: this.props.context || 'ErrorBoundary',
|
|
metadata: {
|
|
componentStack: errorInfo.componentStack,
|
|
errorBoundary: true,
|
|
},
|
|
severity: 'high', // Rendering errors are high severity
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}).catch(console.error);
|
|
|
|
// Log to console with detailed info
|
|
console.group('🚨 Error Boundary - Error Details');
|
|
console.error('Error:', error);
|
|
console.error('Error Info:', errorInfo);
|
|
console.error('Component Stack:', errorInfo.componentStack);
|
|
console.error('Context:', this.props.context || 'Global');
|
|
console.error('Timestamp:', new Date().toISOString());
|
|
console.groupEnd();
|
|
} catch (loggingError) {
|
|
console.error('Failed to log error:', loggingError);
|
|
}
|
|
}
|
|
|
|
handleReset = () => {
|
|
this.setState({
|
|
hasError: false,
|
|
error: null,
|
|
errorInfo: null,
|
|
showDetails: false,
|
|
});
|
|
};
|
|
|
|
handleReload = () => {
|
|
window.location.reload();
|
|
};
|
|
|
|
handleGoHome = () => {
|
|
window.location.href = '/';
|
|
};
|
|
|
|
toggleDetails = () => {
|
|
this.setState((prevState) => ({
|
|
showDetails: !prevState.showDetails,
|
|
}));
|
|
};
|
|
|
|
render() {
|
|
if (this.state.hasError) {
|
|
const { error } = this.state;
|
|
|
|
|
|
// Custom fallback UI provided
|
|
if (this.props.fallback) {
|
|
return this.props.fallback;
|
|
}
|
|
|
|
// Default fallback UI
|
|
const { errorInfo, showDetails } = this.state;
|
|
const { context, showDetails: showDetailsDefault } = this.props;
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
minHeight: '100vh',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
p: { xs: 2, md: 4 },
|
|
}}
|
|
>
|
|
<Container maxWidth="md">
|
|
<Paper
|
|
elevation={24}
|
|
sx={{
|
|
p: { xs: 3, md: 5 },
|
|
borderRadius: 4,
|
|
background: 'rgba(255, 255, 255, 0.98)',
|
|
backdropFilter: 'blur(20px)',
|
|
border: '1px solid rgba(255, 255, 255, 0.3)',
|
|
}}
|
|
>
|
|
<Stack spacing={3} alignItems="center">
|
|
{/* Error Icon */}
|
|
<Box
|
|
sx={{
|
|
width: 80,
|
|
height: 80,
|
|
borderRadius: '50%',
|
|
background: 'linear-gradient(45deg, #f44336 30%, #e91e63 90%)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
boxShadow: '0 8px 32px rgba(244, 67, 54, 0.3)',
|
|
}}
|
|
>
|
|
<ErrorIcon sx={{ fontSize: 48, color: 'white' }} />
|
|
</Box>
|
|
|
|
{/* Error Title */}
|
|
<Typography
|
|
variant="h4"
|
|
component="h1"
|
|
sx={{
|
|
fontWeight: 700,
|
|
color: '#1a1a1a',
|
|
textAlign: 'center',
|
|
}}
|
|
>
|
|
Oops! Something went wrong
|
|
</Typography>
|
|
|
|
{/* Context Message */}
|
|
{context && (
|
|
<Typography
|
|
variant="body1"
|
|
color="text.secondary"
|
|
sx={{ textAlign: 'center' }}
|
|
>
|
|
An error occurred in: <strong>{context}</strong>
|
|
</Typography>
|
|
)}
|
|
|
|
{/* User-friendly message */}
|
|
<Typography
|
|
variant="body1"
|
|
color="text.secondary"
|
|
sx={{ textAlign: 'center', maxWidth: 600 }}
|
|
>
|
|
We're sorry for the inconvenience. The error has been logged and our team will investigate.
|
|
In the meantime, you can try refreshing the page or returning to the home page.
|
|
</Typography>
|
|
|
|
{/* Action Buttons */}
|
|
<Stack
|
|
direction={{ xs: 'column', sm: 'row' }}
|
|
spacing={2}
|
|
sx={{ mt: 2, width: '100%', maxWidth: 500 }}
|
|
>
|
|
<Button
|
|
variant="contained"
|
|
size="large"
|
|
startIcon={<RefreshIcon />}
|
|
onClick={this.handleReload}
|
|
sx={{
|
|
flex: 1,
|
|
py: 1.5,
|
|
background: 'linear-gradient(45deg, #667eea 30%, #764ba2 90%)',
|
|
'&:hover': {
|
|
background: 'linear-gradient(45deg, #5568d3 30%, #6a3f8f 90%)',
|
|
},
|
|
}}
|
|
>
|
|
Reload Page
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outlined"
|
|
size="large"
|
|
startIcon={<HomeIcon />}
|
|
onClick={this.handleGoHome}
|
|
sx={{
|
|
flex: 1,
|
|
py: 1.5,
|
|
borderColor: '#667eea',
|
|
color: '#667eea',
|
|
'&:hover': {
|
|
borderColor: '#5568d3',
|
|
background: 'rgba(102, 126, 234, 0.05)',
|
|
},
|
|
}}
|
|
>
|
|
Go Home
|
|
</Button>
|
|
</Stack>
|
|
|
|
{/* Error Details Toggle (for developers/debugging) */}
|
|
{(showDetailsDefault || process.env.NODE_ENV === 'development') && (
|
|
<>
|
|
<Divider sx={{ width: '100%', my: 2 }} />
|
|
|
|
<Button
|
|
variant="text"
|
|
size="small"
|
|
startIcon={<BugReportIcon />}
|
|
endIcon={
|
|
<ExpandMoreIcon
|
|
sx={{
|
|
transform: showDetails ? 'rotate(180deg)' : 'rotate(0deg)',
|
|
transition: 'transform 0.3s',
|
|
}}
|
|
/>
|
|
}
|
|
onClick={this.toggleDetails}
|
|
sx={{ color: '#666' }}
|
|
>
|
|
{showDetails ? 'Hide' : 'Show'} Technical Details
|
|
</Button>
|
|
|
|
<Collapse in={showDetails} sx={{ width: '100%' }}>
|
|
<Alert
|
|
severity="error"
|
|
icon={<BugReportIcon />}
|
|
sx={{
|
|
textAlign: 'left',
|
|
'& .MuiAlert-message': {
|
|
width: '100%',
|
|
},
|
|
}}
|
|
>
|
|
<Typography variant="subtitle2" fontWeight={600} gutterBottom>
|
|
Error Message:
|
|
</Typography>
|
|
<Typography
|
|
variant="body2"
|
|
sx={{
|
|
fontFamily: 'monospace',
|
|
fontSize: '0.85rem',
|
|
mb: 2,
|
|
p: 1,
|
|
bgcolor: 'rgba(0,0,0,0.05)',
|
|
borderRadius: 1,
|
|
overflowX: 'auto',
|
|
}}
|
|
>
|
|
{error?.toString()}
|
|
</Typography>
|
|
|
|
{error?.stack && (
|
|
<>
|
|
<Typography variant="subtitle2" fontWeight={600} gutterBottom>
|
|
Stack Trace:
|
|
</Typography>
|
|
<Typography
|
|
variant="body2"
|
|
component="pre"
|
|
sx={{
|
|
fontFamily: 'monospace',
|
|
fontSize: '0.75rem',
|
|
whiteSpace: 'pre-wrap',
|
|
wordBreak: 'break-word',
|
|
p: 1,
|
|
bgcolor: 'rgba(0,0,0,0.05)',
|
|
borderRadius: 1,
|
|
maxHeight: 200,
|
|
overflowY: 'auto',
|
|
}}
|
|
>
|
|
{error.stack}
|
|
</Typography>
|
|
</>
|
|
)}
|
|
|
|
{errorInfo?.componentStack && (
|
|
<>
|
|
<Typography variant="subtitle2" fontWeight={600} gutterBottom sx={{ mt: 2 }}>
|
|
Component Stack:
|
|
</Typography>
|
|
<Typography
|
|
variant="body2"
|
|
component="pre"
|
|
sx={{
|
|
fontFamily: 'monospace',
|
|
fontSize: '0.75rem',
|
|
whiteSpace: 'pre-wrap',
|
|
wordBreak: 'break-word',
|
|
p: 1,
|
|
bgcolor: 'rgba(0,0,0,0.05)',
|
|
borderRadius: 1,
|
|
maxHeight: 150,
|
|
overflowY: 'auto',
|
|
}}
|
|
>
|
|
{errorInfo.componentStack}
|
|
</Typography>
|
|
</>
|
|
)}
|
|
</Alert>
|
|
</Collapse>
|
|
</>
|
|
)}
|
|
|
|
{/* Help Text */}
|
|
<Typography
|
|
variant="caption"
|
|
color="text.secondary"
|
|
sx={{ textAlign: 'center', mt: 2 }}
|
|
>
|
|
Error ID: {Date.now().toString(36)} • Timestamp: {new Date().toLocaleString()}
|
|
</Typography>
|
|
</Stack>
|
|
</Paper>
|
|
</Container>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// No error, render children normally
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
export default ErrorBoundary;
|
|
|