Compare commits

...

2 Commits

Author SHA1 Message Date
2cf1224107 change(size-chart): centimeter only (drop cm/inch toggle)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 15:09:01 +03:30
7867db425e feat(chat): share the product into the chat + fix empty-thread preview
- Product-page chat buttons pass the product id when starting a thread, so it's
  shared into the conversation as a card the seller can see (and tap through).
- MessageBubble renders the product card (image, title, price → product page);
  card-only messages no longer render an empty bubble.
- Inbox preview shows 🛍 <product> instead of "بدون پیام" for product-card
  threads. ChatProductCard type + product on message/last-message types.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 15:09:01 +03:30
8 changed files with 117 additions and 45 deletions

View File

@ -19,11 +19,14 @@ export const ContactButton = ({ product }: { product: ProductDetail }) => {
return; return;
} }
if (!product.seller?.id) return; if (!product.seller?.id) return;
createChatThread(product.seller.id, { createChatThread(
{ participantId: product.seller.id, productId: product.id },
{
onSuccess: (threadData) => { onSuccess: (threadData) => {
navigate(`/profile/threads/${threadData.id}`); navigate(`/profile/threads/${threadData.id}`);
}, },
}); }
);
}; };
return ( return (
<> <>

View File

@ -330,9 +330,12 @@ export const ProductDetailView = memo(function ProductDetailView({
return; return;
} }
if (!product.seller?.id) return; if (!product.seller?.id) return;
createChatThread(product.seller.id, { createChatThread(
{ participantId: product.seller.id, productId: product.id },
{
onSuccess: (t) => navigate(`/profile/threads/${t.id}`), onSuccess: (t) => navigate(`/profile/threads/${t.id}`),
}); }
);
}; };
return ( return (

View File

@ -24,11 +24,14 @@ export const ProductHeader = ({ product }: ProductHeaderProps) => {
return; return;
} }
if (!product.seller?.id) return; if (!product.seller?.id) return;
createChatThread(product.seller.id, { createChatThread(
{ participantId: product.seller.id, productId: product.id },
{
onSuccess: (threadData) => { onSuccess: (threadData) => {
navigate(`/profile/threads/${threadData.id}`); navigate(`/profile/threads/${threadData.id}`);
}, },
}); }
);
}; };
return ( return (

View File

@ -20,11 +20,6 @@ interface SizeChartSectionProps {
showError?: boolean; showError?: boolean;
} }
const UNITS: { value: string; label: string }[] = [
{ value: "cm", label: "سانتی‌متر" },
{ value: "inch", label: "اینچ" },
];
export function SizeChartSection({ export function SizeChartSection({
value, value,
onChange, onChange,
@ -171,26 +166,8 @@ export function SizeChartSection({
</p> </p>
)} )}
{/* Unit toggle */} <div className="flex items-center justify-end">
<div className="flex items-center justify-between"> <span className="text-R12 text-GRAY">اندازهها به سانتیمتر</span>
<span className="text-R12 text-GRAY">واحد اندازهگیری</span>
<div className="flex overflow-hidden rounded-lg border border-GRAY3">
{UNITS.map((u) => (
<button
key={u.value}
type="button"
onClick={() => emit({ unit: u.value })}
className={cn(
"px-3 py-1 text-R12 transition-colors cursor-pointer",
unit === u.value
? "bg-BLACK text-WHITE"
: "bg-WHITE text-BLACK2 hover:bg-WHITE3"
)}
>
{u.label}
</button>
))}
</div>
</div> </div>
{/* Grid */} {/* Grid */}

View File

@ -1,4 +1,5 @@
import { Reply, MoreVertical, Trash2, Check } from "lucide-react"; import { Reply, MoreVertical, Trash2, Check } from "lucide-react";
import { Link } from "@remix-run/react";
import { import {
DropdownMenu, DropdownMenu,
@ -81,7 +82,34 @@ export const MessageBubble = ({
{replyToMessageContent || "پاسخ به پیام"} {replyToMessageContent || "پاسخ به پیام"}
</div> </div>
)} )}
<p>{message.content}</p> {message.product && (
<Link
to={`/product/${message.product.id}`}
className="flex items-center gap-2 mb-1 w-[210px] max-w-full rounded-lg bg-WHITE border border-GRAY3 p-2"
>
{message.product.primaryImage ? (
<img
src={message.product.primaryImage}
alt=""
className="w-12 h-12 rounded-md object-cover shrink-0"
/>
) : (
<div className="w-12 h-12 rounded-md bg-WHITE3 shrink-0" />
)}
<div className="min-w-0 text-right">
<p className="text-R12 text-BLACK truncate">
{message.product.title}
</p>
{message.product.basePrice && (
<p className="text-R10 text-GRAY mt-0.5">
{parseInt(message.product.basePrice).toLocaleString("fa-IR")}{" "}
تومان
</p>
)}
</div>
</Link>
)}
{message.content && <p>{message.content}</p>}
</div> </div>
<div className="flex flex-col justify-between items-start"> <div className="flex flex-col justify-between items-start">
<div className="flex flex-col gap-1 items-start"> <div className="flex flex-col gap-1 items-start">

View File

@ -156,7 +156,10 @@ const ThreadsList = ({
<p <p
className={`text-R14 ${hasUnread ? "text-BLACK font-bold" : "text-GRAY"}`} className={`text-R14 ${hasUnread ? "text-BLACK font-bold" : "text-GRAY"}`}
> >
{thread.lastMessage?.content || "بدون پیام"} {thread.lastMessage?.content ||
(thread.lastMessage?.product
? `🛍 ${thread.lastMessage.product.title || "محصول"}`
: "بدون پیام")}
</p> </p>
</div> </div>
</div> </div>

View File

@ -206,15 +206,21 @@ export function useGetThreadMessages(threadId: string) {
export function useCreateChatThread() { export function useCreateChatThread() {
const token = useAuthToken(); const token = useAuthToken();
return useMutation({ return useMutation({
mutationFn: async (participantId: string) => { mutationFn: async (
input: string | { participantId: string; productId?: string }
) => {
if (!token) { if (!token) {
throw new Error("Authentication token and user ID are required"); throw new Error("Authentication token and user ID are required");
} }
// Back-compat: callers may pass a bare participantId string.
const { participantId, productId } =
typeof input === "string" ? { participantId: input, productId: undefined } : input;
try { try {
const chatApi = createChatApi(token); const chatApi = createChatApi(token);
return await chatApi.apiChatV1ThreadsCreateCreate({ return await chatApi.apiChatV1ThreadsCreateCreate({
data: { data: {
participantId: participantId, participantId,
...(productId ? { productId } : {}),
}, },
}); });
} catch (error) { } catch (error) {

View File

@ -2341,6 +2341,43 @@ export interface LastMessage {
* @memberof LastMessage * @memberof LastMessage
*/ */
readonly createdAt?: string; readonly createdAt?: string;
/**
* Product shared into the chat, if this message is a product card.
* @type {ChatProductCard}
* @memberof LastMessage
*/
readonly product?: ChatProductCard | null;
}
/**
* Minimal product info rendered as a card inside a chat.
* @export
* @interface ChatProductCard
*/
export interface ChatProductCard {
/**
*
* @type {string}
* @memberof ChatProductCard
*/
readonly id?: string;
/**
*
* @type {string}
* @memberof ChatProductCard
*/
readonly title?: string;
/**
*
* @type {string}
* @memberof ChatProductCard
*/
readonly basePrice?: string;
/**
*
* @type {string}
* @memberof ChatProductCard
*/
readonly primaryImage?: string | null;
} }
/** /**
* *
@ -2484,6 +2521,12 @@ export interface MessageList {
* @memberof MessageList * @memberof MessageList
*/ */
readonly isThreadCreator?: boolean; readonly isThreadCreator?: boolean;
/**
* Product shared into the chat, if this message is a product card.
* @type {ChatProductCard}
* @memberof MessageList
*/
readonly product?: ChatProductCard | null;
} }
/** /**
* *
@ -6464,6 +6507,12 @@ export interface ThreadCreate {
* @memberof ThreadCreate * @memberof ThreadCreate
*/ */
participantId: string; participantId: string;
/**
* Optional product the chat is about; shared into the thread as a card
* @type {string}
* @memberof ThreadCreate
*/
productId?: string | null;
} }
/** /**
* *