NeahNew/components/notification-badge.tsx
2025-05-04 12:08:32 +02:00

162 lines
6.6 KiB
TypeScript

import React, { memo, useState, useEffect } from 'react';
import Link from 'next/link';
import { Bell, Check, ExternalLink, AlertCircle } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { useNotifications } from '@/hooks/use-notifications';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from '@/components/ui/dropdown-menu';
import { formatDistanceToNow } from 'date-fns';
interface NotificationBadgeProps {
className?: string;
}
// Use React.memo to prevent unnecessary re-renders
export const NotificationBadge = memo(function NotificationBadge({ className }: NotificationBadgeProps) {
const { notifications, notificationCount, markAsRead, markAllAsRead, fetchNotifications, loading, error } = useNotifications();
const hasUnread = notificationCount.unread > 0;
const [isOpen, setIsOpen] = useState(false);
console.log('[NOTIFICATION_BADGE] Current notification count:', notificationCount);
console.log('[NOTIFICATION_BADGE] Current notifications:', notifications.length > 0 ? `${notifications.length} loaded` : 'none loaded');
console.log('[NOTIFICATION_BADGE] Loading state:', loading);
console.log('[NOTIFICATION_BADGE] Error state:', error);
// Fetch notifications when dropdown is opened
useEffect(() => {
if (isOpen) {
console.log('[NOTIFICATION_BADGE] Dropdown opened, fetching notifications');
fetchNotifications(1, 10);
}
}, [isOpen, fetchNotifications]);
const handleMarkAsRead = async (notificationId: string) => {
await markAsRead(notificationId);
};
const handleMarkAllAsRead = async () => {
await markAllAsRead();
setIsOpen(false);
};
// Force fetch when component mounts
useEffect(() => {
console.log('[NOTIFICATION_BADGE] Component mounted, fetching initial notifications');
fetchNotifications(1, 10);
}, [fetchNotifications]);
// Take the latest 10 notifications for the dropdown
const recentNotifications = notifications.slice(0, 10);
const handleOpenChange = (open: boolean) => {
setIsOpen(open);
if (open) {
// Fetch fresh notifications when dropdown opens
console.log('[NOTIFICATION_BADGE] Dropdown opened via handleOpenChange, fetching notifications');
fetchNotifications(1, 10);
}
};
return (
<div className={`relative ${className || ''}`}>
<DropdownMenu open={isOpen} onOpenChange={handleOpenChange}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="text-white/80 hover:text-white relative p-0">
<Bell className='w-5 h-5' />
{hasUnread && (
<Badge
variant="notification"
size="notification"
className="absolute -top-2 -right-2 z-50"
>
{notificationCount.unread > 99 ? '99+' : notificationCount.unread}
</Badge>
)}
<span className="sr-only">Notifications</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-80 max-h-[80vh] overflow-y-auto">
<div className="flex items-center justify-between p-4">
<h3 className="font-medium">Notifications</h3>
{notificationCount.unread > 0 && (
<Button variant="ghost" size="sm" onClick={handleMarkAllAsRead}>
<Check className="h-4 w-4 mr-2" />
Mark all read
</Button>
)}
</div>
<DropdownMenuSeparator />
{loading ? (
<div className="py-8 px-4 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-2"></div>
<p className="text-sm text-muted-foreground">Loading notifications...</p>
</div>
) : error ? (
<div className="py-8 px-4 text-center">
<AlertCircle className="h-8 w-8 text-red-500 mx-auto mb-2" />
<p className="text-sm text-red-500 mb-2">{error}</p>
<Button variant="outline" size="sm" onClick={() => fetchNotifications(1, 10)}>
Retry
</Button>
</div>
) : notifications.length === 0 ? (
<div className="py-8 px-4 text-center">
<p className="text-sm text-muted-foreground">No notifications</p>
</div>
) : (
<>
{recentNotifications.map((notification) => (
<DropdownMenuItem key={notification.id} className="px-4 py-3 cursor-default">
<div className="w-full">
<div className="flex items-start justify-between">
<div>
<p className="text-sm font-medium">
{notification.title}
{!notification.isRead && (
<Badge variant="secondary" className="ml-2 bg-blue-500 text-white">New</Badge>
)}
</p>
<p className="text-xs text-muted-foreground">
{formatDistanceToNow(new Date(notification.timestamp), { addSuffix: true })}
</p>
</div>
<div className="flex space-x-1">
{!notification.isRead && (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={() => handleMarkAsRead(notification.id)}
>
<Check className="h-3.5 w-3.5" />
<span className="sr-only">Mark as read</span>
</Button>
)}
{notification.link && (
<Link href={notification.link} target="_blank">
<Button variant="ghost" size="sm" className="h-6 w-6 p-0">
<ExternalLink className="h-3.5 w-3.5" />
<span className="sr-only">Open</span>
</Button>
</Link>
)}
</div>
</div>
<p className="text-xs mt-1">{notification.message}</p>
</div>
</DropdownMenuItem>
))}
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
});