Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions app/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use client';

import { useEffect } from 'react';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { AlertTriangle, Home, RotateCcw } from 'lucide-react';
import Link from 'next/link';
import { useTranslation } from 'react-i18next';

/**
* Error page for handling runtime errors in the application
*
* This is Next.js App Router's error.tsx convention that creates
* an error boundary for route segments.
*
* Features:
* - Catches errors at the route segment level
* - Provides reset functionality to recover
* - User-friendly UI matching app design
* - Development mode error details
*/
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const { t } = useTranslation();

useEffect(() => {
// Log error to console in development
if (process.env.NODE_ENV === 'development') {
console.error('Error caught by error.tsx:', error);
}
}, [error]);

return (
<div className="flex min-h-screen items-center justify-center bg-muted/30 p-4">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<div className="mx-auto mb-4 flex h-20 w-20 items-center justify-center rounded-full bg-destructive/10">
<AlertTriangle className="h-10 w-10 text-destructive" />
</div>
<CardTitle className="text-2xl font-bold">{t('errors.boundary.title')}</CardTitle>
<CardDescription className="text-base">
{t('errors.boundary.description')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4 text-center">
{process.env.NODE_ENV === 'development' && (
<div className="rounded-lg bg-muted p-4 text-left">
<p className="text-xs font-mono text-muted-foreground break-all">
{error.message}
</p>
{error.digest && (
<p className="text-xs font-mono text-muted-foreground mt-2">
Digest: {error.digest}
</p>
)}
</div>
)}
</CardContent>
<CardFooter className="flex flex-col gap-2 sm:flex-row">
<Button onClick={() => reset()} variant="outline" className="flex-1 w-full sm:w-auto">
<RotateCcw className="mr-2 h-4 w-4" />
{t('errors.boundary.tryAgain')}
</Button>
<Button asChild className="flex-1 w-full sm:w-auto">
<Link href="/dashboard">
<Home className="mr-2 h-4 w-4" />
{t('errors.boundary.goHome')}
</Link>
</Button>
</CardFooter>
</Card>
</div>
);
}
146 changes: 146 additions & 0 deletions app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
'use client';

import { AlertTriangle, Home, RotateCcw } from 'lucide-react';

/**
* Global Error page for handling critical errors in the root layout
*
* This is Next.js App Router's global-error.tsx convention that acts as
* a catch-all error boundary for the entire application, including the
* root layout.
*
* Important: This component must include its own <html> and <body> tags
* because it replaces the root layout when activated.
*
* Note: Cannot use providers/i18n here since they may have failed.
* Uses inline styles as a fallback.
*/
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html lang="en">
<body style={{
margin: 0,
fontFamily: 'system-ui, -apple-system, sans-serif',
backgroundColor: '#f5f5f5',
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '1rem'
}}>
<div style={{
backgroundColor: 'white',
borderRadius: '12px',
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
maxWidth: '400px',
width: '100%',
padding: '2rem',
textAlign: 'center'
}}>
<div style={{
width: '80px',
height: '80px',
borderRadius: '50%',
backgroundColor: '#fee2e2',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto 1.5rem'
}}>
<AlertTriangle style={{ width: '40px', height: '40px', color: '#dc2626' }} />
</div>

<h1 style={{
fontSize: '1.5rem',
fontWeight: 'bold',
color: '#111827',
marginBottom: '0.5rem'
}}>
Something went wrong
</h1>

<p style={{
color: '#6b7280',
marginBottom: '1.5rem',
fontSize: '0.95rem'
}}>
We're sorry, but something unexpected happened. Please try again.
</p>

{process.env.NODE_ENV === 'development' && (
<div style={{
backgroundColor: '#f3f4f6',
borderRadius: '8px',
padding: '1rem',
marginBottom: '1.5rem',
textAlign: 'left'
}}>
<p style={{
fontFamily: 'monospace',
fontSize: '0.75rem',
color: '#6b7280',
wordBreak: 'break-all'
}}>
{error.message}
</p>
</div>
)}

<div style={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem'
}}>
<button
onClick={() => reset()}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '0.5rem',
padding: '0.75rem 1rem',
backgroundColor: 'white',
border: '1px solid #d1d5db',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '0.875rem',
fontWeight: 500,
color: '#374151'
}}
>
<RotateCcw style={{ width: '16px', height: '16px' }} />
Try Again
</button>
<a
href="/dashboard"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '0.5rem',
padding: '0.75rem 1rem',
backgroundColor: '#3b82f6',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '0.875rem',
fontWeight: 500,
color: 'white',
textDecoration: 'none'
}}
>
<Home style={{ width: '16px', height: '16px' }} />
Go to Dashboard
</a>
</div>
</div>
</body>
</html>
);
}
125 changes: 125 additions & 0 deletions components/errors/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
'use client';

import React, { Component, ReactNode } from 'react';
import { ErrorFallback } from './ErrorFallback';

/**
* Props for the ErrorBoundary component
*/
interface ErrorBoundaryProps {
/** Child components to render */
children: ReactNode;
/** Optional custom fallback component */
fallback?: ReactNode;
/** Callback when an error is caught */
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
/** Callback when reset is triggered */
onReset?: () => void;
}

/**
* State for the ErrorBoundary component
*/
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}

/**
* ErrorBoundary Component
*
* A React Error Boundary that catches JavaScript errors in child components
* and displays a fallback UI instead of crashing the application.
*
* Features:
* - Catches errors anywhere in the child component tree
* - Displays user-friendly fallback UI
* - Provides reset functionality to recover from errors
* - Logs errors in development mode
* - Supports custom fallback components
*
* @example
* ```tsx
* <ErrorBoundary onError={(error) => console.error(error)}>
* <MyComponent />
* </ErrorBoundary>
* ```
*
* @example With custom fallback
* ```tsx
* <ErrorBoundary fallback={<CustomErrorUI />}>
* <MyComponent />
* </ErrorBoundary>
* ```
*/
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}

static getDerivedStateFromError(error: Error): ErrorBoundaryState {
// Update state so the next render will show the fallback UI
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
// Log error in development mode
if (process.env.NODE_ENV === 'development') {
console.error('ErrorBoundary caught an error:', error);
console.error('Error info:', errorInfo);
}

// Call optional onError callback
this.props.onError?.(error, errorInfo);
}

resetErrorBoundary = (): void => {
this.props.onReset?.();
this.setState({ hasError: false, error: null });
};

render(): ReactNode {
if (this.state.hasError) {
// Render custom fallback if provided
if (this.props.fallback) {
return this.props.fallback;
}

// Render default ErrorFallback
return (
<ErrorFallback
error={this.state.error ?? undefined}
resetErrorBoundary={this.resetErrorBoundary}
/>
);
}

return this.props.children;
}
}

/**
* Higher-order component that wraps a component with an ErrorBoundary
*
* @example
* ```tsx
* const SafeComponent = withErrorBoundary(MyComponent);
* ```
*/
export function withErrorBoundary<P extends object>(
WrappedComponent: React.ComponentType<P>,
errorBoundaryProps?: Omit<ErrorBoundaryProps, 'children'>
): React.FC<P> {
const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';

const ComponentWithErrorBoundary: React.FC<P> = (props) => (
<ErrorBoundary {...errorBoundaryProps}>
<WrappedComponent {...props} />
</ErrorBoundary>
);

ComponentWithErrorBoundary.displayName = `withErrorBoundary(${displayName})`;

return ComponentWithErrorBoundary;
}
Loading