localgreenchain/components/notifications/NotificationBell.tsx
Claude 62c1ded598
Add comprehensive notification system (Agent 8)
Implement multi-channel notification system with:
- Core notification service with email, push, and in-app channels
- Email templates for all notification types (welcome, plant registered,
  transport alerts, farm alerts, harvest ready, demand matches, weekly digest)
- Push notification support with VAPID authentication
- In-app notification management with read/unread tracking
- Notification scheduler for recurring and scheduled notifications
- API endpoints for notifications CRUD, preferences, and subscriptions
- UI components (NotificationBell, NotificationList, NotificationItem,
  PreferencesForm)
- Full notifications page with preferences management
- Service worker for push notification handling
2025-11-23 03:52:41 +00:00

127 lines
4.1 KiB
TypeScript

/**
* NotificationBell Component
* Header bell icon with unread badge and dropdown
*/
import React, { useState, useEffect, useRef } from 'react';
import { NotificationList } from './NotificationList';
interface NotificationBellProps {
userId?: string;
}
export function NotificationBell({ userId = 'demo-user' }: NotificationBellProps) {
const [isOpen, setIsOpen] = useState(false);
const [unreadCount, setUnreadCount] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
fetchUnreadCount();
// Poll for new notifications every 30 seconds
const interval = setInterval(fetchUnreadCount, 30000);
return () => clearInterval(interval);
}, [userId]);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
async function fetchUnreadCount() {
try {
const response = await fetch(`/api/notifications?userId=${userId}&unreadOnly=true&limit=1`);
const data = await response.json();
if (data.success) {
setUnreadCount(data.data.unreadCount);
}
} catch (error) {
console.error('Failed to fetch unread count:', error);
} finally {
setIsLoading(false);
}
}
function handleNotificationRead() {
setUnreadCount(prev => Math.max(0, prev - 1));
}
function handleAllRead() {
setUnreadCount(0);
}
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="relative p-2 text-gray-600 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-green-500 rounded-full transition-colors"
aria-label={`Notifications${unreadCount > 0 ? ` (${unreadCount} unread)` : ''}`}
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
{!isLoading && unreadCount > 0 && (
<span className="absolute top-0 right-0 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-white transform translate-x-1/2 -translate-y-1/2 bg-red-500 rounded-full">
{unreadCount > 99 ? '99+' : unreadCount}
</span>
)}
</button>
{isOpen && (
<div className="absolute right-0 mt-2 w-96 max-h-[80vh] bg-white rounded-lg shadow-lg border border-gray-200 z-50 overflow-hidden">
<div className="p-4 border-b border-gray-100">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">Notifications</h3>
{unreadCount > 0 && (
<button
onClick={handleAllRead}
className="text-sm text-green-600 hover:text-green-700"
>
Mark all as read
</button>
)}
</div>
</div>
<div className="overflow-y-auto max-h-96">
<NotificationList
userId={userId}
onNotificationRead={handleNotificationRead}
onAllRead={handleAllRead}
compact
/>
</div>
<div className="p-3 border-t border-gray-100 bg-gray-50">
<a
href="/notifications"
className="block text-center text-sm text-green-600 hover:text-green-700"
>
View all notifications
</a>
</div>
</div>
)}
</div>
);
}