140 lines
4.8 KiB
TypeScript
140 lines
4.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { Bell, Check } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { notificationApi } from "@/lib/api/notifications";
|
|
import { Notification } from "@/types/notification";
|
|
import { formatDistanceToNow } from "date-fns";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
export function NotificationsDropdown() {
|
|
const router = useRouter();
|
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
// Fetch notifications
|
|
const fetchNotifications = async () => {
|
|
try {
|
|
const data = await notificationApi.getUnread();
|
|
setNotifications(data.items);
|
|
setUnreadCount(data.unreadCount);
|
|
} catch (error) {
|
|
console.error("Failed to fetch notifications", error);
|
|
}
|
|
};
|
|
|
|
fetchNotifications();
|
|
}, []);
|
|
|
|
const handleMarkAsRead = async (id: number, e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
await notificationApi.markAsRead(id);
|
|
setNotifications((prev) =>
|
|
prev.map((n) => (n.notification_id === id ? { ...n, is_read: true } : n))
|
|
);
|
|
setUnreadCount((prev) => Math.max(0, prev - 1));
|
|
};
|
|
|
|
const handleNotificationClick = async (notification: Notification) => {
|
|
if (!notification.is_read) {
|
|
await notificationApi.markAsRead(notification.notification_id);
|
|
setUnreadCount((prev) => Math.max(0, prev - 1));
|
|
}
|
|
setIsOpen(false);
|
|
if (notification.link) {
|
|
router.push(notification.link);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="relative">
|
|
<Bell className="h-5 w-5 text-gray-600" />
|
|
{unreadCount > 0 && (
|
|
<Badge
|
|
variant="destructive"
|
|
className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 text-[10px] rounded-full"
|
|
>
|
|
{unreadCount}
|
|
</Badge>
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
|
|
<DropdownMenuContent align="end" className="w-80">
|
|
<DropdownMenuLabel className="flex justify-between items-center">
|
|
<span>Notifications</span>
|
|
{unreadCount > 0 && (
|
|
<span className="text-xs font-normal text-muted-foreground">
|
|
{unreadCount} unread
|
|
</span>
|
|
)}
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
|
|
{notifications.length === 0 ? (
|
|
<div className="p-8 text-center text-sm text-muted-foreground">
|
|
No notifications
|
|
</div>
|
|
) : (
|
|
<div className="max-h-[400px] overflow-y-auto">
|
|
{notifications.map((notification) => (
|
|
<DropdownMenuItem
|
|
key={notification.notification_id}
|
|
className={`flex flex-col items-start p-3 cursor-pointer ${
|
|
!notification.is_read ? "bg-muted/30" : ""
|
|
}`}
|
|
onClick={() => handleNotificationClick(notification)}
|
|
>
|
|
<div className="flex w-full justify-between items-start gap-2">
|
|
<div className="font-medium text-sm line-clamp-1">
|
|
{notification.title}
|
|
</div>
|
|
{!notification.is_read && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-5 w-5 text-muted-foreground hover:text-primary"
|
|
onClick={(e) => handleMarkAsRead(notification.notification_id, e)}
|
|
title="Mark as read"
|
|
>
|
|
<Check className="h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground mt-1 line-clamp-2">
|
|
{notification.message}
|
|
</div>
|
|
<div className="text-[10px] text-muted-foreground mt-2 w-full text-right">
|
|
{formatDistanceToNow(new Date(notification.created_at), {
|
|
addSuffix: true,
|
|
})}
|
|
</div>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem className="text-center justify-center text-xs text-muted-foreground cursor-pointer">
|
|
View All Notifications
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|