From c7a2755aaf6f7fc553e1e9929d910f1b69107cc9 Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Sat, 8 Mar 2025 06:35:41 +0000 Subject: [PATCH] notif fix >.< --- components/layout/layout.tsx | 6 +- .../notifications/OrderNotifications.tsx | 228 ++++++++ .../notifications/UnifiedNotifications.tsx | 511 ++++++++++++++++++ 3 files changed, 742 insertions(+), 3 deletions(-) create mode 100644 components/notifications/OrderNotifications.tsx create mode 100644 components/notifications/UnifiedNotifications.tsx diff --git a/components/layout/layout.tsx b/components/layout/layout.tsx index 94bf1b3..62816e8 100644 --- a/components/layout/layout.tsx +++ b/components/layout/layout.tsx @@ -3,8 +3,8 @@ import { useState, useEffect } from "react" import { useTheme } from "next-themes" import Sidebar from "./sidebar" -import ChatNotifications from "@/components/dashboard/ChatNotifications" -import type React from "react" // Added import for React +import UnifiedNotifications from "@/components/notifications/UnifiedNotifications" +import type React from "react" interface LayoutProps { children: React.ReactNode @@ -24,7 +24,7 @@ export default function Layout({ children }: LayoutProps) {
- +
{children}
diff --git a/components/notifications/OrderNotifications.tsx b/components/notifications/OrderNotifications.tsx new file mode 100644 index 0000000..31d74e3 --- /dev/null +++ b/components/notifications/OrderNotifications.tsx @@ -0,0 +1,228 @@ +"use client"; + +import { useEffect, useState, useRef } from "react"; +import { clientFetch } from "@/lib/client-utils"; +import { toast } from "sonner"; +import { Package, Bell } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +interface Order { + _id: string; + orderId: string; + status: string; + totalPrice: number; + orderDate: string; + items?: Array<{ name: string; quantity: number }>; + customerName?: string; +} + +export default function OrderNotifications() { + const router = useRouter(); + const [newOrders, setNewOrders] = useState([]); + const [loading, setLoading] = useState(true); + const seenOrderIds = useRef>(new Set()); + const isInitialFetch = useRef(true); + const audioRef = useRef(null); + + useEffect(() => { + audioRef.current = new Audio('/notification.mp3'); + + // Fallback if notification.mp3 doesn't exist + audioRef.current.addEventListener('error', () => { + audioRef.current = null; + }); + + return () => { + if (audioRef.current) { + audioRef.current = null; + } + }; + }, []); + + // Function to play notification sound + const playNotificationSound = () => { + if (audioRef.current) { + audioRef.current.currentTime = 0; + audioRef.current.play().catch(err => { + console.log('Error playing sound:', err); + // Fallback to simple beep if audio file fails + try { + const context = new (window.AudioContext || (window as any).webkitAudioContext)(); + const oscillator = context.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(800, context.currentTime); + oscillator.connect(context.destination); + oscillator.start(); + oscillator.stop(context.currentTime + 0.2); + } catch (e) { + console.error('Could not play fallback audio', e); + } + }); + } + }; + + useEffect(() => { + // Only run this on dashboard pages + if (typeof window === 'undefined' || !window.location.pathname.includes("/dashboard")) return; + + const checkForNewOrders = async () => { + try { + // Fetch orders from the last 24 hours that are in paid status + const orderData = await clientFetch("/orders?status=paid&limit=10"); + const orders: Order[] = orderData.orders || []; + + // If this is the first fetch, just store the orders without notifications + if (isInitialFetch.current) { + orders.forEach(order => seenOrderIds.current.add(order._id)); + isInitialFetch.current = false; + setLoading(false); + return; + } + + // Check for new paid orders that haven't been seen before + const latestNewOrders = orders.filter(order => !seenOrderIds.current.has(order._id)); + + // Show notifications for new orders + if (latestNewOrders.length > 0) { + // Update the seen orders set + latestNewOrders.forEach(order => seenOrderIds.current.add(order._id)); + + // Show a notification for each new order + latestNewOrders.forEach(order => { + toast.success( +
+

New Paid Order!

+

Order #{order.orderId}

+

£{order.totalPrice.toFixed(2)}

+
, + { + duration: 8000, + icon: , + action: { + label: "View", + onClick: () => window.open(`/dashboard/orders/${order._id}`, "_blank") + } + } + ); + }); + + // Play notification sound + playNotificationSound(); + + // Update the state with new orders for the dropdown + setNewOrders(prev => [...latestNewOrders, ...prev].slice(0, 10)); + } + + setLoading(false); + } catch (error) { + console.error("Error checking for new orders:", error); + setLoading(false); + } + }; + + // Check immediately on component mount (with a small delay) + const initialTimeout = setTimeout(() => { + checkForNewOrders(); + }, 2000); + + // Set up polling interval (every 60 seconds) + const interval = setInterval(checkForNewOrders, 60000); + + // Clean up + return () => { + clearTimeout(initialTimeout); + clearInterval(interval); + }; + }, []); + + const handleOrderClick = (orderId: string) => { + router.push(`/dashboard/orders/${orderId}`); + }; + + const clearNotifications = () => { + setNewOrders([]); + }; + + // Format the price as currency + const formatPrice = (price: number) => { + return `£${price.toFixed(2)}`; + }; + + return ( + + + + + +
+

