import { Link, useNavigate } from "@remix-run/react"; import { useEffect, useState, useRef, useMemo } from "react"; import { Skeleton } from "./ui/skeleton"; import { ProductImageCarousel } from "./product/ProductImageCarousel"; import { EmptyState, ErrorState } from "./UiProvider"; import { Brain, Eye, EyeOff, MoveRight, Pencil, X } from "lucide-react"; import { Dialog, DialogContent, DialogOverlay } from "./ui/dialog"; import { Button } from "./ui/button"; import { useDeleteInstagramPost } from "~/requestHandler/use-instagram-hooks"; import placeholderImage from "~/assets/icons/product.png"; import placeholderImageDark from "~/assets/icons/product-dark.png"; import { useRootData } from "~/hooks/use-root-data"; import discountRed from "~/assets/icons/product/discount-red.svg"; import SellerChip from "./SellerChip"; interface MondrianList { id?: string; title?: string; images?: string[]; isDraft?: boolean; isActive?: boolean; discountPercent?: number; discountPrice?: number; basePrice?: number; sellerName?: string; sellerLogo?: string | null; sellerUsername?: string; } // Utility function to detect media type export const isVideo = (url: string): boolean => { const videoExtensions = [".mp4", ".webm", ".ogg", ".mov"]; return videoExtensions.some((ext) => url.toLowerCase().endsWith(ext)); }; // ProductList component to display product images const MondrianProductList = ({ products, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError, refetch, emptyState, isCommonList = false, isDraft = false, storeId, storePage, }: { products: MondrianList[]; fetchNextPage: () => void; hasNextPage: boolean | undefined; isFetchingNextPage: boolean; isLoading: boolean; isError: boolean; refetch: () => void; emptyState: { image?: React.ReactNode; title?: string; description?: string; }; isCommonList?: boolean; isDraft?: boolean; storeId?: string; storePage?: boolean; }) => { // Handle scroll for infinite loading with throttling useEffect(() => { let ticking = false; const handleScroll = () => { if (!ticking) { window.requestAnimationFrame(() => { const scrollPosition = window.innerHeight + window.scrollY; const documentHeight = document.documentElement.scrollHeight; if ( scrollPosition >= documentHeight - 300 && !isFetchingNextPage && hasNextPage ) { fetchNextPage(); } ticking = false; }); ticking = true; } }; window.addEventListener("scroll", handleScroll, { passive: true }); return () => window.removeEventListener("scroll", handleScroll); }, [fetchNextPage, hasNextPage, isFetchingNextPage]); // Sentinel-based trigger — more reliable than scroll math on desktop, where // the footer can keep the window-scroll threshold from being reached. const loadMoreRef = useRef(null); useEffect(() => { const el = loadMoreRef.current; if (!el) return; const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { fetchNextPage(); } }, { rootMargin: "600px" } ); observer.observe(el); return () => observer.disconnect(); }, [fetchNextPage, hasNextPage, isFetchingNextPage]); // Memoize expensive filtering operations const { oddProducts, evenProducts } = useMemo(() => { const odd = products.filter((_, index) => index % 2 === 0); const even = products.filter((_, index) => index % 2 !== 0); return { oddProducts: odd, evenProducts: even }; }, [products]); const oddHeight = useMemo(() => [220, 260, 320, 200, 280], []); const evenHeight = useMemo(() => [280, 200, 260, 220, 320], []); // Desktop masonry: distribute products across 4 columns (round-robin). const desktopColumns = useMemo(() => { const cols: MondrianList[][] = [[], [], [], []]; products.forEach((p, i) => cols[i % 4].push(p)); return cols; }, [products]); const desktopHeights = useMemo(() => [300, 360, 260, 340, 280, 320], []); return (
{!isCommonList ? ( <> {isLoading ? ( ) : isError ? ( ) : oddProducts.length === 0 && evenProducts.length === 0 ? ( ) : ( <> {/* Mobile / tablet: original two-column masonry (unchanged) */}
{/* Column 1 (Odd products - index 0, 2, ...) */}
{oddProducts.map((product, index: number) => { const height = oddHeight[index % 5]; return ( ); })}
{/* Column 2 (Even products - index 1, 3, ...) */}
{evenProducts.map((product, index: number) => { const height = evenHeight[index % 5]; return ( ); })}
{/* Desktop: 4-column masonry */}
{desktopColumns.map((col, ci) => (
{col.map((product, index) => ( ))}
))}
{isFetchingNextPage && } )} ) : ( <> {isLoading ? ( ) : isError ? ( ) : oddProducts.length === 0 && evenProducts.length === 0 ? ( ) : ( <>
{products.map((product, index: number) => { return ( ); })}
{hasNextPage && } )} )}
); }; const ProductCard = ({ product, height, isDraft, storeId, storePage, }: { product: MondrianList; height: number; isDraft?: boolean; storeId?: string; storePage?: boolean; }) => { const [isPublishModalOpen, setIsPublishModalOpen] = useState(false); const videoRef = useRef(null); const { theme } = useRootData(); const handleVideoClick = () => { if (!videoRef.current) return; if (videoRef.current.paused) { videoRef.current.play(); } else { videoRef.current.pause(); } }; return ( <> {isDraft ? ( <>
{product.images?.[0] && (isVideo(product.images[0]) ? (
) : ( {product.title { e.currentTarget.src = theme === "dark" ? placeholderImageDark : placeholderImage; e.currentTarget.style.objectFit = "contain"; e.currentTarget.style.backgroundColor = theme === "dark" ? "#121212" : "#f2f0f0"; }} /> ))} {/* Draft mode overlay - clickable container */} {product.isDraft && (
{ if (e.key === "Enter") { setIsPublishModalOpen(true); } }} tabIndex={0} onClick={() => { setIsPublishModalOpen(true); }} > {/* Top-right corner for EyeOff icon */}
{/* Semi-transparent overlay for the whole area */}
)} {/* Discount badge */} {product.discountPercent && product.discountPercent !== 0 && ( )}
) : (
{/* Discount badge */} {product.discountPercent && product.discountPercent !== 0 && ( )} {/* Inactive product overlay */} {product.isActive === false && (
{/* Top-right corner for EyeOff icon */}
{/* Semi-transparent overlay for the whole area */}
)} {/* Overlay info - Airbnb style */}
{/* Title */}

{product.title}

{/* Price */}
{product.discountPrice && (

{`${product.discountPrice.toLocaleString()}`} تومان

)} {product.basePrice && product.discountPercent ? (

{product.basePrice.toLocaleString()}

) : null}
)} ); }; const DiscountBadge = ({ discountPercent }: { discountPercent?: number }) => { return (

{discountPercent}

discount-red
); }; const PublishModal = ({ open, onOpenChange, storeId, postId, }: { open: boolean; onOpenChange: (open: boolean) => void; storeId?: string; postId?: string; }) => { const navigate = useNavigate(); const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false); const deletePostMutation = useDeleteInstagramPost(); const handleManualAddClick = (isAI: boolean) => { // Navigate to manual add page if (storeId && postId) { navigate(`/store/add/manual/${storeId}?id=${postId}&ai=${isAI}`); } else { // Fallback to create store if no storeId available navigate("/store/create"); } onOpenChange(false); }; const handleDeleteClick = () => { onOpenChange(false); // Close the publish modal first setIsDeleteConfirmOpen(true); }; const handleConfirmDelete = async () => { if (postId) { try { await deletePostMutation.mutateAsync(postId); onOpenChange(false); setIsDeleteConfirmOpen(false); } catch (error) { console.error("Failed to delete post:", error); } } }; return ( {/* Close button */} onOpenChange(false)} />
{/* Header text */}

نمایش پست

با تکمیل اطلاعات محصولات معلق، آنها را برای مخاطبین قابل مشاهده کنید

{/* Action buttons */}
{/* تکمیل اطلاعات با AI button */} {/* تکمیل اطلاعات button */}
{/* Bottom text */}
{/* Delete Confirmation Modal */}
setIsDeleteConfirmOpen(false)} />

حذف پست

آیا از حذف این پست اطمینان دارید؟ این عمل قابل بازگشت نیست.

); }; export const CommonSkeleton = () => { return (
{Array(9) .fill(1) .map((_, index: number) => { return ( ); })}
); }; const MondrianSkeleton = ({ isMini }: { isMini?: boolean }) => { const oddHeight = isMini ? [200, 240] : [200, 240, 300, 180, 260]; const evenHeight = isMini ? [260, 180] : [260, 180, 240, 200, 300]; return (
{/* Column 1 (Odd products - index 0, 2, ...) */}
{oddHeight.map((height, index: number) => { return ( ); })}
{/* Column 2 (Even products - index 1, 3, ...) */}
{evenHeight.map((height, index: number) => { return ( ); })}
); }; export default MondrianProductList;