veza/apps/web/src/components/notifications/NotificationBell.tsx
senke 6974c12a25 aesthetic-improvements: align spacing to 8px grid (Action 11.2.1.3)
- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid
- Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px)
- Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation
- Modified files across all components to ensure consistent 8px grid alignment
- Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
2026-01-16 11:50:46 +01:00

107 lines
3.6 KiB
TypeScript

import React, { useState, useRef, useEffect } from 'react';
import { Bell, Check } from 'lucide-react';
import { Button } from '../ui/button';
import { Notification } from '../../types';
import { NotificationItem } from './NotificationItem';
interface NotificationBellProps {
notifications: Notification[];
onMarkAllRead: () => void;
onRead: (id: string) => void;
onViewAll: () => void;
}
export const NotificationBell: React.FC<NotificationBellProps> = ({
notifications,
onMarkAllRead,
onRead,
onViewAll,
}) => {
const [isOpen, setIsOpen] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
const unreadCount = notifications.filter((n) => !n.read).length;
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
wrapperRef.current &&
!wrapperRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div ref={wrapperRef} className="relative z-50">
<Button
variant="ghost"
size="sm"
className={`relative text-kodo-secondary hover:text-kodo-primary ${isOpen ? 'text-kodo-primary bg-white/5' : ''}`}
onClick={() => setIsOpen(!isOpen)}
>
<Bell className="w-5 h-5" />
{unreadCount > 0 && (
<span className="absolute top-1.5 right-2 w-2 h-2 bg-kodo-red rounded-full border-2 border-kodo-void animate-pulse"></span>
)}
</Button>
{isOpen && (
<div className="absolute top-full right-0 mt-4 w-96 bg-kodo-graphite border border-kodo-steel rounded-xl shadow-2xl overflow-hidden animate-fadeIn origin-top-right ring-1 ring-white/5 flex flex-col max-h-[80vh]">
<div className="p-4 border-b border-kodo-steel/50 flex justify-between items-center bg-kodo-ink">
<div>
<h3 className="font-bold text-white">Notifications</h3>
<p className="text-xs text-kodo-content-dim">{unreadCount} unread</p>
</div>
{unreadCount > 0 && (
<button
onClick={onMarkAllRead}
className="text-xs text-kodo-steel hover:underline flex items-center gap-1"
>
<Check className="w-3 h-3" /> Mark all read
</button>
)}
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar p-2 space-y-1">
{notifications.length === 0 ? (
<div className="text-center py-12 text-kodo-content-dim">
<Bell className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">No new notifications</p>
</div>
) : (
notifications.slice(0, 5).map((n) => (
<NotificationItem
key={n.id}
notification={n}
onRead={onRead}
onAction={() => {
setIsOpen(false);
onViewAll();
}}
/>
))
)}
</div>
<div className="p-2 border-t border-kodo-steel/30 bg-kodo-ink/50">
<Button
variant="ghost"
size="sm"
className="w-full text-xs"
onClick={() => {
setIsOpen(false);
onViewAll();
}}
>
View All Notifications
</Button>
</div>
</div>
)}
</div>
);
};