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>
This commit is contained in:
Arda Samadi 2026-06-17 13:00:18 +03:30
parent 4af143bccf
commit 257871d5f1
4 changed files with 202 additions and 126 deletions

View File

@ -70,32 +70,41 @@ export default function FinancialDashboard() {
];
return (
<div className="bg-WHITE">
<HeaderWithSupport title="داشبورد مالی" onBack={handleBack} />
{menuItems.map((item, index) => (
<div
onClick={() => {
cashFunds(item.link);
}}
role="button"
key={index}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter") {
<div className="lg:hidden">
<HeaderWithSupport title="داشبورد مالی" onBack={handleBack} />
</div>
<h2 className="hidden lg:block text-[24px] font-extrabold mb-6">
داشبورد مالی
</h2>
<div className="lg:grid lg:grid-cols-3 lg:gap-4">
{menuItems.map((item, index) => (
<div
onClick={() => {
cashFunds(item.link);
}
}}
className="border-b flex items-center w-full p-4 bg-WHITE hover:bg-WHITE3 cursor-pointer"
>
<div className="flex flex-col w-full gap-2">
<div className="flex flex-row w-full gap-2">
{item.icon}
<p className="text-R14 font-bold">{item.title}</p>
}}
role="button"
key={index}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter") {
cashFunds(item.link);
}
}}
className="border-b flex items-center w-full p-4 bg-WHITE hover:bg-WHITE3 cursor-pointer lg:border lg:border-WHITE3 lg:rounded-2xl lg:p-6 lg:items-start lg:hover:shadow-md lg:transition-shadow"
>
<div className="flex flex-col w-full gap-2">
<div className="flex flex-row w-full gap-2 lg:items-center">
<span className="lg:w-11 lg:h-11 lg:rounded-xl lg:bg-WHITE2 lg:flex lg:items-center lg:justify-center">
{item.icon}
</span>
<p className="text-R14 lg:text-B16 font-bold">{item.title}</p>
</div>
<p className="text-R12 text-GRAY lg:mt-1">{item.description}</p>
</div>
<p className="text-R12">{item.description}</p>
<ChevronLeft size={16} className="lg:hidden" />
</div>
<ChevronLeft size={16} />
</div>
))}
))}
</div>
</div>
);
}

View File

