Implement Smart Notification Popover

This commit is contained in:
CycroftX
2026-02-03 23:00:54 +05:30
parent 5d5f1cd494
commit 7834e8f0b2
2 changed files with 276 additions and 12 deletions

View File

@@ -1,4 +1,5 @@
import { Search, Bell, ChevronDown, LogOut } from 'lucide-react';
import { Search, ChevronDown, LogOut } from 'lucide-react';
import { NotificationPopover } from '@/components/notifications/NotificationPopover';
import { cn } from '@/lib/utils';
import { useAuth } from '@/contexts/AuthContext';
import {
@@ -60,17 +61,7 @@ export function TopBar({ title, description }: TopBarProps) {
</div>
{/* Notifications */}
<button
className={cn(
"relative h-10 w-10 flex items-center justify-center rounded-xl",
"neu-button"
)}
>
<Bell className="h-5 w-5 text-muted-foreground" />
<span className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center rounded-full bg-error text-[10px] font-bold text-error-foreground">
3
</span>
</button>
<NotificationPopover />
{/* Profile Dropdown */}
<DropdownMenu>

View File

@@ -0,0 +1,273 @@
import { useState } from 'react';
import {
Bell,
Check,
CheckCircle2,
AlertTriangle,
Banknote,
Info,
X,
CreditCard
} from 'lucide-react';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/components/ui/tabs';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
// --- Types ---
export type NotificationType = 'critical' | 'info' | 'success' | 'warning';
export type NotificationCategory = 'finance' | 'security' | 'system';
export interface Notification {
id: string;
type: NotificationType;
category: NotificationCategory;
title: string;
message: string;
time: string;
actionLabel?: string;
isRead: boolean;
}
// --- Mock Data ---
const initialNotifications: Notification[] = [
{
id: '1',
type: 'critical',
category: 'security',
title: 'Suspicious Login Detected',
message: 'New login from IP 192.168.1.1 (Hamburg, DE).',
time: '2m ago',
actionLabel: 'Block',
isRead: false,
},
{
id: '2',
type: 'info',
category: 'finance',
title: 'Payment Received',
message: 'Partner "Neon Arena" settled invoice #KB-902.',
time: '1h ago',
actionLabel: 'View',
isRead: false,
},
{
id: '3',
type: 'warning',
category: 'system',
title: 'High Server Load',
message: 'CPU usage is continuously above 85%.',
time: '3h ago',
isRead: true,
},
{
id: '4',
type: 'success',
category: 'finance',
title: 'Payout Processed',
message: 'Monthly payouts for 12 partners completed.',
time: '5h ago',
isRead: true,
},
{
id: '5',
type: 'info',
category: 'system',
title: 'System Update',
message: 'Eventify Core v2.4.0 is now live.',
time: '1d ago',
isRead: true,
},
];
export function NotificationPopover() {
const [notifications, setNotifications] = useState<Notification[]>(initialNotifications);
const [isOpen, setIsOpen] = useState(false);
const unreadCount = notifications.filter(n => !n.isRead && n.type === 'critical').length;
const allUnreadCount = notifications.filter(n => !n.isRead).length;
const markAsRead = (id: string) => {
setNotifications(prev => prev.map(n => n.id === id ? { ...n, isRead: true } : n));
};
const markAllAsRead = () => {
setNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
};
const deleteNotification = (id: string, e: React.MouseEvent) => {
e.stopPropagation();
setNotifications(prev => prev.filter(n => n.id !== id));
};
const getIcon = (type: NotificationType, category: NotificationCategory) => {
if (category === 'finance') return <Banknote className="h-4 w-4" />;
if (type === 'critical') return <AlertTriangle className="h-4 w-4" />;
if (type === 'success') return <CheckCircle2 className="h-4 w-4" />;
return <Info className="h-4 w-4" />;
};
const getStyles = (n: Notification) => {
if (n.type === 'critical') return "bg-red-500/10 border-l-4 border-red-500 hover:bg-red-500/15";
if (n.category === 'finance') return "bg-blue-500/10 border-l-4 border-blue-500 hover:bg-blue-500/15";
if (n.type === 'success') return "bg-green-500/10 border-l-4 border-green-500 hover:bg-green-500/15";
return "bg-secondary/30 border-l-4 border-secondary hover:bg-secondary/50";
};
const NotificationList = ({ items }: { items: Notification[] }) => {
if (items.length === 0) {
return (
<div className="flex flex-col items-center justify-center h-[300px] text-center p-4">
<div className="h-16 w-16 bg-secondary/50 rounded-full flex items-center justify-center mb-4">
<Check className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="font-semibold text-foreground">All caught up!</h3>
<p className="text-sm text-muted-foreground">No new alerts to review at this time.</p>
</div>
);
}
return (
<div className="flex flex-col gap-2 p-1">
{items.map((n) => (
<div
key={n.id}
className={cn(
"relative p-3 rounded-md transition-all cursor-pointer group",
getStyles(n),
n.isRead && "opacity-60 bg-transparent border-border hover:bg-secondary/10"
)}
onClick={() => markAsRead(n.id)}
>
<div className="flex items-start gap-3">
<div className={cn(
"h-8 w-8 rounded-full flex items-center justify-center shrink-0",
n.type === 'critical' ? "text-red-500 bg-red-500/20" :
n.category === 'finance' ? "text-blue-500 bg-blue-500/20" :
"text-muted-foreground bg-secondary"
)}>
{getIcon(n.type, n.category)}
</div>
<div className="flex-1 space-y-1">
<div className="flex items-center justify-between">
<p className={cn("text-sm font-semibold", !n.isRead && "text-foreground")}>
{n.title}
</p>
<span className="text-[10px] text-muted-foreground">{n.time}</span>
</div>
<p className="text-xs text-muted-foreground line-clamp-2">
{n.message}
</p>
{n.actionLabel && (
<Button
variant="outline"
size="sm"
className="h-6 text-[10px] mt-2 px-2 border-primary/20 hover:bg-primary/5 hover:text-primary"
onClick={(e) => {
e.stopPropagation();
markAsRead(n.id);
}}
>
{n.actionLabel}
</Button>
)}
</div>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity absolute top-2 right-2 md:static md:opacity-100"
onClick={(e) => deleteNotification(n.id, e)}
>
<X className="h-3 w-3 text-muted-foreground" />
</Button>
</div>
{!n.isRead && (
<span className="absolute top-3 right-3 h-2 w-2 rounded-full bg-primary animate-pulse md:hidden" />
)}
</div>
))}
</div>
);
};
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<button className="relative h-10 w-10 flex items-center justify-center rounded-xl neu-button outline-none">
<Bell className="h-5 w-5 text-muted-foreground" />
{unreadCount > 0 && (
<span className="absolute top-2 right-2 h-2.5 w-2.5 rounded-full bg-red-500 border-2 border-background flex items-center justify-center">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
</span>
)}
</button>
</PopoverTrigger>
<PopoverContent className="w-[380px] p-0 mr-4" align="end">
<Tabs defaultValue="all" className="w-full">
<div className="flex items-center justify-between px-4 py-3 border-b border-border/50">
<h4 className="font-semibold">Notifications</h4>
{allUnreadCount > 0 && (
<Button
variant="ghost"
size="sm"
className="h-auto px-2 py-0.5 text-xs text-primary hover:text-primary/80"
onClick={markAllAsRead}
>
Mark all as read
</Button>
)}
</div>
<TabsList className="w-full justify-start rounded-none border-b border-border/50 bg-transparent p-0 h-auto">
<TabsTrigger
value="all"
className="rounded-none border-b-2 border-transparent px-4 py-2 hover:bg-secondary/20 data-[state=active]:border-primary data-[state=active]:bg-secondary/10"
>
All
</TabsTrigger>
<TabsTrigger
value="alerts"
className="rounded-none border-b-2 border-transparent px-4 py-2 hover:bg-secondary/20 data-[state=active]:border-red-500 data-[state=active]:text-red-500 data-[state=active]:bg-red-500/5"
>
Alerts
{unreadCount > 0 && (
<Badge variant="secondary" className="ml-2 h-4 px-1 text-[10px] bg-red-500 text-white hover:bg-red-500">
{unreadCount}
</Badge>
)}
</TabsTrigger>
<TabsTrigger
value="finance"
className="rounded-none border-b-2 border-transparent px-4 py-2 hover:bg-secondary/20 data-[state=active]:border-blue-500 data-[state=active]:text-blue-500 data-[state=active]:bg-blue-500/5"
>
Finance
</TabsTrigger>
</TabsList>
<ScrollArea className="h-[400px]">
<TabsContent value="all" className="m-0 p-2">
<NotificationList items={notifications} />
</TabsContent>
<TabsContent value="alerts" className="m-0 p-2">
<NotificationList items={notifications.filter(n => n.type === 'critical')} />
</TabsContent>
<TabsContent value="finance" className="m-0 p-2">
<NotificationList items={notifications.filter(n => n.category === 'finance')} />
</TabsContent>
</ScrollArea>
</Tabs>
</PopoverContent>
</Popover>
);
}