import { useParams, useNavigate, useSearchParams } from "react-router-dom"; import { useLoaderData } from "@remix-run/react"; import { safeGoBack } from "~/utils/helpers"; import { MetaFunction, LoaderFunctionArgs, json } from "@remix-run/node"; import { getInternalApiUrl } from "~/utils/api-config"; import { useSellerFollowStatus, useSellerFollow, useSellerUnfollow, useGetSellerProducts, } from "../requestHandler/use-seller-hooks"; import { useCreateChatThread } from "../requestHandler/use-chat-hooks"; import { Star, ChevronDown, X, UserPlus, Check, UserMinus, Shirt, } from "lucide-react"; import HeaderWithSupport from "../components/HeaderWithSupport"; import { useState, useEffect } from "react"; import { DrawerContent, Drawer } from "../components/ui/drawer"; import { Button } from "../components/ui/button"; import { Dialog, DialogContent, DialogOverlay } from "../components/ui/dialog"; import { toast } from "../hooks/use-toast"; import { Seller as SellerType } from "../../src/api/types"; import MondrianProductList from "../components/MondrianProductList"; import UiProvider from "../components/UiProvider"; import { useRootData } from "~/hooks/use-root-data"; import { LoginRequiredDialog } from "~/components/LoginRequiredDialog"; import SellerLogo from "../components/SellerLogo"; import SearchTopBar from "~/components/SearchTopBar"; interface LoaderData { seller: SellerType; } // Map order param to API ordering format const mapOrderToOrdering = (order: string): string | undefined => { switch (order) { case "cheap": return "base_price"; case "expensive": return "-base_price"; case "newest": return "-created_at"; default: return undefined; } }; // Server-side loader to fetch seller data export async function loader({ params }: LoaderFunctionArgs) { const { sellerId } = params; if (!sellerId) { throw new Response("Seller ID is required", { status: 400 }); } try { const response = await fetch(`${getInternalApiUrl()}/seller/${encodeURIComponent(sellerId)}/`); if (!response.ok) { throw new Error(`Failed to fetch seller: ${response.status}`); } const seller = await response.json(); return json({ seller }); } catch (error) { console.error("Error fetching seller:", error); throw new Response("Error loading seller", { status: 500 }); } } // export default function Seller() { const { sellerId } = useParams(); const { seller } = useLoaderData(); const [searchParams] = useSearchParams(); const { data: isFollowed, isLoading: isFollowStatusLoading } = useSellerFollowStatus(seller?.id || ""); const { mutate: followSeller, isPending: isFollowPending } = useSellerFollow(); const { mutate: unfollowSeller, isPending: isUnfollowPending } = useSellerUnfollow(); const navigate = useNavigate(); const [showUnfollowDialog, setShowUnfollowDialog] = useState(false); const { mutate: createChatThread, isPending: isCreatingChatThread } = useCreateChatThread(); const { user } = useRootData(); const [isLoginDialogOpen, setIsLoginDialogOpen] = useState(false); const [searchValue, setSearchValue] = useState(searchParams.get("q") || ""); // Get filter parameters from URL const orderParam = searchParams.get("order"); const priceMinParam = searchParams.get("priceMin"); const priceMaxParam = searchParams.get("priceMax"); // Build filter options const filterOptions = { q: searchParams.get("q") || undefined, priceMin: priceMinParam ? parseInt(priceMinParam, 10) : undefined, priceMax: priceMaxParam ? parseInt(priceMaxParam, 10) : undefined, ordering: orderParam ? mapOrderToOrdering(orderParam) : undefined, }; // Fetch seller products with infinite scroll and filters const { data: productsData, isLoading: isProductsLoading, fetchNextPage, hasNextPage, isFetchingNextPage, isError, refetch, } = useGetSellerProducts(sellerId || "", filterOptions); const products = productsData?.pages.flatMap((page) => page.results) || []; // Update search value when URL changes useEffect(() => { setSearchValue(searchParams.get("q") || ""); }, [searchParams]); const handleFollowToggle = () => { if (!user) { setIsLoginDialogOpen(true); return; } if (!sellerId) return; if (isFollowed) { // If already following, show confirmation dialog setShowUnfollowDialog(true); } else { // If not following, follow followSeller( { sellerId: seller?.id || "" }, { onSuccess: () => { // Close the dialog only after successful unfollow toast({ title: "دنبال شد", description: "شما با موفقیت فروشگاه را دنبال کردید", }); setShowUnfollowDialog(false); }, }, ); } }; const confirmUnfollow = () => { // Don't close the dialog immediately, wait for the request to succeed unfollowSeller( { sellerId: seller?.id || "" }, { onSuccess: () => { // Close the dialog only after successful unfollow toast({ title: "آنفالو شد", description: "شما با موفقیت دنبال کردن فروشگاه را لغو کردید", }); setShowUnfollowDialog(false); }, }, ); }; const handleCreateChatThread = () => { if (!user) { setIsLoginDialogOpen(true); return; } if (!seller?.id) return; createChatThread(seller.id, { onSuccess: (threadData) => { navigate(`/profile/threads/${threadData.id}`); }, }); }; const isFollowMutating = isFollowPending || isUnfollowPending; const handleBack = () => { safeGoBack(navigate); }; return (
{/* Header */}

