From d3122d44bf80d34af3978ae4c23fe7b279734a3c Mon Sep 17 00:00:00 2001 From: talatops Date: Thu, 2 Apr 2026 14:02:41 +0500 Subject: [PATCH] fix: UI audit --- src/app/supplier/chat/page.tsx | 6 +- src/components/chat/ChatAttachmentModal.tsx | 29 +- src/components/chat/ChatView.tsx | 284 +++++++++----------- 3 files changed, 155 insertions(+), 164 deletions(-) diff --git a/src/app/supplier/chat/page.tsx b/src/app/supplier/chat/page.tsx index 2b10418..37a61e1 100644 --- a/src/app/supplier/chat/page.tsx +++ b/src/app/supplier/chat/page.tsx @@ -3,5 +3,9 @@ import { ChatView } from "@/components/chat/ChatView" export default function SupplierChatPage() { - return + return ( +
+ +
+ ) } diff --git a/src/components/chat/ChatAttachmentModal.tsx b/src/components/chat/ChatAttachmentModal.tsx index f2040eb..7a0007e 100644 --- a/src/components/chat/ChatAttachmentModal.tsx +++ b/src/components/chat/ChatAttachmentModal.tsx @@ -1,5 +1,7 @@ "use client" +import { useEffect } from "react" +import { createPortal } from "react-dom" import { cn } from "@/lib/utils" import { Package, ShoppingCart, X } from "lucide-react" @@ -27,17 +29,33 @@ export function ChatAttachmentModal({ ? attachment.title || "Attached product" : `Order ${attachment.orderNumber || ""}`.trim() - return ( + useEffect(() => { + if (!isOpen) return + const prevOverflow = document.body.style.overflow + document.body.style.overflow = "hidden" + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose() + } + window.addEventListener("keydown", onKeyDown) + + return () => { + document.body.style.overflow = prevOverflow + window.removeEventListener("keydown", onKeyDown) + } + }, [isOpen, onClose]) + + const overlay = (
{ if (e.target === e.currentTarget) onClose() }} > -
+
@@ -151,5 +169,8 @@ export function ChatAttachmentModal({
) + + if (typeof document === "undefined") return overlay + return createPortal(overlay, document.body) } diff --git a/src/components/chat/ChatView.tsx b/src/components/chat/ChatView.tsx index ece783d..a9e897a 100644 --- a/src/components/chat/ChatView.tsx +++ b/src/components/chat/ChatView.tsx @@ -1,7 +1,6 @@ "use client" import { useState, useEffect, useRef } from "react" -import { createPortal } from "react-dom" import { usePathname } from "next/navigation" import { useSession } from "next-auth/react" import { useTheme } from "@/components/ThemeProvider" @@ -20,6 +19,7 @@ import { } from "lucide-react" import { cn } from "@/lib/utils" import { format } from "date-fns" +import { ChatAttachmentModal } from "@/components/chat/ChatAttachmentModal" interface Contact { id: string @@ -320,6 +320,87 @@ export function ChatView() { } } + // 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()) @@ -512,6 +593,20 @@ export function ChatView() { 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 (
- {msg.kind === "PRODUCT" && msg.attachment ? ( + {kindToRender === "PRODUCT" && attachmentToRender ? ( - ) : msg.kind === "ORDER" && msg.attachment ? ( + ) : kindToRender === "ORDER" && attachmentToRender ? ( -
- -
- {activeAttachmentKind === "PRODUCT" ? ( -
-
- {activeAttachment.imageUrl ? ( - // eslint-disable-next-line @next/next/no-img-element - {activeAttachment.title - ) : ( -
- -
- )} -
-
-
- {activeAttachment.title || "Product"} -
- {activeAttachment.storeName ? ( -
- {activeAttachment.storeName} -
- ) : null} - {activeAttachment.description ? ( -

- {activeAttachment.description} -

- ) : ( -

- No description provided. -

- )} -
-
- ) : ( -
-
- {activeAttachment.primaryImageUrl ? ( - // eslint-disable-next-line @next/next/no-img-element - Order image - ) : ( -
- -
- {activeAttachment.orderNumber || ""} -
-
- )} -
-
-
- Order {activeAttachment.orderNumber || ""} -
- {activeAttachment.customerName ? ( -
- Customer: {activeAttachment.customerName} -
- ) : null} - {activeAttachment.total_price ? ( -
- {activeAttachment.currency ? `${activeAttachment.currency} ` : ""} - {activeAttachment.total_price} -
- ) : null} -
-
- )} -
-
-
- , - document.body - ) : null} + setAttachmentModalOpen(false)} + kind={activeAttachmentKind} + attachment={activeAttachment} + isLight={isLight} + />