All files / src/components/features/notifications/NotificationBell index.tsx

22.85% Statements 32/140
100% Branches 0/0
0% Functions 0/1
22.85% Lines 32/140

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 1411x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x                                                                                                                                                                                                                         1x 1x  
'use client';
 
import { useState, useCallback } from 'react';
import { Icon } from '@/components/ui/icons';
import { cn } from '@/lib/core';
import { NotificationPanel } from '../NotificationPanel';
import { ConnectionIndicator } from '../ConnectionIndicator';
import { useNotificationsWithFallback } from '@/hooks/useNotificationsWithFallback';
import type { NotificationPayload } from '@/lib/notifications/types';
 
export interface NotificationBellProps {
  /** Polling interval in milliseconds for fallback mode (default: 30000) */
  pollingInterval?: number;
  /** Maximum notifications to show in panel (default: 10) */
  maxNotifications?: number;
  /** Whether to show browser notifications (default: true) */
  showBrowserNotifications?: boolean;
  /** Whether to show connection indicator (default: true) */
  showConnectionStatus?: boolean;
  /** Optional class name */
  className?: string;
}
 
/**
 * NotificationBell - Bell icon with badge and dropdown panel
 *
 * Displays a notification bell icon with unread count badge.
 * Uses WebSocket for real-time updates with polling fallback.
 * Clicking opens a dropdown panel with recent notifications.
 */
export function NotificationBell({
  pollingInterval = 30000,
  maxNotifications = 10,
  showBrowserNotifications = true,
  showConnectionStatus = true,
  className,
}: NotificationBellProps) {
  const [isPanelOpen, setIsPanelOpen] = useState(false);

  const {
    notifications,
    unreadCount,
    isLoading,
    markAsRead,
    markAllAsRead,
    mode,
    isRealtimeConnected,
  } = useNotificationsWithFallback({
    pollingInterval,
    showBrowserNotifications,
  });

  // Limit notifications for display
  const displayNotifications = notifications.slice(0, maxNotifications);

  const handleTogglePanel = useCallback(() => {
    setIsPanelOpen((prev) => !prev);
  }, []);

  const handleClosePanel = useCallback(() => {
    setIsPanelOpen(false);
  }, []);

  const handleMarkAsRead = useCallback(
    async (id: string) => {
      await markAsRead(id);
    },
    [markAsRead]
  );

  const handleMarkAllAsRead = useCallback(async () => {
    await markAllAsRead();
  }, [markAllAsRead]);

  const handleNotificationClick = useCallback(
    (notification: NotificationPayload) => {
      // If notification has a link, the NotificationItem will handle navigation
      // We just close the panel
      if (!notification.read) {
        markAsRead(notification.id);
      }
      setIsPanelOpen(false);
    },
    [markAsRead]
  );

  return (
    <div className={cn('relative', className)}>
      {/* Bell Button */}
      <button
        type="button"
        onClick={handleTogglePanel}
        className={cn(
          'relative p-2 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800',
          isPanelOpen && 'text-gray-900 dark:text-white bg-gray-100 dark:bg-gray-800'
        )}
        aria-label={`Notifications${unreadCount > 0 ? ` (${unreadCount} unread)` : ''}${mode === 'realtime' ? ' - Live updates active' : ''}`}
        aria-expanded={isPanelOpen}
        aria-haspopup="dialog"
      >
        <Icon name="bell" className="w-5 h-5" />

        {/* Badge */}
        {unreadCount > 0 && (
          <span
            className="absolute -top-0.5 -right-0.5 flex items-center justify-center min-w-[18px] h-[18px] px-1 text-xs font-bold text-white bg-red-500 rounded-full"
            aria-hidden="true"
          >
            {unreadCount > 99 ? '99+' : unreadCount}
          </span>
        )}

        {/* Connection Status Indicator */}
        {showConnectionStatus && (
          <ConnectionIndicator
            isConnected={isRealtimeConnected}
            mode={mode}
            size="sm"
            showLabel={false}
            className="absolute -bottom-0.5 -right-0.5"
          />
        )}
      </button>

      {/* Notification Panel */}
      <NotificationPanel
        isOpen={isPanelOpen}
        onClose={handleClosePanel}
        notifications={displayNotifications}
        unreadCount={unreadCount}
        isLoading={isLoading}
        onMarkAsRead={handleMarkAsRead}
        onMarkAllAsRead={handleMarkAllAsRead}
        onNotificationClick={handleNotificationClick}
      />
    </div>
  );
}
 
export default NotificationBell;