Vitron-Front/app/routes/store.$storeId.products.tsx
Arda Samadi 257871d5f1 feat(seller-dashboard): desktop-friendly layouts for list and hub pages
Redesign mobile-style seller dashboard pages for desktop (lg:) while
leaving the mobile view unchanged:

- products: table-style header row (product/price/variant/actions) with a
  bordered card wrapper and an "add product" button; accordion rows align
  to the columns, inline price hidden on desktop.
- orders: pill-style status tabs + search on one row, orders shown as a
  two-column card grid instead of stretched full-width rows.
- financial-dashboard: three-column card grid with icon chips.
- setting: status/theme as side-by-side cards, menu items as a
  three-column card grid (logout styled in red).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 13:00:18 +03:30

295 lines
10 KiB
TypeScript
Raw 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 min-w-0">
{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 lg:w-14 lg:h-14 object-contain rounded"
/>
)}
<div className="text-right min-w-0">
<h3 className="font-medium text-sm lg:text-[15px] truncate">
{product.title}
</h3>
{/* Price shown inline on mobile only; desktop uses its own column */}
{product.basePrice && (
<p className="text-xs text-gray-600 mt-1 lg:hidden">
{parseInt(product.basePrice).toLocaleString("fa-IR")} تومان
</p>
)}
</div>
</div>
{/* Desktop price column */}
<span className="hidden lg:block w-32 text-center text-[14px] font-semibold">
{product.basePrice
? `${parseInt(product.basePrice).toLocaleString("fa-IR")} تومان`
: "—"}
</span>
{/* Desktop variants column */}
<span className="hidden lg:flex w-24 justify-center">
{variants && variants.length > 0 ? (
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
{variants.length} نوع
</span>
) : (
<span className="text-GRAY"></span>
)}
</span>
<div className="flex items-center gap-2 lg:w-28 lg:justify-center">
{variants && variants.length > 0 && (
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded lg:hidden">
{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 navigate = useNavigate();
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 lg:bg-transparent">
<div className="border-b border-gray-200 px-4 py-3 lg:border-0 lg:px-0 lg:py-0 lg:mb-6 lg:flex lg:items-end lg:justify-between">
<div>
<h1 className="text-lg font-semibold text-right lg:text-[24px] lg:font-extrabold">
محصولات فروشگاه
</h1>
<p className="text-sm text-gray-500 mt-1">{products.length} محصول</p>
</div>
<button
onClick={() => navigate(`/store/add/manual/${storeId}`)}
className="hidden lg:inline-flex items-center gap-2 rounded-xl bg-VITROWN_BLUE text-white px-5 py-2.5 text-[14px] font-bold hover:opacity-90 transition"
>
افزودن محصول جدید
</button>
</div>
{/* Desktop table-style header */}
<div className="hidden lg:flex items-center px-4 py-3 text-GRAY text-[13px] font-bold border border-WHITE3 rounded-t-2xl bg-WHITE2">
<span className="flex-1">محصول</span>
<span className="w-32 text-center">قیمت</span>
<span className="w-24 text-center">تنوع</span>
<span className="w-28 text-center">عملیات</span>
</div>
<div className="divide-y divide-gray-200 lg:border lg:border-t-0 lg:border-WHITE3 lg:rounded-b-2xl lg:overflow-hidden lg:bg-WHITE">
{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 />;
}