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 system (`ErrorBoundary` component) to catch React render errors, preventing white screens and offering a "Try Again" recovery option.
- 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
12 changes: 5 additions & 7 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,6 @@
- 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

### Mobile

- [ ] **[ux]** Pull-to-refresh with haptic feedback on all list screens
Expand Down Expand Up @@ -155,4 +148,9 @@
- Files modified: `web/pages/Auth.tsx`
- Impact: Users know immediately if input is valid via inline error messages and red borders.

- [x] **[ux]** Error boundary with retry for API failures
- Completed: 2026-01-13
- Files modified: `web/components/ErrorBoundary.tsx`, `web/App.tsx`
- Impact: Prevents app crashes from showing blank screens; allows user recovery via retry button.

_No tasks completed yet. Move tasks here after completion._
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
102 changes: 102 additions & 0 deletions web/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
import { THEMES } from '../constants';
import { Button } from './ui/Button';
import { Card } from './ui/Card';

interface Props {
children: ReactNode;
}

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

const ErrorFallback: React.FC<{ error: Error | null; resetErrorBoundary: () => void }> = ({
error,
resetErrorBoundary
}) => {
const { style } = useTheme();
const isNeo = style === THEMES.NEOBRUTALISM;

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

return (
<div className="min-h-screen w-full flex items-center justify-center p-4">
<Card className="max-w-md w-full flex flex-col items-center text-center">
<div className={`mb-4 p-4 rounded-full ${isNeo ? 'bg-red-200 border-2 border-black' : 'bg-red-500/20'}`}>
<AlertTriangle className={`w-12 h-12 ${isNeo ? 'text-black' : 'text-red-500'}`} aria-hidden="true" />
</div>

<h2 className={`text-2xl font-bold mb-2 ${isNeo ? 'uppercase' : ''}`}>
Something went wrong
</h2>

<p className="opacity-70 mb-6">
We encountered an unexpected error. Please try again.
</p>

{error && (
<div
role="alert"
className={`w-full p-3 mb-6 text-sm text-left overflow-auto max-h-32 rounded ${
isNeo ? 'bg-gray-100 border-2 border-black font-mono' : 'bg-black/10 font-mono'
}`}>
{error.message}
</div>
)}

<div className="flex gap-4 w-full">
<Button
variant="ghost"
onClick={handleHome}
className="flex-1"
>
<Home className="w-4 h-4 mr-2" aria-hidden="true" />
Home
</Button>
<Button
variant="primary"
onClick={resetErrorBoundary}
className="flex-1"
>
<RefreshCw className="w-4 h-4 mr-2" aria-hidden="true" />
Try Again
</Button>
</div>
</Card>
</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 = () => {
// For a top-level boundary, reloading is often the safest way to recover
window.location.reload();
};

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

return this.props.children;
}
}
Loading