import { useState, useEffect, useMemo, memo } from "react"; import { ProductImageCarousel, ProductActions, ProductInfo, ProductDescription, ColorSelector, SizeSelector, ContactButton, ReviewSection, BottomBar, } from "./index"; import { ProductDetail as ProductDetailType } from "../../../src/api/types"; import { ArrowRight, Eye, EyeOff, Heart, Sparkles, Star, ShoppingCart, Truck, RotateCcw, ShieldCheck, MessageCircle, } from "lucide-react"; import { Button } from "../ui/button"; import { useNavigate, Link } from "@remix-run/react"; import { useSimilarProductsInfinite } from "~/requestHandler/use-product-hooks"; import MondrianProductList from "../MondrianProductList"; import SellerLogo from "../SellerLogo"; import { useServiceGetProductRating, useServiceGetComments, useServiceCheckBookmark, useServiceBookmarkProduct, useServiceDeleteBookmark, } from "~/utils/RequestHandler"; import { useAddToCart } from "~/requestHandler/use-cart-hooks"; import { useCreateChatThread } from "~/requestHandler/use-chat-hooks"; import { useRecordProductView } from "~/requestHandler/use-view-hooks"; import { findVariantId, formatPrice, safeGoBack } from "~/utils/helpers"; import { useToast } from "~/hooks/use-toast"; import { useRootData } from "~/hooks/use-root-data"; import { LoginRequiredDialog } from "../LoginRequiredDialog"; import AIPromoteModal from "../AIPromoteModal"; import { SizeGuideDialog } from "./SizeGuideDialog"; interface ProductDetailViewProps { product: ProductDetailType; isOwner: boolean; } export const ProductDetailView = memo(function ProductDetailView({ product, isOwner, }: ProductDetailViewProps) { // Local state for product active status (to handle real-time updates) const [productActiveStatus, setProductActiveStatus] = useState( product.isActive || false ); const [isChangeStatusDialogOpen, setIsChangeStatusDialogOpen] = useState(false); // Fetch similar products with infinite scroll const { data: similarProductsData, isLoading: isLoadingSimilar, isError: isErrorSimilar, refetch: refetchSimilar, fetchNextPage, hasNextPage, isFetchingNextPage, } = useSimilarProductsInfinite(product.id || "", 20); // Sync local state when product prop changes (e.g., on navigation) useEffect(() => { setProductActiveStatus(product.isActive || false); }, [product.isActive]); // Record a product view for the recommendation system (skip the owner's own // product; only tracks authenticated users, deduped per product). useRecordProductView(product.id, { enabled: !isOwner }); // Memoize available colors and sizes const { availableColors, availableSizes } = useMemo( () => ({ availableColors: product.attributes?.COLOR?.filter((p) => p.value !== "UNSELECTED") || [], availableSizes: product.attributes?.SIZE?.filter((p) => p.value !== "UNSELECTED") || [], }), [product.attributes] ); // Get variants with stock const stockedVariants = useMemo( () => product.variants?.filter( (variant) => variant.stock && variant.stock > 0 ) || [], [product.variants] ); // Memoize in-stock colors and sizes const { inStockColors, inStockSizes } = useMemo(() => { const colors = availableColors.filter((color) => { return stockedVariants.some( (variant) => variant.resolvedAttributes?.COLOR === color.value ); }); const sizes = availableSizes.filter((size) => { return stockedVariants.some( (variant) => variant.resolvedAttributes?.SIZE === size.value ); }); return { inStockColors: colors, inStockSizes: sizes }; }, [availableColors, availableSizes, stockedVariants]); // Memoize stock checks const { hasAnyStock, hasColorVariants, hasSizeVariants } = useMemo( () => ({ hasAnyStock: stockedVariants.length > 0, hasColorVariants: inStockColors.length > 0, hasSizeVariants: inStockSizes.length > 0, }), [stockedVariants.length, inStockColors.length, inStockSizes.length] ); // Effective pricing for a variant: original price + sale price (or null when // there's no real discount). Mirrors how the list/tile serializer computes // discount_price so the detail page stays consistent with the tiles. const pricingOf = (v?: (typeof stockedVariants)[number] | null) => ({ price: v?.price || product.basePrice, discountPrice: v?.discountPrice && v.discountPrice !== "0" && // Only surface as a "discount" when it's a real saving. A value equal // to (or somehow larger than) `price` is stale/mis-entered state, not a // discount — sellers occasionally end up with matching numbers when // they set "no discount" on edit, and rendering a strikethrough on that // fakes a promo the seller didn't intend. Number(v.discountPrice) < Number(v.price) ? v.discountPrice : null, }); // Cheapest in-stock variant by final (post-discount) price — this is the // "starting from" price the product tile shows. Used as the default // selection and as the fallback when no exact color/size combo is matched. const defaultVariant = useMemo(() => { const finalOf = (v: (typeof stockedVariants)[number]) => { const pr = pricingOf(v); return Number(pr.discountPrice ?? pr.price ?? 0); }; const pool = stockedVariants.length > 0 ? stockedVariants : product.variants ?? []; if (pool.length === 0) return null; return pool.reduce((best, v) => (finalOf(v) < finalOf(best) ? v : best)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [stockedVariants, product.variants]); const [selectedColor, setSelectedColor] = useState(() => { if (!hasColorVariants || inStockColors.length === 0) return null; const colorVal = defaultVariant?.resolvedAttributes?.COLOR; const match = inStockColors.find((c) => c.value === colorVal) ?? inStockColors[0]; return (match.info as unknown as { detail: string })?.detail ?? null; }); const [selectedSize, setSelectedSize] = useState(() => { if (!hasSizeVariants || inStockSizes.length === 0) return null; return defaultVariant?.resolvedAttributes?.SIZE ?? inStockSizes[0].value; }); // Handle product status change const handleProductStatusChange = (newStatus: boolean) => { setProductActiveStatus(newStatus); }; const [currentPrice, setCurrentPrice] = useState( () => pricingOf(defaultVariant).price ); const [currentDiscountPrice, setCurrentDiscountPrice] = useState< string | null >(() => pricingOf(defaultVariant).discountPrice); const [currentStock, setCurrentStock] = useState(null); useEffect(() => { const matchingVariant = product.variants?.find((variant) => { let colorMatch = true; let sizeMatch = true; // Check color match only if product has color variants if (hasColorVariants) { if (selectedColor) { // Find the color value that matches the selected color detail const selectedColorValue = inStockColors.find( (color) => (color.info as unknown as { detail: string })?.detail === selectedColor )?.value; colorMatch = variant.resolvedAttributes?.COLOR === selectedColorValue; } else { colorMatch = false; // No color selected but product has color variants } } // Check size match only if product has size variants if (hasSizeVariants) { if (selectedSize) { sizeMatch = variant.resolvedAttributes?.SIZE === selectedSize; } else { sizeMatch = false; // No size selected but product has size variants } } return colorMatch && sizeMatch; }); if (matchingVariant) { const pr = pricingOf(matchingVariant); setCurrentPrice(pr.price); setCurrentDiscountPrice(pr.discountPrice); setCurrentStock(matchingVariant.stock || null); } else if ( !hasColorVariants && !hasSizeVariants && stockedVariants.length > 0 ) { // No color/size dimensions → use the first available variant. const firstVariant = stockedVariants[0]; const pr = pricingOf(firstVariant); setCurrentPrice(pr.price); setCurrentDiscountPrice(pr.discountPrice); setCurrentStock(firstVariant.stock || null); } else { // Color/size not fully matched yet: show the cheapest variant's price + // sale (same as the product tile) instead of basePrice with no discount. const pr = pricingOf(defaultVariant); setCurrentPrice(pr.price); setCurrentDiscountPrice(pr.discountPrice); setCurrentStock(null); } }, [ selectedColor, selectedSize, product, inStockColors, hasColorVariants, hasSizeVariants, stockedVariants, defaultVariant, ]); const navigate = useNavigate(); // --- Desktop buy/actions state & data (overlays + info column) --- const { user } = useRootData(); const { toast } = useToast(); const [qty, setQty] = useState(1); const [isAIOpen, setIsAIOpen] = useState(false); const [isLoginOpen, setIsLoginOpen] = useState(false); const [isSizeGuideOpen, setIsSizeGuideOpen] = useState(false); const hasSizeChart = !!( product.sizeChart && product.sizeChart.columns?.length && product.sizeChart.rows?.length ); const { data: ratingData } = useServiceGetProductRating(product.id || ""); const { data: commentsData } = useServiceGetComments(1, product.id || ""); const { data: bookmarkData } = useServiceCheckBookmark(product.id || ""); const { mutate: bookmarkProduct } = useServiceBookmarkProduct(); const { mutate: deleteBookmark } = useServiceDeleteBookmark(); const addToCartMutation = useAddToCart(); const { mutate: createChatThread } = useCreateChatThread(); const isBookmarked = bookmarkData?.bookmarked === true; const avgRating = ratingData?.averageRating || 0; const reviewCount = commentsData?.count || 0; const isOutOfStock = currentStock === null || currentStock <= 0; const selectedColorValue = inStockColors.find( (color) => (color.info as unknown as { detail: string })?.detail === selectedColor )?.value || null; const handleToggleBookmark = () => { if (!user?.access_token) { setIsLoginOpen(true); return; } if (isBookmarked) deleteBookmark(product.id || ""); else bookmarkProduct(product.id || ""); }; const handleDesktopAddToCart = async () => { if (!user?.access_token) { setIsLoginOpen(true); return; } if (isOutOfStock) return; const variantId = findVariantId(product, selectedColorValue, selectedSize); if (!variantId) { toast({ title: "خطا", description: "محصول با ویژگی‌های انتخابی شما یافت نشد", variant: "destructive", }); return; } try { await addToCartMutation.mutateAsync({ variantId, quantity: qty }); toast({ title: "سبد خرید", description: "محصول با موفقیت به سبد خرید اضافه شد", }); } catch (e) { toast({ title: "خطا", description: "افزودن به سبد خرید با مشکل مواجه شد", variant: "destructive", }); } }; const handleChat = () => { if (!user?.access_token) { setIsLoginOpen(true); return; } if (!product.seller?.id) return; createChatThread( { participantId: product.seller.id, productId: product.id }, { onSuccess: (t) => navigate(`/profile/threads/${t.id}`), } ); }; return (
{/* Desktop breadcrumb */} {/* Top section: gallery | info (two columns on desktop) */}
{/* Gallery */}
{!isOwner && ( <> {/* Save (desktop overlay) */} {/* AI try-on (desktop overlay) */} )} {/* Floating back button (mobile) */} {!isOwner && ( )}
{/* Info */}
{/* Brand row (mobile) — moved out of the top bar, sits below image */} {!isOwner && product.seller?.username ? (
{product.seller.username}
) : null} {/* Seller (desktop) */} {!isOwner && product.seller?.username ? ( {product.seller.username} ) : null} {/* Inactive Product Status */} {!productActiveStatus && isOwner ? (
این محصول در حال حاضر غیرفعال است
) : !isOwner ? (
) : null} {/* Desktop: title + rating + price */}

{product.title}

{[1, 2, 3, 4, 5].map((i) => ( ))} {avgRating > 0 ? ( {avgRating.toFixed(1)} ) : null} {reviewCount} دیدگاه
{formatPrice(currentDiscountPrice || currentPrice)} تومان {currentDiscountPrice ? ( {formatPrice(currentPrice)} ) : null}
{/* Mobile: title + description */}
{/* Mobile: rating (only when > 0) */} {!isOwner && avgRating > 0 ? (
{[1, 2, 3, 4, 5].map((i) => ( ))} {avgRating.toFixed(1)} {reviewCount} دیدگاه
) : null} {hasAnyStock ? ( <> {hasColorVariants && ( (color.info as unknown as { detail: string })?.detail )} selectedColor={selectedColor} setSelectedColor={setSelectedColor} /> )} {hasSizeVariants && ( size.value || "")} selectedSize={selectedSize} setSelectedSize={setSelectedSize} onSizeGuide={ hasSizeChart ? () => setIsSizeGuideOpen(true) : undefined } /> )} ) : (

موجود نیست

)} {isOwner && (
)} {/* Desktop buy row + assurances */} {!isOwner && (
{qty}
ارسال سریع ۲ تا ۴ روز کاری
۷ روز بازگشت ضمانت تعویض کالا
پرداخت امن تضمین اصالت کالا
)} {/* Buy box (fixed bottom bar — mobile only) */} {!isOwner && (
(color.info as unknown as { detail: string })?.detail === selectedColor )?.value || null } selectedSize={selectedSize} />
)}
{/* Full-width below the fold: description, contact, reviews, similar */} {productActiveStatus && ( <> {!isOwner && (
)} {/* Similar Products Section */} {!isOwner && (

محصولات مشابه

محصولات مرتبط که ممکن است بپسندید

page.results || []) .map((product) => ({ id: product?.id || "", title: product?.title || "", images: Array.isArray(product?.imageUrls) ? product.imageUrls : [], discountPercent: product?.discountPercent || 0, discountPrice: product?.discountPrice || 0, basePrice: product?.basePrice || 0, })) || [] } fetchNextPage={fetchNextPage} hasNextPage={hasNextPage} isFetchingNextPage={isFetchingNextPage} isLoading={isLoadingSimilar} isError={isErrorSimilar} refetch={refetchSimilar} emptyState={{ title: "محصول مشابهی یافت نشد", description: "در حال حاضر محصول مشابهی برای نمایش وجود ندارد", }} />
)} )} {/* Desktop overlay/buy modals */} setIsAIOpen(false)} productId={product.id || ""} /> {hasSizeChart && product.sizeChart && ( )}
); });