obs-ss-plugin-webui/components/Toast.tsx
Decobus b6ff9a3cb6
Some checks failed
Lint and Build / build (20) (pull_request) Failing after 35s
Lint and Build / build (22) (pull_request) Failing after 51s
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>
2025-07-19 05:39:21 -04:00

115 lines
No EOL
3 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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