Spaces:
Sleeping
Sleeping
File size: 2,966 Bytes
9dfccd9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | import * as Dialog from '@radix-ui/react-dialog'
import { useNotifications, type AppNotification } from '@/hooks/useNotifications'
import { cn } from '@/lib/utils'
const TYPE_ICON: Record<AppNotification['type'], string> = {
query_answered: 'β',
escalation_spike: 'β ',
breaking_change: 'π΄',
data_sync_failed: 'β',
knowledge_gap: 'π‘',
}
interface Props {
open: boolean
onClose: () => void
}
export function NotificationCenter({ open, onClose }: Props) {
const { notifications, unreadCount, markRead, markAllRead } = useNotifications()
return (
<Dialog.Root open={open} onOpenChange={(o) => { if (!o) onClose() }}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 z-40 bg-black/20" />
<Dialog.Content className="fixed right-4 top-16 z-50 flex w-80 flex-col overflow-hidden rounded-xl border border-surface-subtle bg-white shadow-xl dark:bg-stone-900">
<div className="flex items-center justify-between border-b border-surface-subtle px-4 py-3">
<Dialog.Title className="text-sm font-semibold">
Notifications {unreadCount > 0 && <span className="ml-1 text-brand">({unreadCount})</span>}
</Dialog.Title>
<div className="flex items-center gap-2">
{unreadCount > 0 && (
<button onClick={markAllRead} className="text-xs text-stone-400 hover:text-stone-600">
Mark all read
</button>
)}
<Dialog.Close className="text-stone-400 hover:text-stone-600" aria-label="Close">β</Dialog.Close>
</div>
</div>
<div className="max-h-[480px] overflow-y-auto">
{notifications.length === 0 && (
<p className="px-4 py-8 text-center text-sm text-stone-400">No notifications yet</p>
)}
{notifications.map((n) => (
<button
key={n.id}
onClick={() => markRead(n.id)}
className={cn(
'flex w-full items-start gap-3 px-4 py-3 text-left text-sm transition-colors hover:bg-stone-50 dark:hover:bg-stone-800',
!n.read && 'bg-brand/5',
)}
>
<span className="mt-0.5 shrink-0 text-base" aria-hidden>
{TYPE_ICON[n.type]}
</span>
<div className="min-w-0 flex-1">
<p className={cn('text-stone-700 dark:text-stone-300', !n.read && 'font-medium')}>
{n.message}
</p>
<p className="mt-0.5 text-xs text-stone-400">{n.timestamp}</p>
</div>
{!n.read && (
<span className="mt-1.5 h-2 w-2 shrink-0 rounded-full bg-brand" aria-hidden />
)}
</button>
))}
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
)
}
|