diff --git a/frontend/src/components/ErrorBoundary.css b/frontend/src/components/ErrorBoundary.css new file mode 100644 index 0000000..915fa37 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.css @@ -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%; + } +} + diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..eebe8b7 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.tsx @@ -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 { + constructor(props: Props) { + super(props); + this.state = { + hasError: false, + error: null, + errorInfo: null, + }; + } + + static getDerivedStateFromError(error: Error): Partial { + 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 ( +
+
+
⚠️
+

Something Went Wrong

+

+ The application encountered an unexpected error and couldn't continue. +

+ + {this.state.error && ( +
+

Error Details:

+
+                  {this.state.error.toString()}
+                
+
+ )} + +
+ + +
+ + {process.env.NODE_ENV === 'development' && this.state.errorInfo && ( +
+ Component Stack Trace (Development Only) +
{this.state.errorInfo.componentStack}
+
+ )} +
+
+ ); + } + + return this.props.children; + } +} + diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 2339d59..79479b8 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -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( - + + + );