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
1 change: 1 addition & 0 deletions .Jules/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 24 additions & 0 deletions .Jules/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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: App doesn't crash completely on runtime errors; users can recover via retry or home button.

### Mobile

Expand Down
Binary file added verification_error_boundary.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
92 changes: 92 additions & 0 deletions web/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={`min-h-[80vh] w-full flex flex-col items-center justify-center p-6 text-center`}>
<div className={`p-8 max-w-lg w-full ${
isNeo
? 'bg-white border-2 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]'
: 'bg-white/10 backdrop-blur-md border border-white/20 shadow-2xl rounded-2xl'
}`}>
<div className="flex justify-center mb-6">
<div className={`p-4 rounded-full ${
isNeo ? 'bg-neo-second text-black border-2 border-black' : 'bg-red-500/20 text-red-200'
}`}>
<AlertTriangle size={48} />
</div>
</div>

<h2 className={`text-3xl font-bold mb-3 ${isNeo ? 'font-mono uppercase' : 'font-sans text-white'}`}>
Whoops!
</h2>

<p className={`mb-8 ${isNeo ? 'font-mono text-base' : 'text-white/70'}`}>
{error?.message || "Something went wrong while displaying this page. Try reloading or going back to home."}
</p>

<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button onClick={handleReload} variant="primary">
<RefreshCw size={18} />
Try Again
</Button>
<Button onClick={handleHome} variant="secondary">
<Home size={18} />
Back to Home
</Button>
</div>
</div>
</div>
);
};

export class ErrorBoundary extends Component<Props, State> {
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 (
<ErrorFallback
error={this.state.error}
resetErrorBoundary={() => this.setState({ hasError: false, error: null })}
/>
);
}

return this.props.children;
}
}
Loading