"use client" import { useState, useEffect, useRef } from "react" import { usePathname } from "next/navigation" import { useSession } from "next-auth/react" import { useTheme } from "@/components/ThemeProvider" import { Send, Search, ShieldCheck, Loader2, Plus, MessageSquare, CheckCheck, Package, ShoppingCart, X, Settings } from "lucide-react" import { cn } from "@/lib/utils" import { format } from "date-fns" import { ChatAttachmentModal } from "@/components/chat/ChatAttachmentModal" interface Contact { id: string name: string email: string image: string | null role: string } interface Message { id: string senderId: string receiverId: string content: string kind?: "TEXT" | "PRODUCT" | "ORDER" | string | null attachment?: any createdAt: string } function AlertCircle(props: React.SVGProps) { return ( ) } type AttachOrder = { id: number name: string orderNumber?: string total_price: string currency?: string storeId?: string storeName?: string customerName?: string | null primaryImageUrl?: string | null } type AttachProduct = { id: number | string title: string storeId?: string storeName?: string imageUrl?: string | null description?: string | null } export function ChatView() { const pathname = usePathname() const isSupplier = pathname.startsWith("/supplier") const { theme } = useTheme() const isLight = theme === "light" const { data: session } = useSession() const userId = (session?.user as { id?: string })?.id const [contacts, setContacts] = useState([]) const [selectedContact, setSelectedContact] = useState(null) const [messages, setMessages] = useState([]) const [newMessage, setNewMessage] = useState("") const [draftKind, setDraftKind] = useState<"PRODUCT" | "ORDER" | null>(null) const [draftAttachment, setDraftAttachment] = useState(null) const [attachmentModalOpen, setAttachmentModalOpen] = useState(false) const [activeAttachmentKind, setActiveAttachmentKind] = useState<"PRODUCT" | "ORDER" | null>(null) const [activeAttachment, setActiveAttachment] = useState(null) const [loadingContacts, setLoadingContacts] = useState(true) const [loadingMessages, setLoadingMessages] = useState(false) const [sending, setSending] = useState(false) const [searchQuery, setSearchQuery] = useState("") const [contactsError, setContactsError] = useState(null) const [messagesError, setMessagesError] = useState(null) const [sendError, setSendError] = useState(null) const [attachOpen, setAttachOpen] = useState(false) const [attachLoading, setAttachLoading] = useState(false) const [attachOrders, setAttachOrders] = useState([]) const [attachProducts, setAttachProducts] = useState([]) const attachRef = useRef(null) const messagesEndRef = useRef(null) const fetchContacts = async () => { try { setLoadingContacts(true) setContactsError(null) const res = await fetch("/api/chat/contacts") if (res.ok) { const data = await res.json() setContacts(data.contacts) } else { setContactsError("Failed to load contacts. Please try again.") } } catch (error) { console.error("Fetch contacts error", error) setContactsError("Failed to load contacts. Please try again.") } finally { setLoadingContacts(false) } } const fetchMessages = async (contact: Contact, silent = false) => { try { if (!silent) { setLoadingMessages(true) setMessagesError(null) } const res = await fetch(`/api/chat/messages?contactId=${contact.id}`) if (res.ok) { const data = await res.json() const typedMessages = data.messages.map((m: any) => ({ id: m.id, senderId: m.senderId, receiverId: m.receiverId, content: m.content ?? "", kind: m.kind ?? "TEXT", attachment: m.attachment ?? null, createdAt: m.createdAt })) setMessages(typedMessages) } else if (!silent) { setMessagesError("Failed to load messages. Please try again.") } } catch (error) { console.error("Fetch messages error", error) if (!silent) setMessagesError("Failed to load messages. Please try again.") } finally { if (!silent) setLoadingMessages(false) } } useEffect(() => { fetchContacts() }, []) useEffect(() => { if (selectedContact) { fetchMessages(selectedContact) } else { setMessages([]) } }, [selectedContact]) // Poll for new messages when a contact is selected const POLL_INTERVAL_MS = 8000 useEffect(() => { if (!selectedContact) return const interval = setInterval(() => { fetchMessages(selectedContact, true) }, POLL_INTERVAL_MS) return () => clearInterval(interval) }, [selectedContact?.id]) useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) }, [messages]) useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (attachRef.current && !attachRef.current.contains(e.target as Node)) { setAttachOpen(false) } } if (attachOpen) { document.addEventListener("mousedown", handleClickOutside) return () => document.removeEventListener("mousedown", handleClickOutside) } }, [attachOpen]) const openAttach = async () => { setAttachOpen(true) setAttachLoading(true) setAttachOrders([]) setAttachProducts([]) try { if (isSupplier) { const res = await fetch("/api/supplier/products") if (res.ok) { const data = await res.json() const list = (data.products || []).map((p: any) => ({ id: p.id, title: p.title || p.shopifyProductTitle || "Product", imageUrl: p.imageUrl ?? p.image_url ?? null, description: p.description ?? null, storeName: p.vendor?.businessName ?? null })) setAttachProducts(list) } } else { const [ordersRes, productsRes] = await Promise.all([ fetch("/api/chat/attach-orders"), fetch("/api/chat/attach-products") ]) if (ordersRes.ok) { const ordersData = await ordersRes.json() setAttachOrders(ordersData.orders || []) } if (productsRes.ok) { const productsData = await productsRes.json() setAttachProducts(productsData.products || []) } } } catch { // Silent fail } finally { setAttachLoading(false) } } const insertOrder = (order: AttachOrder) => { setDraftKind("ORDER") setDraftAttachment({ orderNumber: order.orderNumber ?? order.name, customerName: order.customerName ?? null, primaryImageUrl: order.primaryImageUrl ?? null, total_price: order.total_price, currency: order.currency ?? null, storeName: order.storeName ?? null, shopifyOrderId: order.id }) const summary = `Order ${order.orderNumber ?? order.name}${order.customerName ? ` - ${order.customerName}` : ""}${order.storeName ? ` · ${order.storeName}` : ""}` setNewMessage(prev => (prev.trim() ? `${prev.trim()}\n${summary}` : summary)) setAttachOpen(false) } const insertProduct = (product: AttachProduct) => { setDraftKind("PRODUCT") setDraftAttachment({ title: product.title, description: product.description ?? null, imageUrl: product.imageUrl ?? null, storeName: product.storeName ?? null, productId: product.id }) const summary = `Product: ${product.title}${product.storeName ? ` · ${product.storeName}` : ""}` setNewMessage(prev => (prev.trim() ? `${prev.trim()}\n${summary}` : summary)) setAttachOpen(false) } const handleSendMessage = async (e: React.FormEvent) => { e.preventDefault() if (!selectedContact) return const hasText = !!newMessage.trim() const hasAttachment = !!draftKind && !!draftAttachment if (!hasText && !hasAttachment) return try { setSending(true) setSendError(null) const payload: any = { receiverId: selectedContact.id, content: newMessage } if (hasAttachment) { payload.kind = draftKind payload.attachment = draftAttachment } const res = await fetch("/api/chat/messages", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }) if (res.ok) { const msg = await res.json() const optimiticMsg: Message = { id: msg.id, senderId: msg.senderId, receiverId: msg.receiverId, content: newMessage, kind: msg.kind ?? "TEXT", attachment: msg.attachment ?? null, createdAt: msg.createdAt } setMessages(prev => [...prev, optimiticMsg]) setNewMessage("") setDraftKind(null) setDraftAttachment(null) } else { setSendError("Failed to send message. Please try again.") } } catch (error) { console.error("Send message error", error) setSendError("Failed to send message. Please try again.") } finally { setSending(false) } } // Best-effort compatibility for legacy bracket-style chat messages. // Example legacy content: // - [Product: Title · Store (#123)] // - [Order 1005 · Store - USD 25.00] const parseLegacyAttachment = (raw: string): { kind: "PRODUCT" | "ORDER"; attachment: any } | null => { const text = (raw || "").trim() if (!text) return null // PRODUCT if (text.startsWith("[Product:") && text.includes("]")) { const body = text .replace(/^\[Product:\s*/i, "") .replace(/\]\s*$/, "") .trim() const cleanedBody = body.replace(/\(#\d+\)\s*$/, "").trim() const parts = cleanedBody.split(" · ") const title = (parts[0] || "").trim() const storeName = parts.length > 1 ? parts.slice(1).join(" · ").trim() : null if (!title) return null return { kind: "PRODUCT", attachment: { title, description: null, imageUrl: null, storeName: storeName || null, }, } } // ORDER if (text.startsWith("[Order") && text.includes("]")) { const body = text .replace(/^\[Order\s*/i, "") .replace(/\]\s*$/, "") .trim() // Split totals: " · - USD 25.00" const [leftPart, totalPartRaw] = body.split(" - ", 2) if (!leftPart) return null let orderNumber = leftPart.trim() let storeName: string | null = null if (leftPart.includes(" · ")) { const [leftOrder, ...rest] = leftPart.split(" · ") orderNumber = (leftOrder || "").trim() storeName = rest.join(" · ").trim() || null } const totalPart = (totalPartRaw || "").trim() let currency: string | null = null let total_price: string | null = null if (totalPart) { const tokens = totalPart.split(/\s+/) if (tokens.length >= 2 && tokens[0].length === 3) { currency = tokens[0] total_price = tokens.slice(1).join(" ") } else { total_price = tokens.join(" ") } } if (!orderNumber) return null return { kind: "ORDER", attachment: { orderNumber, customerName: null, primaryImageUrl: null, total_price: total_price || null, currency: currency || null, storeName: storeName || null, }, } } return null } const filteredContacts = contacts.filter(c => c.name.toLowerCase().includes(searchQuery.toLowerCase()) || c.email.toLowerCase().includes(searchQuery.toLowerCase()) ) return (
{/* Sidebar - Contacts (fixed; only inner list scrolls) */}

JUNO CHAT

setSearchQuery(e.target.value)} className={cn( "w-full pl-10 pr-4 py-3 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition-all font-medium", isLight ? "bg-slate-50 border border-slate-200 text-slate-900 placeholder:text-slate-400" : "bg-white/5 border border-white/10 text-white placeholder:text-slate-600" )} />
{contactsError && (
{contactsError}
)}
{loadingContacts ? (
Loading...
) : filteredContacts.length === 0 ? (

No Contacts

) : (
{filteredContacts.map(contact => ( ))}
)}
Y

