/** * Notification Toast Component * * Displays real-time notifications as toast messages. */ import React, { useEffect, useState, useCallback } from 'react'; import classNames from 'classnames'; import { useSocketContext } from '../../lib/realtime/SocketContext'; import type { RealtimeNotification } from '../../lib/realtime/types'; interface NotificationToastProps { position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'; maxVisible?: number; autoHideDuration?: number; className?: string; } /** * Get position classes */ function getPositionClasses(position: NotificationToastProps['position']): string { switch (position) { case 'top-left': return 'top-4 left-4'; case 'bottom-right': return 'bottom-4 right-4'; case 'bottom-left': return 'bottom-4 left-4'; case 'top-right': default: return 'top-4 right-4'; } } /** * Get notification type styles */ function getTypeStyles(type: RealtimeNotification['type']): { bg: string; border: string; icon: string; iconColor: string; } { switch (type) { case 'success': return { bg: 'bg-green-50', border: 'border-green-200', icon: '✓', iconColor: 'text-green-600', }; case 'warning': return { bg: 'bg-yellow-50', border: 'border-yellow-200', icon: '⚠', iconColor: 'text-yellow-600', }; case 'error': return { bg: 'bg-red-50', border: 'border-red-200', icon: '✕', iconColor: 'text-red-600', }; case 'info': default: return { bg: 'bg-blue-50', border: 'border-blue-200', icon: 'ℹ', iconColor: 'text-blue-600', }; } } /** * Single toast notification */ function Toast({ notification, onDismiss, autoHideDuration, }: { notification: RealtimeNotification; onDismiss: (id: string) => void; autoHideDuration: number; }) { const [isVisible, setIsVisible] = useState(false); const [isLeaving, setIsLeaving] = useState(false); const styles = getTypeStyles(notification.type); // Animate in useEffect(() => { const timer = setTimeout(() => setIsVisible(true), 10); return () => clearTimeout(timer); }, []); // Auto hide useEffect(() => { if (autoHideDuration <= 0) return; const timer = setTimeout(() => { handleDismiss(); }, autoHideDuration); return () => clearTimeout(timer); }, [autoHideDuration]); const handleDismiss = useCallback(() => { setIsLeaving(true); setTimeout(() => { onDismiss(notification.id); }, 300); }, [notification.id, onDismiss]); return (
{/* Icon */} {styles.icon} {/* Content */}

{notification.title}

{notification.message}

{/* Close button */}
); } /** * Notification Toast container */ export function NotificationToast({ position = 'top-right', maxVisible = 5, autoHideDuration = 5000, className, }: NotificationToastProps) { const { notifications, dismissNotification } = useSocketContext(); // Only show non-read, non-dismissed notifications const visibleNotifications = notifications .filter((n) => !n.read && !n.dismissed) .slice(0, maxVisible); if (visibleNotifications.length === 0) { return null; } return (
{visibleNotifications.map((notification) => ( ))}
); } /** * Notification bell with badge */ export function NotificationBell({ onClick, className, }: { onClick?: () => void; className?: string; }) { const { unreadCount } = useSocketContext(); return ( ); } /** * Notification list dropdown */ export function NotificationList({ className, onClose, }: { className?: string; onClose?: () => void; }) { const { notifications, markNotificationRead, markAllRead } = useSocketContext(); return (
{/* Header */}

Notifications

{notifications.length > 0 && ( )}
{/* List */}
{notifications.length === 0 ? (
🔔

No notifications

) : ( notifications.map((notification) => (
markNotificationRead(notification.id)} >
{getTypeStyles(notification.type).icon}

{notification.title}

{notification.message}

{new Date(notification.timestamp).toLocaleTimeString()}

)) )}
); } export default NotificationToast;