Implement Smart Notification Popover
This commit is contained in:
@@ -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 { cn } from '@/lib/utils';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import {
|
import {
|
||||||
@@ -60,17 +61,7 @@ export function TopBar({ title, description }: TopBarProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Notifications */}
|
{/* Notifications */}
|
||||||
<button
|
<NotificationPopover />
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Profile Dropdown */}
|
{/* Profile Dropdown */}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|||||||
273
src/components/notifications/NotificationPopover.tsx
Normal file
273
src/components/notifications/NotificationPopover.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user