diff --git a/apps/web/src/components/ErrorBoundary.tsx b/apps/web/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..12a813a --- /dev/null +++ b/apps/web/src/components/ErrorBoundary.tsx @@ -0,0 +1,54 @@ +import { Component, type ErrorInfo, type ReactNode } from 'react'; + +interface Props { + children: ReactNode; + /** Optional fallback rendered in place of `children` when an error is caught. */ + fallback?: (error: Error) => ReactNode; +} + +interface State { + error: Error | null; +} + +/** + * Catches render errors in its subtree and renders a fallback instead of + * letting them crash the whole page. Used to isolate failures in object + * viewers (e.g. WebGL context loss, model/texture load errors). + */ +export default class ErrorBoundary extends Component { + override state: State = { error: null }; + + static getDerivedStateFromError(error: Error): State { + return { error }; + } + + override componentDidCatch(error: Error, info: ErrorInfo) { + // Surface the error for debugging without breaking the UI. + console.error('Viewer error:', error, info.componentStack); + } + + override render() { + if (this.state.error) { + return this.props.fallback + ? this.props.fallback(this.state.error) + : ; + } + return this.props.children; + } +} + +function DefaultFallback({ error }: { error: Error }) { + return ( +
+

Couldn't render this object

+

+ {error.message} +

+ {error.stack && ( +
+          {error.stack}
+        
+ )} +
+ ); +} \ No newline at end of file diff --git a/apps/web/src/pages/ModPage.tsx b/apps/web/src/pages/ModPage.tsx index 9433506..3f0caf6 100644 --- a/apps/web/src/pages/ModPage.tsx +++ b/apps/web/src/pages/ModPage.tsx @@ -6,6 +6,7 @@ import { useModStore } from '../stores/modStore'; import { useSearchStore } from '../stores/searchStore'; import { modFileUrl } from '../api'; import ObjectTree from '../components/ObjectTree'; +import ErrorBoundary from '../components/ErrorBoundary'; import { resolveViewer } from '../components/viewers'; import { iconsForObject } from '../components/objectIcons'; // Register the 3D viewers (side effect) so `resolveViewer` can find them. @@ -78,15 +79,17 @@ export default function ModPage() {

{Viewer && ( - - Loading viewer… - - } - > - - + + + Loading viewer… + + } + > + + + )} ) : (