Spaces:
Configuration error
Configuration error
| // --- File: client/src/components/ErrorBoundary.tsx --- | |
| // --- Tech Used --- | |
| // - React: For building the user interface. Uses both Class Components (for ErrorBoundary itself) and Functional Components (for hooks/HOCs). | |
| // - TypeScript: For static typing, improving code quality and maintainability. | |
| // - Shadcn/ui (Likely): Based on "@/components/ui/..." imports. This library provides accessible and customizable UI components, often built on Tailwind CSS and Radix UI. | |
| // - Lucide Icons (`lucide-react`): For clean and consistent SVG icons. | |
| // - Custom Error Handling (`@/lib/errorHandler`): A centralized module for logging/processing errors. | |
| // --- Imports --- | |
| import React, { Component, ReactNode, ErrorInfo } from 'react'; // React.ErrorInfo provides types for componentDidCatch | |
| import { Button } from "@/components/ui/button"; | |
| import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; // Added CardFooter | |
| import { AlertTriangle, RefreshCw, Home, Copy } from "lucide-react"; // Added Copy icon | |
| import { errorHandler } from "@/lib/errorHandler"; // Your custom error logging utility | |
| // --- Component Props Interface --- | |
| // Defines the expected properties for the ErrorBoundary component. | |
| interface Props { | |
| children: ReactNode; // The React elements that this boundary will wrap. | |
| fallback?: ReactNode; // Optional custom UI to render when an error is caught. | |
| moduleName?: string; // A more descriptive name for the module/component being wrapped. | |
| onRetry?: () => void; // Optional callback for when the user clicks "Try Again". | |
| } | |
| // --- Component State Interface --- | |
| // Defines the structure of the ErrorBoundary's internal state. | |
| interface State { | |
| hasError: boolean; // True if an error has been caught. | |
| error?: Error; // The caught JavaScript error object. | |
| errorInfo?: ErrorInfo; // Additional info from React about the component stack. | |
| errorId?: string; // An ID for the logged error, useful for tracking. | |
| } | |
| // --- ErrorBoundary Class Component --- | |
| // Function: Catches JavaScript errors in its child component tree, logs them, and displays a fallback UI. | |
| export class ErrorBoundary extends Component<Props, State> { | |
| constructor(props: Props) { | |
| super(props); | |
| // Initialize state: no error by default. | |
| this.state = { hasError: false }; | |
| } | |
| // Lifecycle method: Called during the "render" phase if an error is thrown by a descendant. | |
| // Updates the state to trigger a re-render with the fallback UI. | |
| static getDerivedStateFromError(error: Error): Partial<State> { | |
| return { hasError: true, error }; | |
| } | |
| // Lifecycle method: Called during the "commit" phase after an error is thrown and getDerivedStateFromError has been called. | |
| // Suitable for side effects like logging. | |
| componentDidCatch(error: Error, errorInfo: ErrorInfo) { | |
| const { moduleName } = this.props; | |
| const errorContext = moduleName || 'UnknownComponent'; | |
| // Log the error using the custom error handler. | |
| const errorId = errorHandler.logError( | |
| error, | |
| errorContext, | |
| 'react_component_crash', // A category for this type of error | |
| { componentStack: errorInfo.componentStack } // Include component stack | |
| ); | |
| // Update state with the error information and ID. | |
| this.setState({ errorInfo, errorId }); | |
| // Log to console for development purposes. | |
| console.error(`ErrorBoundary (${errorContext}): Uncaught error:`, error, errorInfo); | |
| } | |
| // Resets the error state, allowing children to attempt re-rendering. | |
| handleRetry = () => { | |
| if (this.props.onRetry) { | |
| this.props.onRetry(); // Call custom retry logic if provided | |
| } | |
| this.setState({ hasError: false, error: undefined, errorInfo: undefined, errorId: undefined }); | |
| }; | |
| // Navigates to the home page. | |
| // For SPAs, using a router's navigation method (e.g., useNavigate from react-router-dom) is better than a hard reload. | |
| handleGoHome = () => { | |
| // TODO: Replace with router navigation if using react-router or similar. | |
| // Example with react-router: navigate('/'); | |
| window.location.href = '/'; | |
| }; | |
| // Copies the error ID to the clipboard. | |
| handleCopyErrorId = () => { | |
| if (this.state.errorId) { | |
| navigator.clipboard.writeText(this.state.errorId) | |
| .then(() => { | |
| // Optional: Show a temporary "Copied!" message | |
| console.info("Error ID copied to clipboard:", this.state.errorId); | |
| }) | |
| .catch(err => { | |
| console.error("Failed to copy error ID:", err); | |
| }); | |
| } | |
| }; | |
| render() { | |
| if (this.state.hasError) { | |
| // Render custom fallback if provided | |
| if (this.props.fallback) { | |
| return this.props.fallback; | |
| } | |
| // Default fallback UI | |
| const { error, errorId, errorInfo } = this.state; | |
| const { moduleName } = this.props; | |
| const componentName = moduleName || 'the application'; | |
| return ( | |
| <div role="alert" className="min-h-screen bg-background text-foreground flex items-center justify-center p-4"> | |
| <Card className="max-w-lg w-full shadow-xl border-destructive"> | |
| <CardHeader className="text-center bg-destructive/10"> | |
| <div className="mx-auto w-16 h-16 bg-destructive/20 rounded-full flex items-center justify-center mb-4 ring-4 ring-destructive/30"> | |
| <AlertTriangle className="w-8 h-8 text-destructive" /> | |
| </div> | |
| <CardTitle className="text-2xl font-bold text-destructive">An Error Occurred</CardTitle> | |
| <CardDescription className="text-muted-foreground"> | |
| Sorry, something went wrong in {componentName}. | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-4 pt-6"> | |
| <div className="bg-muted p-4 rounded-md border text-sm"> | |
| <strong className="text-card-foreground">Message:</strong> | |
| <p className="font-mono text-destructive break-words whitespace-pre-wrap"> | |
| {error?.message || 'No specific error message available.'} | |
| </p> | |
| {errorId && ( | |
| <div className="mt-3 text-xs text-muted-foreground flex items-center justify-between"> | |
| <span>Error ID: {errorId}</span> | |
| <Button variant="ghost" size="sm" onClick={this.handleCopyErrorId} aria-label="Copy Error ID"> | |
| <Copy className="w-3 h-3 mr-1" /> Copy ID | |
| </Button> | |
| </div> | |
| )} | |
| </div> | |
| {errorInfo?.componentStack && process.env.NODE_ENV === 'development' && ( | |
| <details className="text-xs bg-muted p-2 rounded"> | |
| <summary className="cursor-pointer text-muted-foreground">Component Stack (Dev Only)</summary> | |
| <pre className="mt-1 whitespace-pre-wrap break-all text-card-foreground/80"> | |
| {errorInfo.componentStack} | |
| </pre> | |
| </details> | |
| )} | |
| </CardContent> | |
| <CardFooter className="flex flex-col sm:flex-row sm:justify-end gap-3 pt-6 border-t"> | |
| <Button onClick={this.handleGoHome} variant="outline" className="w-full sm:w-auto"> | |
| <Home className="w-4 h-4 mr-2" /> Go to Dashboard | |
| </Button> | |
| <Button onClick={this.handleRetry} variant="default" className="w-full sm:w-auto"> | |
| <RefreshCw className="w-4 h-4 mr-2" /> Try Again | |
| </Button> | |
| </CardFooter> | |
| </Card> | |
| </div> | |
| ); | |
| } | |
| return this.props.children; // Render children if no error | |
| } | |
| } | |
| // --- Custom Hook: useErrorHandler --- | |
| // Function: Provides convenient error handling functions for use in Functional Components. | |
| export function useErrorHandler(moduleName: string) { // moduleName: Context for the error (e.g., component name). | |
| // Function: Manually logs an error using the centralized errorHandler. | |
| const handleError = (error: Error, userActionContext?: string): string | undefined => { | |
| console.error(`Error handled in module '${moduleName}' (action: ${userActionContext || 'N/A'}):`, error); | |
| return errorHandler.logError(error, moduleName, userActionContext); | |
| }; | |
| // Function: Wraps an asynchronous operation (Promise) to automatically catch and log errors. | |
| // T: Generic type for the expected data from the successful operation. | |
| // SIMPLIFIED VERSION FOR TESTING | |
| const handleAsyncOperation = async ( // Generic <T> removed for simplicity during testing | |
| operation: any, // Using 'any' for the operation itself | |
| userActionContext?: string | undefined // Adding userActionContext back as an optional string | |
| ): Promise<any> => { // Using 'any' for the Promise return type | |
| try { | |
| const data = await operation(); | |
| return { success: true, data }; | |
| } catch (err) { | |
| // Ensure the caught item is an Error instance before logging. | |
| const error = err instanceof Error ? err : new Error(String(err)); | |
| const errorId = handleError(error, userActionContext); | |
| return { success: false, errorId }; | |
| } | |
| }; | |
| return { handleError, handleAsyncOperation }; | |
| } | |
| // --- Higher-Order Component (HOC): withErrorBoundary --- | |
| // Function: A utility to easily wrap existing components with the ErrorBoundary. | |
| // P: Generic type for the props of the component being wrapped. | |
| export function withErrorBoundary<P extends React.JSX.IntrinsicAttributes>( // Use a more appropriate constraint for P | |
| WrappedComponent: React.ComponentType<P>, | |
| options: { moduleName: string; fallback?: ReactNode; onRetry?: () => void } // Options object | |
| ) { | |
| const { moduleName, fallback, onRetry } = options; | |
| // Returns a new component that renders the WrappedComponent inside an ErrorBoundary. | |
| const ComponentWithBoundary = (props: P) => ( // Give component a display name | |
| <ErrorBoundary moduleName={moduleName} fallback={fallback} onRetry={onRetry}> | |
| <WrappedComponent {...props} /> | |
| </ErrorBoundary> | |
| ); | |
| // Set a display name for easier debugging in React DevTools | |
| const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; | |
| ComponentWithBoundary.displayName = `WithErrorBoundary(${wrappedComponentName})`; | |
| return ComponentWithBoundary; | |
| } |