mirror of
https://github.com/pezkuwichain/pezkuwi-sdk-ui.git
synced 2026-04-21 23:47:59 +00:00
208 lines
6.2 KiB
TypeScript
208 lines
6.2 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Bell, Check, Trash2 } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from '@/components/ui/popover';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { supabase } from '@/lib/supabase';
|
|
import { formatDistanceToNow } from 'date-fns';
|
|
|
|
interface Notification {
|
|
id: string;
|
|
title: string;
|
|
message: string;
|
|
type: 'info' | 'success' | 'warning' | 'error' | 'system';
|
|
read: boolean;
|
|
action_url?: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export default function NotificationBell() {
|
|
const { user } = useAuth();
|
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
const [open, setOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (user) {
|
|
loadNotifications();
|
|
|
|
subscribeToNotifications();
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [user]);
|
|
|
|
const loadNotifications = async () => {
|
|
if (!user) return;
|
|
|
|
const { data } = await supabase
|
|
.from('notifications')
|
|
.select('*')
|
|
.eq('user_id', user.id)
|
|
.order('created_at', { ascending: false })
|
|
.limit(10);
|
|
|
|
if (data) {
|
|
setNotifications(data);
|
|
setUnreadCount(data.filter(n => !n.read).length);
|
|
}
|
|
};
|
|
|
|
const subscribeToNotifications = () => {
|
|
const channel = supabase
|
|
.channel('notifications')
|
|
.on(
|
|
'postgres_changes',
|
|
{
|
|
event: 'INSERT',
|
|
schema: 'public',
|
|
table: 'notifications',
|
|
filter: `user_id=eq.${user?.id}`,
|
|
},
|
|
(payload) => {
|
|
setNotifications(prev => [payload.new as Notification, ...prev]);
|
|
setUnreadCount(prev => prev + 1);
|
|
}
|
|
)
|
|
.subscribe();
|
|
|
|
return () => {
|
|
supabase.removeChannel(channel);
|
|
};
|
|
};
|
|
|
|
const markAsRead = async (notificationId: string) => {
|
|
await supabase.functions.invoke('notifications-manager', {
|
|
body: {
|
|
action: 'markRead',
|
|
userId: user?.id,
|
|
notificationId
|
|
}
|
|
});
|
|
|
|
setNotifications(prev =>
|
|
prev.map(n => n.id === notificationId ? { ...n, read: true } : n)
|
|
);
|
|
setUnreadCount(prev => Math.max(0, prev - 1));
|
|
};
|
|
|
|
const markAllAsRead = async () => {
|
|
await supabase.functions.invoke('notifications-manager', {
|
|
body: {
|
|
action: 'markAllRead',
|
|
userId: user?.id
|
|
}
|
|
});
|
|
|
|
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
|
|
setUnreadCount(0);
|
|
};
|
|
|
|
const deleteNotification = async (notificationId: string) => {
|
|
await supabase.functions.invoke('notifications-manager', {
|
|
body: {
|
|
action: 'delete',
|
|
userId: user?.id,
|
|
notificationId
|
|
}
|
|
});
|
|
|
|
setNotifications(prev => prev.filter(n => n.id !== notificationId));
|
|
};
|
|
|
|
const getTypeColor = (type: string) => {
|
|
switch (type) {
|
|
case 'success': return 'text-green-600';
|
|
case 'warning': return 'text-yellow-600';
|
|
case 'error': return 'text-red-600';
|
|
case 'system': return 'text-blue-600';
|
|
default: return 'text-gray-600';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="relative">
|
|
<Bell className="h-5 w-5" />
|
|
{unreadCount > 0 && (
|
|
<Badge className="absolute -top-1 -right-1 h-5 w-5 p-0 flex items-center justify-center">
|
|
{unreadCount}
|
|
</Badge>
|
|
)}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-80 p-0" align="end">
|
|
<div className="flex items-center justify-between p-4 border-b">
|
|
<h3 className="font-semibold">Notifications</h3>
|
|
{unreadCount > 0 && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={markAllAsRead}
|
|
>
|
|
Mark all read
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<ScrollArea className="h-96">
|
|
{notifications.length === 0 ? (
|
|
<div className="p-4 text-center text-muted-foreground">
|
|
No notifications
|
|
</div>
|
|
) : (
|
|
<div className="divide-y">
|
|
{notifications.map((notification) => (
|
|
<div
|
|
key={notification.id}
|
|
className={`p-4 hover:bg-muted/50 transition-colors ${
|
|
!notification.read ? 'bg-muted/20' : ''
|
|
}`}
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<p className={`font-medium ${getTypeColor(notification.type)}`}>
|
|
{notification.title}
|
|
</p>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
{notification.message}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
{formatDistanceToNow(new Date(notification.created_at), { addSuffix: true })}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-1 ml-2">
|
|
{!notification.read && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={() => markAsRead(notification.id)}
|
|
>
|
|
<Check className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={() => deleteNotification(notification.id)}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</ScrollArea>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
} |