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 `ErrorBoundary` component that catches uncaught errors and displays a user-friendly dual-theme 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
23 changes: 23 additions & 0 deletions .Jules/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,29 @@ addToast('Message', 'success|error|info');
- Auto-dismisses after 3 seconds
- Stacks vertically in bottom-right

### Error Boundary Pattern

**Date:** 2026-02-01
**Context:** Global error handling in `App.tsx`

React Error Boundaries must be class components to use `componentDidCatch`. To support hooks (like `useTheme`), render a functional `ErrorFallback` component.

```tsx
// Class component captures error
class ErrorBoundary extends Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) { return { hasError: true, error }; }

render() {
if (this.state.hasError) {
// Functional component handles UI + Hooks
return <ErrorFallback error={this.state.error} reset={() => this.setState(...)} />;
}
return this.props.children;
}
}
```

### Form Validation Pattern

**Date:** 2026-01-01
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-02-01
- Files modified: `web/components/ErrorBoundary.tsx`, `web/App.tsx`
- Impact: Prevents white-screen crashes and allows user recovery with "Try Again"

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

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 containerClass = isNeo
? 'bg-neo-bg text-black'
: 'bg-gradient-to-br from-gray-900 via-purple-900 to-violet-900 text-white';

const cardClass = isNeo
? 'bg-white border-4 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)] p-8 max-w-md w-full'
: 'bg-white/10 backdrop-blur-xl border border-white/20 shadow-2xl rounded-2xl p-8 max-w-md w-full';

return (
<div className={`min-h-screen w-full flex items-center justify-center p-4 ${containerClass}`}>
<div className={cardClass}>
<div className="flex flex-col items-center text-center gap-6">
<div className={`p-4 rounded-full ${isNeo ? 'bg-red-200 border-2 border-black' : 'bg-red-500/20'}`}>
<AlertTriangle size={48} className={isNeo ? 'text-black' : 'text-red-400'} />
</div>

<div className="space-y-2">
<h1 className={`text-2xl font-bold ${isNeo ? 'uppercase tracking-wide' : ''}`}>
Something went wrong
</h1>
<p className={isNeo ? 'text-gray-800' : 'text-gray-300'}>
We encountered an unexpected error. Our team has been notified.
</p>
{error && (
<div className={`mt-4 p-3 text-sm font-mono text-left overflow-auto max-h-32 ${
isNeo ? 'bg-gray-100 border-2 border-black' : 'bg-black/30 rounded-lg text-red-300'
}`}>
{error.message}
</div>
)}
</div>

<div className="flex flex-col sm:flex-row gap-4 w-full pt-2">
<Button
onClick={resetErrorBoundary}
className="flex-1"
variant="primary"
>
<RefreshCw size={18} />
Try Again
</Button>
<Button
onClick={() => window.location.href = window.location.origin}
className="flex-1"
variant="secondary"
>
<Home size={18} />
Back to Home
</Button>
</div>
</div>
</div>
</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 render() {
if (this.state.hasError) {
return (
<ErrorFallback
error={this.state.error}
resetErrorBoundary={() => {
this.setState({ hasError: false, error: null });
window.location.reload();
}}
/>
);
}

return this.props.children;
}
}
Loading