Implement professional error handling and user feedback system
Some checks failed
Lint and Build / build (20) (pull_request) Failing after 35s
Lint and Build / build (22) (pull_request) Failing after 51s

Major UX Improvements:
- Replace all alert() dialogs with elegant toast notifications
- Add comprehensive loading states for all async operations
- Implement client-side form validation with visual feedback
- Add React Error Boundary for graceful error recovery

Components Added:
- Toast notification system with success/error/warning/info types
- useToast hook for easy notification management
- ErrorBoundary component with development error details
- Form validation with real-time feedback

User Experience:
- Professional toast notifications instead of browser alerts
- Loading indicators prevent double-clicks and show progress
- Immediate validation feedback prevents submission errors
- Graceful error recovery with retry options
- Enhanced accessibility with proper ARIA labels

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Decobus 2025-07-19 05:39:21 -04:00
parent dc1e3a62a1
commit b6ff9a3cb6
5 changed files with 350 additions and 26 deletions

View file

@ -0,0 +1,76 @@
'use client';
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary caught an error:', error, errorInfo);
}
public render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="glass p-8 text-center max-w-md mx-auto mt-8">
<div className="text-6xl mb-4"></div>
<h2 className="text-xl font-semibold mb-4 text-red-400">
Something went wrong
</h2>
<p className="text-white/80 mb-6">
An unexpected error occurred. Please refresh the page or try again later.
</p>
<div className="button-group" style={{ justifyContent: 'center' }}>
<button
onClick={() => window.location.reload()}
className="btn btn-success"
>
<span className="icon">🔄</span>
Refresh Page
</button>
<button
onClick={() => this.setState({ hasError: false, error: undefined })}
className="btn-secondary"
>
<span className="icon">🔄</span>
Try Again
</button>
</div>
{process.env.NODE_ENV === 'development' && this.state.error && (
<details className="mt-6 text-left">
<summary className="text-red-400 cursor-pointer font-mono text-sm">
Error Details (Development)
</summary>
<pre className="text-red-300 text-xs mt-2 p-3 bg-red-500/10 rounded border border-red-500/20 overflow-auto">
{this.state.error.stack}
</pre>
</details>
)}
</div>
);
}
return this.props.children;
}
}

115
components/Toast.tsx Normal file
View file

@ -0,0 +1,115 @@
'use client';
import { useEffect, useState } from 'react';
export type ToastType = 'success' | 'error' | 'warning' | 'info';
export interface Toast {
id: string;
type: ToastType;
title: string;
message?: string;
duration?: number;
}
interface ToastProps {
toast: Toast;
onRemove: (id: string) => void;
}
export function ToastComponent({ toast, onRemove }: ToastProps) {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
// Animate in
setTimeout(() => setIsVisible(true), 10);
// Auto remove after duration
const timer = setTimeout(() => {
setIsVisible(false);
setTimeout(() => onRemove(toast.id), 300);
}, toast.duration || 5000);
return () => clearTimeout(timer);
}, [toast.id, toast.duration, onRemove]);
const getToastStyles = () => {
switch (toast.type) {
case 'success':
return 'bg-green-500/20 border-green-500/40 text-green-300';
case 'error':
return 'bg-red-500/20 border-red-500/40 text-red-300';
case 'warning':
return 'bg-yellow-500/20 border-yellow-500/40 text-yellow-300';
case 'info':
return 'bg-blue-500/20 border-blue-500/40 text-blue-300';
default:
return 'bg-gray-500/20 border-gray-500/40 text-gray-300';
}
};
const getIcon = () => {
switch (toast.type) {
case 'success':
return '✅';
case 'error':
return '❌';
case 'warning':
return '⚠️';
case 'info':
return '';
default:
return '📢';
}
};
return (
<div
className={`
glass p-4 border rounded-lg min-w-80 max-w-md
transform transition-all duration-300 ease-out
${isVisible ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'}
${getToastStyles()}
`}
>
<div className="flex items-start gap-3">
<span className="text-lg flex-shrink-0">{getIcon()}</span>
<div className="flex-1 min-w-0">
<div className="font-semibold text-sm">{toast.title}</div>
{toast.message && (
<div className="text-sm opacity-90 mt-1">{toast.message}</div>
)}
</div>
<button
onClick={() => {
setIsVisible(false);
setTimeout(() => onRemove(toast.id), 300);
}}
className="text-white/60 hover:text-white text-lg leading-none flex-shrink-0"
aria-label="Close notification"
>
×
</button>
</div>
</div>
);
}
interface ToastContainerProps {
toasts: Toast[];
onRemove: (id: string) => void;
}
export function ToastContainer({ toasts, onRemove }: ToastContainerProps) {
if (toasts.length === 0) return null;
return (
<div className="fixed top-4 right-4 z-50 space-y-3 pointer-events-none">
<div className="space-y-3 pointer-events-auto">
{toasts.map((toast) => (
<ToastComponent key={toast.id} toast={toast} onRemove={onRemove} />
))}
</div>
</div>
);
}