CarboAny / src /components /NotificationBell.tsx
Esketch's picture
deploy: [P4] Strangler Pattern API Adapter Release (Orphan Clean Build v2)
daaf9d7
Raw
History Blame Contribute Delete
4.04 kB
// Notification bell icon + dropdown for sidebar
import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { useNotifications } from '../hooks/useNotifications';
export default function NotificationBell() {
const { user } = useAuth();
const navigate = useNavigate();
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const userId = user?.id;
const { notifications, unreadCount, markAsRead, markAllAsRead } = useNotifications(userId);
// Close on outside click
useEffect(() => {
function handleClick(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
}
}
if (open) document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, [open]);
return (
<div ref={ref} className="relative">
<button
type="button"
onClick={() => setOpen(!open)}
className="relative flex items-center justify-center w-8 h-8 rounded-full hover:bg-paper-cream transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4 text-ink-dark">
<path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z" />
</svg>
{unreadCount > 0 && (
<span className="absolute -top-0.5 -right-0.5 min-w-[14px] h-[14px] flex items-center justify-center text-[8px] font-bold bg-status-error text-paper-white px-0.5 rounded-full">
{unreadCount > 99 ? '99+' : unreadCount}
</span>
)}
</button>
{open && (
<div className="absolute right-0 top-full mt-1 w-72 border border-border-default bg-paper-white shadow-lg z-50 rounded-lg overflow-hidden">
<div className="flex items-center justify-between px-3 py-2 border-b border-border-default">
<p className="text-xs font-bold text-ink-black">์•Œ๋ฆผ</p>
{unreadCount > 0 && (
<button
type="button"
onClick={() => markAllAsRead()}
className="text-[10px] text-ink-medium underline"
>
๋ชจ๋‘ ์ฝ์Œ
</button>
)}
</div>
<div className="max-h-64 overflow-y-auto">
{notifications.length === 0 ? (
<p className="text-xs text-ink-light text-center py-6">์•Œ๋ฆผ์ด ์—†์Šต๋‹ˆ๋‹ค</p>
) : (
notifications.slice(0, 10).map((n) => (
<button
key={n.id}
type="button"
onClick={() => {
if (!n.read) markAsRead(n.id);
if (n.linkTo) navigate(n.linkTo);
setOpen(false);
}}
className={`w-full text-left px-3 py-2 border-b border-border-default hover:bg-paper-cream transition-colors ${
!n.read ? 'bg-paper-cream/50' : ''
}`}
>
<div className="flex items-start gap-2">
{!n.read && <span className="w-1.5 h-1.5 bg-status-info mt-1.5 shrink-0 rounded-full" />}
<div className="flex-1 min-w-0">
<p className={`text-xs ${!n.read ? 'font-semibold text-ink-black' : 'text-ink-dark'}`}>
{n.title}
</p>
<p className="text-[10px] text-ink-light truncate">{n.body}</p>
<p className="text-[10px] text-ink-light mt-0.5">
{new Date(n.createdAt).toLocaleDateString('ko-KR')}
</p>
</div>
</div>
</button>
))
)}
</div>
</div>
)}
</div>
);
}