265 lines
9.5 KiB
TypeScript
265 lines
9.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { usePathname } from "next/navigation";
|
|
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 { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { useNotifications } from "@/contexts/notification-context";
|
|
import {
|
|
Bell,
|
|
Check,
|
|
CheckCheck,
|
|
Trash2,
|
|
ExternalLink,
|
|
AlertCircle,
|
|
CheckCircle,
|
|
Info,
|
|
AlertTriangle,
|
|
} from "lucide-react";
|
|
import { formatDistanceToNow } from "date-fns";
|
|
import { enUS } from "date-fns/locale";
|
|
import Link from "next/link";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
|
|
export function NotificationDropdown() {
|
|
const pathname = usePathname();
|
|
const isCompanyDashboard = pathname?.startsWith("/dashboard/company");
|
|
const notificationsUrl = isCompanyDashboard
|
|
? "/dashboard/company/notifications"
|
|
: "/dashboard/candidate/notifications";
|
|
const {
|
|
notifications,
|
|
unreadCount,
|
|
markAsRead,
|
|
markAllAsRead,
|
|
removeNotification,
|
|
clearAllNotifications,
|
|
} = useNotifications();
|
|
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
const getNotificationIcon = (type: string) => {
|
|
switch (type) {
|
|
case "success":
|
|
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
|
case "error":
|
|
return <AlertCircle className="h-4 w-4 text-red-500" />;
|
|
case "warning":
|
|
return <AlertTriangle className="h-4 w-4 text-yellow-500" />;
|
|
default:
|
|
return <Info className="h-4 w-4 text-blue-500" />;
|
|
}
|
|
};
|
|
|
|
const recentNotifications = notifications.slice(0, 10);
|
|
|
|
return (
|
|
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="relative">
|
|
<Bell className="h-5 w-5" />
|
|
<AnimatePresence>
|
|
{unreadCount > 0 && (
|
|
<motion.div
|
|
initial={{ scale: 0 }}
|
|
animate={{ scale: 1 }}
|
|
exit={{ scale: 0 }}
|
|
className="absolute -top-1 -right-1"
|
|
>
|
|
<Badge
|
|
variant="destructive"
|
|
className="h-5 w-5 p-0 flex items-center justify-center text-xs"
|
|
>
|
|
{unreadCount > 9 ? "9+" : unreadCount}
|
|
</Badge>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
|
|
<DropdownMenuContent align="end" className="w-80">
|
|
<DropdownMenuLabel className="flex items-center justify-between">
|
|
<span>Notifications</span>
|
|
{unreadCount > 0 && (
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={markAllAsRead}
|
|
className="h-6 px-2 text-xs"
|
|
>
|
|
<CheckCheck className="h-3 w-3 mr-1" />
|
|
Mark all
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</DropdownMenuLabel>
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
{recentNotifications.length === 0 ? (
|
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
|
<Bell className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
|
No notifications
|
|
</div>
|
|
) : (
|
|
<ScrollArea className="h-80">
|
|
<div className="p-1">
|
|
<AnimatePresence>
|
|
{recentNotifications.map((notification, index) => (
|
|
<motion.div
|
|
key={notification.id}
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -10 }}
|
|
transition={{ delay: index * 0.05 }}
|
|
>
|
|
<div
|
|
className={`p-3 rounded-lg mb-2 relative group cursor-pointer transition-colors ${
|
|
notification.read
|
|
? "bg-muted/50 hover:bg-muted"
|
|
: "bg-primary/5 hover:bg-primary/10 border border-primary/20"
|
|
}`}
|
|
onClick={() =>
|
|
!notification.read && markAsRead(notification.id)
|
|
}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
{getNotificationIcon(notification.type)}
|
|
|
|
<div className="flex-1 min-w-0 space-y-1">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<h4
|
|
className={`text-sm font-medium leading-tight ${
|
|
notification.read
|
|
? "text-muted-foreground"
|
|
: "text-foreground"
|
|
}`}
|
|
>
|
|
{notification.title}
|
|
</h4>
|
|
|
|
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
{!notification.read && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
markAsRead(notification.id);
|
|
}}
|
|
>
|
|
<Check className="h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6 text-destructive"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
removeNotification(notification.id);
|
|
}}
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<p
|
|
className={`text-xs leading-relaxed ${
|
|
notification.read
|
|
? "text-muted-foreground"
|
|
: "text-muted-foreground"
|
|
}`}
|
|
>
|
|
{notification.message}
|
|
</p>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs text-muted-foreground">
|
|
{formatDistanceToNow(
|
|
new Date(notification.createdAt),
|
|
{
|
|
addSuffix: true,
|
|
locale: enUS,
|
|
}
|
|
)}
|
|
</span>
|
|
|
|
{notification.actionUrl &&
|
|
notification.actionLabel && (
|
|
<Link href={notification.actionUrl}>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 px-2 text-xs text-primary"
|
|
onClick={() => setIsOpen(false)}
|
|
>
|
|
{notification.actionLabel}
|
|
<ExternalLink className="h-3 w-3 ml-1" />
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{!notification.read && (
|
|
<div className="w-2 h-2 bg-primary rounded-full shrink-0 mt-1" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</AnimatePresence>
|
|
</div>
|
|
</ScrollArea>
|
|
)}
|
|
|
|
{notifications.length > 0 && (
|
|
<>
|
|
<DropdownMenuSeparator />
|
|
<div className="p-2">
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
clearAllNotifications();
|
|
setIsOpen(false);
|
|
}}
|
|
className="flex-1 text-xs"
|
|
>
|
|
<Trash2 className="h-3 w-3 mr-1" />
|
|
Clear all
|
|
</Button>
|
|
<Link href={notificationsUrl} className="flex-1">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full text-xs"
|
|
onClick={() => setIsOpen(false)}
|
|
>
|
|
View all
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|