2026-08-16 01:17:18 +02:00
|
|
|
// TODO: P2-F8 — Migrate from /notifications to communication API
|
2026-07-26 03:02:25 +02:00
|
|
|
/**
|
|
|
|
|
* NotificationBell — bell icon with unread badge + dropdown.
|
|
|
|
|
*
|
|
|
|
|
* - Uses useUnreadNotificationCount() with 30 s polling
|
|
|
|
|
* - Bell icon (lucide-react Bell) with red badge count if > 0
|
|
|
|
|
* - Click toggles dropdown
|
|
|
|
|
* - Click outside closes dropdown
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import React, { useState, useRef, useEffect } from 'react';
|
|
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
|
import { Bell } from 'lucide-react';
|
|
|
|
|
import { useUnreadNotificationCount } from '@/api/notifications';
|
|
|
|
|
import { NotificationDropdown } from '@/components/notifications/NotificationDropdown';
|
|
|
|
|
|
|
|
|
|
export function NotificationBell() {
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
const { data: unreadCount } = useUnreadNotificationCount({
|
|
|
|
|
refetchInterval: 30_000,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Close on click outside
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!open) return;
|
|
|
|
|
const handleClickOutside = (e: MouseEvent) => {
|
|
|
|
|
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
|
|
|
setOpen(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
|
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
|
|
|
}, [open]);
|
|
|
|
|
|
|
|
|
|
// Close on Escape
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!open) return;
|
|
|
|
|
const handleEscape = (e: KeyboardEvent) => {
|
|
|
|
|
if (e.key === 'Escape') setOpen(false);
|
|
|
|
|
};
|
|
|
|
|
document.addEventListener('keydown', handleEscape);
|
|
|
|
|
return () => document.removeEventListener('keydown', handleEscape);
|
|
|
|
|
}, [open]);
|
|
|
|
|
|
|
|
|
|
const count = unreadCount ?? 0;
|
|
|
|
|
const displayCount = count > 99 ? '99+' : String(count);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div ref={containerRef} className="relative">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setOpen(!open)}
|
|
|
|
|
className="relative p-2 rounded-md hover:bg-secondary-100 text-secondary-600 min-h-touch min-w-touch flex items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
|
|
|
|
aria-label={t('notifications.title', 'Benachrichtigungen')}
|
|
|
|
|
aria-expanded={open}
|
|
|
|
|
aria-haspopup="menu"
|
|
|
|
|
>
|
|
|
|
|
<Bell className="w-5 h-5" strokeWidth={2} aria-hidden="true" />
|
|
|
|
|
|
|
|
|
|
{count > 0 && (
|
|
|
|
|
<span
|
|
|
|
|
className="absolute -top-0.5 -right-0.5 flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-danger-500 text-white text-[10px] font-bold leading-none"
|
|
|
|
|
aria-label={`${count} ungelesene Benachrichtigungen`}
|
|
|
|
|
>
|
|
|
|
|
{displayCount}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{open && <NotificationDropdown />}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|