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:** Added a global `ErrorBoundary` component that catches unexpected errors and displays a friendly, dual-themed fallback UI with a retry action.
- 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
14 changes: 14 additions & 0 deletions .Jules/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ mobile/

## Theming System

### Error Boundary Pattern

**Date:** 2026-02-01
**Context:** Implemented global error handling

**Implementation:**
- Use Class Component for `componentDidCatch`
- Use inner Functional Component for UI to support hooks (`useTheme`)
- Place inside `ThemeProvider` but outside `Router` or deeply nested providers
- Use `import.meta.env.DEV` to show stack traces in development
- Ensure fallback UI supports both `NEOBRUTALISM` and `GLASSMORPHISM`

**Files:** `web/components/ErrorBoundary.tsx`

### Web Dual-Theme Pattern
**Date:** 2026-01-01
**Context:** Understanding theme-switching mechanism
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: App doesn't crash, users can recover gracefully

### Mobile

Expand Down
19 changes: 11 additions & 8 deletions 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 @@ -48,14 +49,16 @@ const AppRoutes = () => {
const App = () => {
return (
<ThemeProvider>
<ToastProvider>
<AuthProvider>
<HashRouter>
<AppRoutes />
<ToastContainer />
</HashRouter>
</AuthProvider>
</ToastProvider>
<ErrorBoundary>
<ToastProvider>
<AuthProvider>
<HashRouter>
<AppRoutes />
<ToastContainer />
</HashRouter>
</AuthProvider>
</ToastProvider>
</ErrorBoundary>
</ThemeProvider>
);
};
Expand Down
100 changes: 100 additions & 0 deletions web/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { Component, ReactNode } from 'react';
import { AlertTriangle, RefreshCw } from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
import { THEMES } from '../constants';
import { Button } from './ui/Button';

interface Props {
children: ReactNode;
}

interface State {
hasError: boolean;
error?: Error;
}

const ErrorFallback = ({ error, resetErrorBoundary }: { error?: Error, resetErrorBoundary: () => void }) => {
const { style, mode } = useTheme();
const isNeo = style === THEMES.NEOBRUTALISM;
const isDark = mode === 'dark';

return (
<div className={`min-h-[500px] w-full flex flex-col items-center justify-center p-6 text-center
${isNeo ? 'bg-neo-bg text-black' : isDark ? 'text-white' : 'text-gray-900'}
`}>
<div className={`
max-w-md w-full p-8 flex flex-col items-center gap-6
${isNeo
? 'bg-white border-2 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]'
: `backdrop-blur-xl border ${isDark ? 'bg-white/10 border-white/20' : 'bg-white/70 border-white/40'} rounded-2xl shadow-xl`
}
`}>
<div className={`
p-4 rounded-full flex items-center justify-center
${isNeo
? 'bg-red-400 border-2 border-black'
: 'bg-red-500/20 text-red-500'
}
`}>
<AlertTriangle size={48} className={isNeo ? 'text-black' : 'currentColor'} aria-hidden="true" />
</div>

<div className="space-y-3">
<h2 className="text-2xl font-bold" role="alert">Something went wrong</h2>
<p className={`${isNeo ? 'text-gray-700' : isDark ? 'text-gray-300' : 'text-gray-600'}`}>
We encountered an unexpected error. Please try reloading the page.
</p>

{import.meta.env.DEV && error && (
<div className="mt-4 w-full">
<p className="text-xs font-mono text-left mb-1 opacity-70">Error details:</p>
<pre className={`
p-3 text-xs text-left overflow-auto max-h-40 w-full rounded
${isNeo ? 'bg-gray-100 border border-black text-black' : 'bg-black/30 text-white/90'}
`}>
{error.message}
</pre>
</div>
)}
</div>

<Button
onClick={resetErrorBoundary}
variant="primary"
className="w-full flex items-center justify-center gap-2 mt-2"
>
<RefreshCw size={18} aria-hidden="true" />
Reload Page
</Button>
</div>
</div>
);
};

export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}

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

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

resetErrorBoundary = () => {
this.setState({ hasError: false, error: undefined });
window.location.reload();
};

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

return this.props.children;
}
}
Loading