New Paid Orders

+ {newOrders.length > 0 && ( + + )} +
+ + {newOrders.length === 0 ? ( +
+

No new paid orders

+
+ ) : ( + <> +
+ {newOrders.map((order) => ( + handleOrderClick(order._id)} + > +
+
+

Order #{order.orderId}

+

+ {formatPrice(order.totalPrice)} +

+
+ Paid +
+
+ ))} +
+
+ +
+ + )} +
+
+ ); +} \ No newline at end of file diff --git a/components/notifications/UnifiedNotifications.tsx b/components/notifications/UnifiedNotifications.tsx new file mode 100644 index 0000000..6af0533 --- /dev/null +++ b/components/notifications/UnifiedNotifications.tsx @@ -0,0 +1,511 @@ +"use client"; + +import React, { useState, useEffect, useRef } from "react"; +import { useRouter } from "next/navigation"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { BellRing, Package, MessageCircle } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { clientFetch } from "@/lib/client-utils"; +import { toast } from "sonner"; +import { getCookie } from "@/lib/client-utils"; +import axios from "axios"; + +interface Order { + _id: string; + orderId: string; + status: string; + totalPrice: number; + orderDate: string; +} + +interface ChatMessage { + chatId: string; + buyerId: string; + messageCount: number; +} + +interface UnreadCounts { + totalUnread: number; + chatCounts: Record; +} + +export default function UnifiedNotifications() { + const router = useRouter(); + + // Chat notification state + const [unreadCounts, setUnreadCounts] = useState({ totalUnread: 0, chatCounts: {} }); + const [previousUnreadTotal, setPreviousUnreadTotal] = useState(0); + const [chatMetadata, setChatMetadata] = useState>({}); + + // Order notification state + const [newOrders, setNewOrders] = useState([]); + const seenOrderIds = useRef>(new Set()); + const isInitialOrdersFetch = useRef(true); + + // Shared state + const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState("all"); + const audioRef = useRef(null); + + // Total notifications count + const totalNotifications = unreadCounts.totalUnread + newOrders.length; + + // Initialize audio + useEffect(() => { + audioRef.current = new Audio('/notification.mp3'); + + audioRef.current.addEventListener('error', () => { + audioRef.current = null; + }); + + return () => { + if (audioRef.current) { + audioRef.current = null; + } + }; + }, []); + + // Function to play notification sound + const playNotificationSound = () => { + if (audioRef.current) { + audioRef.current.currentTime = 0; + audioRef.current.play().catch(err => { + console.log('Error playing sound:', err); + // Fallback beep if audio file fails + try { + const context = new (window.AudioContext || (window as any).webkitAudioContext)(); + const oscillator = context.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(800, context.currentTime); + oscillator.connect(context.destination); + oscillator.start(); + oscillator.stop(context.currentTime + 0.2); + } catch (e) { + console.error('Could not play fallback audio', e); + } + }); + } + }; + + // Check for new paid orders + useEffect(() => { + // Only run this on dashboard pages + if (typeof window === 'undefined' || !window.location.pathname.includes("/dashboard")) return; + + const checkForNewOrders = async () => { + try { + // Fetch orders from the last 24 hours that are in paid status + const orderData = await clientFetch("/orders?status=paid&limit=10"); + const orders: Order[] = orderData.orders || []; + + // If this is the first fetch, just store the orders without notifications + if (isInitialOrdersFetch.current) { + orders.forEach(order => seenOrderIds.current.add(order._id)); + isInitialOrdersFetch.current = false; + return; + } + + // Check for new paid orders that haven't been seen before + const latestNewOrders = orders.filter(order => !seenOrderIds.current.has(order._id)); + + // Show notifications for new orders + if (latestNewOrders.length > 0) { + // Update the seen orders set + latestNewOrders.forEach(order => seenOrderIds.current.add(order._id)); + + // Show a toast notification for each new order + latestNewOrders.forEach(order => { + toast.success( +
+

New Paid Order!

+

Order #{order.orderId}

+

£{order.totalPrice.toFixed(2)}

+
, + { + duration: 8000, + icon: , + action: { + label: "View", + onClick: () => window.open(`/dashboard/orders/${order._id}`, "_blank") + } + } + ); + }); + + // Play notification sound + playNotificationSound(); + + // Update the state with new orders for the dropdown + setNewOrders(prev => [...latestNewOrders, ...prev].slice(0, 10)); + } + } catch (error) { + console.error("Error checking for new orders:", error); + } + }; + + // Check for new orders every minute + const orderInterval = setInterval(checkForNewOrders, 60000); + + // Initial check for orders + checkForNewOrders(); + + return () => { + clearInterval(orderInterval); + }; + }, []); + + // Fetch unread chat counts + useEffect(() => { + // Only run this on dashboard pages + if (typeof window === 'undefined' || !window.location.pathname.includes("/dashboard")) return; + + const fetchUnreadCounts = async () => { + try { + const authToken = getCookie("Authorization"); + + if (!authToken) return; + + const authAxios = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, + headers: { + Authorization: `Bearer ${authToken}` + } + }); + + // Get vendor info from profile endpoint + const vendorResponse = await authAxios.get('/auth/me'); + + // Access correct property - the vendor ID is in vendor._id + const vendorId = vendorResponse.data.vendor?._id; + + if (!vendorId) { + console.error("Vendor ID not found in profile response:", vendorResponse.data); + return; + } + + const response = await authAxios.get(`/chats/vendor/${vendorId}/unread`); + + // Check if there are new notifications and play sound if needed + if (!loading && response.data.totalUnread > previousUnreadTotal) { + playNotificationSound(); + } + + // Update chat state + setUnreadCounts(response.data); + setPreviousUnreadTotal(response.data.totalUnread); + + if (response.data.totalUnread > 0) { + const chatIds = Object.keys(response.data.chatCounts); + + if (chatIds.length > 0) { + // Create a simplified metadata object with just needed info + const metadata: Record = {}; + + // Fetch each chat to get buyer IDs + await Promise.all( + chatIds.map(async (chatId) => { + try { + // Use markAsRead=false to ensure we don't mark messages as read + const chatResponse = await authAxios.get(`/chats/${chatId}?markAsRead=false`); + metadata[chatId] = { + buyerId: chatResponse.data.buyerId, + }; + } catch (error) { + console.error(`Error fetching chat ${chatId}:`, error); + } + }) + ); + + setChatMetadata(metadata); + } + } + + setLoading(false); + } catch (error) { + console.error("Error fetching unread counts:", error); + setLoading(false); + } + }; + + // Initial fetch + fetchUnreadCounts(); + + // Set polling interval (every 10 seconds for more responsive chat notifications) + const chatInterval = setInterval(fetchUnreadCounts, 10000); + + return () => clearInterval(chatInterval); + }, [loading, previousUnreadTotal]); + + // Navigation handlers + const handleChatClick = (chatId: string) => { + router.push(`/dashboard/chats/${chatId}`); + }; + + const handleOrderClick = (orderId: string) => { + router.push(`/dashboard/orders/${orderId}`); + }; + + // Clear notification handlers + const clearOrderNotifications = () => { + setNewOrders([]); + }; + + // Format the price as currency + const formatPrice = (price: number) => { + return `£${price.toFixed(2)}`; + }; + + return ( + + + + + +
+ + + + All + {totalNotifications > 0 && ( + + {totalNotifications} + + )} + + + Messages + {unreadCounts.totalUnread > 0 && ( + + {unreadCounts.totalUnread} + + )} + + + Orders + {newOrders.length > 0 && ( + + {newOrders.length} + + )} + + + +
+ + + {totalNotifications === 0 ? ( +
+

No new notifications

+
+ ) : ( +
+ {/* Messages Section */} + {unreadCounts.totalUnread > 0 && ( + <> +
+ Unread Messages +
+ {Object.entries(unreadCounts.chatCounts).slice(0, 3).map(([chatId, count]) => ( + handleChatClick(chatId)} + > +
+
+ +
+

+ Customer {chatMetadata[chatId]?.buyerId.slice(-4) || 'Unknown'} +

+

+ {count} new {count === 1 ? 'message' : 'messages'} +

+
+
+ {count} +
+
+ ))} + {Object.keys(unreadCounts.chatCounts).length > 3 && ( +
+ + {Object.keys(unreadCounts.chatCounts).length - 3} more unread chats +
+ )} + + )} + + {/* Divider if both types are present */} + {unreadCounts.totalUnread > 0 && newOrders.length > 0 && ( + + )} + + {/* Orders Section */} + {newOrders.length > 0 && ( + <> +
+ New Paid Orders + +
+ {newOrders.slice(0, 3).map((order) => ( + handleOrderClick(order._id)} + > +
+
+ +
+

Order #{order.orderId}

+

+ {formatPrice(order.totalPrice)} +

+
+
+ Paid +
+
+ ))} + {newOrders.length > 3 && ( +
+ + {newOrders.length - 3} more new orders +
+ )} + + )} +
+ )} +
+ + + {unreadCounts.totalUnread === 0 ? ( +
+

No unread messages

+
+ ) : ( + <> +
+ {Object.entries(unreadCounts.chatCounts).map(([chatId, count]) => ( + handleChatClick(chatId)} + > +
+
+ +
+

+ Customer {chatMetadata[chatId]?.buyerId.slice(-4) || 'Unknown'} +

+

+ {count} new {count === 1 ? 'message' : 'messages'} +

+
+
+ {count} +
+
+ ))} +
+
+ +
+ + )} +
+ + + {newOrders.length === 0 ? ( +
+

No new paid orders

+
+ ) : ( + <> +
+ New Paid Orders + +
+
+ {newOrders.map((order) => ( + handleOrderClick(order._id)} + > +
+
+ +
+

Order #{order.orderId}

+

+ {formatPrice(order.totalPrice)} +

+
+
+ Paid +
+
+ ))} +
+
+ +
+ + )} +
+
+
+ ); +} \ No newline at end of file