elmeg-demo/frontend/components/layout/notification-bell.tsx

140 lines
5.6 KiB
TypeScript

"use client"
import { Bell } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { useEffect, useState } from "react"
import { getApiUrl } from "@/lib/api-config"
import Link from "next/link"
import { cn } from "@/lib/utils"
interface Notification {
id: number
type: string
title: string
message: string
link?: string
is_read: boolean
created_at: string
}
export function NotificationBell() {
const [notifications, setNotifications] = useState<Notification[]>([])
const [unreadCount, setUnreadCount] = useState(0)
const [open, setOpen] = useState(false)
const fetchNotifications = () => {
const token = localStorage.getItem("token")
if (!token) return
fetch(`${getApiUrl()}/notifications/`, {
headers: { Authorization: `Bearer ${token}` }
})
.then(res => res.json())
.then(data => {
setNotifications(data)
setUnreadCount(data.filter((n: Notification) => !n.is_read).length)
})
.catch(console.error)
}
useEffect(() => {
fetchNotifications()
// Poll every 60 seconds
const interval = setInterval(fetchNotifications, 60000)
return () => clearInterval(interval)
}, [])
const markAsRead = (id: number) => {
const token = localStorage.getItem("token")
if (!token) return
fetch(`${getApiUrl()}/notifications/${id}/read`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` }
}).then(() => {
setNotifications(prev => prev.map(n => n.id === id ? { ...n, is_read: true } : n))
setUnreadCount(prev => Math.max(0, prev - 1))
})
}
const markAllRead = () => {
const token = localStorage.getItem("token")
if (!token) return
fetch(`${getApiUrl()}/notifications/mark-all-read`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` }
}).then(() => {
setNotifications(prev => prev.map(n => ({ ...n, is_read: true })))
setUnreadCount(0)
})
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="relative">
<Bell className="h-5 w-5" />
{unreadCount > 0 && (
<span className="absolute top-1.5 right-1.5 h-2 w-2 rounded-full bg-red-600" />
)}
<span className="sr-only">Notifications</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align="end">
<div className="flex items-center justify-between p-4 border-b">
<h4 className="font-semibold">Notifications</h4>
{unreadCount > 0 && (
<Button variant="ghost" size="sm" onClick={markAllRead} className="text-xs h-auto p-0 text-muted-foreground hover:text-foreground">
Mark all read
</Button>
)}
</div>
<div className="max-h-[300px] overflow-y-auto">
{notifications.length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground">
No notifications
</div>
) : (
notifications.map((notification) => (
<div
key={notification.id}
className={cn(
"p-4 border-b last:border-0 hover:bg-muted/50 transition-colors cursor-pointer",
!notification.is_read && "bg-muted/20"
)}
onClick={() => {
if (!notification.is_read) markAsRead(notification.id)
}}
>
<Link href={notification.link || "#"} className="block">
<div className="flex items-start justify-between gap-2">
<div className="space-y-1">
<p className="text-sm font-medium leading-none">
{notification.title}
</p>
<p className="text-sm text-muted-foreground line-clamp-2">
{notification.message}
</p>
<p className="text-xs text-muted-foreground">
{new Date(notification.created_at).toLocaleDateString()}
</p>
</div>
{!notification.is_read && (
<span className="h-2 w-2 rounded-full bg-blue-600 flex-shrink-0 mt-1" />
)}
</div>
</Link>
</div>
))
)}
</div>
</PopoverContent>
</Popover>
)
}