Files
cameleer-server/ui/src/components/NotificationBell.tsx

52 lines
1.8 KiB
TypeScript
Raw Normal View History

import { Link } from 'react-router';
import { Bell } from 'lucide-react';
import { useUnreadCount } from '../api/queries/alerts';
import { useSelectedEnv } from '../api/queries/alertMeta';
import { usePageVisible } from '../hooks/usePageVisible';
import css from './NotificationBell.module.css';
/**
* Global notification bell shown in the layout header. Links to the alerts
* inbox and renders a badge with the unread-alert count for the currently
* selected environment.
*
* Polling is driven by `useUnreadCount` (30s interval, paused in background
* via TanStack Query's `refetchIntervalInBackground: false`). The
* `usePageVisible` hook is retained as a defense-in-depth signal so future
* UI behavior (e.g. animations, live-region updates) can key off visibility
* without re-wiring the polling logic.
*
* TODO (spec §13): per-severity badge coloring the backend
* `UnreadCountResponse` currently exposes only a scalar `count` field. To
* colour the badge by max unread severity (CRITICAL error, WARNING
* amber, INFO muted) the DTO must grow a `bySeverity` map; deferred to a
* future task. Until then the badge uses a single `var(--error)` tint.
*/
export function NotificationBell() {
const env = useSelectedEnv();
// Subscribe to visibility so the component re-renders on tab focus, even
// though the polling pause itself is handled inside useUnreadCount.
usePageVisible();
const { data } = useUnreadCount();
const count = data?.count ?? 0;
if (!env) return null;
return (
<Link
to="/alerts/inbox"
role="button"
aria-label={`Notifications (${count} unread)`}
className={css.bell}
>
<Bell size={16} />
{count > 0 && (
<span className={css.badge} aria-hidden>
{count > 99 ? '99+' : count}
</span>
)}
</Link>
);
}