Added onboarding progress tracking & landing page
This commit is contained in:
392
frontend/src/components/shared/ErrorBoundary.tsx
Normal file
392
frontend/src/components/shared/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,392 @@
|
||||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Typography,
|
||||
Paper,
|
||||
Container,
|
||||
Stack,
|
||||
Alert,
|
||||
Collapse,
|
||||
IconButton,
|
||||
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) {
|
||||
// Custom fallback UI provided
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
// Default fallback UI
|
||||
const { error, 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;
|
||||
|
||||
Reference in New Issue
Block a user