- 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>
145 lines
4.3 KiB
TypeScript
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>
|
|
);
|
|
}
|