diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2c5b6bdd..3f820b8d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { BrowserRouter } from 'react-router'; import { AppRoutes } from '@/routes/AppRoutes'; import { ThemeProvider } from '@/contexts/ThemeContext'; +import { ErrorBoundary } from '@/components/ErrorBoundary'; import QueryClientProviders from '@/config/QueryClientProvider'; import { GlobalLoader } from './components/Loader/GlobalLoader'; import { InfoDialog } from './components/Dialog/InfoDialog'; @@ -19,19 +20,21 @@ const App: React.FC = () => { } = useSelector((state: RootState) => state.infoDialog); return ( - - - - - - - + + + + + + + + + ); }; diff --git a/frontend/src/components/ErrorBoundary/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary/ErrorBoundary.tsx new file mode 100644 index 00000000..2bd9a90d --- /dev/null +++ b/frontend/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -0,0 +1,76 @@ +import { Component, ErrorInfo, ReactNode } from 'react'; +import { AlertTriangle, RotateCw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +interface ErrorBoundaryProps { + children: ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; + isRetrying: boolean; +} + +/** + * ErrorBoundary catches JavaScript errors anywhere in the child component tree, + * logs those errors, and displays a fallback UI instead of crashing the app. + */ +class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false, error: null, isRetrying: false }; + } + + static getDerivedStateFromError(error: Error): Partial { + return { hasError: true, error, isRetrying: false }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + console.error('ErrorBoundary caught an error:', error); + console.error('Component stack:', errorInfo.componentStack); + } + + handleRetry = (): void => { + this.setState({ isRetrying: true }, () => { + // Small delay to show the spinner, then attempt recovery + setTimeout(() => { + this.setState({ hasError: false, error: null, isRetrying: false }); + }, 500); + }); + }; + + render(): ReactNode { + if (this.state.hasError) { + return ( +
+
+
+ +
+ +

+ Something went wrong +

+ +

+ An unexpected error occurred. Please try reloading the + application. +

+ + +
+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/frontend/src/components/ErrorBoundary/index.ts b/frontend/src/components/ErrorBoundary/index.ts new file mode 100644 index 00000000..e5d6dda2 --- /dev/null +++ b/frontend/src/components/ErrorBoundary/index.ts @@ -0,0 +1 @@ +export { default as ErrorBoundary } from './ErrorBoundary';