diff --git a/.Jules/changelog.md b/.Jules/changelog.md index d438210..2f96aa0 100644 --- a/.Jules/changelog.md +++ b/.Jules/changelog.md @@ -7,6 +7,7 @@ ## [Unreleased] ### Added +- **Global Error Boundary**: Implemented a dual-theme compatible error boundary (`web/components/ErrorBoundary.tsx`) to catch app crashes gracefully and provide a "Try Again" mechanism. - Inline form validation in Auth page with real-time feedback and proper ARIA accessibility support (`aria-invalid`, `aria-describedby`, `role="alert"`). - Dashboard skeleton loading state (`DashboardSkeleton`) to improve perceived performance during data fetch. - Comprehensive `EmptyState` component for Groups and Friends pages to better guide new users. diff --git a/.Jules/knowledge.md b/.Jules/knowledge.md index d69c659..8ad8c57 100644 --- a/.Jules/knowledge.md +++ b/.Jules/knowledge.md @@ -336,6 +336,13 @@ Common patterns: Use `AnimatePresence` for exit animations. +### Testing React Error Boundaries +**Date:** 2026-01-14 +**Context:** Verifying global Error Boundary + +React Error Boundaries **do not catch errors inside event handlers**. They only catch errors during the render phase, in lifecycle methods, and in constructors of the whole tree below them. +To test an Error Boundary, you must throw an error *during render* (e.g., inside the component body based on state), not inside an `onClick` handler. + --- ## Best Practices Learned diff --git a/.Jules/todo.md b/.Jules/todo.md index 894e27f..0ca3ab7 100644 --- a/.Jules/todo.md +++ b/.Jules/todo.md @@ -34,8 +34,9 @@ - Impact: Guides new users, makes app feel polished - Size: ~70 lines -- [ ] **[ux]** Error boundary with retry for API failures - - Files: Create `web/components/ErrorBoundary.tsx`, wrap app +- [x] **[ux]** Error boundary with retry for API failures + - Completed: 2026-01-14 + - Files: `web/components/ErrorBoundary.tsx`, `web/App.tsx` - Context: Catch errors gracefully with retry button - Impact: App doesn't crash, users can recover - Size: ~60 lines diff --git a/web/App.tsx b/web/App.tsx index 1461005..63612e0 100644 --- a/web/App.tsx +++ b/web/App.tsx @@ -6,6 +6,7 @@ import { AuthProvider, useAuth } from './contexts/AuthContext'; import { ThemeProvider } from './contexts/ThemeContext'; import { ToastProvider } from './contexts/ToastContext'; import { ToastContainer } from './components/ui/Toast'; +import { ErrorBoundary } from './components/ErrorBoundary'; import { Auth } from './pages/Auth'; import { Dashboard } from './pages/Dashboard'; import { Friends } from './pages/Friends'; @@ -51,8 +52,10 @@ const App = () => { + + diff --git a/web/components/ErrorBoundary.tsx b/web/components/ErrorBoundary.tsx new file mode 100644 index 0000000..ef774e2 --- /dev/null +++ b/web/components/ErrorBoundary.tsx @@ -0,0 +1,89 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; +import { useTheme } from '../contexts/ThemeContext'; +import { THEMES } from '../constants'; +import { Button } from './ui/Button'; +import { AlertTriangle, RefreshCcw } from 'lucide-react'; + +interface ErrorFallbackProps { + error: Error; + resetErrorBoundary: () => void; +} + +const ErrorFallback: React.FC = ({ error, resetErrorBoundary }) => { + const { style } = useTheme(); + const isNeo = style === THEMES.NEOBRUTALISM; + + return ( +
+
+
+ +
+ +

+ Something went wrong +

+ +

+ {error.message || "An unexpected error occurred. Please try again."} +

+ + +
+
+ ); +}; + +interface ErrorBoundaryProps { + children: ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +export class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error("Uncaught error:", error, errorInfo); + } + + handleReset = () => { + this.setState({ hasError: false, error: null }); + // Optional: reload page if state reset isn't enough + window.location.reload(); + }; + + render() { + if (this.state.hasError && this.state.error) { + return ; + } + + return this.props.children; + } +} diff --git a/web/pages/Auth.tsx b/web/pages/Auth.tsx index 4f35a28..0376f94 100644 --- a/web/pages/Auth.tsx +++ b/web/pages/Auth.tsx @@ -334,6 +334,7 @@ export const Auth = () => { : 'Already have an account? Log In'} +