JUNOHUB-test/src/components/chat/ChatView.tsx
2026-04-02 14:02:41 +05:00

890 lines
52 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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<SVGSVGElement>) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
)
}
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<Contact[]>([])
const [selectedContact, setSelectedContact] = useState<Contact | null>(null)
const [messages, setMessages] = useState<Message[]>([])
const [newMessage, setNewMessage] = useState("")
const [draftKind, setDraftKind] = useState<"PRODUCT" | "ORDER" | null>(null)
const [draftAttachment, setDraftAttachment] = useState<any>(null)
const [attachmentModalOpen, setAttachmentModalOpen] = useState(false)
const [activeAttachmentKind, setActiveAttachmentKind] = useState<"PRODUCT" | "ORDER" | null>(null)
const [activeAttachment, setActiveAttachment] = useState<any>(null)
const [loadingContacts, setLoadingContacts] = useState(true)
const [loadingMessages, setLoadingMessages] = useState(false)
const [sending, setSending] = useState(false)
const [searchQuery, setSearchQuery] = useState("")
const [contactsError, setContactsError] = useState<string | null>(null)
const [messagesError, setMessagesError] = useState<string | null>(null)
const [sendError, setSendError] = useState<string | null>(null)
const [attachOpen, setAttachOpen] = useState(false)
const [attachLoading, setAttachLoading] = useState(false)
const [attachOrders, setAttachOrders] = useState<AttachOrder[]>([])
const [attachProducts, setAttachProducts] = useState<AttachProduct[]>([])
const attachRef = useRef<HTMLDivElement>(null)
const messagesEndRef = useRef<HTMLDivElement>(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: "<orderNumber> · <storeName> - 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 (
<div className={cn(
"h-full flex flex-col md:flex-row overflow-hidden min-h-0",
isLight ? "bg-slate-50" : "bg-white/5 md:bg-transparent"
)}>
{/* Sidebar - Contacts (fixed; only inner list scrolls) */}
<div className={cn(
"w-full md:w-80 flex flex-col flex-shrink-0 min-h-0 border-r backdrop-blur-xl transition-all",
selectedContact ? "hidden md:flex" : "flex",
isLight ? "bg-white border-slate-200 shadow-sm" : "border-white/10 glass-panel bg-black/20"
)}>
<div className={cn(
"p-6 border-b flex items-center justify-between flex-shrink-0",
isLight ? "border-slate-200" : "border-white/10"
)}>
<h2 className={cn("text-xl font-black tracking-tighter", isLight ? "text-slate-900" : "text-white")}>JUNO CHAT</h2>
<div className="flex gap-2">
<div className={cn(
"w-8 h-8 rounded-lg flex items-center justify-center",
isLight ? "bg-emerald-500/10 border border-emerald-500/20" : "bg-green-500/20 border border-green-500/30"
)}>
<ShieldCheck className={cn("w-4 h-4", isLight ? "text-emerald-600" : "text-green-400")} />
</div>
</div>
</div>
<div className="p-4 flex-shrink-0">
<div className="relative group">
<Search className={cn("absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 transition-colors", isLight ? "text-slate-400 group-focus-within:text-indigo-600" : "text-slate-500 group-focus-within:text-indigo-400")} />
<input
type="text"
placeholder="Find connections..."
value={searchQuery}
onChange={(e) => 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"
)}
/>
</div>
{contactsError && (
<div className="mt-3 px-3 py-2 rounded-lg bg-red-500/10 border border-red-500/20 text-red-600 text-xs font-medium flex items-center justify-between gap-2">
<span>{contactsError}</span>
<button type="button" onClick={() => setContactsError(null)} className="shrink-0 text-red-500 hover:text-red-700" aria-label="Dismiss">×</button>
</div>
)}
</div>
<div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar">
{loadingContacts ? (
<div className={cn("p-10 flex flex-col items-center gap-2", isLight ? "text-slate-500" : "text-slate-500")}>
<Loader2 className="w-6 h-6 animate-spin" />
<span className="text-xs font-bold uppercase tracking-widest">Loading...</span>
</div>
) : filteredContacts.length === 0 ? (
<div className="p-10 text-center opacity-50">
<MessageSquare className={cn("w-10 h-10 mx-auto mb-3", isLight ? "text-slate-400" : "text-slate-600")} />
<p className={cn("text-sm font-bold uppercase tracking-widest", isLight ? "text-slate-500" : "text-slate-500")}>No Contacts</p>
</div>
) : (
<div className="px-2 space-y-1">
{filteredContacts.map(contact => (
<button
key={contact.id}
onClick={() => setSelectedContact(contact)}
className={cn(
"w-full flex items-center gap-3 p-4 rounded-2xl transition-all group relative",
selectedContact?.id === contact.id
? isLight ? "bg-indigo-600 shadow-lg text-white" : "bg-indigo-600 shadow-lg shadow-indigo-600/20"
: isLight ? "hover:bg-slate-100" : "hover:bg-white/5"
)}
>
<div className="relative">
<div className={cn(
"w-12 h-12 rounded-xl flex items-center justify-center font-black transition-all",
selectedContact?.id === contact.id
? "bg-white text-indigo-600"
: isLight ? "bg-slate-100 text-slate-600 group-hover:bg-indigo-100 group-hover:text-indigo-600" : "bg-white/10 text-slate-300 group-hover:bg-indigo-500/20 group-hover:text-indigo-400"
)}>
{contact.name?.[0].toUpperCase() || "?"}
</div>
<div className={cn("absolute -bottom-1 -right-1 w-4 h-4 rounded-full border-2 bg-green-500", isLight ? "border-white" : "border-slate-900")} />
</div>
<div className="flex-1 text-left min-w-0">
<h4 className={cn(
"text-sm font-black truncate tracking-tight",
selectedContact?.id === contact.id ? "text-white" : isLight ? "text-slate-900" : "text-slate-200"
)}>
{contact.name}
</h4>
<p className={cn(
"text-[10px] uppercase font-black tracking-widest mt-0.5",
selectedContact?.id === contact.id ? "text-indigo-100/70" : isLight ? "text-slate-500" : "text-slate-500"
)}>
{contact.role}
</p>
</div>
<div className="w-2 h-2 rounded-full bg-green-500/50" />
</button>
))}
</div>
)}
</div>
<div className={cn(
"p-6 border-t flex-shrink-0",
isLight ? "border-slate-200 bg-slate-50" : "border-white/10 bg-black/40"
)}>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-tr from-pink-600 to-indigo-600 flex items-center justify-center text-white font-black text-sm">
Y
</div>
<div className="flex-1 min-w-0">
<p className={cn("text-xs font-black truncate", isLight ? "text-slate-900" : "text-white")}>YOU</p>
<p className={cn("text-[10px] font-bold flex items-center gap-1.5 uppercase tracking-tighter", isLight ? "text-emerald-600" : "text-green-400")}>
<ShieldCheck className="w-3 h-3" /> Ready
</p>
</div>
<Settings className={cn("w-4 h-4 cursor-pointer transition-colors", isLight ? "text-slate-500 hover:text-slate-700" : "text-slate-600 hover:text-white")} />
</div>
</div>
</div>
{/* Chat Area - fixed header and input; only messages scroll */}
<div className={cn(
"flex-1 flex flex-col min-h-0 overflow-hidden",
isLight ? "bg-slate-100" : "bg-slate-950/20",
!selectedContact ? "hidden md:flex items-center justify-center" : "flex"
)}>
{selectedContact ? (
<>
<div className={cn(
"p-4 md:p-6 border-b flex items-center justify-between flex-shrink-0 z-10",
isLight ? "bg-white border-slate-200 shadow-sm" : "border-white/10 glass-panel backdrop-blur-md"
)}>
<div className="flex items-center gap-4">
<button
onClick={() => setSelectedContact(null)}
className={cn("md:hidden w-10 h-10 rounded-xl border flex items-center justify-center", isLight ? "border-slate-200 text-slate-600" : "border-white/10 text-slate-400")}
>
<Plus className="w-5 h-5 rotate-45" />
</button>
<div className={cn(
"w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center font-black",
isLight ? "bg-indigo-100 border border-indigo-200 text-indigo-600" : "bg-indigo-500/20 border border-indigo-500/30 text-indigo-400"
)}>
{selectedContact.name[0].toUpperCase()}
</div>
<div>
<h3 className={cn("font-black text-sm md:text-lg tracking-tight leading-none mb-1", isLight ? "text-slate-900" : "text-white")}>
{selectedContact.name}
</h3>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500" />
<span className={cn("text-[10px] font-black uppercase tracking-widest", isLight ? "text-slate-500" : "text-slate-500")}>Active Now</span>
<div className={cn("h-3 w-[1px] mx-1", isLight ? "bg-slate-200" : "bg-white/10")} />
<span className={cn("text-[10px] font-black uppercase tracking-widest flex items-center gap-1", isLight ? "text-indigo-600" : "text-indigo-400")}>
Chat
</span>
</div>
</div>
</div>
</div>
<div className="flex-1 min-h-0 overflow-y-auto p-4 md:p-8 space-y-6 custom-scrollbar scroll-smooth">
{messagesError && (
<div className="mb-4 px-4 py-3 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm font-medium flex items-center justify-between gap-2">
<span>{messagesError}</span>
<button type="button" onClick={() => setMessagesError(null)} className="shrink-0 text-red-400 hover:text-red-300" aria-label="Dismiss">×</button>
</div>
)}
{loadingMessages ? (
<div className="h-full flex flex-col items-center justify-center gap-3">
<Loader2 className="w-8 h-8 animate-spin text-indigo-400" />
<p className="text-xs font-black uppercase tracking-widest text-slate-600">Syncing History...</p>
</div>
) : messages.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-center max-w-xs mx-auto opacity-80">
<ShieldCheck className={cn("w-16 h-16 mb-4", isLight ? "text-indigo-500" : "text-indigo-500")} />
<h4 className={cn("text-lg font-black mb-2", isLight ? "text-slate-900" : "text-white")}>Conversation Started</h4>
<p className={cn("text-sm font-medium", isLight ? "text-slate-500" : "text-slate-400")}>Start chatting with your connection.</p>
</div>
) : (
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 (
<div
key={msg.id}
className={cn(
"flex flex-col animate-in fade-in slide-in-from-bottom-2 duration-500",
isMe ? "items-end" : "items-start"
)}
>
<div className={cn(
"max-w-[85%] md:max-w-[70%] px-5 py-4 rounded-2xl text-sm font-medium relative group",
isMe
? "bg-indigo-600 text-white rounded-tr-none shadow-lg shadow-indigo-600/10"
: isLight
? "bg-white border border-slate-200 text-slate-800 rounded-tl-none shadow-md"
: "bg-white/5 border border-white/10 text-slate-200 rounded-tl-none backdrop-blur-sm shadow-xl"
)}>
{kindToRender === "PRODUCT" && attachmentToRender ? (
<button
type="button"
onClick={() => {
setActiveAttachmentKind("PRODUCT")
setActiveAttachment(attachmentToRender)
setAttachmentModalOpen(true)
}}
className={cn("w-full text-left bg-transparent border-0 p-0 m-0")}
>
<div className="flex items-center gap-3">
<div className="w-14 h-14 rounded-xl overflow-hidden bg-black/10 border border-white/10 flex-shrink-0 flex items-center justify-center">
{attachmentToRender.imageUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={attachmentToRender.imageUrl} alt={attachmentToRender.title || "Product"} className="w-full h-full object-cover" />
) : (
<div className="flex flex-col items-center justify-center px-1">
<Package className="w-5 h-5 text-indigo-400" />
<div className="text-[10px] font-black mt-1 leading-none text-indigo-200">
{attachmentToRender.title ? attachmentToRender.title : ""}
</div>
</div>
)}
</div>
<div className="min-w-0">
<div className="font-black truncate">
{attachmentToRender.title || msg.content}
</div>
{attachmentToRender.description ? (
<div className={cn("text-xs opacity-90 mt-1 line-clamp-2")}>
{attachmentToRender.description}
</div>
) : null}
{attachmentToRender.storeName ? (
<div className={cn("text-[10px] font-black uppercase tracking-widest mt-1 opacity-80")}>
{attachmentToRender.storeName}
</div>
) : null}
</div>
</div>
</button>
) : kindToRender === "ORDER" && attachmentToRender ? (
<button
type="button"
onClick={() => {
setActiveAttachmentKind("ORDER")
setActiveAttachment(attachmentToRender)
setAttachmentModalOpen(true)
}}
className={cn("w-full text-left bg-transparent border-0 p-0 m-0")}
>
<div className="flex items-center gap-3">
<div className="w-14 h-14 rounded-xl overflow-hidden bg-black/10 border border-white/10 flex-shrink-0 flex items-center justify-center">
{attachmentToRender.primaryImageUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={attachmentToRender.primaryImageUrl} alt="Order item" className="w-full h-full object-cover" />
) : (
<div className="flex flex-col items-center justify-center px-1">
<ShoppingCart className="w-5 h-5 text-indigo-400" />
<div className="text-[10px] font-black mt-1 leading-none text-indigo-200">
{attachmentToRender.orderNumber || "#"}
</div>
</div>
)}
</div>
<div className="min-w-0">
<div className="font-black truncate">
Order {attachmentToRender.orderNumber || msg.content}
</div>
{attachmentToRender.customerName ? (
<div className={cn("text-xs opacity-90 mt-1 line-clamp-1")}>
{attachmentToRender.customerName}
</div>
) : null}
{attachmentToRender.total_price ? (
<div className={cn("text-[10px] font-black uppercase tracking-widest mt-1 opacity-80")}>
{attachmentToRender.currency ? `${attachmentToRender.currency} ` : ""}{attachmentToRender.total_price}
</div>
) : null}
</div>
</div>
</button>
) : (
msg.content
)}
<div className={cn(
"absolute bottom-2 -right-6 flex items-center gap-1 transition-opacity opacity-0 group-hover:opacity-100",
isMe ? "-left-8 right-auto flex-row-reverse" : ""
)}>
{isMe && <CheckCheck className={cn("w-3.5 h-3.5", isLight ? "text-indigo-300" : "text-indigo-400")} />}
</div>
</div>
{showTime && (
<span className={cn("text-[10px] font-black mt-2 uppercase tracking-widest px-1", isLight ? "text-slate-500" : "text-slate-600")}>
{format(new Date(msg.createdAt), "HH:mm")}
</span>
)}
</div>
)
})
)}
<div ref={messagesEndRef} />
<ChatAttachmentModal
isOpen={attachmentModalOpen}
onClose={() => setAttachmentModalOpen(false)}
kind={activeAttachmentKind}
attachment={activeAttachment}
isLight={isLight}
/>
</div>
<div className="p-6 flex-shrink-0">
{sendError && (
<div className="mb-4 px-4 py-3 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm font-medium flex flex-wrap items-center justify-between gap-2">
<span>{sendError}</span>
<div className="flex items-center gap-2 shrink-0">
<button type="button" onClick={() => setSendError(null)} className="text-red-400 hover:text-red-300" aria-label="Dismiss">×</button>
</div>
</div>
)}
<form
onSubmit={handleSendMessage}
className={cn(
"relative flex items-end gap-3 p-3 rounded-2xl transition-all shadow-lg",
isLight
? "bg-white border border-slate-200 focus-within:border-indigo-400 focus-within:ring-2 focus-within:ring-indigo-500/20"
: "glass-panel bg-white/5 border border-white/10 focus-within:border-indigo-500/50 focus-within:bg-white/10"
)}
>
<div className="relative" ref={attachRef}>
<button
type="button"
onClick={openAttach}
className={cn("p-3 rounded-xl transition-all", isLight ? "text-slate-500 hover:bg-slate-100 hover:text-indigo-600" : "hover:bg-white/5 text-slate-500 hover:text-indigo-400")}
aria-label="Attach order or product"
>
<Plus className="w-5 h-5" />
</button>
{attachOpen && (
<div
className={cn(
"absolute bottom-full left-0 mb-2 w-80 max-h-72 overflow-hidden rounded-2xl shadow-xl border z-50 flex flex-col",
isLight ? "bg-white border-slate-200" : "bg-slate-800/95 border-white/10 backdrop-blur-xl"
)}
>
<div className={cn("p-2 border-b flex items-center justify-between flex-shrink-0", isLight ? "border-slate-200" : "border-white/10")}>
<span className={cn("text-xs font-bold uppercase tracking-wider", isLight ? "text-slate-500" : "text-slate-400")}>
{isSupplier ? "Attach product" : "Attach order or product"}
</span>
<button type="button" onClick={() => setAttachOpen(false)} className="p-1.5 rounded-lg hover:bg-white/10 text-slate-500" aria-label="Close">
<X className="w-4 h-4" />
</button>
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar p-2">
{attachLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className={cn("w-6 h-6 animate-spin", isLight ? "text-indigo-500" : "text-indigo-400")} />
</div>
) : (
<>
{!isSupplier && attachOrders.length > 0 && (
<div className="mb-3">
<p className={cn("text-[10px] font-bold uppercase tracking-wider mb-1.5 flex items-center gap-1", isLight ? "text-slate-500" : "text-slate-400")}>
<ShoppingCart className="w-3 h-3" /> Orders
</p>
<div className="space-y-1">
{attachOrders.map((order) => (
<button
key={`${order.storeId}-${order.id}`}
type="button"
onClick={() => insertOrder(order)}
className={cn("w-full text-left px-3 py-2 rounded-xl text-sm font-medium transition-colors", isLight ? "hover:bg-slate-100 text-slate-800" : "hover:bg-white/10 text-slate-200")}
>
<span className="font-semibold">{order.name}</span>
{order.storeName && <span className={cn("ml-1.5 text-[10px] uppercase tracking-wider", isLight ? "text-slate-500" : "text-slate-400")}>· {order.storeName}</span>}
<span className="block text-xs opacity-80">{order.currency || ""}{order.total_price}</span>
</button>
))}
</div>
</div>
)}
{attachProducts.length > 0 && (
<div>
<p className={cn("text-[10px] font-bold uppercase tracking-wider mb-1.5 flex items-center gap-1", isLight ? "text-slate-500" : "text-slate-400")}>
<Package className="w-3 h-3" /> Products
</p>
<div className="space-y-1">
{attachProducts.map((product) => (
<button
key={product.storeId ? `${product.storeId}-${product.id}` : String(product.id)}
type="button"
onClick={() => insertProduct(product)}
className={cn("w-full text-left px-3 py-2 rounded-xl text-sm font-medium transition-colors", isLight ? "hover:bg-slate-100 text-slate-800" : "hover:bg-white/10 text-slate-200")}
>
<span className="line-clamp-1">{product.title}</span>
{product.storeName && <span className={cn("text-[10px] uppercase tracking-wider block mt-0.5", isLight ? "text-slate-500" : "text-slate-400")}>{product.storeName}</span>}
</button>
))}
</div>
</div>
)}
{!attachLoading && attachOrders.length === 0 && attachProducts.length === 0 && (
<p className={cn("py-6 text-center text-sm", isLight ? "text-slate-500" : "text-slate-500")}>
{isSupplier ? "No products to attach." : "No orders or products found."}
</p>
)}
</>
)}
</div>
</div>
)}
</div>
<textarea
rows={1}
placeholder="Type a message..."
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
handleSendMessage(e)
}
}}
className={cn(
"flex-1 max-h-40 py-3 bg-transparent border-none focus:outline-none focus:ring-0 text-sm resize-none font-medium custom-scrollbar",
isLight ? "text-slate-900 placeholder:text-slate-400" : "text-white placeholder:text-slate-600"
)}
/>
<button
type="submit"
disabled={(!newMessage.trim() && !(draftKind && draftAttachment)) || sending}
className={cn(
"p-4 rounded-xl transition-all shadow-lg active:scale-90 disabled:opacity-50",
(newMessage.trim() || (draftKind && draftAttachment))
? "bg-indigo-600 text-white shadow-indigo-600/20"
: isLight ? "bg-slate-100 text-slate-500" : "bg-white/5 text-slate-700"
)}
>
{sending ? <Loader2 className="w-5 h-5 animate-spin" /> : <Send className="w-5 h-5" />}
</button>
</form>
<p className={cn("mt-3 text-[9px] font-black uppercase tracking-[0.2em] text-center", isLight ? "text-slate-500" : "text-slate-600")}>
Real-time chat enabled
</p>
</div>
</>
) : (
<div className="flex flex-col items-center text-center p-12 max-w-sm">
<div className={cn("w-24 h-24 rounded-full flex items-center justify-center mb-8 relative", isLight ? "bg-indigo-100" : "bg-indigo-500/5")}>
<div className={cn("absolute inset-0 rounded-full animate-ping opacity-20", isLight ? "bg-indigo-300" : "bg-indigo-500/10")} />
<MessageSquare className={cn("w-10 h-10", isLight ? "text-indigo-400" : "text-indigo-500/40")} />
</div>
<h3 className={cn("text-2xl font-black mb-3 tracking-tight", isLight ? "text-slate-900" : "text-white")}>Your Direct Line</h3>
<p className={cn("text-sm font-medium leading-relaxed", isLight ? "text-slate-500" : "text-slate-500")}>
Messaging workspace for vendors and suppliers.
Select a contact to start a session.
</p>
</div>
)}
</div>
</div>
)
}