'use client'; import { Component, ErrorInfo, ReactNode } from 'react'; import { Stack, Typography } from '@mui/material'; import AppButton from './AppButton'; import AppIcon from './AppIcon'; export interface ErrorBoundaryProps { children: ReactNode; /** Diagnostic-only segment name (console log label) — never shown to the user. */ name: string; /** Already-translated fallback copy — kept presentational/caller-owned (like every other shared * primitive in this kit) so this file never imports next-intl; it sits at the top of the `common` * barrel, so an eager i18n dependency here would drag `next-intl` into every test that imports * anything from `@/components`, mocked or not. */ title: string; body: string; retryLabel: string; } interface State { hasError: boolean; error?: Error; errorInfo?: ErrorInfo; } /** * Error boundary wrapping a subtree of the app (a shell's content area). Renders a branded fallback with * a retry affordance (re-mounts the children by clearing `hasError`); the raw error + component stack are * **logged, never shown**, in production — only a development build renders the dev-only `
` * block. * @component ErrorBoundary */ class ErrorBoundary extends Component { constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error: Error) { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { this.setState({ error, errorInfo }); console.error(`[ErrorBoundary:${this.props.name}]`, error, errorInfo.componentStack); } handleRetry = () => { this.setState({ hasError: false, error: undefined, errorInfo: undefined }); }; render() { if (this.state.hasError) { const { error, errorInfo } = this.state; return ( {this.props.title} {this.props.body} {this.props.retryLabel} {process.env.NODE_ENV !== 'production' && (error || errorInfo) ? ( Dev-only error detail {error?.toString()} {'\n'} {errorInfo?.componentStack} ) : null} ); } return this.props.children; } } export default ErrorBoundary;