| |
| 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); |
|
|
| |
| 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> |
| ); |
| } |
|
|