YOU

Ready

{/* Chat Area - fixed header and input; only messages scroll */}
{selectedContact ? ( <>
{selectedContact.name[0].toUpperCase()}

{selectedContact.name}

Active Now
Chat
{messagesError && (
{messagesError}
)} {loadingMessages ? (

Syncing History...

) : messages.length === 0 ? (

Conversation Started

Start chatting with your connection.

) : ( messages.map((msg, idx) => { const isMe = msg.senderId !== selectedContact.id const prevMsg = idx > 0 ? messages[idx - 1] : null const showTime = !prevMsg || format(new Date(msg.createdAt), "HH:mm") !== format(new Date(prevMsg.createdAt), "HH:mm") const legacy = msg.kind === "TEXT" || !msg.kind ? parseLegacyAttachment(msg.content) : null const kindToRender = msg.kind === "PRODUCT" || msg.kind === "ORDER" ? msg.kind : legacy?.kind const attachmentToRender = kindToRender === "PRODUCT" || kindToRender === "ORDER" ? msg.kind ? msg.attachment : legacy?.attachment : null return (
{kindToRender === "PRODUCT" && attachmentToRender ? ( ) : kindToRender === "ORDER" && attachmentToRender ? ( ) : ( msg.content )}
{isMe && }
{showTime && ( {format(new Date(msg.createdAt), "HH:mm")} )}
) }) )}
setAttachmentModalOpen(false)} kind={activeAttachmentKind} attachment={activeAttachment} isLight={isLight} />
{sendError && (
{sendError}
)}
{attachOpen && (
{isSupplier ? "Attach product" : "Attach order or product"}
{attachLoading ? (
) : ( <> {!isSupplier && attachOrders.length > 0 && (

Orders

{attachOrders.map((order) => ( ))}
)} {attachProducts.length > 0 && (

Products

{attachProducts.map((product) => ( ))}
)} {!attachLoading && attachOrders.length === 0 && attachProducts.length === 0 && (

{isSupplier ? "No products to attach." : "No orders or products found."}

)} )}
)}