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 Error Boundary**: Implemented a dual-theme compatible error boundary (`web/components/ErrorBoundary.tsx`) to catch app crashes gracefully and provide a "Try Again" mechanism.
- 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
7 changes: 7 additions & 0 deletions .Jules/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,13 @@ Common patterns:

Use `AnimatePresence` for exit animations.

### Testing React Error Boundaries
**Date:** 2026-01-14
**Context:** Verifying global Error Boundary

React Error Boundaries **do not catch errors inside event handlers**. They only catch errors during the render phase, in lifecycle methods, and in constructors of the whole tree below them.
To test an Error Boundary, you must throw an error *during render* (e.g., inside the component body based on state), not inside an `onClick` handler.

---

## Best Practices Learned
Expand Down
5 changes: 3 additions & 2 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@
- 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
- [x] **[ux]** Error boundary with retry for API failures
- Completed: 2026-01-14
- Files: `web/components/ErrorBoundary.tsx`, `web/App.tsx`
- Context: Catch errors gracefully with retry button
- Impact: App doesn't crash, users can recover
- Size: ~60 lines
Expand Down
3 changes: 3 additions & 0 deletions 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>
</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, ReactNode } from 'react';
import { useTheme } from '../contexts/ThemeContext';
import { THEMES } from '../constants';
import { Button } from './ui/Button';
import { AlertTriangle, RefreshCcw } from 'lucide-react';

interface ErrorFallbackProps {
error: Error;
resetErrorBoundary: () => void;
}

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

return (
<div className="min-h-screen w-full flex items-center justify-center p-4">
<div
className={`
max-w-md w-full p-8 flex flex-col items-center text-center
${isNeo
? 'bg-neo-bg border-4 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]'
: 'bg-white/10 backdrop-blur-xl border border-white/20 shadow-2xl rounded-2xl'
}
`}
role="alert"
>
<div className={`mb-6 p-4 rounded-full ${isNeo ? 'bg-red-500 border-2 border-black' : 'bg-red-500/20'}`}>
<AlertTriangle size={48} className={isNeo ? 'text-white' : 'text-red-500'} />
</div>

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

<p className={`mb-8 ${isNeo ? 'font-mono' : 'text-white/70'}`}>
{error.message || "An unexpected error occurred. Please try again."}
</p>

<Button
onClick={resetErrorBoundary}
variant={isNeo ? 'primary' : 'primary'}
className="w-full"
>
<RefreshCcw size={18} />
Try Again
</Button>
</div>
</div>
);
};

interface ErrorBoundaryProps {
children: ReactNode;
}

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

export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}

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

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("Uncaught error:", error, errorInfo);
}

handleReset = () => {
this.setState({ hasError: false, error: null });
// Optional: reload page if state reset isn't enough
window.location.reload();
};

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

return this.props.children;
}
}
1 change: 1 addition & 0 deletions web/pages/Auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ export const Auth = () => {
: 'Already have an account? Log In'}
</button>
</div>

</div>
</div>
</div>
Expand Down
Loading