Files
ember-market-frontend/components/dashboard/NewChatForm.tsx
g ed229c5dd6 Parallelize vendor and store fetch in NewChatForm
Updated the data fetching logic to retrieve vendor profile and store information concurrently using Promise.all, improving performance and reducing wait time in the NewChatForm component.
2026-01-07 13:02:12 +00:00

379 lines
12 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { ArrowLeft, Send, RefreshCw, Search, User } from "lucide-react";
import axios from "axios";
import { toast } from "sonner";
import { getCookie } from "@/lib/api";
import debounce from "lodash/debounce";
interface User {
telegramUserId: string;
telegramUsername: string | null;
}
export default function NewChatForm() {
const router = useRouter();
const searchParams = useSearchParams();
const [buyerId, setBuyerId] = useState("");
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState<User[]>([]);
const [searching, setSearching] = useState(false);
const [open, setOpen] = useState(false);
const [initialMessage, setInitialMessage] = useState("");
const [loading, setLoading] = useState(false);
const [loadingUser, setLoadingUser] = useState(false);
const [vendorStores, setVendorStores] = useState<
{ _id: string; name: string }[]
>([]);
const [selectedStore, setSelectedStore] = useState<string>("");
const [selectedUser, setSelectedUser] = useState<User | null>(null);
// Create an axios instance with auth
const getAuthAxios = () => {
const authToken = getCookie("Authorization");
if (!authToken) return null;
return axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: { Authorization: `Bearer ${authToken}` },
});
};
// Parse URL parameters for buyerId and fetch user details if present
useEffect(() => {
const buyerIdParam = searchParams.get("buyerId");
if (buyerIdParam) {
setBuyerId(buyerIdParam);
// We'll fetch user details after stores are loaded
}
}, [searchParams]);
// Fetch user information by ID
const fetchUserById = async (userId: string) => {
if (!userId || !vendorStores[0]?._id) return;
const authAxios = getAuthAxios();
if (!authAxios) {
toast.error("You need to be logged in");
router.push("/auth/login");
return;
}
setLoadingUser(true);
try {
const response = await authAxios.get(`/chats/user/${userId}`);
if (response.data) {
setSelectedUser(response.data);
setSearchQuery(response.data.telegramUsername || `User ${userId}`);
} else {
// Just leave the buyerId as is without username display
}
} catch (error) {
console.error("Error fetching user:", error);
// Still keep the buyerId
} finally {
setLoadingUser(false);
}
};
// Debounced search function
const searchUsers = debounce(async (query: string) => {
if (!query.trim() || !vendorStores[0]?._id) return;
const authAxios = getAuthAxios();
if (!authAxios) return;
try {
setSearching(true);
const response = await authAxios.get(
`/chats/search/users?query=${encodeURIComponent(query)}&storeId=${vendorStores[0]._id}`,
);
setSearchResults(response.data);
} catch (error) {
console.error("Error searching users:", error);
toast.error("Failed to search users");
} finally {
setSearching(false);
}
}, 300);
// Handle search input change
const handleSearchChange = (value: string) => {
setSearchQuery(value);
searchUsers(value);
setOpen(true);
};
// Handle user selection
const handleUserSelect = (user: User) => {
setBuyerId(user.telegramUserId);
setSelectedUser(user);
setSearchQuery(user.telegramUsername || user.telegramUserId);
setOpen(false);
};
// Fetch vendor stores
useEffect(() => {
const fetchVendorStores = async () => {
const authAxios = getAuthAxios();
if (!authAxios) {
toast.error("You must be logged in to start a chat");
router.push("/auth/login");
return;
}
try {
// Fetch vendor profile and store in parallel
const [vendorResponse, storeResponse] = await Promise.all([
authAxios.get("/auth/me"),
authAxios.get("/storefront"),
]);
// Extract vendor ID properly
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;
}
// Handle both array and single object responses
if (Array.isArray(storeResponse.data)) {
setVendorStores(storeResponse.data);
if (storeResponse.data.length > 0) {
setSelectedStore(storeResponse.data[0]._id);
}
} else if (
storeResponse.data &&
typeof storeResponse.data === "object" &&
storeResponse.data._id
) {
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");
}
// Now that we have the store, fetch user details if buyerId was set
const buyerIdParam = searchParams.get("buyerId");
if (buyerIdParam) {
fetchUserById(buyerIdParam);
}
} catch (error) {
console.error("Error fetching store:", error);
toast.error("Failed to load store");
setVendorStores([]);
}
};
fetchVendorStores();
}, []);
const handleBackClick = () => {
router.push("/dashboard/chats");
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!buyerId) {
toast.error("Please select a customer");
return;
}
if (vendorStores.length === 0) {
toast.error("No store available. Please create a store first.");
return;
}
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");
return;
}
const response = await authAxios.post("/chats/create", {
buyerId,
storeId: storeId,
initialMessage: initialMessage.trim() || undefined,
});
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}`);
} else {
toast.error("Failed to create chat");
}
} finally {
setLoading(false);
}
};
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Button variant="ghost" size="icon" onClick={handleBackClick}>
<ArrowLeft className="h-5 w-5" />
</Button>
<span>Start a New Conversation</span>
</CardTitle>
<CardDescription>
Start a new conversation with a customer by their Telegram ID
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="buyerId">Customer Telegram ID or Username</Label>
<div className="relative">
<Input
id="buyerId"
value={searchQuery}
onChange={(e) => handleSearchChange(e.target.value)}
placeholder="Search by username or ID..."
className="pr-10"
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
{loadingUser ? (
<RefreshCw className="h-4 w-4 animate-spin text-muted-foreground" />
) : (
<Search className="h-4 w-4 text-muted-foreground" />
)}
</div>
{open && searchQuery && (
<div className="absolute z-50 w-full mt-1 border bg-card rounded-md shadow-md max-h-[300px] overflow-auto">
{searching ? (
<div className="flex items-center justify-center py-6 text-sm text-muted-foreground">
<RefreshCw className="h-4 w-4 animate-spin mr-2" />
Searching...
</div>
) : searchResults.length === 0 ? (
<div className="py-6 text-center text-sm text-muted-foreground">
No customers found
</div>
) : (
<div className="py-1">
{searchResults.map((user) => (
<button
key={user.telegramUserId}
onClick={() => handleUserSelect(user)}
type="button"
className="w-full flex items-center px-4 py-3 hover:bg-accent text-left"
>
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-primary mr-3">
<User className="h-4 w-4" />
</div>
<div className="flex flex-col">
<span className="font-medium">
{user.telegramUsername || "No username"}
</span>
<span className="text-xs text-muted-foreground">
ID: {user.telegramUserId}
</span>
</div>
</button>
))}
</div>
)}
</div>
)}
</div>
<p className="text-sm text-muted-foreground">
This is the customer's Telegram ID or username. You can search for
them above or ask them to use the /myid command in your Telegram
bot.
</p>
{buyerId && !searchQuery && (
<div className="p-2 border rounded-md bg-muted/20 mt-2">
<p className="text-sm font-medium">
Using Telegram ID: {buyerId}
</p>
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="initialMessage">Initial Message (Optional)</Label>
<Textarea
id="initialMessage"
value={initialMessage}
onChange={(e) => setInitialMessage(e.target.value)}
placeholder="Hello! How can I help you today?"
rows={4}
/>
</div>
<div className="flex justify-end space-x-2">
<Button
type="button"
variant="outline"
onClick={handleBackClick}
disabled={loading}
>
Cancel
</Button>
<Button
type="submit"
disabled={loading || !buyerId || vendorStores.length === 0}
>
{loading ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
<>
<Send className="mr-2 h-4 w-4" />
Start Conversation
</>
)}
</Button>
</div>
</form>
</CardContent>
</Card>
);
}