Vitron-Front/app/routes/store.$storeId.products.tsx
2026-04-29 01:44:16 +03:30

257 lines
8.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 />;
}