← back to Grant

components/ErrorBoundary.tsx

127 lines

'use client';

import { Component, ErrorInfo, ReactNode } from 'react';
import { AlertTriangle, RefreshCw } from 'lucide-react';

interface ErrorBoundaryProps {
  children: ReactNode;
}

interface ErrorBoundaryState {
  hasError: boolean;
  error: Error | null;
}

export default class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    // Log error for debugging -- could be sent to an error reporting service
    if (typeof window !== 'undefined') {
      console.error('ErrorBoundary caught:', error, errorInfo);
    }
  }

  handleReload = () => {
    window.location.reload();
  };

  handleReset = () => {
    this.setState({ hasError: false, error: null });
  };

  render() {
    if (this.state.hasError) {
      return (
        <div
          className="flex flex-col items-center justify-center p-8 text-center"
          style={{
            minHeight: '60vh',
            backgroundColor: 'var(--color-bg)',
          }}
        >
          <div
            className="w-16 h-16 rounded-2xl flex items-center justify-center mb-6"
            style={{
              backgroundColor: 'rgba(239, 68, 68, 0.12)',
              border: '1px solid rgba(239, 68, 68, 0.25)',
            }}
          >
            <AlertTriangle size={28} style={{ color: '#f87171' }} />
          </div>

          <h2
            className="text-lg font-bold mb-2"
            style={{ color: 'var(--color-text)' }}
          >
            Something went wrong
          </h2>
          <p
            className="text-sm max-w-md mb-6 leading-relaxed"
            style={{ color: 'var(--color-text-muted)' }}
          >
            An unexpected error occurred while rendering this section.
            You can try reloading the page or resetting the view.
          </p>

          {/* Error details (collapsed by default for dev) */}
          {this.state.error && (
            <details
              className="mb-6 w-full max-w-lg text-left"
              style={{
                backgroundColor: 'var(--color-surface)',
                border: '1px solid var(--color-border)',
                borderRadius: 10,
                padding: '12px 16px',
              }}
            >
              <summary
                className="text-xs font-medium cursor-pointer"
                style={{ color: 'var(--color-text-secondary)' }}
              >
                Error details
              </summary>
              <pre
                className="mt-2 text-xs overflow-auto"
                style={{
                  color: '#f87171',
                  whiteSpace: 'pre-wrap',
                  wordBreak: 'break-word',
                  maxHeight: 160,
                }}
              >
                {this.state.error.message}
              </pre>
            </details>
          )}

          <div className="flex gap-3">
            <button
              className="btn btn-secondary"
              onClick={this.handleReset}
            >
              Try Again
            </button>
            <button
              className="btn btn-primary"
              onClick={this.handleReload}
            >
              <RefreshCw size={15} />
              Reload Page
            </button>
          </div>
        </div>
      );
    }

    return this.props.children;
  }
}