Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .Jules/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
- Comprehensive `EmptyState` component for Groups and Friends pages to better guide new users.
- Toast notification system (`ToastContext`, `Toast` component) for providing non-blocking user feedback.
- Keyboard navigation support for Groups page, enabling accessibility for power users.
- Global `ErrorBoundary` component to catch React render errors and display a dual-theme friendly fallback UI.
- "Try Again" and "Back to Home" recovery actions for crashed application states.

### Changed
- Wrapped `AppRoutes` in `web/App.tsx` with `ErrorBoundary` to ensure global error catching coverage.
- Updated JULES_PROMPT.md based on review of successful PRs:
- Emphasized complete system implementation over piecemeal changes
- Added best practices from successful PRs (Toast system, keyboard navigation iteration)
Expand Down
23 changes: 23 additions & 0 deletions .Jules/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,29 @@ _Document errors and their solutions here as you encounter them._

---

### ✅ Successful PR Pattern: Error Boundary with Dual Theme

**Date:** 2026-01-14
**Context:** Implementing global error handling

**What was implemented:**
1. Created `ErrorBoundary` class component (required for `componentDidCatch`).
2. Created separate `ErrorFallback` functional component to support hooks (`useTheme`, `useNavigate` equivalent).
3. Wrapped `AppRoutes` but kept `ToastContainer` outside to allow notifications during crashes.
4. Implemented dual-theme styles (Glassmorphism/Neobrutalism) for the fallback UI.

**Why it succeeded:**
- ✅ Solved the "Class components can't use hooks" limitation by separating logic and UI.
- ✅ Preserved the dual-theme requirement even in error states.
- ✅ Verified using a temporary Playwright script that simulated specific render errors.

**Key learnings:**
- **Hooks in Error Boundaries:** You cannot use hooks in the `ErrorBoundary` class itself. Pass props or use a child component for the UI.
- **Verification:** To verify `componentDidCatch`, you must throw a real error in the render phase. Event handler errors are NOT caught by React boundaries.
- **Playwright Mocking:** When mocking routes for verification, ensure specific paths (e.g., `/users/me/balance-summary`) are registered *after* general wildcards (e.g., `**/users/me`) if the generic one handles the request first, or use strict matching.

---

## Dependencies Reference

### Web
Expand Down
10 changes: 4 additions & 6 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: Catches render errors, prevents white screens, and provides "Try Again" / "Back to Home" recovery options with dual-theme support.

### Mobile

Expand Down
5 changes: 4 additions & 1 deletion web/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -51,8 +52,10 @@ const App = () => {
<ToastProvider>
<AuthProvider>
<HashRouter>
<ErrorBoundary>
<AppRoutes />
<ToastContainer />
</ErrorBoundary>
<ToastContainer />
</HashRouter>
</AuthProvider>
</ToastProvider>
Expand Down
104 changes: 104 additions & 0 deletions web/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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;
}

// Functional component for the fallback UI to use Hooks
const ErrorFallback = ({ error, resetErrorBoundary }: { error: Error | null, resetErrorBoundary: () => void }) => {
const { style } = useTheme();

const isNeo = style === THEMES.NEOBRUTALISM;

const handleHome = () => {
window.location.href = window.location.origin;
};

const containerClasses = isNeo
? "bg-neo-bg border-4 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]"
: "bg-white/10 backdrop-blur-xl border border-white/20 shadow-2xl";

const textClasses = isNeo
? "text-black font-mono"
: "text-white";

return (
<div className="min-h-[60vh] flex flex-col items-center justify-center p-6 text-center">
<div className={`p-8 rounded-xl max-w-md w-full flex flex-col items-center gap-6 ${containerClasses}`}>
<div className={`p-4 rounded-full ${isNeo ? 'bg-red-400 border-2 border-black' : 'bg-red-500/20'}`}>
<AlertTriangle size={48} className={isNeo ? "text-black" : "text-red-200"} />
</div>

<div className="space-y-2">
<h2 className={`text-2xl font-bold ${textClasses}`}>
Something went wrong
</h2>
<p className={isNeo ? "text-gray-800 font-mono text-sm" : "text-gray-200"}>
We encountered an unexpected error.
</p>
{error && (
<div className={`mt-4 p-3 text-left overflow-auto max-h-32 text-xs rounded ${isNeo ? 'bg-gray-200 border border-black' : 'bg-black/30 text-red-200 font-mono'}`}>
{error.message}
</div>
)}
</div>

<div className="flex flex-col sm:flex-row gap-3 w-full">
<Button
onClick={resetErrorBoundary}
variant="primary"
className="flex-1"
>
<RefreshCw size={18} />
Try Again
</Button>
<Button
onClick={handleHome}
variant="secondary"
className="flex-1"
>
<Home size={18} />
Back to Home
</Button>
</div>
</div>
</div>
);
};

export class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
error: null
};

public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}

public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("Uncaught error:", error, errorInfo);
}

public resetErrorBoundary = () => {
this.setState({ hasError: false, error: null });
window.location.reload(); // Hard reload to ensure clean state
};

public render() {
if (this.state.hasError) {
return <ErrorFallback error={this.state.error} resetErrorBoundary={this.resetErrorBoundary} />;
}

return this.props.children;
}
}
Loading