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
2 changes: 2 additions & 0 deletions .Jules/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
## [Unreleased]

### Added
- Global `ErrorBoundary` component with dual-theme support (`Glassmorphism`/`Neobrutalism`) to prevent white screen of death.
- Recovery options for users when errors occur ("Try Again" and "Back to Home").
- 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
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-20
- Files modified: `web/components/ErrorBoundary.tsx`, `web/App.tsx`
- Impact: Catches unexpected runtime errors and provides a branded recovery UI (try again / back to home) that supports both themes.

### Mobile

Expand Down
5 changes: 4 additions & 1 deletion web/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ThemeWrapper } from './components/layout/ThemeWrapper';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import { ThemeProvider } from './contexts/ThemeContext';
import { ToastProvider } from './contexts/ToastContext';
import { ErrorBoundary } from './components/ErrorBoundary';
import { ToastContainer } from './components/ui/Toast';
import { Auth } from './pages/Auth';
import { Dashboard } from './pages/Dashboard';
Expand Down Expand Up @@ -51,8 +52,10 @@ const App = () => {
<ToastProvider>
<AuthProvider>
<HashRouter>
<ErrorBoundary>
<AppRoutes />
<ToastContainer />
</ErrorBoundary>
<ToastContainer />
</HashRouter>
</AuthProvider>
</ToastProvider>
Expand Down
90 changes: 90 additions & 0 deletions web/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { useTheme } from '../contexts/ThemeContext';
import { Card } from './ui/Card';
import { Button } from './ui/Button';
import { AlertTriangle, Home, RefreshCw } from 'lucide-react';

// Functional component for the UI to use hooks like useTheme
const ErrorFallback: React.FC<{
error: Error | null;
resetErrorBoundary: () => void;
}> = ({ error, resetErrorBoundary }) => {
// style is used by Card internally, we don't need it here unless we have specific styles

return (
<div className="min-h-[50vh] flex items-center justify-center p-4">
<Card className="max-w-md w-full flex flex-col items-center text-center">
<div className="bg-red-100 p-4 rounded-full mb-4 text-red-600">
<AlertTriangle size={48} strokeWidth={1.5} />
</div>

<h2 className="text-2xl font-bold mb-2">Something went wrong</h2>

<p className="text-gray-500 mb-6 dark:text-gray-400">
{error?.message || "An unexpected error occurred. Please try again."}
</p>

<div className="flex flex-col sm:flex-row gap-3 w-full justify-center">
<Button
variant="primary"
onClick={resetErrorBoundary}
className="flex-1"
>
<RefreshCw size={18} className="mr-2" />
Try Again
</Button>

<Button
variant="ghost"
onClick={() => window.location.reload()}
className="flex-1"
>
<Home size={18} className="mr-2" />
Back to Home
</Button>
</div>
</Card>
</div>
);
};

interface ErrorBoundaryProps {
children: ReactNode;
}

interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}

export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}

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

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary caught an error:', error, errorInfo);
}

resetErrorBoundary = () => {
this.setState({ hasError: false, error: null });
};

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

return this.props.children;
}
}
Loading