Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions frontend/src/components/ErrorBoundary.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
.error-boundary {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
}

.error-boundary-content {
background: white;
border-radius: 12px;
padding: 3rem;
max-width: 600px;
text-align: center;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}

.error-icon {
font-size: 4rem;
margin-bottom: 1rem;
animation: shake 0.5s ease-in-out;
}

@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-10px);
}
75% {
transform: translateX(10px);
}
}

.error-boundary-content h1 {
font-size: 2rem;
color: #2d3748;
margin-bottom: 1rem;
}

.error-message {
color: #4a5568;
font-size: 1.1rem;
margin-bottom: 2rem;
line-height: 1.6;
}

.error-details {
background: #f7fafc;
border-left: 4px solid #fc8181;
padding: 1rem;
margin: 1.5rem 0;
text-align: left;
}

.error-details h3 {
color: #c53030;
margin-top: 0;
margin-bottom: 0.5rem;
font-size: 1rem;
}

.error-stack {
background: #2d3748;
color: #fc8181;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.875rem;
white-space: pre-wrap;
word-break: break-all;
}

.error-actions {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 2rem;
}

.btn-primary,
.btn-secondary {
padding: 0.75rem 2rem;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}

.btn-primary {
background: #667eea;
color: white;
}

.btn-primary:hover {
background: #5a67d8;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}

.btn-secondary {
background: #e2e8f0;
color: #2d3748;
}

.btn-secondary:hover {
background: #cbd5e0;
transform: translateY(-2px);
}

.error-stack-trace {
margin-top: 2rem;
text-align: left;
}

.error-stack-trace summary {
cursor: pointer;
color: #4a5568;
font-weight: 600;
padding: 0.5rem;
background: #edf2f7;
border-radius: 4px;
}

.error-stack-trace summary:hover {
background: #e2e8f0;
}

.error-stack-trace pre {
background: #2d3748;
color: #a0aec0;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.75rem;
margin-top: 0.5rem;
}

@media (max-width: 768px) {
.error-boundary-content {
padding: 2rem;
}

.error-boundary-content h1 {
font-size: 1.5rem;
}

.error-actions {
flex-direction: column;
}

.btn-primary,
.btn-secondary {
width: 100%;
}
}

101 changes: 101 additions & 0 deletions frontend/src/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* ErrorBoundary Component
* Catches React errors and provides graceful fallback UI
*/
import { Component, ErrorInfo, ReactNode } from 'react';
import './ErrorBoundary.css';

interface Props {
children: ReactNode;
}

interface State {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}

export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}

static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Log error to console (in production, send to error tracking service)
console.error('React Error Boundary caught error:', {
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
});

this.setState({
error,
errorInfo,
});
}

handleRetry = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
});
};

handleReload = () => {
window.location.reload();
};

render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<div className="error-boundary-content">
<div className="error-icon">⚠️</div>
<h1>Something Went Wrong</h1>
<p className="error-message">
The application encountered an unexpected error and couldn't continue.
</p>

{this.state.error && (
<div className="error-details">
<h3>Error Details:</h3>
<pre className="error-stack">
{this.state.error.toString()}
</pre>
</div>
)}

<div className="error-actions">
<button onClick={this.handleRetry} className="btn-primary">
Try Again
</button>
<button onClick={this.handleReload} className="btn-secondary">
Reload Page
</button>
</div>

{process.env.NODE_ENV === 'development' && this.state.errorInfo && (
<details className="error-stack-trace">
<summary>Component Stack Trace (Development Only)</summary>
<pre>{this.state.errorInfo.componentStack}</pre>
</details>
)}
</div>
</div>
);
}

return this.props.children;
}
}

5 changes: 4 additions & 1 deletion frontend/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { ErrorBoundary } from './components/ErrorBoundary';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
<ErrorBoundary>
<App />
</ErrorBoundary>
</React.StrictMode>
);
Loading