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 (`web/components/ErrorBoundary.tsx`) wrapping the web application to catch render errors.
- Dual-theme supported Error UI with "Try Again" (reset boundary) and "Reload Page" options.
- 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-14
- Files modified: `web/components/ErrorBoundary.tsx`, `web/App.tsx`
- Impact: Catches global and render errors, presenting a theme-aware UI with retry/reload options instead of a white screen.

### 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
94 changes: 94 additions & 0 deletions web/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { useTheme } from '../contexts/ThemeContext';
import { THEMES } from '../constants';
import { Button } from './ui/Button';
import { Card } from './ui/Card';
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;

return (
<div className="min-h-[60vh] flex items-center justify-center p-4">
<Card className="max-w-md w-full text-center">
<div className={`flex justify-center mb-6 ${isNeo ? 'text-black' : 'text-red-400'}`}>
<AlertTriangle size={64} strokeWidth={isNeo ? 2.5 : 1.5} />
</div>

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

<div className={`p-4 mb-6 rounded text-sm text-left overflow-auto max-h-32 ${
isNeo
? 'bg-red-100 border-2 border-black text-black font-mono'
: 'bg-red-500/10 border border-red-500/20 text-red-200'
}`}>
{error?.message || "An unexpected error occurred."}
</div>

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

<Button
onClick={() => window.location.reload()}
variant="ghost"
className="w-full sm:w-auto"
>
<Home size={18} />
Reload Page
</Button>
</div>
</Card>
</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("Uncaught 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