UNPKG

@ai-growth/nextjs

Version:

Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering

244 lines (243 loc) 12.1 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import React, { Component } from 'react'; // ============================================================================ // BASE ERROR BOUNDARY COMPONENT // ============================================================================ /** * Base ErrorBoundary component that catches JavaScript errors anywhere in the child component tree * and displays a fallback UI instead of crashing the entire application. * * @example * ```tsx * <ErrorBoundary * fallback={<ErrorFallback />} * onError={(errorDetails) => logError(errorDetails)} * enableRetry={true} * > * <MyComponent /> * </ErrorBoundary> * ``` */ export class ErrorBoundary extends Component { constructor(props) { super(props); this.retryTimeoutId = null; this.handleRetry = () => { const { maxRetries = 3 } = this.props; if (this.state.retryCount < maxRetries) { this.setState(prevState => ({ hasError: false, error: null, errorInfo: null, retryCount: prevState.retryCount + 1, })); } }; this.handleReset = () => { this.setState({ hasError: false, error: null, errorInfo: null, retryCount: 0, errorId: '', }); }; this.state = { hasError: false, error: null, errorInfo: null, retryCount: 0, errorId: '', }; } static getDerivedStateFromError(error) { const errorId = `error-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; return { hasError: true, error, errorId, }; } componentDidCatch(error, errorInfo) { const userAgent = typeof window !== 'undefined' ? window.navigator.userAgent : undefined; const url = typeof window !== 'undefined' ? window.location.href : undefined; const errorDetails = { error, errorInfo, timestamp: new Date(), }; if (userAgent) errorDetails.userAgent = userAgent; if (url) errorDetails.url = url; if (this.props.context) errorDetails.additionalContext = this.props.context; // Update state with error info this.setState({ errorInfo }); // Call error handler if provided if (this.props.onError) { this.props.onError(errorDetails); } // Log error to console in development if (process.env.NODE_ENV === 'development') { console.group('🚨 Error Boundary Caught Error'); console.error('Error:', error); console.error('Error Info:', errorInfo); console.error('Component Stack:', errorInfo.componentStack); console.error('Error Boundary Props:', this.props); console.groupEnd(); } } componentWillUnmount() { if (this.retryTimeoutId) { clearTimeout(this.retryTimeoutId); } } render() { const { children, fallback, enableRetry = false, maxRetries = 3, className, showErrorDetails = process.env.NODE_ENV === 'development' } = this.props; const { hasError, error, errorInfo, retryCount } = this.state; if (hasError && error) { const errorDetails = { error, errorInfo: errorInfo, timestamp: new Date(), }; if (typeof window !== 'undefined' && window.navigator.userAgent) { errorDetails.userAgent = window.navigator.userAgent; } if (typeof window !== 'undefined' && window.location.href) { errorDetails.url = window.location.href; } if (this.props.context) { errorDetails.additionalContext = this.props.context; } // Render custom fallback if provided if (fallback) { const fallbackElement = typeof fallback === 'function' ? fallback(errorDetails) : fallback; return (_jsx("div", { className: className, children: fallbackElement })); } // Render default error UI return (_jsxs("div", { className: className, style: { padding: '2rem', margin: '1rem', border: '1px solid #e53e3e', borderRadius: '8px', backgroundColor: '#fed7d7', color: '#742a2a', fontFamily: 'system-ui, sans-serif', }, children: [_jsxs("div", { style: { marginBottom: '1rem' }, children: [_jsx("h2", { style: { margin: '0 0 0.5rem 0', fontSize: '1.25rem', fontWeight: 'bold' }, children: "\u26A0\uFE0F Something went wrong" }), _jsx("p", { style: { margin: '0', opacity: 0.8 }, children: "We encountered an error while rendering this content." })] }), showErrorDetails && (_jsxs("details", { style: { marginBottom: '1rem' }, children: [_jsx("summary", { style: { cursor: 'pointer', fontWeight: 'bold' }, children: "Error Details" }), _jsx("pre", { style: { background: '#fff', padding: '0.5rem', border: '1px solid #ccc', borderRadius: '4px', fontSize: '0.875rem', overflow: 'auto', margin: '0.5rem 0', }, children: error.message }), errorInfo && (_jsx("pre", { style: { background: '#fff', padding: '0.5rem', border: '1px solid #ccc', borderRadius: '4px', fontSize: '0.75rem', overflow: 'auto', margin: '0.5rem 0', }, children: errorInfo.componentStack }))] })), _jsxs("div", { style: { display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }, children: [enableRetry && retryCount < maxRetries && (_jsxs("button", { onClick: this.handleRetry, style: { padding: '0.5rem 1rem', background: '#3182ce', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontWeight: 'bold', }, children: ["Retry (", maxRetries - retryCount, " attempts left)"] })), _jsx("button", { onClick: this.handleReset, style: { padding: '0.5rem 1rem', background: '#718096', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', }, children: "Reset" }), _jsx("button", { onClick: () => window.location.reload(), style: { padding: '0.5rem 1rem', background: '#38a169', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', }, children: "Reload Page" })] }), retryCount > 0 && (_jsxs("p", { style: { margin: '1rem 0 0 0', fontSize: '0.875rem', opacity: 0.7 }, children: ["Retry attempts: ", retryCount, "/", maxRetries] }))] })); } return children; } } export const CmsErrorBoundary = ({ children, fallback, contentSlug, contentType, context = {}, ...props }) => { const cmsContext = { ...context, contentSlug, contentType, boundaryType: 'cms', }; const cmsFallback = fallback || ((_errorDetails) => (_jsxs("div", { style: { padding: '2rem', textAlign: 'center', border: '1px dashed #e2e8f0', borderRadius: '8px', backgroundColor: '#f7fafc', color: '#4a5568', }, children: [_jsx("h3", { style: { margin: '0 0 1rem 0' }, children: "Content Unavailable" }), _jsx("p", { style: { margin: '0 0 1rem 0' }, children: "We couldn't load the requested content. This might be a temporary issue." }), contentSlug && (_jsxs("p", { style: { fontSize: '0.875rem', opacity: 0.7, margin: '0' }, children: ["Content: ", contentSlug] }))] }))); return (_jsx(ErrorBoundary, { ...props, fallback: cmsFallback, context: cmsContext, errorLevel: "error", children: children })); }; export const ApiErrorBoundary = ({ children, fallback, endpoint, operation, context = {}, ...props }) => { const apiContext = { ...context, endpoint, operation, boundaryType: 'api', }; const apiFallback = fallback || ((_errorDetails) => (_jsxs("div", { style: { padding: '1.5rem', border: '1px solid #fed7d7', borderRadius: '6px', backgroundColor: '#fffaf0', color: '#c53030', }, children: [_jsx("h4", { style: { margin: '0 0 0.5rem 0' }, children: "\u26A0\uFE0F Service Unavailable" }), _jsx("p", { style: { margin: '0', fontSize: '0.875rem' }, children: "We're having trouble connecting to our services. Please try again in a moment." })] }))); return (_jsx(ErrorBoundary, { ...props, fallback: apiFallback, context: apiContext, errorLevel: "warning", enableRetry: true, maxRetries: 2, children: children })); }; export const TemplateErrorBoundary = ({ children, fallback, templateName, contentType, context = {}, ...props }) => { const templateContext = { ...context, templateName, contentType, boundaryType: 'template', }; const templateFallback = fallback || ((_errorDetails) => (_jsxs("div", { style: { padding: '2rem', border: '2px dashed #e2e8f0', borderRadius: '8px', backgroundColor: '#f8f9fa', color: '#6c757d', textAlign: 'center', }, children: [_jsx("h3", { style: { margin: '0 0 1rem 0' }, children: "Template Error" }), _jsx("p", { style: { margin: '0 0 1rem 0' }, children: "There was an issue rendering this template. A fallback template will be used." }), templateName && (_jsxs("p", { style: { fontSize: '0.875rem', opacity: 0.7, margin: '0' }, children: ["Template: ", templateName] }))] }))); return (_jsx(ErrorBoundary, { ...props, fallback: templateFallback, context: templateContext, errorLevel: "warning", enableRetry: false, children: children })); }; // ============================================================================ // UTILITY COMPONENTS AND HOOKS // ============================================================================ /** * Higher-order component that wraps a component with an error boundary */ export function withErrorBoundary(Component, errorBoundaryProps) { const WrappedComponent = (props) => (_jsx(ErrorBoundary, { ...errorBoundaryProps, children: _jsx(Component, { ...props }) })); WrappedComponent.displayName = `withErrorBoundary(${Component.displayName || Component.name})`; return WrappedComponent; } /** * Hook to throw an error for testing error boundaries */ export function useErrorThrower() { return React.useCallback((error) => { const errorToThrow = typeof error === 'string' ? new Error(error) : error; throw errorToThrow; }, []); } export default ErrorBoundary;