Implement professional error handling and user feedback system
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:
parent
dc1e3a62a1
commit
b6ff9a3cb6
5 changed files with 350 additions and 26 deletions
115
components/Toast.tsx
Normal file
115
components/Toast.tsx
Normal 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>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue