gohorsejobs/frontend/src/components/notification-dropdown.tsx
2025-12-22 15:30:06 -03:00

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>
);
}