108 lines
5.2 KiB
TypeScript
108 lines
5.2 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useState } from "react"
|
|
import { Bell, Check, Trash2, Info, CheckCircle, AlertTriangle, XCircle } from "lucide-react"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu"
|
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
import { useNotificationsStore } from "@/lib/store/notifications-store"
|
|
import { cn } from "@/lib/utils"
|
|
import { Badge } from "@/components/ui/badge"
|
|
|
|
const getIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'success': return <CheckCircle className="h-4 w-4 text-green-500" />;
|
|
case 'warning': return <AlertTriangle className="h-4 w-4 text-amber-500" />;
|
|
case 'error': return <XCircle className="h-4 w-4 text-red-500" />;
|
|
default: return <Info className="h-4 w-4 text-blue-500" />;
|
|
}
|
|
}
|
|
|
|
export function NotificationsDropdown() {
|
|
const { notifications, unreadCount, fetchNotifications, markAsRead, markAllAsRead } = useNotificationsStore()
|
|
const [open, setOpen] = useState(false)
|
|
|
|
useEffect(() => {
|
|
// Fetch on mount and set up polling every 30s
|
|
fetchNotifications()
|
|
const interval = setInterval(fetchNotifications, 30000)
|
|
return () => clearInterval(interval)
|
|
}, [fetchNotifications])
|
|
|
|
return (
|
|
<DropdownMenu open={open} onOpenChange={setOpen}>
|
|
<DropdownMenuTrigger 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 ring-2 ring-background" />
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-80 p-0">
|
|
<div className="flex items-center justify-between p-4 border-b">
|
|
<h4 className="font-semibold leading-none">Notifications</h4>
|
|
{unreadCount > 0 && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-auto px-2 text-xs"
|
|
onClick={() => markAllAsRead()}
|
|
>
|
|
<Check className="mr-2 h-3 w-3" />
|
|
Mark all
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<ScrollArea className="h-[300px]">
|
|
{notifications.length === 0 ? (
|
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
|
No notifications
|
|
</div>
|
|
) : (
|
|
<div className="grid gap-1 p-1">
|
|
{notifications.map((notification) => (
|
|
<div
|
|
key={notification.id}
|
|
className={cn(
|
|
"flex flex-col gap-1 p-3 rounded-md hover:bg-muted/50 cursor-pointer transition-colors relative",
|
|
!notification.readAt && "bg-muted/20"
|
|
)}
|
|
onClick={() => markAsRead(notification.id)}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<div className="mt-1">
|
|
{getIcon(notification.type)}
|
|
</div>
|
|
<div className="flex-1 space-y-1">
|
|
<p className={cn("text-sm font-medium leading-none", !notification.readAt && "font-bold")}>
|
|
{notification.title}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground line-clamp-2">
|
|
{notification.message}
|
|
</p>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
{new Date(notification.createdAt).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
{!notification.readAt && (
|
|
<div className="h-2 w-2 rounded-full bg-primary mt-1" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</ScrollArea>
|
|
<div className="p-2 border-t text-center">
|
|
<Button variant="ghost" size="sm" className="w-full text-xs">View all notifications</Button>
|
|
</div>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
}
|