diff --git a/.Jules/changelog.md b/.Jules/changelog.md index d438210..c3bb655 100644 --- a/.Jules/changelog.md +++ b/.Jules/changelog.md @@ -7,6 +7,7 @@ ## [Unreleased] ### Added +- Error Boundary (`ErrorBoundary`) wrapping the web application to catch runtime errors gracefully. Includes a dual-themed `ErrorFallback` UI with "Try Again" and "Back to Home" actions. - 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..518396e 100644 --- a/.Jules/knowledge.md +++ b/.Jules/knowledge.md @@ -619,6 +619,30 @@ _Document errors and their solutions here as you encounter them._ --- +### ✅ Successful PR Pattern: Error Boundary Implementation + +**Date:** 2026-01-14 +**Context:** Adding robust error handling to Web App + +**What was implemented:** +1. Created `ErrorBoundary` Class Component (required for `getDerivedStateFromError`). +2. Created `ErrorFallback` Functional Component (to use `useTheme` hook). +3. Wrapped `AppRoutes` in `App.tsx` (inside `ThemeProvider`). +4. Provided "Try Again" (reload) and "Back to Home" (href redirect) options. + +**Why it succeeded:** +- ✅ Separation of concerns: Class for logic, Function for UI/Hooks. +- ✅ Full dual-theme support (Neo/Glass) in fallback UI. +- ✅ Safety: `window.location.reload()` clears bad state reliably. +- ✅ Integration: Placed correctly within Provider hierarchy. + +**Key learnings:** +- React Error Boundaries *must* be class components. +- Use a child functional component if you need Hooks (like `useTheme`) in the error UI. +- `window.location.href = '/'` is safer than `navigate('/')` for "Back to Home" when the app is in an undefined state. + +--- + ## Dependencies Reference ### Web diff --git a/.Jules/todo.md b/.Jules/todo.md index 894e27f..421cd0f 100644 --- a/.Jules/todo.md +++ b/.Jules/todo.md @@ -34,12 +34,10 @@ - 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 - - Context: Catch errors gracefully with retry button - - Impact: App doesn't crash, users can recover - - Size: ~60 lines - - Added: 2026-01-01 +- [x] **[ux]** Error boundary with retry for API failures + - Completed: 2026-01-14 + - Files modified: `web/components/ErrorBoundary.tsx`, `web/App.tsx` + - Impact: App doesn't crash completely on runtime errors; users can recover via retry or home button. ### Mobile diff --git a/verification_error_boundary.png b/verification_error_boundary.png new file mode 100644 index 0000000..f32f4f5 Binary files /dev/null and b/verification_error_boundary.png differ diff --git a/web/App.tsx b/web/App.tsx index 1461005..66511c9 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..dac8b08 --- /dev/null +++ b/web/components/ErrorBoundary.tsx @@ -0,0 +1,92 @@ +import React, { Component, ReactNode } from 'react'; +import { useTheme } from '../contexts/ThemeContext'; +import { THEMES } from '../constants'; +import { Button } from './ui/Button'; +import { AlertTriangle, RefreshCw, Home } from 'lucide-react'; + +interface Props { + children: ReactNode; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +const ErrorFallback = ({ error, resetErrorBoundary }: { error: Error | null, resetErrorBoundary: () => void }) => { + const { style } = useTheme(); + const isNeo = style === THEMES.NEOBRUTALISM; + + const handleReload = () => { + window.location.reload(); + }; + + const handleHome = () => { + window.location.href = '/'; + }; + + return ( +
+
+
+
+ +
+
+ +

+ Whoops! +

+ +

+ {error?.message || "Something went wrong while displaying this page. Try reloading or going back to home."} +

+ +
+ + +
+
+
+ ); +}; + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error("Uncaught error:", error, errorInfo); + } + + render() { + if (this.state.hasError) { + return ( + this.setState({ hasError: false, error: null })} + /> + ); + } + + return this.props.children; + } +}