diff --git a/.Jules/changelog.md b/.Jules/changelog.md index d438210..84d0158 100644 --- a/.Jules/changelog.md +++ b/.Jules/changelog.md @@ -7,6 +7,7 @@ ## [Unreleased] ### Added +- **Global Error Boundary System**: Implemented `ErrorBoundary` component that catches runtime errors and displays a user-friendly, dual-themed UI with "Try Again" and "Back to Home" actions. Prevents white-screen crashes. - 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..c4a7984 100644 --- a/.Jules/knowledge.md +++ b/.Jules/knowledge.md @@ -571,6 +571,30 @@ _Document errors and their solutions here as you encounter them._ --- +### ✅ Successful PR Pattern: Error Boundary System + +**Date:** 2026-01-14 +**Context:** Implemented a global Error Boundary with dual-theme support + +**What was implemented:** +1. Created `ErrorBoundary` class component to catch errors. +2. Created `ErrorFallback` functional component for UI rendering. +3. Used `ErrorFallback` to leverage `useTheme` hooks (impossible in class component). +4. Added "Try Again" (reload) and "Back to Home" (window location reset) actions. +5. Integrated into `App.tsx` wrapping the router. + +**Gotchas & Solutions:** +- **Hooks in Error Boundary:** Class components can't use hooks. Solution: Pass the `error` state to a child functional component (`ErrorFallback`) which CAN use hooks like `useTheme`. +- **Typing Imports:** `componentDidCatch` uses `ErrorInfo` type. This MUST be imported from `react` to avoid TS build errors. +- **HashRouter Navigation:** Inside an Error Boundary (especially if it wraps the Router), use `window.location.href = window.location.origin` to reliably reset to the home page, rather than router hooks which might be broken or inaccessible. + +**Key learnings:** +- Always separate the Error Boundary logic (class) from the UI logic (function) when using contexts/hooks. +- Verify imports for TypeScript types. +- Robust "escape hatches" (like full page reload) are better than complex recovery logic for generic error boundaries. + +--- + ### ✅ Successful PR Pattern: EmptyState Component (#226) **Date:** 2026-01-13 diff --git a/.Jules/todo.md b/.Jules/todo.md index 894e27f..00bc423 100644 --- a/.Jules/todo.md +++ b/.Jules/todo.md @@ -34,12 +34,11 @@ - 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: Created `web/components/ErrorBoundary.tsx`, modified `web/App.tsx` + - Impact: App gracefully handles crashes with a retry option, preventing blank screens. + - Size: ~90 lines ### Mobile @@ -154,5 +153,7 @@ - Completed: 2026-01-11 - Files modified: `web/pages/Auth.tsx` - Impact: Users know immediately if input is valid via inline error messages and red borders. - -_No tasks completed yet. Move tasks here after completion._ +- [x] **[ux]** Error boundary with retry for API failures + - Completed: 2026-01-14 + - Files modified: `web/components/ErrorBoundary.tsx`, `web/App.tsx` + - Impact: Prevents app crashes from showing a blank screen, providing a retry mechanism instead. diff --git a/web/App.tsx b/web/App.tsx index 1461005..478306f 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'; @@ -48,14 +49,16 @@ const AppRoutes = () => { const App = () => { return ( - - - - - - - - + + + + + + + + + + ); }; diff --git a/web/components/ErrorBoundary.tsx b/web/components/ErrorBoundary.tsx new file mode 100644 index 0000000..de7938e --- /dev/null +++ b/web/components/ErrorBoundary.tsx @@ -0,0 +1,125 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; +import { AlertTriangle, RefreshCw, Home } from 'lucide-react'; +import { Button } from './ui/Button'; +import { useTheme } from '../contexts/ThemeContext'; +import { THEMES } from '../constants'; + +interface Props { + children: ReactNode; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +const ErrorFallback: React.FC<{ error: Error | null, resetErrorBoundary: () => void }> = ({ error, resetErrorBoundary }) => { + const { style, mode } = useTheme(); + + const isNeo = style === THEMES.NEOBRUTALISM; + const isDark = mode === 'dark'; + + // Base container styles + const containerClasses = `min-h-screen w-full flex items-center justify-center p-4 transition-colors duration-300 + ${isNeo + ? (isDark ? 'bg-zinc-900' : 'bg-yellow-50') + : (isDark ? 'bg-gradient-to-br from-gray-900 via-gray-800 to-black' : 'bg-gradient-to-br from-blue-50 via-white to-purple-50') + }`; + + // Card styles + const cardClasses = `max-w-md w-full p-8 transition-all duration-300 + ${isNeo + ? `border-2 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)] ${isDark ? 'bg-zinc-800 text-white' : 'bg-white text-black'} rounded-none` + : `backdrop-blur-xl border border-white/20 shadow-2xl rounded-2xl ${isDark ? 'bg-white/5 text-white' : 'bg-white/70 text-gray-800'}` + }`; + + // Text styles + const titleClasses = `text-3xl font-bold mb-4 ${isNeo ? 'uppercase tracking-tighter' : 'tracking-tight'}`; + const messageClasses = `text-lg mb-6 opacity-80 ${isNeo ? 'font-mono text-sm' : ''}`; + const codeClasses = `p-4 mb-8 rounded text-sm font-mono overflow-auto + ${isNeo + ? 'border border-black bg-gray-100 text-black' + : (isDark ? 'bg-black/30 text-red-300' : 'bg-red-50 text-red-600') + }`; + + return ( +
+
+
+
+
+ +

+ Something went wrong +

+ +

+ We encountered an unexpected error. Please try again or return to the dashboard. +

+ + {error && ( +
+ {error.message} +
+ )} + +
+ + + +
+
+
+
+ ); +}; + +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: ErrorInfo) { + console.error('ErrorBoundary caught an error:', error, errorInfo); + // Here you would typically log to a service like Sentry + } + + handleReset = () => { + this.setState({ hasError: false, error: null }); + // Reloading the page is often the safest "retry" for client-side errors + window.location.reload(); + }; + + render() { + if (this.state.hasError) { + return ; + } + + return this.props.children; + } +}