@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
JavaScript
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;