Files
ember-market-frontend/components/notifications/OrderNotifications.tsx
g adb01009eb Handle play() and load() promises for audio elements
Updated audio playback and preloading logic to check for and handle returned promises from play() and load() methods. This prevents uncaught promise rejections in browsers where these methods may return undefined, improving reliability and error handling for notification sounds.
2025-12-09 22:17:20 +00:00

237 lines
7.7 KiB
TypeScript

"use client";
import { useEffect, useState, useRef } from "react";
import { clientFetch } from "@/lib/api";
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<Order[]>([]);
const [loading, setLoading] = useState(true);
const seenOrderIds = useRef<Set<string>>(new Set());
const isInitialFetch = useRef(true);
const audioRef = useRef<HTMLAudioElement | null>(null);
useEffect(() => {
audioRef.current = new Audio('/hohoho.mp3');
// Fallback if hohoho.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;
const playPromise = audioRef.current.play();
if (playPromise !== undefined) {
playPromise.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 {
// Get orders from the last 24 hours with a more efficient query
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const timestamp = yesterday.toISOString();
// Use orderDate parameter instead of 'after' to avoid backend casting errors
// The error logs show that the 'after' parameter is being interpreted as 'orderId' incorrectly
const orderData = await clientFetch(`/orders?status=paid&limit=10&orderDate[gte]=${timestamp}`);
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(
<div className="flex flex-col">
<p className="font-semibold">New Paid Order!</p>
<p className="text-sm">Order #{order.orderId}</p>
<p className="text-sm font-semibold">£{order.totalPrice.toFixed(2)}</p>
</div>,
{
duration: 8000,
icon: <Package className="h-5 w-5" />,
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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="relative" disabled={loading}>
<Package className="h-5 w-5" />
{newOrders.length > 0 && (
<Badge
variant="destructive"
className="absolute -top-1 -right-1 px-1.5 py-0.5 text-xs"
>
{newOrders.length}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-72">
<div className="p-2 border-b flex justify-between items-center">
<h3 className="font-medium">New Paid Orders</h3>
{newOrders.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={clearNotifications}
className="h-7 text-xs"
>
Clear
</Button>
)}
</div>
{newOrders.length === 0 ? (
<div className="p-4 flex items-center justify-center">
<p className="text-sm text-muted-foreground">No new paid orders</p>
</div>
) : (
<>
<div className="max-h-80 overflow-y-auto">
{newOrders.map((order) => (
<DropdownMenuItem
key={order._id}
className="p-3 cursor-pointer"
onClick={() => handleOrderClick(order._id)}
>
<div className="flex items-center justify-between w-full">
<div>
<p className="font-medium">Order #{order.orderId}</p>
<p className="text-sm text-muted-foreground">
{formatPrice(order.totalPrice)}
</p>
</div>
<Badge className="bg-green-500 hover:bg-green-600">Paid</Badge>
</div>
</DropdownMenuItem>
))}
</div>
<div className="p-2 border-t">
<Button
variant="outline"
className="w-full"
onClick={() => router.push('/dashboard/orders')}
>
View All Orders
</Button>
</div>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}