{seller?.followerCount}

دنبال کننده

{seller?.productReviewsCount ? ( <>

{seller?.reviewAverage}

(از {seller?.productReviewsCount} نظر)

{Array.from({ length: 5 }).map((_, index) => ( ))}
) : (

بدون امتیاز

)}

{seller?.productCount}

تعداد محصولات

{/* Search Bar */} {/* seller products */} ({ id: product?.id || "", title: product?.title || "", images: product?.imageUrls || [], basePrice: product?.basePrice ? parseInt(product?.basePrice) : undefined, discountPercent: product?.discountPercent ? product?.discountPercent : undefined, discountPrice: product?.discountPrice ? product?.discountPrice : undefined, }))} fetchNextPage={fetchNextPage} hasNextPage={!!hasNextPage} isFetchingNextPage={isFetchingNextPage} isLoading={isProductsLoading} isError={isError} refetch={refetch} emptyState={{ image: , title: "این فروشگاه هیچ محصولی ندارد", description: "به زودی محصولات جدیدی از این فروشگاه خواهیم داشت", }} />

لغو دنبال کردن

میخواهید دنبال کردن این فروشگاه را لغو کنید؟

); } function ReadMoreText({ text, maxLength, sellerData, }: { text: string; maxLength: number; sellerData: SellerType | undefined; }) { const [aboutDrawerOpen, setAboutDrawerOpen] = useState(false); if (text.length <= maxLength) { return

{text}

; } return ( <>

{text.substring(0, maxLength)} ...

setAboutDrawerOpen(false)} />

{sellerData?.storeName}

{text}

); } export const meta: MetaFunction = ({ params, data }) => { const sellerId = params.sellerId; const sellerName = data?.seller?.storeName; const title = `فروشگاه ${sellerName} | ویترون`; const description = `مشاهده محصولات و اطلاعات فروشگاه ${sellerName} در ویترون`; const imageUrl = data?.seller?.storeLogo || "https://vitrownstatics.s3.ir-thr-at1.arvanstorage.ir/SVG-06.svg?versionId="; const siteUrl = `${import.meta.env.VITE_SITE_URL || "https://vitrown.com"}/seller/${sellerId}`; return [ { title }, { name: "description", content: description }, { name: "keywords", content: `فروشگاه، ${sellerName}، محصولات، ویترون`, }, { name: "robots", content: "index, follow" }, // Open Graph { property: "og:type", content: "website" }, { property: "og:title", content: title }, { property: "og:description", content: description }, { property: "og:image", content: imageUrl }, { property: "og:site_name", content: "ویترون" }, { property: "og:locale", content: "fa_IR" }, { property: "og:url", content: siteUrl }, // Twitter Card { name: "twitter:card", content: "summary_large_image" }, { name: "twitter:title", content: title }, { name: "twitter:description", content: description }, { name: "twitter:image", content: imageUrl }, ]; };