257 lines
8.4 KiB
TypeScript
257 lines
8.4 KiB
TypeScript
import { useState, useRef, useEffect } from "react";
|
||
import { ChevronDown, ChevronUp, Edit } from "lucide-react";
|
||
import { useNavigate, useParams } from "@remix-run/react";
|
||
import {
|
||
useGetSellerProductsList,
|
||
useUpdateVariantStock,
|
||
} from "../requestHandler/use-product-hooks";
|
||
import { cn } from "../lib/utils";
|
||
import { SellerProductListItem } from "../../src/api/types";
|
||
import placeholderImage from "~/assets/icons/product.png";
|
||
import placeholderImageDark from "~/assets/icons/product-dark.png";
|
||
import { useRootData } from "~/hooks/use-root-data";
|
||
import { ThrottledQuantityControl } from "../components/cart/ThrottledQuantityControl";
|
||
|
||
function ProductAccordionItem({
|
||
product,
|
||
isOpen,
|
||
onToggle,
|
||
isSticky,
|
||
storeId,
|
||
}: {
|
||
product: SellerProductListItem;
|
||
isOpen: boolean;
|
||
onToggle: () => void;
|
||
isSticky: boolean;
|
||
storeId: string;
|
||
}) {
|
||
const navigate = useNavigate();
|
||
const { theme } = useRootData();
|
||
const updateVariantStock = useUpdateVariantStock();
|
||
const [updatingVariantId, setUpdatingVariantId] = useState<string | null>(
|
||
null
|
||
);
|
||
// Use real variants from API
|
||
const variants = product.variants || [];
|
||
|
||
const handleEditProduct = (e: React.MouseEvent) => {
|
||
e.stopPropagation();
|
||
navigate(`/store/${storeId}/product/${product.id}`);
|
||
};
|
||
|
||
return (
|
||
<div className="border-b border-gray-200">
|
||
<div
|
||
className={cn(
|
||
"transition-all duration-200",
|
||
isSticky && isOpen && "sticky top-0 z-10 bg-WHITE shadow-md"
|
||
)}
|
||
>
|
||
<button
|
||
onClick={onToggle}
|
||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-WHITE3 transition-colors"
|
||
>
|
||
<div className="flex items-center gap-4 flex-1">
|
||
{product.primaryImage && (
|
||
<img
|
||
src={
|
||
product.primaryImage ||
|
||
(theme === "dark" ? placeholderImageDark : placeholderImage)
|
||
}
|
||
alt={product.title}
|
||
onError={(e) => {
|
||
e.currentTarget.src =
|
||
theme === "dark" ? placeholderImageDark : placeholderImage;
|
||
}}
|
||
className="w-12 h-12 object-contain rounded"
|
||
/>
|
||
)}
|
||
<div className="text-right">
|
||
<h3 className="font-medium text-sm">{product.title}</h3>
|
||
{product.basePrice && (
|
||
<p className="text-xs text-gray-600 mt-1">
|
||
{parseInt(product.basePrice).toLocaleString("fa-IR")} تومان
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
{variants && variants.length > 0 && (
|
||
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
|
||
{variants.length} نوع
|
||
</span>
|
||
)}
|
||
<button
|
||
onClick={handleEditProduct}
|
||
className="p-1.5 hover:bg-gray-200 rounded-lg transition-colors"
|
||
title="ویرایش محصول"
|
||
>
|
||
<Edit className="w-4 h-4 text-gray-600" />
|
||
</button>
|
||
{isOpen ? (
|
||
<ChevronUp className="w-5 h-5 text-gray-400" />
|
||
) : (
|
||
<ChevronDown className="w-5 h-5 text-gray-400" />
|
||
)}
|
||
</div>
|
||
</button>
|
||
</div>
|
||
|
||
{isOpen && variants && variants.length > 0 && (
|
||
<div className="px-4 py-3 bg-gray-50">
|
||
<div className="space-y-2">
|
||
{variants.map((variant, index) => {
|
||
const color = variant.attributes?.find(
|
||
(attribute) => attribute.name === "COLOR"
|
||
);
|
||
const size = variant.attributes?.find(
|
||
(attribute) => attribute.name === "SIZE"
|
||
);
|
||
return (
|
||
<div
|
||
key={variant.id || `variant-${index}`}
|
||
className="flex items-center justify-between p-3 bg-WHITE rounded-lg border border-gray-100"
|
||
>
|
||
<div className="flex items-center justify-between gap-3">
|
||
{color && (
|
||
<div
|
||
className="w-10 h-10 rounded-sm shadow-sm"
|
||
style={{
|
||
backgroundColor: color.info.detail,
|
||
}}
|
||
/>
|
||
)}
|
||
{size && (
|
||
<div
|
||
className="w-10 h-10 flex items-center justify-center rounded-sm shadow-sm"
|
||
style={{
|
||
backgroundColor: "black",
|
||
color: "white",
|
||
fontSize: "10px",
|
||
}}
|
||
>
|
||
{size.value}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="flex-1 flex justify-end">
|
||
<ThrottledQuantityControl
|
||
key={`${variant.id}-${variant.stock}`}
|
||
initialQuantity={variant.stock || 0}
|
||
maxQuantity={999}
|
||
onQuantityUpdate={(newQuantity) => {
|
||
const variantId = variant.id || "";
|
||
setUpdatingVariantId(variantId);
|
||
updateVariantStock.mutate(
|
||
{
|
||
variantId,
|
||
stock: newQuantity,
|
||
},
|
||
{
|
||
onSettled: () => {
|
||
setUpdatingVariantId(null);
|
||
},
|
||
}
|
||
);
|
||
}}
|
||
isPending={updatingVariantId === variant.id}
|
||
allowManualEdit={true}
|
||
throttleDelay={1000}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function StoreProductsContent() {
|
||
const { storeId } = useParams();
|
||
const [openProductId, setOpenProductId] = useState<string | null>(null);
|
||
const [stickyProductId, setStickyProductId] = useState<string | null>(null);
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
|
||
const {
|
||
data: products = [],
|
||
isLoading,
|
||
isError,
|
||
} = useGetSellerProductsList();
|
||
|
||
useEffect(() => {
|
||
const handleScroll = () => {
|
||
if (openProductId) {
|
||
setStickyProductId(openProductId);
|
||
}
|
||
};
|
||
|
||
window.addEventListener("scroll", handleScroll);
|
||
return () => window.removeEventListener("scroll", handleScroll);
|
||
}, [openProductId]);
|
||
|
||
const handleToggleAccordion = (productId: string) => {
|
||
if (openProductId === productId) {
|
||
setOpenProductId(null);
|
||
setStickyProductId(null);
|
||
} else {
|
||
setOpenProductId(productId);
|
||
setStickyProductId(productId);
|
||
}
|
||
};
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className="flex justify-center items-center min-h-[400px]">
|
||
<div className="text-gray-500">در حال بارگذاری محصولات...</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (isError) {
|
||
return (
|
||
<div className="flex justify-center items-center min-h-[400px]">
|
||
<div className="text-red-500">خطا در بارگذاری محصولات</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (products.length === 0) {
|
||
return (
|
||
<div className="flex justify-center items-center min-h-[400px]">
|
||
<div className="text-gray-500">محصولی یافت نشد</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div ref={containerRef} className="bg-WHITE">
|
||
<div className="border-b border-gray-200 px-4 py-3">
|
||
<h1 className="text-lg font-semibold text-right">محصولات فروشگاه</h1>
|
||
<p className="text-sm text-gray-500 mt-1">{products.length} محصول</p>
|
||
</div>
|
||
|
||
<div className="divide-y divide-gray-200">
|
||
{products.map((product) =>
|
||
product.id ? (
|
||
<ProductAccordionItem
|
||
key={product.id}
|
||
product={product}
|
||
isOpen={openProductId === product.id}
|
||
onToggle={() => handleToggleAccordion(product.id || "")}
|
||
isSticky={stickyProductId === product.id}
|
||
storeId={storeId || ""}
|
||
/>
|
||
) : null
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function StoreProducts() {
|
||
return <StoreProductsContent />;
|
||
}
|