348 lines
12 KiB
TypeScript
348 lines
12 KiB
TypeScript
import { useState, useEffect, useMemo, memo } from "react";
|
||
import {
|
||
ProductHeader,
|
||
ProductImageCarousel,
|
||
ProductActions,
|
||
ProductInfo,
|
||
ColorSelector,
|
||
SizeSelector,
|
||
ContactButton,
|
||
ReviewSection,
|
||
BottomBar,
|
||
} from "./index";
|
||
import { ProductDetail as ProductDetailType } from "../../../src/api/types";
|
||
import { Eye, EyeOff } from "lucide-react";
|
||
import { Button } from "../ui/button";
|
||
import { useNavigate } from "@remix-run/react";
|
||
import { useSimilarProductsInfinite } from "~/requestHandler/use-product-hooks";
|
||
import MondrianProductList from "../MondrianProductList";
|
||
|
||
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]);
|
||
|
||
// 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]
|
||
);
|
||
|
||
const [selectedColor, setSelectedColor] = useState<string | null>(
|
||
hasColorVariants && inStockColors.length > 0
|
||
? (inStockColors[0].info as unknown as { detail: string })?.detail
|
||
: null
|
||
);
|
||
|
||
const [selectedSize, setSelectedSize] = useState<string | null>(
|
||
hasSizeVariants && inStockSizes.length > 0 ? inStockSizes[0].value : null
|
||
);
|
||
|
||
// Handle product status change
|
||
const handleProductStatusChange = (newStatus: boolean) => {
|
||
setProductActiveStatus(newStatus);
|
||
};
|
||
|
||
const [currentPrice, setCurrentPrice] = useState(product.basePrice);
|
||
const [currentDiscountPrice, setCurrentDiscountPrice] = useState<
|
||
string | null
|
||
>(null);
|
||
const [currentStock, setCurrentStock] = useState<number | null>(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) {
|
||
setCurrentPrice(matchingVariant.price || product.basePrice);
|
||
setCurrentDiscountPrice(
|
||
matchingVariant.discountPrice &&
|
||
matchingVariant.discountPrice !== "0" &&
|
||
Number(matchingVariant.discountPrice) !==
|
||
Number(matchingVariant.price)
|
||
? matchingVariant.discountPrice
|
||
: null
|
||
);
|
||
setCurrentStock(matchingVariant.stock || null);
|
||
} else {
|
||
// If no matching variant found, but we have variants and no color/size requirements
|
||
// Use the first available variant (for products with no color/size variants)
|
||
if (!hasColorVariants && !hasSizeVariants && stockedVariants.length > 0) {
|
||
const firstVariant = stockedVariants[0];
|
||
setCurrentPrice(firstVariant.price || product.basePrice);
|
||
setCurrentDiscountPrice(
|
||
firstVariant.discountPrice &&
|
||
firstVariant.discountPrice !== "0" &&
|
||
Number(firstVariant.discountPrice) !== Number(firstVariant.price)
|
||
? firstVariant.discountPrice
|
||
: null
|
||
);
|
||
setCurrentStock(firstVariant.stock || null);
|
||
} else {
|
||
setCurrentPrice(product.basePrice);
|
||
setCurrentDiscountPrice(null);
|
||
setCurrentStock(null);
|
||
}
|
||
}
|
||
}, [
|
||
selectedColor,
|
||
selectedSize,
|
||
product,
|
||
inStockColors,
|
||
hasColorVariants,
|
||
hasSizeVariants,
|
||
stockedVariants,
|
||
]);
|
||
|
||
const navigate = useNavigate();
|
||
return (
|
||
<div className="flex flex-col pb-20">
|
||
{!isOwner && <ProductHeader product={product} />}
|
||
<ProductImageCarousel
|
||
images={product.imageUrls || []}
|
||
productId={product.id}
|
||
storeId={product.seller?.username}
|
||
isOwner={isOwner}
|
||
borderRadius={0}
|
||
isActive={productActiveStatus}
|
||
controlsIsOn={true}
|
||
onStatusChange={handleProductStatusChange}
|
||
isChangeStatusDialogOpen={isChangeStatusDialogOpen}
|
||
setIsChangeStatusDialogOpen={setIsChangeStatusDialogOpen}
|
||
/>
|
||
{/* Inactive Product Status */}
|
||
{!productActiveStatus && isOwner ? (
|
||
<div className="flex gap-1 px-4 w-full justify-between items-center mt-2">
|
||
<div className="flex items-center w-full gap-2 p-2 bg-WHITE3 rounded-lg">
|
||
<EyeOff size={16} className="text-BLACK2 font-bold" />
|
||
<span className="text-B12 font-bold text-BLACK2">
|
||
این محصول در حال حاضر غیرفعال است
|
||
</span>
|
||
</div>
|
||
<Button
|
||
variant="dark"
|
||
size="sm"
|
||
className="text-B10 h-8 rounded-md"
|
||
onClick={() => setIsChangeStatusDialogOpen(true)}
|
||
>
|
||
فعال کردن
|
||
</Button>
|
||
</div>
|
||
) : !isOwner ? (
|
||
<ProductActions productId={product.id || ""} />
|
||
) : null}
|
||
<ProductInfo
|
||
productDiscountPercent={Number(
|
||
!currentDiscountPrice ||
|
||
!currentPrice ||
|
||
Number(currentDiscountPrice) === Number(currentPrice)
|
||
? 0
|
||
: (
|
||
((Number(currentPrice) - Number(currentDiscountPrice)) /
|
||
Number(currentPrice)) *
|
||
100
|
||
).toFixed(2)
|
||
)}
|
||
product={{ ...product, isActive: productActiveStatus }}
|
||
/>
|
||
|
||
{hasAnyStock ? (
|
||
<>
|
||
{hasColorVariants && (
|
||
<ColorSelector
|
||
colors={inStockColors.map(
|
||
(color) => (color.info as unknown as { detail: string })?.detail
|
||
)}
|
||
selectedColor={selectedColor}
|
||
setSelectedColor={setSelectedColor}
|
||
/>
|
||
)}
|
||
|
||
{hasSizeVariants && (
|
||
<SizeSelector
|
||
sizes={inStockSizes.map((size) => size.value || "")}
|
||
selectedSize={selectedSize}
|
||
setSelectedSize={setSelectedSize}
|
||
/>
|
||
)}
|
||
</>
|
||
) : (
|
||
<div className="flex flex-col gap-4 mt-10 px-4">
|
||
<p className="text-B18 font-bold">موجود نیست</p>
|
||
</div>
|
||
)}
|
||
{isOwner && (
|
||
<div className="w-full p-4 flex justify-center items-center">
|
||
<Button
|
||
variant="dark"
|
||
size="sm"
|
||
className="text-B10 h-8 rounded-md bg-VITROWN_BLUE text-white"
|
||
onClick={() => {
|
||
navigate(`/product/${product.id}?mode=store`);
|
||
}}
|
||
>
|
||
<Eye size={16} className="text-WHITE" />
|
||
دیدن محصول از نگاه مشتری
|
||
</Button>
|
||
</div>
|
||
)}
|
||
{productActiveStatus && (
|
||
<>
|
||
{!isOwner && <ContactButton product={product} />}
|
||
<ReviewSection productId={product.id || ""} isOwner={isOwner} />
|
||
{/* Similar Products Section */}
|
||
{!isOwner && (
|
||
<section className="w-full gap-2 flex flex-col max-w-[100vw] my-6">
|
||
<div className="w-full px-4">
|
||
<p className="text-B16 font-bold">محصولات مشابه</p>
|
||
<p className="text-R14">محصولات مرتبط که ممکن است بپسندید</p>
|
||
</div>
|
||
<MondrianProductList
|
||
products={
|
||
similarProductsData?.pages
|
||
?.flatMap((page) => 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: "در حال حاضر محصول مشابهی برای نمایش وجود ندارد",
|
||
}}
|
||
/>
|
||
</section>
|
||
)}
|
||
{/* Bottom bar handles variant selection and pricing */}
|
||
</>
|
||
)}
|
||
{!isOwner && (
|
||
<BottomBar
|
||
currentPrice={currentPrice}
|
||
currentDiscountPrice={currentDiscountPrice}
|
||
currentStock={currentStock}
|
||
product={product}
|
||
selectedColor={
|
||
inStockColors.find(
|
||
(color) =>
|
||
(color.info as unknown as { detail: string })?.detail ===
|
||
selectedColor
|
||
)?.value || null
|
||
}
|
||
selectedSize={selectedSize}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
});
|