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,7 +70,13 @@ export default function FinancialDashboard() {
]; ];
return ( return (
<div className="bg-WHITE"> <div className="bg-WHITE">
<div className="lg:hidden">
<HeaderWithSupport title="داشبورد مالی" onBack={handleBack} /> <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) => ( {menuItems.map((item, index) => (
<div <div
onClick={() => { onClick={() => {
@ -84,18 +90,21 @@ export default function FinancialDashboard() {
cashFunds(item.link); cashFunds(item.link);
} }
}} }}
className="border-b flex items-center w-full p-4 bg-WHITE hover:bg-WHITE3 cursor-pointer" 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-col w-full gap-2">
<div className="flex flex-row 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} {item.icon}
<p className="text-R14 font-bold">{item.title}</p> </span>
<p className="text-R14 lg:text-B16 font-bold">{item.title}</p>
</div> </div>
<p className="text-R12">{item.description}</p> <p className="text-R12 text-GRAY lg:mt-1">{item.description}</p>
</div> </div>
<ChevronLeft size={16} /> <ChevronLeft size={16} className="lg:hidden" />
</div> </div>
))} ))}
</div> </div>
</div>
); );
} }

View File

@ -129,7 +129,7 @@ function OrderItem({
navigate(`/store/${storeId}/orders/${order.id}`); 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"> <div className="flex flex-col space-y-3">
{/* Header */} {/* Header */}
@ -235,22 +235,29 @@ export default function Orders() {
}; };
return ( return (
<div className="bg-WHITE"> <div className="bg-WHITE lg:bg-transparent">
{/* Header */} {/* Header */}
<div className="lg:hidden">
<HeaderWithSupport title="سفارش‌های دریافتی" onBack={handleBack} /> <HeaderWithSupport title="سفارش‌های دریافتی" onBack={handleBack} />
</div>
<h1 className="hidden lg:block text-[24px] font-extrabold mt-2">
سفارشهای دریافتی
</h1>
<div className="py-4"> <div className="py-4">
{/* 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 */} {/* Tabs */}
<div className="bg-WHITE px-4 mb-4 overflow-hidden"> <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"> <div className="overflow-x-auto grid grid-cols-4 lg:flex lg:gap-2 lg:w-auto">
{orderTabs.map((tab) => ( {orderTabs.map((tab) => (
<button <button
key={tab.key} key={tab.key}
onClick={() => setActiveTab(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 ${ 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 activeTab === tab.key
? "" ? "lg:bg-BLACK lg:text-white"
: "border-transparent text-GRAY hover:text-BLACK2 hover:bg-WHITE3" : "border-transparent text-GRAY hover:text-BLACK2 hover:bg-WHITE3 lg:border lg:border-WHITE3"
}`} }`}
> >
{tab.icon} {tab.icon}
@ -261,7 +268,7 @@ export default function Orders() {
</div> </div>
{/* Search Bar */} {/* Search Bar */}
<div className="mb-4 px-4"> <div className="mb-4 lg:mb-0 px-4 lg:px-0 lg:w-72 lg:shrink-0">
<div className="relative"> <div className="relative">
<input <input
type="text" type="text"
@ -275,10 +282,11 @@ export default function Orders() {
</div> </div>
</div> </div>
</div> </div>
</div>
{/* Orders List */} {/* Orders List */}
{filteredOrders.length > 0 ? ( {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) => ( {filteredOrders.map((order, index) => (
<OrderItem <OrderItem
key={order.id} key={order.id}

View File

@ -51,7 +51,7 @@ function ProductAccordionItem({
onClick={onToggle} onClick={onToggle}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-WHITE3 transition-colors" 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 && ( {product.primaryImage && (
<img <img
src={ src={
@ -63,23 +63,42 @@ function ProductAccordionItem({
e.currentTarget.src = e.currentTarget.src =
theme === "dark" ? placeholderImageDark : placeholderImage; 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"> <div className="text-right min-w-0">
<h3 className="font-medium text-sm">{product.title}</h3> <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 && ( {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")} تومان {parseInt(product.basePrice).toLocaleString("fa-IR")} تومان
</p> </p>
)} )}
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> {/* Desktop price column */}
{variants && variants.length > 0 && ( <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"> <span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
{variants.length} نوع {variants.length} نوع
</span> </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 <button
onClick={handleEditProduct} onClick={handleEditProduct}
@ -171,6 +190,7 @@ function ProductAccordionItem({
function StoreProductsContent() { function StoreProductsContent() {
const { storeId } = useParams(); const { storeId } = useParams();
const navigate = useNavigate();
const [openProductId, setOpenProductId] = useState<string | null>(null); const [openProductId, setOpenProductId] = useState<string | null>(null);
const [stickyProductId, setStickyProductId] = useState<string | null>(null); const [stickyProductId, setStickyProductId] = useState<string | null>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@ -227,13 +247,31 @@ function StoreProductsContent() {
} }
return ( return (
<div ref={containerRef} className="bg-WHITE"> <div ref={containerRef} className="bg-WHITE lg:bg-transparent">
<div className="border-b border-gray-200 px-4 py-3"> <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">
<h1 className="text-lg font-semibold text-right">محصولات فروشگاه</h1> <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> <p className="text-sm text-gray-500 mt-1">{products.length} محصول</p>
</div> </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) => {products.map((product) =>
product.id ? ( product.id ? (
<ProductAccordionItem <ProductAccordionItem

View File

@ -141,17 +141,26 @@ export default function StoreSetting() {
return ( return (
<div className="flex flex-col bg-WHITE"> <div className="flex flex-col bg-WHITE">
{/* Header */} {/* Header */}
<div className="lg:hidden">
<HeaderWithSupport title="تنظیمات فروشگاه" onBack={handleBack} /> <HeaderWithSupport title="تنظیمات فروشگاه" onBack={handleBack} />
</div>
<h2 className="hidden lg:block text-[24px] font-extrabold mb-6">
تنظیمات فروشگاه
</h2>
{/* Status + Theme cards */}
<div className="lg:grid lg:grid-cols-2 lg:gap-4 lg:mb-6">
{/* Store Status */} {/* Store Status */}
<div className="p-4 bg-WHITE border-b border-inner-border"> <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 w-full items-center justify-between">
<div className="flex flex-col gap-1 w-full"> <div className="flex flex-col gap-1 w-full">
<div className="flex w-full gap-1 items-center"> <div className="flex w-full gap-1 items-center">
<Code size={20} /> <Code size={20} />
<p className="text-R12">وضعیت فروشگاه (فعال)</p> <p className="text-R12 lg:text-B16 font-bold">
وضعیت فروشگاه (فعال)
</p>
</div> </div>
<p className="text-R10 text-GRAY"> <p className="text-R10 lg:text-R12 text-GRAY">
در حالت فعال، فروشگاه برای مخاطبان نمایش داده میشود. در حالت فعال، فروشگاه برای مخاطبان نمایش داده میشود.
</p> </p>
</div> </div>
@ -168,28 +177,33 @@ export default function StoreSetting() {
</div> </div>
{/* Theme Toggle */} {/* Theme Toggle */}
<div className="p-4 bg-WHITE border-b border-inner-border"> <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 w-full items-center justify-between">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<p className="text-R12"> <p className="text-R12 lg:text-B16 font-bold">
{initialTheme === "dark" ? "حالت تاریک" : "حالت روشن"} {initialTheme === "dark" ? "حالت تاریک" : "حالت روشن"}
</p> </p>
<p className="text-R10 text-GRAY"> <p className="text-R10 lg:text-R12 text-GRAY">
تغییر حالت نمایش به تاریک یا روشن تغییر حالت نمایش به تاریک یا روشن
</p> </p>
</div> </div>
<SunMoonToggle theme={initialTheme} /> <SunMoonToggle theme={initialTheme} />
</div> </div>
</div> </div>
</div>
{/* Menu Items */} {/* Menu Items */}
<div className="flex flex-col gap-0 w-full bg-WHITE"> <div className="flex flex-col gap-0 w-full bg-WHITE lg:grid lg:grid-cols-3 lg:gap-4">
{storeMenuItems.map((item, index) => ( {storeMenuItems.map((item, index) => {
const isLogout = item.pageName === "logout";
return (
<div <div
key={index} key={index}
className={`flex flex-row gap-2 items-center justify-between px-[16px] active:bg-hover transition cursor-pointer ${ className={`flex flex-row gap-2 items-center justify-between px-[16px] active:bg-hover transition cursor-pointer ${
item.hasBorder && "border-t-[2px]" item.hasBorder && "border-t-[2px]"
} h-[55px] w-full`} } 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)} onClick={() => handleItemClick(item)}
role="button" role="button"
tabIndex={0} tabIndex={0}
@ -200,12 +214,19 @@ export default function StoreSetting() {
}} }}
> >
<div className="flex flex-row gap-2 items-center"> <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} {item.image}
<p className={"text-R12"}>{item.title}</p> </span>
<p className="text-R12 lg:text-B16 font-bold">{item.title}</p>
</div> </div>
<ChevronLeft className="size-4" /> <ChevronLeft className="size-4 lg:hidden" />
</div> </div>
))} );
})}
</div> </div>
{/* Instagram Sync Drawer */} {/* Instagram Sync Drawer */}