Files
flyer-crawler.projectium.com/src/components/NotificationBell.tsx
Torben Sorensen cf476e7afc
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 18m47s
ADR-022 - websocket notificaitons - also more test fixes with stores
2026-01-19 10:53:42 -08:00

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