From f6a2a69ac460e7cb2e804dae213f58ed5a8fc462 Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Sun, 23 Mar 2025 23:18:17 +0000 Subject: [PATCH] i hope this fixed it --- components/dashboard/BuyerOrderInfo.tsx | 28 +--- components/dashboard/ChatDetail.tsx | 205 +++++++++++++----------- components/dashboard/NewChatForm.tsx | 134 ++++++++++------ lib/client-utils.ts | 7 +- 4 files changed, 209 insertions(+), 165 deletions(-) diff --git a/components/dashboard/BuyerOrderInfo.tsx b/components/dashboard/BuyerOrderInfo.tsx index af1c231..6bdfeb4 100644 --- a/components/dashboard/BuyerOrderInfo.tsx +++ b/components/dashboard/BuyerOrderInfo.tsx @@ -11,7 +11,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { getCookie } from "@/lib/client-utils"; -import axios from "axios"; +import { clientFetch } from "@/lib/client-utils"; import { useRouter } from "next/navigation"; interface Order { @@ -62,27 +62,13 @@ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps) setLoading(true); try { - const authToken = getCookie("Authorization"); + // Use clientFetch instead of direct axios calls + // This ensures the request goes through Next.js API rewrites + const response = await clientFetch(`/chats/${chatId}/orders?limit=10`); - if (!authToken) { - isFetchingRef.current = false; - setLoading(false); - return; - } - - const authAxios = axios.create({ - baseURL: process.env.NEXT_PUBLIC_API_URL, - headers: { - Authorization: `Bearer ${authToken}` - } - }); - - // Use the new endpoint that works with sub-users - const response = await authAxios.get(`/chats/${chatId}/orders?limit=10`); // Limit to fewer orders for faster response - - if (response.data && response.data.orders) { - setOrders(response.data.orders); - setHasOrders(response.data.orders.length > 0); + if (response && response.orders) { + setOrders(response.orders); + setHasOrders(response.orders.length > 0); } else { setHasOrders(false); } diff --git a/components/dashboard/ChatDetail.tsx b/components/dashboard/ChatDetail.tsx index 15ee4a9..25a9f94 100644 --- a/components/dashboard/ChatDetail.tsx +++ b/components/dashboard/ChatDetail.tsx @@ -11,7 +11,7 @@ import { formatDistanceToNow } from "date-fns"; import axios from "axios"; import { toast } from "sonner"; import { ArrowLeft, Send, RefreshCw, File, FileText, Image as ImageIcon, Download } from "lucide-react"; -import { getCookie } from "@/lib/client-utils"; +import { getCookie, clientFetch } from "@/lib/client-utils"; import { ImageViewerModal } from "@/components/modals/image-viewer-modal"; import BuyerOrderInfo from "./BuyerOrderInfo"; @@ -174,121 +174,132 @@ export default function ChatDetail({ chatId }: { chatId: string }) { } }; - // Fetch chat data - const fetchChat = async () => { + // Loading effect + useEffect(() => { + if (chatId) { + fetchChatData(); + } + }, [chatId]); + + // Get a fresh auth axios instance + const getAuthAxios = () => { + const authToken = getCookie("Authorization"); + if (!authToken) return null; + + return axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + } + }); + }; + + // Fetch chat information and messages + const fetchChatData = async () => { + if (!chatId) return; + try { - // Get auth token from cookies - const authToken = getCookie("Authorization"); + setLoading(true); - if (!authToken) { - toast.error("You need to be logged in"); - router.push("/auth/login"); - return; - } + // Use clientFetch instead of direct axios calls + const response = await clientFetch(`/chats/${chatId}`); - // Set up axios with the auth token - const authAxios = axios.create({ - baseURL: process.env.NEXT_PUBLIC_API_URL, - headers: { - Authorization: `Bearer ${authToken}` - } - }); + setChatData(response); + setMessages(Array.isArray(response.messages) ? response.messages : []); - // Always fetch messages without marking as read - const response = await authAxios.get(`/chats/${chatId}?markAsRead=false`); - - // Check if there are new messages - const hasNewMessages = chat && response.data.messages.length > chat.messages.length; - const unreadBuyerMessages = response.data.messages.some( - (msg: Message) => msg.sender === 'buyer' && !msg.read - ); - - if (hasNewMessages) { - // Don't play sound for messages we sent (vendor) - const lastMessage = response.data.messages[response.data.messages.length - 1]; - if (lastMessage.sender === 'buyer') { - playNotificationSound(); - } - } - - setChat(response.data); - - // Update the previous message count - if (response.data.messages) { - setPreviousMessageCount(response.data.messages.length); - } - - setLoading(false); - - // Clear any existing timeout - if (markReadTimeoutRef.current) { - clearTimeout(markReadTimeoutRef.current); - } - - // Only mark as read with a delay if there are unread buyer messages - if (unreadBuyerMessages) { - // Add a 3-second delay before marking messages as read to allow notification to appear - markReadTimeoutRef.current = setTimeout(() => { - markMessagesAsRead(); - }, 3000); - } + // Scroll to bottom on initial load + setTimeout(() => { + scrollToBottom(); + }, 100); } catch (error) { - console.error("Error fetching chat:", error); - toast.error("Failed to load conversation"); + console.error("Error fetching chat data:", error); + toast.error("Failed to load chat"); + } finally { setLoading(false); } }; + // Fetch new messages periodically useEffect(() => { - fetchChat(); + if (!chatId || !chatData) return; - // Poll for updates every 10 seconds - const intervalId = setInterval(fetchChat, 10000); - - return () => clearInterval(intervalId); - }, [chatId]); - - // Scroll to bottom when messages change - useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [chat?.messages]); - - // Send a message - const handleSendMessage = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!message.trim()) return; - - setSending(true); - try { - // Get auth token from cookies - const authToken = getCookie("Authorization"); + const checkForNewMessages = async () => { + if (isPollingRef.current) return; - if (!authToken) { - toast.error("You need to be logged in"); - router.push("/auth/login"); - return; - } + isPollingRef.current = true; - // Set up axios with the auth token - const authAxios = axios.create({ - baseURL: process.env.NEXT_PUBLIC_API_URL, - headers: { - Authorization: `Bearer ${authToken}` + try { + // Use clientFetch instead of direct axios calls + const response = await clientFetch(`/chats/${chatId}?markAsRead=true`); + + if ( + response && + Array.isArray(response.messages) && + response.messages.length !== messages.length + ) { + setMessages(response.messages); + // Only auto-scroll if we're already near the bottom + if (isNearBottom()) { + setTimeout(() => { + scrollToBottom(); + }, 100); + } } + } catch (error) { + console.error("Error checking for new messages:", error); + } finally { + isPollingRef.current = false; + } + }; + + // Check for new messages every 3 seconds + const intervalId = setInterval(checkForNewMessages, 3000); + + return () => { + clearInterval(intervalId); + }; + }, [chatId, chatData, messages.length]); + + // Send a message + const sendMessage = async () => { + if (!chatId || !message.trim()) return; + + // Optimistically add message to UI + const tempId = `temp-${Date.now()}`; + const tempMessage = { + _id: tempId, + chatId, + sender: "vendor", + message: message.trim(), + timestamp: new Date().toISOString(), + isTemp: true, + }; + + setMessages(prev => [...prev, tempMessage]); + scrollToBottom(); + + // Clear input + setMessage(""); + + try { + // Use clientFetch instead of direct axios calls + const response = await clientFetch(`/chats/${chatId}/messages`, { + method: 'POST', + body: JSON.stringify({ message: message.trim() }), }); - await authAxios.post(`/chats/${chatId}/message`, { - content: message - }); - - setMessage(""); - await fetchChat(); // Refresh chat after sending + // Replace temp message with real one from server + setMessages(prev => + prev.filter(m => m._id !== tempId) + .concat(response) + ); } catch (error) { console.error("Error sending message:", error); toast.error("Failed to send message"); - } finally { - setSending(false); + + // Remove temp message on error + setMessages(prev => prev.filter(m => m._id !== tempId)); } }; diff --git a/components/dashboard/NewChatForm.tsx b/components/dashboard/NewChatForm.tsx index d8fdb83..ec9ad03 100644 --- a/components/dashboard/NewChatForm.tsx +++ b/components/dashboard/NewChatForm.tsx @@ -12,6 +12,7 @@ import axios from "axios"; import { toast } from "sonner"; import { getCookie } from "@/lib/client-utils"; import debounce from "lodash/debounce"; +import { clientFetch } from "@/lib/client-utils"; interface User { telegramUserId: string; @@ -32,6 +33,7 @@ export default function NewChatForm() { const [vendorStores, setVendorStores] = useState<{ _id: string, name: string }[]>([]); const [selectedStore, setSelectedStore] = useState(""); const [selectedUser, setSelectedUser] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); // Create an axios instance with auth const getAuthAxios = () => { @@ -81,24 +83,47 @@ export default function NewChatForm() { } }; - // Debounced search function - const searchUsers = debounce(async (query: string) => { - if (!query.trim() || !vendorStores[0]?._id) return; - - const authAxios = getAuthAxios(); - if (!authAxios) return; - + // Check if a chat already exists with this user + const checkExistingChat = async (userId: string) => { try { - setSearching(true); - const response = await authAxios.get(`/chats/search/users?query=${encodeURIComponent(query)}&storeId=${vendorStores[0]._id}`); - setSearchResults(response.data); + // Use clientFetch instead of direct axios calls + const response = await clientFetch(`/chats/user/${userId}`); + + // If a chat is found, redirect to it + if (response && Array.isArray(response) && response.length > 0) { + toast.info("Chat already exists, redirecting..."); + router.push(`/dashboard/chats/${response[0]._id}`); + return true; + } + return false; + } catch (error) { + console.error("Error checking existing chat:", error); + return false; + } + }; + + // Search for users by Telegram username or ID + const searchUsers = async (query: string) => { + setSearching(true); + + try { + if (!query || query.length < 2) { + setSearchResults([]); + return; + } + + // Use clientFetch instead of direct axios calls + const response = await clientFetch(`/chats/search/users?query=${encodeURIComponent(query)}&storeId=${vendorStores[0]._id}`); + + setSearchResults(response || []); } catch (error) { console.error("Error searching users:", error); - toast.error("Failed to search users"); + toast.error("Failed to search for users"); + setSearchResults([]); } finally { setSearching(false); } - }, 300); + }; // Handle search input change const handleSearchChange = (value: string) => { @@ -176,54 +201,71 @@ export default function NewChatForm() { router.push("/dashboard/chats"); }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + // Create a new chat with the selected user + const createChat = async (e?: React.FormEvent) => { + if (e) e.preventDefault(); - if (!buyerId) { - toast.error("Please select a customer"); + if (!selectedUser) { + toast.error("Please select a user first"); return; } - if (vendorStores.length === 0) { - toast.error("No store available. Please create a store first."); - return; - } + setIsSubmitting(true); - const storeId = vendorStores[0]._id; - - setLoading(true); try { - const authAxios = getAuthAxios(); - if (!authAxios) { - toast.error("You need to be logged in"); - router.push("/auth/login"); + // Check for existing chat first + const chatExists = await checkExistingChat(selectedUser.telegramUserId); + if (chatExists) { + setIsSubmitting(false); return; } - const response = await authAxios.post("/chats/create", { - buyerId, - storeId: storeId, - initialMessage: initialMessage.trim() || undefined + // Get current vendor info + // Use clientFetch instead of direct axios calls + const vendorResponse = await clientFetch('/auth/me'); + const vendorId = vendorResponse.vendor?._id; + + if (!vendorId) { + toast.error("Vendor ID not found"); + return; + } + + // Get store ID from the current vendor + // Use clientFetch instead of direct axios calls + const storeResponse = await clientFetch(`/storefront`); + const storeId = storeResponse?.store?._id; + + if (!storeId) { + toast.error("Store ID not found"); + return; + } + + // Create chat data object + const chatData = { + buyerId: selectedUser.telegramUserId, + vendorId, + storeId, + initialMessage: initialMessage.trim() || "Hello! How can I help you today?", + }; + + // Create the chat + // Use clientFetch instead of direct axios calls + const response = await clientFetch('/chats', { + method: 'POST', + body: JSON.stringify(chatData), }); - if (response.data.chatId) { - toast.success("Chat created successfully!"); - router.push(`/dashboard/chats/${response.data.chatId}`); - } else if (response.data.error === "Chat already exists") { - toast.info("Chat already exists, redirecting..."); - router.push(`/dashboard/chats/${response.data.chatId}`); - } - } catch (error: any) { - console.error("Error creating chat:", error); - - if (error.response?.status === 409) { - toast.info("Chat already exists, redirecting..."); - router.push(`/dashboard/chats/${error.response.data.chatId}`); + if (response && response._id) { + toast.success("Chat created successfully"); + router.push(`/dashboard/chats/${response._id}`); } else { toast.error("Failed to create chat"); } + } catch (error) { + console.error("Error creating chat:", error); + toast.error("Failed to create chat"); } finally { - setLoading(false); + setIsSubmitting(false); } }; @@ -242,7 +284,7 @@ export default function NewChatForm() { -
+ createChat(e)} className="space-y-6">
diff --git a/lib/client-utils.ts b/lib/client-utils.ts index dec31df..95d3326 100644 --- a/lib/client-utils.ts +++ b/lib/client-utils.ts @@ -16,13 +16,18 @@ export async function clientFetch(url: string, options: RequestInit = {}): Promi // Ensure the url doesn't start with a slash if it's going to be appended to a URL that ends with one const cleanUrl = url.startsWith('/') ? url.substring(1) : url; - const baseUrl = process.env.NEXT_PUBLIC_API_URL || '/api'; + + // IMPORTANT: Always use /api as the base URL for client-side requests + // This ensures all requests go through Next.js API rewrite rules + const baseUrl = '/api'; // Ensure there's only one slash between the base URL and endpoint const fullUrl = baseUrl.endsWith('/') ? `${baseUrl}${cleanUrl}` : `${baseUrl}/${cleanUrl}`; + console.log(`[clientFetch] Requesting: ${fullUrl}`); + const res = await fetch(fullUrl, { ...options, headers,