import { router } from '@inertiajs/react'; import { Bell, Check, Trash2 } from 'lucide-react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; interface Notification { id: number; title: string; body: string | null; url: string | null; is_read: boolean; created_at: string; } export function NotificationBell() { const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [isOpen, setIsOpen] = useState(false); const intervalRef = useRef | null>(null); const mountedRef = useRef(true); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); const fetchUnreadCount = useCallback(async () => { try { const response = await fetch('/api/notifications/unread-count', { headers: { 'X-Requested-With': 'XMLHttpRequest', }, }); if (response.ok && mountedRef.current) { const data = await response.json(); setUnreadCount(data.count); } } catch { // Silently fail } }, []); const fetchNotifications = useCallback(async () => { try { const response = await fetch('/api/notifications', { headers: { 'X-Requested-With': 'XMLHttpRequest', }, }); if (response.ok && mountedRef.current) { const data = await response.json(); setNotifications(data); } } catch { // Silently fail } }, []); const markAsRead = useCallback(async (id: number) => { try { await fetch(`/api/notifications/${id}/read`, { method: 'PATCH', headers: { 'X-Requested-With': 'XMLHttpRequest', }, }); setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, is_read: true } : n)), ); setUnreadCount((prev) => Math.max(0, prev - 1)); } catch { // Silently fail } }, []); const deleteNotification = useCallback(async (id: number) => { try { await fetch(`/api/notifications/${id}`, { method: 'DELETE', headers: { 'X-Requested-With': 'XMLHttpRequest', }, }); setNotifications((prev) => prev.filter((n) => n.id !== id)); setUnreadCount((prev) => Math.max(0, prev - 1)); } catch { // Silently fail } }, []); const markAllAsRead = useCallback(async () => { try { await fetch('/api/notifications/read-all', { method: 'PATCH', headers: { 'X-Requested-With': 'XMLHttpRequest', }, }); setNotifications((prev) => prev.map((n) => ({ ...n, is_read: true })), ); setUnreadCount(0); } catch { // Silently fail } }, []); useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect -- initial data fetch is safe void fetchUnreadCount(); intervalRef.current = setInterval(() => { void fetchUnreadCount(); }, 30000); return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } }; }, [fetchUnreadCount]); useEffect(() => { if (isOpen) { // eslint-disable-next-line react-hooks/set-state-in-effect -- fetch on dropdown open is safe void fetchNotifications(); } }, [isOpen, fetchNotifications]); return (
Notifikasi {unreadCount > 0 && ( )}
{notifications.length === 0 ? (
Tidak ada notifikasi
) : ( notifications.map((notification) => (
{ if (notification.url) { router.visit(notification.url); } }} > {notification.title} {notification.body && ( {notification.body} )} {new Date( notification.created_at, ).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', })}
{!notification.is_read && ( )}
)) )}
); }