91 lines
3.0 KiB
TypeScript
91 lines
3.0 KiB
TypeScript
|
|
import { Component, type ReactNode } from 'react';
|
||
|
|
|
||
|
|
interface Props {
|
||
|
|
children: ReactNode;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface State {
|
||
|
|
hasError: boolean;
|
||
|
|
error: Error | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default class ErrorBoundary extends Component<Props, State> {
|
||
|
|
constructor(props: Props) {
|
||
|
|
super(props);
|
||
|
|
this.state = { hasError: false, error: null };
|
||
|
|
}
|
||
|
|
|
||
|
|
static getDerivedStateFromError(error: Error): State {
|
||
|
|
return { hasError: true, error };
|
||
|
|
}
|
||
|
|
|
||
|
|
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||
|
|
console.error('ErrorBoundary caught:', error, info.componentStack);
|
||
|
|
try {
|
||
|
|
window.electronAPI?.logError?.(error.message, error.stack || '', info.componentStack || '');
|
||
|
|
} catch {}
|
||
|
|
}
|
||
|
|
|
||
|
|
handleReload = () => {
|
||
|
|
window.location.reload();
|
||
|
|
};
|
||
|
|
|
||
|
|
handleReset = () => {
|
||
|
|
try {
|
||
|
|
localStorage.clear();
|
||
|
|
sessionStorage.clear();
|
||
|
|
} catch {}
|
||
|
|
window.location.reload();
|
||
|
|
};
|
||
|
|
|
||
|
|
render() {
|
||
|
|
if (this.state.hasError) {
|
||
|
|
return (
|
||
|
|
<div className="h-screen flex flex-col items-center justify-center gap-6 bg-editor-bg px-6">
|
||
|
|
<div className="flex flex-col items-center gap-3 max-w-md text-center">
|
||
|
|
<div className="w-12 h-12 rounded-full bg-red-500/20 flex items-center justify-center">
|
||
|
|
<svg className="w-6 h-6 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
<h2 className="text-lg font-semibold text-editor-text">Something went wrong</h2>
|
||
|
|
<p className="text-xs text-editor-text-muted leading-relaxed">
|
||
|
|
An unexpected error occurred. Your work may still be recoverable.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{this.state.error && (
|
||
|
|
<details className="max-w-md w-full">
|
||
|
|
<summary className="text-xs text-editor-text-muted cursor-pointer hover:text-editor-text">
|
||
|
|
Error details
|
||
|
|
</summary>
|
||
|
|
<pre className="mt-2 p-3 rounded bg-editor-surface border border-editor-border text-[10px] text-red-300 overflow-auto max-h-32 whitespace-pre-wrap">
|
||
|
|
{this.state.error.message}
|
||
|
|
{'\n'}
|
||
|
|
{this.state.error.stack}
|
||
|
|
</pre>
|
||
|
|
</details>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="flex flex-col items-center gap-2">
|
||
|
|
<button
|
||
|
|
onClick={this.handleReload}
|
||
|
|
className="px-4 py-2 bg-editor-accent hover:bg-editor-accent-hover rounded-lg text-sm font-medium transition-colors"
|
||
|
|
>
|
||
|
|
Reload App
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={this.handleReset}
|
||
|
|
className="text-xs text-editor-text-muted hover:text-editor-text underline transition-colors"
|
||
|
|
>
|
||
|
|
Reset & Clear All Data
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return this.props.children;
|
||
|
|
}
|
||
|
|
}
|