This commit is contained in:
NotII
2025-03-08 05:35:40 +00:00
parent 517d81080d
commit 027e380ce2
2 changed files with 22 additions and 389 deletions

View File

@@ -1,379 +0,0 @@
"use client"
import React, { useState, useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { formatDistanceToNow } from "date-fns";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Plus,
MessageCircle,
Loader2,
RefreshCw
} from "lucide-react";
import axios from "axios";
import { toast } from "sonner";
import { getCookie } from "@/lib/client-utils";
interface Chat {
_id: string;
buyerId: string;
vendorId: string;
storeId: string;
lastUpdated: string;
orderId?: string;
}
interface UnreadCounts {
totalUnread: number;
chatCounts: Record<string, number>;
}
export default function ChatList() {
const router = useRouter();
const [chats, setChats] = useState<Chat[]>([]);
const [loading, setLoading] = useState(true);
const [unreadCounts, setUnreadCounts] = useState<UnreadCounts>({ totalUnread: 0, chatCounts: {} });
const [previousTotalUnread, setPreviousTotalUnread] = useState<number>(0);
const [selectedStore, setSelectedStore] = useState<string>("");
const [vendorStores, setVendorStores] = useState<{ _id: string, name: string }[]>([]);
const audioRef = useRef<HTMLAudioElement | null>(null);
// Initialize audio element
useEffect(() => {
// Create audio element for notification sound
audioRef.current = new Audio('/notification.mp3');
// Fallback if notification.mp3 doesn't exist - use browser API for a simple beep
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);
}
});
} else {
// Fallback to simple beep if audio element is not available
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);
}
}
};
// Fetch vendor ID and stores
useEffect(() => {
const fetchVendorData = async () => {
try {
// Get auth token from cookies
const authToken = getCookie("Authorization");
if (!authToken) {
toast.error("You need to be logged in to view chats");
router.push("/auth/login");
return;
}
// Set up axios with the auth token
const authAxios = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: {
Authorization: `Bearer ${authToken}`
}
});
// First, get vendor info using the /auth/me endpoint
const vendorResponse = await authAxios.get('/auth/me');
console.log("Vendor auth response:", vendorResponse.data);
// 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);
toast.error("Could not retrieve vendor information");
return;
}
// Fetch vendor's store using storefront endpoint
const storeResponse = await authAxios.get(`/storefront`);
console.log("Store response:", storeResponse.data);
// Handle both array and single object responses
if (Array.isArray(storeResponse.data)) {
// If it's an array, use it as is
setVendorStores(storeResponse.data);
if (storeResponse.data.length > 0) {
setSelectedStore(storeResponse.data[0]._id);
}
} else if (storeResponse.data && typeof storeResponse.data === 'object' && storeResponse.data._id) {
// If it's a single store object, convert it to an array with one element
const singleStore = [storeResponse.data];
setVendorStores(singleStore);
setSelectedStore(storeResponse.data._id);
} else {
console.error("Expected store data but received:", storeResponse.data);
setVendorStores([]);
toast.error("Failed to load store data in expected format");
}
} catch (error) {
console.error("Error fetching vendor data:", error);
toast.error("Failed to load vendor data");
setVendorStores([]);
}
};
fetchVendorData();
}, [router]);
// Fetch chats and unread counts when store is selected
const fetchChats = async () => {
if (!selectedStore && vendorStores.length > 0) {
setSelectedStore("all"); // Set default "all" if we have stores but none selected
}
setLoading(true);
try {
const endpoint = selectedStore && selectedStore !== "all"
? `/chats?storeId=${selectedStore}`
: "/chats";
const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/api${endpoint}`, {
headers: {
Authorization: getCookie("Authorization") || "",
"Content-Type": "application/json",
},
});
setChats(response.data || []);
// Fetch unread counts after loading chats
await fetchUnreadCounts();
} catch (error) {
console.error("Failed to fetch chats:", error);
toast.error("Failed to load chat conversations");
setChats([]);
} finally {
setLoading(false);
}
};
// Add polling effect
useEffect(() => {
if (selectedStore) {
fetchChats();
// Poll for updates every 10 seconds
const intervalId = setInterval(fetchChats, 10000);
return () => clearInterval(intervalId);
}
}, [selectedStore]);
// Handle chat selection
const handleChatClick = (chatId: string) => {
router.push(`/dashboard/chats/${chatId}`);
};
// Handle store change
const handleStoreChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedStore(e.target.value);
};
// Create a new chat
const handleCreateChat = () => {
router.push("/dashboard/chats/new");
};
if (loading) {
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex justify-between items-center">
<span>Loading chats...</span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="animate-pulse space-y-4">
{[1, 2, 3].map((n) => (
<div key={n} className="h-16 bg-muted rounded-md"></div>
))}
</div>
</CardContent>
</Card>
);
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex items-center space-x-4">
<Select
value={selectedStore || "all"}
onValueChange={(value) => setSelectedStore(value)}
>
<SelectTrigger className="w-48">
<SelectValue placeholder="All Stores" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Stores</SelectItem>
{vendorStores.map((store) => (
<SelectItem key={store._id} value={store._id}>
{store.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
onClick={() => fetchChats()}
disabled={loading}
>
{loading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
<span className="ml-2">Refresh</span>
</Button>
</div>
<Button onClick={handleCreateChat} size="sm">
<Plus className="h-4 w-4 mr-2" />
New Chat
</Button>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[200px]">Customer</TableHead>
<TableHead>Last Activity</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={4} className="h-24 text-center">
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : chats.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="h-24 text-center">
<div className="flex flex-col items-center justify-center">
<MessageCircle className="h-8 w-8 text-muted-foreground mb-2" />
<p className="text-muted-foreground">No chats found</p>
</div>
</TableCell>
</TableRow>
) : (
chats.map((chat) => (
<TableRow
key={chat._id}
className="cursor-pointer hover:bg-muted/50"
onClick={() => handleChatClick(chat._id)}
>
<TableCell>
<div className="flex items-center space-x-3">
<Avatar>
<AvatarFallback>
{chat.buyerId?.slice(0, 2).toUpperCase() || 'CU'}
</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">Customer {chat.buyerId.slice(0, 4)}</div>
{chat.orderId && (
<div className="text-xs text-muted-foreground">
Order #{chat.orderId}
</div>
)}
</div>
</div>
</TableCell>
<TableCell>
{formatDistanceToNow(new Date(chat.lastUpdated), { addSuffix: true })}
</TableCell>
<TableCell>
{unreadCounts.chatCounts[chat._id] > 0 ? (
<Badge variant="destructive" className="ml-1">
{unreadCounts.chatCounts[chat._id]} new
</Badge>
) : (
<Badge variant="outline">Read</Badge>
)}
</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleChatClick(chat._id);
}}
>
View
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@@ -250,16 +250,28 @@ export default function ChatTable() {
)} )}
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<Button <div className="flex justify-end space-x-2">
variant="ghost" <Button
size="icon" variant="ghost"
onClick={(e) => { size="icon"
e.stopPropagation(); onClick={(e) => {
handleChatClick(chat._id); e.stopPropagation();
}} handleChatClick(chat._id);
> }}
<Eye className="h-4 w-4" /> >
</Button> <Eye className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
window.open(`/dashboard/chats/${chat._id}`, '_blank');
}}
>
<MessageCircle className="h-4 w-4" />
</Button>
</div>
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))