trading-platform-frontend-v2/src/modules/notifications/components/NotificationDropdown.tsx
Adrian Flores Cortes b7de2a3d58 feat: Add NotificationCenter UI components
- Add notification service for API calls
- Add notification Zustand store with WebSocket integration
- Create NotificationBell component with badge
- Create NotificationDropdown with recent notifications
- Create NotificationItem with icons and actions
- Update MainLayout to use NotificationBell

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 03:57:45 -06:00

145 lines
4.3 KiB
TypeScript

/**
* NotificationDropdown Component
* Dropdown panel showing recent notifications
*/
import { useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
import { CheckCheck, Settings, Bell } from 'lucide-react';
import { useNotificationStore } from '../../../stores/notificationStore';
import NotificationItem from './NotificationItem';
interface NotificationDropdownProps {
isOpen: boolean;
onClose: () => void;
}
export default function NotificationDropdown({ isOpen, onClose }: NotificationDropdownProps) {
const dropdownRef = useRef<HTMLDivElement>(null);
const {
notifications,
unreadCount,
loading,
fetchNotifications,
markAsRead,
markAllAsRead,
deleteNotification,
} = useNotificationStore();
// Fetch notifications when dropdown opens
useEffect(() => {
if (isOpen) {
fetchNotifications();
}
}, [isOpen, fetchNotifications]);
// Close on click outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
onClose();
}
}
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen, onClose]);
// Close on Escape key
useEffect(() => {
function handleEscape(event: KeyboardEvent) {
if (event.key === 'Escape') {
onClose();
}
}
if (isOpen) {
document.addEventListener('keydown', handleEscape);
}
return () => {
document.removeEventListener('keydown', handleEscape);
};
}, [isOpen, onClose]);
if (!isOpen) return null;
const recentNotifications = notifications.slice(0, 10);
return (
<div
ref={dropdownRef}
className="absolute right-0 top-full mt-2 w-96 bg-gray-800 border border-gray-700 rounded-xl shadow-xl z-50 overflow-hidden"
>
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700">
<h3 className="font-semibold text-white">Notificaciones</h3>
<div className="flex items-center gap-2">
{unreadCount > 0 && (
<button
onClick={() => markAllAsRead()}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-400 hover:text-white rounded hover:bg-gray-700 transition-colors"
title="Marcar todas como leídas"
>
<CheckCheck className="w-3.5 h-3.5" />
<span>Leer todas</span>
</button>
)}
<Link
to="/settings/notifications"
onClick={onClose}
className="p-1 text-gray-400 hover:text-white rounded hover:bg-gray-700 transition-colors"
title="Configuración"
>
<Settings className="w-4 h-4" />
</Link>
</div>
</div>
{/* Notification List */}
<div className="max-h-96 overflow-y-auto">
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary-500" />
</div>
) : recentNotifications.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-gray-400">
<Bell className="w-12 h-12 mb-3 opacity-50" />
<p className="text-sm">No tienes notificaciones</p>
</div>
) : (
<div className="p-2 space-y-1">
{recentNotifications.map((notification) => (
<NotificationItem
key={notification.id}
notification={notification}
onMarkAsRead={markAsRead}
onDelete={deleteNotification}
compact
/>
))}
</div>
)}
</div>
{/* Footer */}
{notifications.length > 0 && (
<div className="px-4 py-3 border-t border-gray-700 bg-gray-800/50">
<Link
to="/notifications"
onClick={onClose}
className="block text-center text-sm text-primary-400 hover:text-primary-300 transition-colors"
>
Ver todas las notificaciones
</Link>
</div>
)}
</div>
);
}