All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 18m47s
132 lines
4.2 KiB
TypeScript
132 lines
4.2 KiB
TypeScript
// src/components/NotificationBell.tsx
|
|
|
|
/**
|
|
* Real-time notification bell component
|
|
* Displays WebSocket connection status and unread notification count
|
|
* Integrates with useWebSocket hook for real-time updates
|
|
*/
|
|
|
|
import { useState, useCallback } from 'react';
|
|
import { Bell, Wifi, WifiOff } from 'lucide-react';
|
|
import { useWebSocket } from '../hooks/useWebSocket';
|
|
import { useEventBus } from '../hooks/useEventBus';
|
|
import type { DealNotificationData } from '../types/websocket';
|
|
|
|
interface NotificationBellProps {
|
|
/**
|
|
* Callback when bell is clicked
|
|
*/
|
|
onClick?: () => void;
|
|
|
|
/**
|
|
* Whether to show the connection status indicator
|
|
* @default true
|
|
*/
|
|
showConnectionStatus?: boolean;
|
|
|
|
/**
|
|
* Custom CSS classes for the bell container
|
|
*/
|
|
className?: string;
|
|
}
|
|
|
|
export function NotificationBell({
|
|
onClick,
|
|
showConnectionStatus = true,
|
|
className = '',
|
|
}: NotificationBellProps) {
|
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
const { isConnected, error } = useWebSocket({ autoConnect: true });
|
|
|
|
// Handle incoming deal notifications
|
|
const handleDealNotification = useCallback((data?: DealNotificationData) => {
|
|
if (data) {
|
|
setUnreadCount((prev) => prev + 1);
|
|
}
|
|
}, []);
|
|
|
|
// Listen for deal notifications via event bus
|
|
useEventBus('notification:deal', handleDealNotification);
|
|
|
|
// Reset count when clicked
|
|
const handleClick = () => {
|
|
setUnreadCount(0);
|
|
onClick?.();
|
|
};
|
|
|
|
return (
|
|
<div className={`relative inline-block ${className}`}>
|
|
{/* Notification Bell Button */}
|
|
<button
|
|
onClick={handleClick}
|
|
className="relative p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
aria-label={`Notifications${unreadCount > 0 ? ` (${unreadCount} unread)` : ''}`}
|
|
title={
|
|
error
|
|
? `WebSocket error: ${error}`
|
|
: isConnected
|
|
? 'Connected to live notifications'
|
|
: 'Connecting...'
|
|
}
|
|
>
|
|
<Bell
|
|
className={`w-6 h-6 ${unreadCount > 0 ? 'text-blue-600 dark:text-blue-400' : 'text-gray-600 dark:text-gray-400'}`}
|
|
/>
|
|
|
|
{/* Unread Badge */}
|
|
{unreadCount > 0 && (
|
|
<span className="absolute top-0 right-0 inline-flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-600 rounded-full transform translate-x-1 -translate-y-1">
|
|
{unreadCount > 99 ? '99+' : unreadCount}
|
|
</span>
|
|
)}
|
|
|
|
{/* Connection Status Indicator */}
|
|
{showConnectionStatus && (
|
|
<span
|
|
className="absolute bottom-0 right-0 inline-block w-3 h-3 rounded-full border-2 border-white dark:border-gray-900 transform translate-x-1 translate-y-1"
|
|
style={{
|
|
backgroundColor: isConnected ? '#10b981' : error ? '#ef4444' : '#f59e0b',
|
|
}}
|
|
title={isConnected ? 'Connected' : error ? 'Disconnected' : 'Connecting'}
|
|
/>
|
|
)}
|
|
</button>
|
|
|
|
{/* Connection Status Tooltip (shown on hover when disconnected) */}
|
|
{!isConnected && error && (
|
|
<div className="absolute top-full right-0 mt-2 px-3 py-2 bg-gray-900 text-white text-sm rounded-lg shadow-lg whitespace-nowrap z-50 opacity-0 hover:opacity-100 transition-opacity pointer-events-none">
|
|
<div className="flex items-center gap-2">
|
|
<WifiOff className="w-4 h-4 text-red-400" />
|
|
<span>Live notifications unavailable</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Simple connection status indicator (no bell, just status)
|
|
*/
|
|
export function ConnectionStatus() {
|
|
const { isConnected, error } = useWebSocket({ autoConnect: true });
|
|
|
|
return (
|
|
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-gray-100 dark:bg-gray-800 text-sm">
|
|
{isConnected ? (
|
|
<>
|
|
<Wifi className="w-4 h-4 text-green-600 dark:text-green-400" />
|
|
<span className="text-gray-700 dark:text-gray-300">Live</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<WifiOff className="w-4 h-4 text-red-600 dark:text-red-400" />
|
|
<span className="text-gray-700 dark:text-gray-300">
|
|
{error ? 'Offline' : 'Connecting...'}
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|