Vitron-Front/app/components/thread/ThreadsPage.tsx
Arda Samadi 3502168a8b feat(seller-team): split buyer inbox vs store inbox in chat UI
Seller dashboard threads (type="store") now fetch the store's inbox via
useGetStoreThreads(storeId) -> /threads/list/?store_id=; the profile threads page
(type="user") keeps the personal buyer inbox. Depends on the seller-team backend
(store-aware threads + store_id-by-username), so it ships on this branch, not main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:00:23 +03:30

169 lines
5.5 KiB
TypeScript

import { Link } from "@remix-run/react";
import AppInput from "../AppInput";
import { useState } from "react";
import {
useGetUserThreads,
useGetStoreThreads,
} from "../../requestHandler/use-chat-hooks";
import {
ThreadList,
ThreadParticipant,
UserProfile,
} from "../../../src/api/types";
import { useServiceGetProfile } from "../../requestHandler/use-profile-hooks";
import UiProvider from "../UiProvider";
import SellerLogo from "../SellerLogo";
import { Search, User } from "lucide-react";
import ProfilePagesHeader from "../ProfilePagesHeader";
interface ThreadsPageProps {
type: "user" | "store";
storeId?: string;
}
export default function ThreadsPage({ type, storeId }: ThreadsPageProps) {
const [searchValue, setSearchValue] = useState("");
// Seller dashboard shows the STORE's inbox; the profile page shows the user's
// personal (buyer) chats. Two different endpoints so the two never mix.
const userThreads = useGetUserThreads();
const storeThreads = useGetStoreThreads(type === "store" ? storeId : undefined);
const {
data: threads,
isLoading,
isError,
refetch,
} = type === "store" ? storeThreads : userThreads;
const sortedThreads = threads?.sort((a, b) => {
const dateA = new Date(a.lastMessageTime || "2025-01-18T00:00:00.000Z");
const dateB = new Date(b.lastMessageTime || "2025-01-18T00:00:00.000Z");
return dateB.getTime() - dateA.getTime();
});
const { data: dataProfile, isFetching: isFetchingProfile } =
useServiceGetProfile();
return (
<div
className={`flex flex-col w-full ${isLoading || isFetchingProfile ? "" : "pb-20"}`}
>
<ProfilePagesHeader title="پیام‌ها" />
<div className="w-full px-4 py-2">
<div className="w-full h-[40px] flex items-center gap-2 bg-WHITE2 rounded-[10px]">
<AppInput
inputIcon={Search}
inputDir="rtl"
inputTitle={""}
className="bg-WHITE2"
inputValue={searchValue}
inputPlaceholder={"جستجو..."}
inputOnChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setSearchValue(value);
}}
/>
</div>
</div>
<UiProvider
isLoading={isLoading || isFetchingProfile}
isError={isError}
isEmpty={!!(threads && threads.length === 0)}
onRetry={refetch}
type="threads"
>
<ThreadsList
searchValue={searchValue}
threads={sortedThreads}
dataProfile={dataProfile}
type={type}
storeId={storeId}
/>
</UiProvider>
</div>
);
}
const ThreadsList = ({
searchValue,
threads,
dataProfile,
type,
storeId,
}: {
searchValue: string;
threads: ThreadList[] | undefined;
dataProfile: UserProfile | undefined;
type: "user" | "store";
storeId?: string;
}) => {
const filteredThreads = threads?.filter((thread) => {
return thread.participants?.some((participant: ThreadParticipant) =>
participant.username?.toLowerCase().includes(searchValue.toLowerCase())
);
});
return (
<div className="flex flex-col w-full">
{filteredThreads?.map((thread) => {
const otherParticipant = thread.participants?.find(
(participant: ThreadParticipant) =>
participant.username !== dataProfile?.username
);
const hasUnread = Number(thread.unreadCount || "0") > 0;
// Generate link based on type
const linkTo =
type === "user"
? `/profile/threads/${thread.id}`
: `/store/${storeId}/threads/${thread.id}`;
return (
<Link to={linkTo} key={thread.id} className="w-full">
<div className={`w-full h-[80px] flex items-center px-4 gap-4`}>
{thread.isCreator ? (
<SellerLogo
src={otherParticipant?.storeLogo}
alt="Participant Avatar"
/>
) : (
<User size={48} className="text-GRAY" />
)}
<div className="flex flex-col w-full">
<div className="w-full flex justify-between items-center">
<p className={`text-R14 ${hasUnread ? "font-bold" : ""}`}>
{thread.isCreator
? otherParticipant?.storeName ||
otherParticipant?.username
: otherParticipant?.name}
</p>
<div className="flex flex-col items-center justify-center">
{thread.lastMessageTime && (
<p className="text-R10 text-GRAY">
{new Date(thread.lastMessageTime).toLocaleTimeString(
"fa-IR",
{
hour: "2-digit",
minute: "2-digit",
}
)}
</p>
)}
{hasUnread && (
<div className="w-5 h-5 flex items-center justify-center bg-blue-500 text-WHITE text-R10 rounded-full">
{thread.unreadCount}
</div>
)}
</div>
</div>
<p
className={`text-R14 ${hasUnread ? "text-BLACK font-bold" : "text-GRAY"}`}
>
{thread.lastMessage?.content || "بدون پیام"}
</p>
</div>
</div>
</Link>
);
})}
</div>
);
};