From 7d3a3dd2ffb8f13d7d76194d2c894fffce729790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brayan=20Steven=20Mar=C3=ADn=20Quir=C3=B3s?= <49928451+BrayanMQ@users.noreply.github.com> Date: Sun, 11 Jan 2026 10:58:39 -0600 Subject: [PATCH 1/4] feat: add ErrorBoundary and ErrorFallback components for improved error handling - Introduced ErrorBoundary component to catch JavaScript errors in child components and display a fallback UI. - Added ErrorFallback component for user-friendly error display, featuring a clean design and options to reset the error state or navigate to the dashboard. - Implemented higher-order component `withErrorBoundary` to wrap components with error handling capabilities. - Updated index file to export the new components for easy access. --- components/errors/ErrorBoundary.tsx | 125 ++++++++++++++++++++++++++++ components/errors/ErrorFallback.tsx | 94 +++++++++++++++++++++ components/errors/index.ts | 2 + 3 files changed, 221 insertions(+) create mode 100644 components/errors/ErrorBoundary.tsx create mode 100644 components/errors/ErrorFallback.tsx create mode 100644 components/errors/index.ts diff --git a/components/errors/ErrorBoundary.tsx b/components/errors/ErrorBoundary.tsx new file mode 100644 index 0000000..d964429 --- /dev/null +++ b/components/errors/ErrorBoundary.tsx @@ -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 + * console.error(error)}> + * + * + * ``` + * + * @example With custom fallback + * ```tsx + * }> + * + * + * ``` + */ +export class ErrorBoundary extends Component { + 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 ( + + ); + } + + return this.props.children; + } +} + +/** + * Higher-order component that wraps a component with an ErrorBoundary + * + * @example + * ```tsx + * const SafeComponent = withErrorBoundary(MyComponent); + * ``` + */ +export function withErrorBoundary

( + WrappedComponent: React.ComponentType

, + errorBoundaryProps?: Omit +): React.FC

{ + const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; + + const ComponentWithErrorBoundary: React.FC

= (props) => ( + + + + ); + + ComponentWithErrorBoundary.displayName = `withErrorBoundary(${displayName})`; + + return ComponentWithErrorBoundary; +} diff --git a/components/errors/ErrorFallback.tsx b/components/errors/ErrorFallback.tsx new file mode 100644 index 0000000..8cbdbc5 --- /dev/null +++ b/components/errors/ErrorFallback.tsx @@ -0,0 +1,94 @@ +'use client'; + +import React 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'; + +/** + * Props for the ErrorFallback component + */ +interface ErrorFallbackProps { + /** The error that was caught */ + error?: Error; + /** Function to reset the error boundary */ + resetErrorBoundary?: () => void; + /** Custom title for the error message */ + title?: string; + /** Custom description for the error message */ + description?: string; +} + +/** + * ErrorFallback Component + * + * A user-friendly error display component that matches the app's design system. + * Used as the fallback UI for error boundaries and error pages. + * + * Features: + * - Clean, card-based design matching the app style + * - "Try Again" button to reset the error state + * - "Go to Dashboard" button for navigation + * - i18n support for translations + * - Development mode error details + * + * @example + * ```tsx + * reset()} + * /> + * ``` + */ +export function ErrorFallback({ + error, + resetErrorBoundary, + title, + description +}: ErrorFallbackProps) { + const { t } = useTranslation(); + + const displayTitle = title || t('errors.boundary.title'); + const displayDescription = description || t('errors.boundary.description'); + + return ( +

+ + +
+ +
+ {displayTitle} + + {displayDescription} + +
+ + {process.env.NODE_ENV === 'development' && error && ( +
+

+ {error.message} +

+
+ )} +
+ + {resetErrorBoundary && ( + + )} + + +
+
+ ); +} diff --git a/components/errors/index.ts b/components/errors/index.ts new file mode 100644 index 0000000..6827cbd --- /dev/null +++ b/components/errors/index.ts @@ -0,0 +1,2 @@ +export { ErrorBoundary, withErrorBoundary } from './ErrorBoundary'; +export { ErrorFallback } from './ErrorFallback'; From 838bdf42c6c389d1f62b518ded7ca1afe3b21522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brayan=20Steven=20Mar=C3=ADn=20Quir=C3=B3s?= <49928451+BrayanMQ@users.noreply.github.com> Date: Sun, 11 Jan 2026 10:58:58 -0600 Subject: [PATCH 2/4] feat: implement centralized error handling with Error and GlobalError components - Added Error component for handling runtime errors at the route segment level, featuring a user-friendly UI and reset functionality. - Introduced GlobalError component as a catch-all error boundary for critical errors in the root layout, ensuring a consistent user experience across the application. - Created a centralized errors utility module for structured error handling, including functions for categorizing and logging errors, as well as parsing Supabase errors into user-friendly messages. --- app/error.tsx | 79 +++++++++++++++++ app/global-error.tsx | 146 +++++++++++++++++++++++++++++++ lib/errors.ts | 199 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 424 insertions(+) create mode 100644 app/error.tsx create mode 100644 app/global-error.tsx create mode 100644 lib/errors.ts diff --git a/app/error.tsx b/app/error.tsx new file mode 100644 index 0000000..c415afe --- /dev/null +++ b/app/error.tsx @@ -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 ( +
+ + +
+ +
+ {t('errors.boundary.title')} + + {t('errors.boundary.description')} + +
+ + {process.env.NODE_ENV === 'development' && ( +
+

+ {error.message} +

+ {error.digest && ( +

+ Digest: {error.digest} +

+ )} +
+ )} +
+ + + + +
+
+ ); +} diff --git a/app/global-error.tsx b/app/global-error.tsx new file mode 100644 index 0000000..88e1312 --- /dev/null +++ b/app/global-error.tsx @@ -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 and 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 ( + + +
+
+ +
+ +

+ Something went wrong +

+ +

+ We're sorry, but something unexpected happened. Please try again. +

+ + {process.env.NODE_ENV === 'development' && ( +
+

+ {error.message} +

+
+ )} + +
+ + + + Go to Dashboard + +
+
+ + + ); +} diff --git a/lib/errors.ts b/lib/errors.ts new file mode 100644 index 0000000..08cbb13 --- /dev/null +++ b/lib/errors.ts @@ -0,0 +1,199 @@ +/** + * Centralized Error Handling Utilities + * + * This module provides utilities for consistent error handling across the application. + */ + +import type { PostgrestError } from '@supabase/supabase-js'; + +/** + * Error types for categorization + */ +export type ErrorType = + | 'network' + | 'auth' + | 'validation' + | 'server' + | 'unknown'; + +/** + * Structured app error + */ +export interface AppError { + type: ErrorType; + message: string; + originalError?: unknown; +} + +/** + * Check if an error is a network-related error + * + * @param error - The error to check + * @returns true if the error is network-related + */ +export function isNetworkError(error: unknown): boolean { + if (error instanceof TypeError) { + const message = error.message.toLowerCase(); + return ( + message.includes('failed to fetch') || + message.includes('network request failed') || + message.includes('networkerror') + ); + } + + if (error && typeof error === 'object' && 'status' in error) { + const status = (error as { status: number }).status; + return status === 0 || status === 503; + } + + return false; +} + +/** + * Check if the browser is currently offline + * + * @returns true if the browser is offline + */ +export function isOffline(): boolean { + if (typeof navigator !== 'undefined') { + return !navigator.onLine; + } + return false; +} + +/** + * Parse a Supabase error into a user-friendly message + * + * @param error - PostgrestError from Supabase + * @returns User-friendly error message key for i18n + */ +export function parseSupabaseError(error: PostgrestError): string { + const { code, message } = error; + + // Authentication-related codes + if (code === 'PGRST301' || message.includes('JWT')) { + return 'auth.errors.sessionExpired'; + } + + // Row-level security violations + if (code === '42501') { + return 'auth.errors.unauthorized'; + } + + // Not found + if (code === 'PGRST116') { + return 'errors.notFound'; + } + + // Rate limiting + if (code === '429') { + return 'errors.tooManyRequests'; + } + + // Generic server error + if (code?.startsWith('5')) { + return 'errors.serverError.description'; + } + + return 'errors.boundary.description'; +} + +/** + * Extract a message from an unknown error type + * + * @param error - Any error type + * @returns The error message string + */ +export function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + + if (typeof error === 'string') { + return error; + } + + if (error && typeof error === 'object' && 'message' in error) { + return String((error as { message: unknown }).message); + } + + return 'An unexpected error occurred'; +} + +/** + * Create a structured AppError from an unknown error + * + * @param error - Any error type + * @returns Structured AppError object + */ +export function createAppError(error: unknown): AppError { + const message = getErrorMessage(error); + + if (isNetworkError(error) || isOffline()) { + return { + type: 'network', + message: 'Network error. Please check your connection.', + originalError: error, + }; + } + + if (error instanceof Error) { + // Check for auth-related errors + if (message.toLowerCase().includes('auth') || + message.toLowerCase().includes('unauthorized') || + message.toLowerCase().includes('unauthenticated')) { + return { + type: 'auth', + message: 'Authentication error. Please sign in again.', + originalError: error, + }; + } + } + + return { + type: 'unknown', + message, + originalError: error, + }; +} + +/** + * Log an error in development mode only + * + * @param context - Context string describing where the error occurred + * @param error - The error to log + */ +export function logError(context: string, error: unknown): void { + if (process.env.NODE_ENV === 'development') { + console.error(`[${context}]`, error); + + if (error instanceof Error && error.stack) { + console.error('Stack trace:', error.stack); + } + } +} + +/** + * Log with additional metadata for better debugging + * + * @param context - Context string describing where the error occurred + * @param error - The error to log + * @param metadata - Additional metadata to log + */ +export function logErrorWithMetadata( + context: string, + error: unknown, + metadata?: Record +): void { + if (process.env.NODE_ENV === 'development') { + console.group(`[Error: ${context}]`); + console.error('Error:', error); + if (metadata) { + console.error('Metadata:', metadata); + } + if (error instanceof Error && error.stack) { + console.error('Stack:', error.stack); + } + console.groupEnd(); + } +} From 7d22509480054498f09dbcbf522cf3217222368f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brayan=20Steven=20Mar=C3=ADn=20Quir=C3=B3s?= <49928451+BrayanMQ@users.noreply.github.com> Date: Sun, 11 Jan 2026 10:59:14 -0600 Subject: [PATCH 3/4] feat: add NetworkStatusProvider for online/offline monitoring - Introduced NetworkStatusProvider component to monitor the browser's online/offline status and provide context to child components. - Implemented useNetworkStatus hook for easy access to network status. - Added toast notifications for network status changes, enhancing user experience. - Updated English and Spanish localization files with network-related error messages. --- .../providers/NetworkStatusProvider.tsx | 108 ++++++++++++++++++ i18n/locales/en.json | 20 ++++ i18n/locales/es.json | 20 ++++ 3 files changed, 148 insertions(+) create mode 100644 components/providers/NetworkStatusProvider.tsx diff --git a/components/providers/NetworkStatusProvider.tsx b/components/providers/NetworkStatusProvider.tsx new file mode 100644 index 0000000..e9b4334 --- /dev/null +++ b/components/providers/NetworkStatusProvider.tsx @@ -0,0 +1,108 @@ +'use client'; + +import React, { createContext, useContext, useEffect, useState, useCallback, ReactNode } from 'react'; +import { toast } from 'sonner'; +import { useTranslation } from 'react-i18next'; + +/** + * Network status context value + */ +interface NetworkStatusContextValue { + /** Whether the browser is currently online */ + isOnline: boolean; + /** Whether the network status has been detected */ + isInitialized: boolean; +} + +const NetworkStatusContext = createContext({ + isOnline: true, + isInitialized: false, +}); + +/** + * Hook to access network status + * + * @returns Network status context value + * + * @example + * ```tsx + * function MyComponent() { + * const { isOnline } = useNetworkStatus(); + * + * if (!isOnline) { + * return
You're offline
; + * } + * + * return
Online content
; + * } + * ``` + */ +export function useNetworkStatus(): NetworkStatusContextValue { + return useContext(NetworkStatusContext); +} + +interface NetworkStatusProviderProps { + children: ReactNode; +} + +/** + * NetworkStatusProvider Component + * + * Monitors the browser's online/offline status and provides it to child components. + * Also shows toast notifications when the connection status changes. + * + * Features: + * - Detects online/offline status changes + * - Shows user-friendly toast notifications + * - Provides useNetworkStatus hook for components + * + * @example + * ```tsx + * // In your providers + * + * + * + * ``` + */ +export function NetworkStatusProvider({ children }: NetworkStatusProviderProps) { + const [isOnline, setIsOnline] = useState(true); + const [isInitialized, setIsInitialized] = useState(false); + const { t } = useTranslation(); + + const handleOnline = useCallback(() => { + setIsOnline(true); + toast.success(t('errors.network.online'), { + description: t('errors.network.onlineDesc'), + }); + }, [t]); + + const handleOffline = useCallback(() => { + setIsOnline(false); + toast.error(t('errors.network.offline'), { + description: t('errors.network.offlineDesc'), + }); + }, [t]); + + useEffect(() => { + // Set initial state based on navigator.onLine + if (typeof window !== 'undefined') { + setIsOnline(navigator.onLine); + setIsInitialized(true); + + // Add event listeners + window.addEventListener('online', handleOnline); + window.addEventListener('offline', handleOffline); + + return () => { + window.removeEventListener('online', handleOnline); + window.removeEventListener('offline', handleOffline); + }; + } + }, [handleOnline, handleOffline]); + + return ( + + {children} + + ); +} diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 72371ef..16c1ea6 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -26,6 +26,26 @@ "network": "Network error. Please check your connection and try again." } }, + "errors": { + "boundary": { + "title": "Something went wrong", + "description": "We're sorry, but something unexpected happened.", + "tryAgain": "Try Again", + "goHome": "Go to Dashboard" + }, + "serverError": { + "title": "Server Error", + "description": "We're having trouble connecting. Please try again later." + }, + "network": { + "offline": "You're offline", + "offlineDesc": "Please check your internet connection.", + "online": "You're back online", + "onlineDesc": "Connection restored." + }, + "notFound": "The requested resource was not found.", + "tooManyRequests": "Too many requests. Please wait a moment and try again." + }, "dashboard": { "loading": "Loading habits...", "errorTitle": "Error loading habits", diff --git a/i18n/locales/es.json b/i18n/locales/es.json index 54cd2ca..655bd89 100644 --- a/i18n/locales/es.json +++ b/i18n/locales/es.json @@ -26,6 +26,26 @@ "network": "Error de red. Por favor revisa tu conexión e inténtalo de nuevo." } }, + "errors": { + "boundary": { + "title": "Algo salió mal", + "description": "Lo sentimos, pero ocurrió algo inesperado.", + "tryAgain": "Intentar de nuevo", + "goHome": "Ir al Dashboard" + }, + "serverError": { + "title": "Error del servidor", + "description": "Tenemos problemas para conectar. Por favor, inténtalo más tarde." + }, + "network": { + "offline": "Estás sin conexión", + "offlineDesc": "Por favor, revisa tu conexión a internet.", + "online": "Estás conectado de nuevo", + "onlineDesc": "Conexión restaurada." + }, + "notFound": "El recurso solicitado no fue encontrado.", + "tooManyRequests": "Demasiadas solicitudes. Por favor, espera un momento e inténtalo de nuevo." + }, "dashboard": { "loading": "Cargando hábitos...", "errorTitle": "Error al cargar hábitos", From c12f179720bf67c3d96454987994b38d68701db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brayan=20Steven=20Mar=C3=ADn=20Quir=C3=B3s?= <49928451+BrayanMQ@users.noreply.github.com> Date: Sun, 11 Jan 2026 10:59:48 -0600 Subject: [PATCH 4/4] feat: wrap children with NetworkStatusProvider in Providers component - Updated the Providers component to include NetworkStatusProvider, enhancing the app's ability to monitor online/offline status for child components. --- components/providers/Providers.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/providers/Providers.tsx b/components/providers/Providers.tsx index d2b7251..f84c5f7 100644 --- a/components/providers/Providers.tsx +++ b/components/providers/Providers.tsx @@ -8,6 +8,7 @@ import { Toaster } from '@/components/ui/sonner'; import { ServiceWorkerProvider } from './ServiceWorkerProvider'; import { ThemeSync } from './ThemeSync'; import { TopLoadingBar } from './TopLoadingBar'; +import { NetworkStatusProvider } from './NetworkStatusProvider'; /** * Providers component that wraps the app with React Query @@ -65,7 +66,9 @@ export function Providers({ children }: { children: React.ReactNode }) { - {children} + + {children} + {/* Only show devtools in development */} {process.env.NODE_ENV === 'development' && }