abbe7a18fc
- P0: hooks.py 3-tuple fix, trigger_dispatcher Contract, contacts/plugin unregister_actions_by_owner - P0: 5 test files — check_permission mocks removed, hardcoded DB credential → env var - P1: attachment_service DmsFile via Contract helper, restore_registry/history_hooks dedup - P1: mail/plugin restore unregister, mcp_client datetime.now(UTC), saved_views/filters patterns - P1: ProtectedRoute fail-closed, 13 test assertion fixes (bcrypt, DB-URLs, SECRET_KEYs) - P2: deprecated notifications → post_system_message (3 files), forgejo Base, report_generator lazy import - P2: webhooks permissions, deps.py/roles.py plugin perms removed, import_export default - P2: address/tags/entity_links patterns removed, worker.py Contract-Umgehungen fixed - P2: 28 frontend TODOs (hardcoded constants, deprecated notification API) - P3: dead code, duplicates, deprecated imports, private attr, __import__ inline - P3: 8 frontend TODOs (LucideIcons, inline styles, XSS, i18n) - ruff: 838 → 0 (612 auto-fix + 246 manual + 27 F821 regression fix) - F821: 30 → 0 (AutomationDefinition, DmsFile, user_id, Path, Any, String) - Contract-Umgehungen: 2 neue gefunden (worker.py:169, worker.py:280) und gefixt
76 lines
2.6 KiB
TypeScript
76 lines
2.6 KiB
TypeScript
// TODO: P2-F8 — Migrate from /notifications to communication API
|
|
/**
|
|
* 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>
|
|
);
|
|
}
|