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
- **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.
Expand Down
24 changes: 24 additions & 0 deletions .Jules/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 9 additions & 8 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
19 changes: 11 additions & 8 deletions 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 @@ -48,14 +49,16 @@ const AppRoutes = () => {
const App = () => {
return (
<ThemeProvider>
<ToastProvider>
<AuthProvider>
<HashRouter>
<AppRoutes />
<ToastContainer />
</HashRouter>
</AuthProvider>
</ToastProvider>
<ErrorBoundary>
<ToastProvider>
<AuthProvider>
<HashRouter>
<AppRoutes />
<ToastContainer />
</HashRouter>
</AuthProvider>
</ToastProvider>
</ErrorBoundary>
</ThemeProvider>
);
};
Expand Down
125 changes: 125 additions & 0 deletions web/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={containerClasses}>
<div
role="alert"
className={cardClasses}
aria-labelledby="error-title"
aria-describedby="error-desc"
>
<div className="flex flex-col items-center text-center">
<div className={`mb-6 p-4 rounded-full ${isNeo ? 'bg-red-500 border-2 border-black' : 'bg-red-500/10'}`}>
<AlertTriangle className={`w-12 h-12 ${isNeo ? 'text-white' : 'text-red-500'}`} aria-hidden="true" />
</div>

<h1 id="error-title" className={titleClasses}>
Something went wrong
</h1>

<p id="error-desc" className={messageClasses}>
We encountered an unexpected error. Please try again or return to the dashboard.
</p>

{error && (
<div className={codeClasses}>
{error.message}
</div>
)}

<div className="flex flex-col sm:flex-row gap-4 w-full justify-center">
<Button
onClick={resetErrorBoundary}
variant="primary"
className="w-full sm:w-auto"
>
<RefreshCw className="w-4 h-4" />
Try Again
</Button>

<Button
onClick={() => window.location.href = window.location.origin}
variant="secondary"
className="w-full sm:w-auto"
>
<Home className="w-4 h-4" />
Back to Home
</Button>
</div>
</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: 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 <ErrorFallback error={this.state.error} resetErrorBoundary={this.handleReset} />;
}

return this.props.children;
}
}
Loading