Files
leocrm/frontend/src/components/layout/NotificationBell.tsx
T
Agent Zero abbe7a18fc fix(audit): P0-P3 audit fixes — 838 ruff errors → 0, 30 F821 bugs fixed, 118 files changed
- 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
2026-08-16 01:17:18 +02:00

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>
);
}