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 (
+
{
+ 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 && (
+
+ )}
+
+
+ {resetErrorBoundary && (
+
+
+ {t('errors.boundary.tryAgain')}
+
+ )}
+
+
+
+ {t('errors.boundary.goHome')}
+
+
+
+
+
+ );
+}
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';
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/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' && }
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",
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();
+ }
+}