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

### Added
- **Error Boundary System**: Added `ErrorBoundary` component to catch React errors gracefully.
- Displays a user-friendly error card with "Try Again" and "Home" actions.
- Supports both Glassmorphism and Neobrutalism themes.
- Shows stack trace in development mode.
- Wraps `AppRoutes` to prevent white screen of death on route errors.
- 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
27 changes: 27 additions & 0 deletions .Jules/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,33 @@ colors: {

## Component Patterns

### Error Boundary Pattern

**Date:** 2026-01-14
**Context:** Implemented `ErrorBoundary` in `web/components/ErrorBoundary.tsx`

React Error Boundaries must be class components. However, to use hooks (like `useTheme`), you should split the UI into a functional component (`ErrorFallback`) and render it from the class component.

```tsx
// Functional component for UI (can use hooks)
const ErrorFallback = ({ error, resetErrorBoundary }) => {
const { style } = useTheme(); // Hooks work here
return <ThemeWrapper>...</ThemeWrapper>;
};

// Class component for logic
class ErrorBoundary extends Component {
// ...
render() {
if (this.state.hasError) {
return <ErrorFallback ... />;
}
return this.props.children;
}
}
```
**Key Learning:** Wrap `ErrorFallback` in `ThemeWrapper` so the error page inherits the correct background and theme context.

### Button Component Variants

**Date:** 2026-01-01
Expand Down
8 changes: 6 additions & 2 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@
- Impact: Guides new users, makes app feel polished
- Size: ~70 lines

- [ ] **[ux]** Error boundary with retry for API failures
- [x] **[ux]** Error boundary with retry for API failures
- Completed: 2026-01-14
- 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

Expand Down Expand Up @@ -154,5 +154,9 @@
- 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.
- [x] **[ux]** Error boundary with retry for API failures
- Completed: 2026-01-14
- Files modified: `web/components/ErrorBoundary.tsx`, `web/App.tsx`
- Impact: App recovers gracefully from crashes.

_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
89 changes: 89 additions & 0 deletions web/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React, { Component, ErrorInfo } from 'react';
import { ThemeWrapper } from './layout/ThemeWrapper';
import { Card } from './ui/Card';
import { Button } from './ui/Button';
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';

interface Props {
children: React.ReactNode;
}

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

const ErrorFallback = ({ error, resetErrorBoundary }: { error: Error | null, resetErrorBoundary: () => void }) => {
const isDev = import.meta.env.DEV;

return (
<ThemeWrapper>
<div className="flex items-center justify-center min-h-screen p-4">
<Card className="max-w-md w-full" title="Something went wrong">
<div className="flex flex-col items-center text-center space-y-4">
<div className="p-3 rounded-full bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400">
<AlertTriangle size={32} />
</div>

<p className="text-gray-600 dark:text-gray-300">
We encountered an unexpected error. Please try again.
</p>

{isDev && error && (
<div className="w-full p-2 mt-2 text-xs text-left font-mono bg-black/5 dark:bg-white/5 rounded overflow-auto max-h-32 border border-gray-200 dark:border-gray-700">
{error.toString()}
</div>
)}

<div className="flex gap-3 w-full mt-4">
<Button
variant="primary"
className="flex-1 flex items-center justify-center gap-2"
onClick={resetErrorBoundary}
>
<RefreshCw size={16} />
Try Again
</Button>
<Button
variant="secondary"
className="flex-1 flex items-center justify-center gap-2"
onClick={() => window.location.href = '/'}
>
<Home size={16} />
Home
</Button>
</div>
</div>
</Card>
</div>
</ThemeWrapper>
);
};

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);
}

private handleReset = () => {
this.setState({ hasError: false, error: null });
window.location.reload();
};

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

return this.props.children;
}
}
Loading