@ -129,7 +129,7 @@ function OrderItem({
navigate(`/store/${storeId}/orders/${order.id}`);
}
}}
className={`border-t ${isLastOrder && "border-b"} p-2 bg-WHITE hover:bg-WHITE3 cursor-pointer transition-colors duration-300`}
className={`border-t ${isLastOrder && "border-b"} p-2 bg-WHITE hover:bg-WHITE3 cursor-pointer transition-colors duration-300 lg:border lg:border-WHITE3 lg:rounded-2xl lg:p-5 lg:hover:shadow-md lg:hover:bg-WHITE`}
>
<div className="flex flex-col space-y-3">
{/* Header */}
@ -235,50 +235,58 @@ export default function Orders() {
};
return (
<div className="bg-WHITE">
<div className="bg-WHITE lg:bg-transparent">
{/* Header */}
<HeaderWithSupport title="سفارش‌های دریافتی" onBack={handleBack} />
<div className="lg:hidden">
<HeaderWithSupport title="سفارش‌های دریافتی" onBack={handleBack} />
</div>
<h1 className="hidden lg:block text-[24px] font-extrabold mt-2">
سفارشهای دریافتی
</h1>
<div className="py-4">
{/* Tabs */}
<div className="bg-WHITE px-4 mb-4 overflow-hidden">
<div className="overflow-x-auto grid grid-cols-4">
{orderTabs.map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`flex-shrink-0 flex flex-col items-center justify-center gap-1 py-1 text-R12 font-medium border-b-2 transition-colors ${
activeTab === tab.key
? ""
: "border-transparent text-GRAY hover:text-BLACK2 hover:bg-WHITE3"
}`}
>
{tab.icon}
{tab.label}
</button>
))}
{/* Tabs + Search row (desktop puts them side by side) */}
<div className="lg:flex lg:items-center lg:justify-between lg:gap-4 lg:mb-4">
{/* Tabs */}
<div className="bg-WHITE lg:bg-transparent px-4 lg:px-0 mb-4 lg:mb-0 overflow-hidden">
<div className="overflow-x-auto grid grid-cols-4 lg:flex lg:gap-2 lg:w-auto">
{orderTabs.map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`flex-shrink-0 flex flex-col lg:flex-row items-center justify-center gap-1 lg:gap-2 py-1 lg:py-2 lg:px-4 text-R12 lg:text-[13px] font-medium border-b-2 lg:border-b-0 lg:rounded-xl transition-colors lg:[&_svg]:size-4 ${
activeTab === tab.key
? "lg:bg-BLACK lg:text-white"
: "border-transparent text-GRAY hover:text-BLACK2 hover:bg-WHITE3 lg:border lg:border-WHITE3"
}`}
>
{tab.icon}
{tab.label}
</button>
))}
</div>
</div>
</div>
{/* Search Bar */}
<div className="mb-4 px-4">
<div className="relative">
<input
type="text"
placeholder="جستجو..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full h-10 px-3 py-2 pr-10 bg-WHITE2 border border-WHITE2 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-transparent"
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Search size={24} className="text-BLACK2" />
{/* Search Bar */}
<div className="mb-4 lg:mb-0 px-4 lg:px-0 lg:w-72 lg:shrink-0">
<div className="relative">
<input
type="text"
placeholder="جستجو..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full h-10 px-3 py-2 pr-10 bg-WHITE2 border border-WHITE2 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-transparent"
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Search size={24} className="text-BLACK2" />
</div>
</div>
</div>
</div>
{/* Orders List */}
{filteredOrders.length > 0 ? (
<div className="space-y-4">
<div className="space-y-4 lg:space-y-0 lg:grid lg:grid-cols-2 lg:gap-4">
{filteredOrders.map((order, index) => (
<OrderItem
key={order.id}

View File

@ -51,7 +51,7 @@ function ProductAccordionItem({
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">
<div className="flex items-center gap-4 flex-1 min-w-0">
{product.primaryImage && (
<img
src={
@ -63,23 +63,42 @@ function ProductAccordionItem({
e.currentTarget.src =
theme === "dark" ? placeholderImageDark : placeholderImage;
}}
className="w-12 h-12 object-contain rounded"
className="w-12 h-12 lg:w-14 lg:h-14 object-contain rounded"
/>
)}
<div className="text-right">
<h3 className="font-medium text-sm">{product.title}</h3>
<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">
<p className="text-xs text-gray-600 mt-1 lg:hidden">
{parseInt(product.basePrice).toLocaleString("fa-IR")} تومان
</p>
)}
</div>
</div>
<div className="flex items-center gap-2">
{variants && variants.length > 0 && (
{/* 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}
@ -171,6 +190,7 @@ function ProductAccordionItem({
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);
@ -227,13 +247,31 @@ function StoreProductsContent() {
}
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 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>
<div className="divide-y divide-gray-200">
{/* 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

View File

@ -141,71 +141,92 @@ export default function StoreSetting() {
return (
<div className="flex flex-col bg-WHITE">
{/* Header */}
<HeaderWithSupport title="تنظیمات فروشگاه" onBack={handleBack} />
{/* Store Status */}
<div className="p-4 bg-WHITE border-b border-inner-border">
<div className="flex w-full items-center justify-between">
<div className="flex flex-col gap-1 w-full">
<div className="flex w-full gap-1 items-center">
<Code size={20} />
<p className="text-R12">وضعیت فروشگاه (فعال)</p>
</div>
<p className="text-R10 text-GRAY">
در حالت فعال، فروشگاه برای مخاطبان نمایش داده میشود.
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={data?.isVerified || false}
className="sr-only peer"
aria-label="وضعیت فروشگاه"
/>
<div className="w-11 h-6 bg-WHITE2 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-WHITE after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-WHITE after:border-GRAY3 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-VITROWN_BLUE border"></div>
</label>
</div>
<div className="lg:hidden">
<HeaderWithSupport title="تنظیمات فروشگاه" onBack={handleBack} />
</div>
<h2 className="hidden lg:block text-[24px] font-extrabold mb-6">
تنظیمات فروشگاه
</h2>
{/* Theme Toggle */}
<div className="p-4 bg-WHITE border-b border-inner-border">
<div className="flex w-full items-center justify-between">
<div className="flex flex-col gap-1">
<p className="text-R12">
{initialTheme === "dark" ? "حالت تاریک" : "حالت روشن"}
</p>
<p className="text-R10 text-GRAY">
تغییر حالت نمایش به تاریک یا روشن
</p>
{/* Status + Theme cards */}
<div className="lg:grid lg:grid-cols-2 lg:gap-4 lg:mb-6">
{/* Store Status */}
<div className="p-4 bg-WHITE border-b border-inner-border lg:border lg:border-WHITE3 lg:rounded-2xl lg:p-6">
<div className="flex w-full items-center justify-between">
<div className="flex flex-col gap-1 w-full">
<div className="flex w-full gap-1 items-center">
<Code size={20} />
<p className="text-R12 lg:text-B16 font-bold">
وضعیت فروشگاه (فعال)
</p>
</div>
<p className="text-R10 lg:text-R12 text-GRAY">
در حالت فعال، فروشگاه برای مخاطبان نمایش داده میشود.
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={data?.isVerified || false}
className="sr-only peer"
aria-label="وضعیت فروشگاه"
/>
<div className="w-11 h-6 bg-WHITE2 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-WHITE after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-WHITE after:border-GRAY3 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-VITROWN_BLUE border"></div>
</label>
</div>
</div>
{/* Theme Toggle */}
<div className="p-4 bg-WHITE border-b border-inner-border lg:border lg:border-WHITE3 lg:rounded-2xl lg:p-6">
<div className="flex w-full items-center justify-between">
<div className="flex flex-col gap-1">
<p className="text-R12 lg:text-B16 font-bold">
{initialTheme === "dark" ? "حالت تاریک" : "حالت روشن"}
</p>
<p className="text-R10 lg:text-R12 text-GRAY">
تغییر حالت نمایش به تاریک یا روشن
</p>
</div>
<SunMoonToggle theme={initialTheme} />
</div>
<SunMoonToggle theme={initialTheme} />
</div>
</div>
{/* Menu Items */}
<div className="flex flex-col gap-0 w-full bg-WHITE">
{storeMenuItems.map((item, index) => (
<div
key={index}
className={`flex flex-row gap-2 items-center justify-between px-[16px] active:bg-hover transition cursor-pointer ${
item.hasBorder && "border-t-[2px]"
} h-[55px] w-full`}
onClick={() => handleItemClick(item)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleItemClick(item);
}
}}
>
<div className="flex flex-row gap-2 items-center">
{item.image}
<p className={"text-R12"}>{item.title}</p>
<div className="flex flex-col gap-0 w-full bg-WHITE lg:grid lg:grid-cols-3 lg:gap-4">
{storeMenuItems.map((item, index) => {
const isLogout = item.pageName === "logout";
return (
<div
key={index}
className={`flex flex-row gap-2 items-center justify-between px-[16px] active:bg-hover transition cursor-pointer ${
item.hasBorder && "border-t-[2px]"
} h-[55px] w-full lg:h-auto lg:px-0 lg:border-t-0 lg:border lg:border-WHITE3 lg:rounded-2xl lg:p-5 lg:items-center lg:hover:shadow-md lg:transition-shadow ${
isLogout ? "lg:border-RED/40 lg:text-RED" : ""
}`}
onClick={() => handleItemClick(item)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleItemClick(item);
}
}}
>
<div className="flex flex-row gap-2 items-center">
<span
className={`lg:w-11 lg:h-11 lg:rounded-xl lg:flex lg:items-center lg:justify-center ${
isLogout ? "lg:bg-RED/10" : "lg:bg-WHITE2"
}`}
>
{item.image}
</span>
<p className="text-R12 lg:text-B16 font-bold">{item.title}</p>
</div>
<ChevronLeft className="size-4 lg:hidden" />
</div>
<ChevronLeft className="size-4" />
</div>
))}
);
})}
</div>
{/* Instagram Sync Drawer */}