Files
pn-new-crm/src/components/alerts/alert-bell.tsx
Matt Ciaccio 8699f81879
Some checks failed
Build & Push Docker Images / lint (push) Failing after 1m18s
Build & Push Docker Images / build-and-push (push) Has been skipped
chore(style): codebase em-dash sweep + minor layout polish
Replaces every em-dash and en-dash with regular ASCII hyphens
across comments, JSX strings, and dev-facing logs. Mostly cosmetic
but stops the inconsistent mix that crept in over the last few
months (some files used em-dashes in comments, others didn't,
some used both).

Bundles two small dashboard-layout tweaks that touch a couple of
already-modified files:
- (dashboard)/layout.tsx main padding goes from p-6 to pt-3 px-6
  pb-6 so page content sits closer to the topbar.
- Sidebar now receives the ports list it needs for the footer
  port switcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:57:01 +02:00

85 lines
3.0 KiB
TypeScript

'use client';
import { useState } from 'react';
import Link from 'next/link';
import { ShieldAlert } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator';
import { useUIStore } from '@/stores/ui-store';
import { cn } from '@/lib/utils';
import { AlertCard, AlertCardEmpty } from './alert-card';
import { useAlertCount, useAlertList, useAlertRealtime } from './use-alerts';
export function AlertBell() {
const portSlug = useUIStore((s) => s.currentPortSlug);
const [open, setOpen] = useState(false);
// Count is cheap (one aggregate query) - fire on every page so the badge stays live.
// List is heavier - only fetch when the popover is actually open.
const { data: count } = useAlertCount();
const { data: list, isLoading } = useAlertList('open', open);
useAlertRealtime();
const total = count?.total ?? 0;
const critical = count?.bySeverity.critical ?? 0;
const alerts = list?.data ?? [];
const top = alerts.slice(0, 5);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
className="relative"
aria-label={`Alerts${total > 0 ? ` (${total} active)` : ''}`}
data-testid="alert-bell"
>
<ShieldAlert className="h-5 w-5" />
{total > 0 ? (
<span
key={total}
data-testid="alert-bell-badge"
className={cn(
'absolute -top-0.5 -right-0.5 flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-bold text-white shadow-sm ring-2 ring-background animate-badge-pop',
critical > 0 ? 'bg-destructive' : 'bg-amber-500',
)}
>
{total > 99 ? '99+' : total}
</span>
) : null}
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-96 p-0">
<div className="flex items-center justify-between px-4 py-3">
<h4 className="text-sm font-semibold">Active alerts</h4>
<Link
href={portSlug ? (`/${portSlug}/alerts` as never) : ('/alerts' as never)}
className="text-xs text-muted-foreground hover:text-foreground"
>
View all
</Link>
</div>
<Separator />
<ScrollArea className="max-h-[420px]">
{isLoading ? (
<div className="px-4 py-6 text-center text-sm text-muted-foreground">Loading</div>
) : top.length === 0 ? (
<div className="p-3">
<AlertCardEmpty />
</div>
) : (
<div className="space-y-2 p-3">
{top.map((a) => (
<AlertCard key={a.id} alert={a} />
))}
</div>
)}
</ScrollArea>
</PopoverContent>
</Popover>
);
}