mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 04:27:56 +00:00
4f683538d3
Add full internationalization across 127+ components and pages. 790+ translation keys in en, tr, kmr, ckb, ar, fa locales. Remove duplicate keys and delete unused .json locale files.
287 lines
8.7 KiB
TypeScript
287 lines
8.7 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import {
|
|
Bell,
|
|
MessageSquare,
|
|
DollarSign,
|
|
CheckCircle2,
|
|
XCircle,
|
|
AlertTriangle,
|
|
Star,
|
|
Loader2,
|
|
CheckCheck,
|
|
} from 'lucide-react';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { supabase } from '@/lib/supabase';
|
|
|
|
interface Notification {
|
|
id: string;
|
|
user_id: string;
|
|
type: string;
|
|
title: string;
|
|
message: string;
|
|
reference_type?: string;
|
|
reference_id?: string;
|
|
is_read: boolean;
|
|
created_at: string;
|
|
}
|
|
|
|
export function NotificationBell() {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const { user } = useAuth();
|
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
const [loading, setLoading] = useState(true);
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
// Fetch notifications
|
|
const fetchNotifications = useCallback(async () => {
|
|
if (!user) return;
|
|
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('p2p_notifications')
|
|
.select('*')
|
|
.eq('user_id', user.id)
|
|
.order('created_at', { ascending: false })
|
|
.limit(20);
|
|
|
|
if (error) throw error;
|
|
|
|
setNotifications(data || []);
|
|
setUnreadCount(data?.filter(n => !n.is_read).length || 0);
|
|
} catch (error) {
|
|
console.error('Fetch notifications error:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [user]);
|
|
|
|
// Initial fetch
|
|
useEffect(() => {
|
|
fetchNotifications();
|
|
}, [fetchNotifications]);
|
|
|
|
// Real-time subscription
|
|
useEffect(() => {
|
|
if (!user) return;
|
|
|
|
const channel = supabase
|
|
.channel(`notifications-${user.id}`)
|
|
.on(
|
|
'postgres_changes',
|
|
{
|
|
event: 'INSERT',
|
|
schema: 'public',
|
|
table: 'p2p_notifications',
|
|
filter: `user_id=eq.${user.id}`,
|
|
},
|
|
(payload) => {
|
|
const newNotif = payload.new as Notification;
|
|
setNotifications(prev => [newNotif, ...prev.slice(0, 19)]);
|
|
setUnreadCount(prev => prev + 1);
|
|
}
|
|
)
|
|
.subscribe();
|
|
|
|
return () => {
|
|
supabase.removeChannel(channel);
|
|
};
|
|
}, [user]);
|
|
|
|
// Mark as read
|
|
const markAsRead = async (notificationId: string) => {
|
|
try {
|
|
await supabase
|
|
.from('p2p_notifications')
|
|
.update({ is_read: true })
|
|
.eq('id', notificationId);
|
|
|
|
setNotifications(prev =>
|
|
prev.map(n => n.id === notificationId ? { ...n, is_read: true } : n)
|
|
);
|
|
setUnreadCount(prev => Math.max(0, prev - 1));
|
|
} catch (error) {
|
|
console.error('Mark as read error:', error);
|
|
}
|
|
};
|
|
|
|
// Mark all as read
|
|
const markAllAsRead = async () => {
|
|
if (!user) return;
|
|
|
|
try {
|
|
await supabase
|
|
.from('p2p_notifications')
|
|
.update({ is_read: true })
|
|
.eq('user_id', user.id)
|
|
.eq('is_read', false);
|
|
|
|
setNotifications(prev => prev.map(n => ({ ...n, is_read: true })));
|
|
setUnreadCount(0);
|
|
} catch (error) {
|
|
console.error('Mark all as read error:', error);
|
|
}
|
|
};
|
|
|
|
// Handle notification click
|
|
const handleClick = (notification: Notification) => {
|
|
// Mark as read
|
|
if (!notification.is_read) {
|
|
markAsRead(notification.id);
|
|
}
|
|
|
|
// Navigate to reference
|
|
if (notification.reference_type === 'trade' && notification.reference_id) {
|
|
navigate(`/p2p/trade/${notification.reference_id}`);
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
|
|
// Get icon for notification type
|
|
const getIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'new_message':
|
|
return <MessageSquare className="w-4 h-4 text-blue-400" />;
|
|
case 'payment_sent':
|
|
return <DollarSign className="w-4 h-4 text-yellow-400" />;
|
|
case 'payment_confirmed':
|
|
return <CheckCircle2 className="w-4 h-4 text-green-400" />;
|
|
case 'trade_cancelled':
|
|
return <XCircle className="w-4 h-4 text-gray-400" />;
|
|
case 'dispute_opened':
|
|
return <AlertTriangle className="w-4 h-4 text-red-400" />;
|
|
case 'new_rating':
|
|
return <Star className="w-4 h-4 text-yellow-400" />;
|
|
case 'new_order':
|
|
return <DollarSign className="w-4 h-4 text-green-400" />;
|
|
default:
|
|
return <Bell className="w-4 h-4 text-gray-400" />;
|
|
}
|
|
};
|
|
|
|
// Format time ago
|
|
const formatTimeAgo = (dateString: string) => {
|
|
const seconds = Math.floor((Date.now() - new Date(dateString).getTime()) / 1000);
|
|
if (seconds < 60) return t('p2p.justNow');
|
|
const minutes = Math.floor(seconds / 60);
|
|
if (minutes < 60) return t('p2p.minutesAgo', { count: minutes });
|
|
const hours = Math.floor(minutes / 60);
|
|
if (hours < 24) return t('p2p.hoursAgo', { count: hours });
|
|
const days = Math.floor(hours / 24);
|
|
return t('p2p.daysAgo', { count: days });
|
|
};
|
|
|
|
if (!user) return null;
|
|
|
|
return (
|
|
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="relative text-gray-400 hover:text-white"
|
|
>
|
|
<Bell className="w-5 h-5" />
|
|
{unreadCount > 0 && (
|
|
<span className="absolute -top-1 -right-1 w-5 h-5 flex items-center justify-center bg-red-500 text-white text-xs rounded-full">
|
|
{unreadCount > 9 ? '9+' : unreadCount}
|
|
</span>
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
|
|
<DropdownMenuContent
|
|
align="end"
|
|
className="w-80 bg-gray-900 border-gray-800"
|
|
>
|
|
<DropdownMenuLabel className="flex items-center justify-between">
|
|
<span className="text-white">{t('p2pNotif.title')}</span>
|
|
{unreadCount > 0 && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={markAllAsRead}
|
|
className="text-xs text-gray-400 hover:text-white h-auto py-1"
|
|
>
|
|
<CheckCheck className="w-3 h-3 mr-1" />
|
|
{t('p2pNotif.markAllRead')}
|
|
</Button>
|
|
)}
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuSeparator className="bg-gray-800" />
|
|
|
|
<ScrollArea className="h-[300px]">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2 className="w-6 h-6 animate-spin text-gray-500" />
|
|
</div>
|
|
) : notifications.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-8 text-gray-500">
|
|
<Bell className="w-8 h-8 mb-2" />
|
|
<p className="text-sm">{t('p2pNotif.noNotifications')}</p>
|
|
</div>
|
|
) : (
|
|
notifications.map((notification) => (
|
|
<DropdownMenuItem
|
|
key={notification.id}
|
|
onClick={() => handleClick(notification)}
|
|
className={`
|
|
flex items-start gap-3 p-3 cursor-pointer
|
|
${!notification.is_read ? 'bg-gray-800/50' : ''}
|
|
hover:bg-gray-800
|
|
`}
|
|
>
|
|
<div className="mt-0.5">{getIcon(notification.type)}</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className={`text-sm ${!notification.is_read ? 'text-white font-medium' : 'text-gray-300'}`}>
|
|
{notification.title}
|
|
</p>
|
|
{notification.message && (
|
|
<p className="text-xs text-gray-500 truncate">
|
|
{notification.message}
|
|
</p>
|
|
)}
|
|
<p className="text-xs text-gray-600 mt-1">
|
|
{formatTimeAgo(notification.created_at)}
|
|
</p>
|
|
</div>
|
|
{!notification.is_read && (
|
|
<div className="w-2 h-2 bg-blue-500 rounded-full mt-1.5" />
|
|
)}
|
|
</DropdownMenuItem>
|
|
))
|
|
)}
|
|
</ScrollArea>
|
|
|
|
{notifications.length > 0 && (
|
|
<>
|
|
<DropdownMenuSeparator className="bg-gray-800" />
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
navigate('/p2p/orders');
|
|
setIsOpen(false);
|
|
}}
|
|
className="justify-center text-gray-400 hover:text-white cursor-pointer"
|
|
>
|
|
{t('p2pNotif.viewAllTrades')}
|
|
</DropdownMenuItem>
|
|
</>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|