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 ( return (
<div className="bg-WHITE"> <div className="bg-WHITE">
<HeaderWithSupport title="داشبورد مالی" onBack={handleBack} /> <div className="lg:hidden">
{menuItems.map((item, index) => ( <HeaderWithSupport title="داشبورد مالی" onBack={handleBack} />
<div </div>
onClick={() => { <h2 className="hidden lg:block text-[24px] font-extrabold mb-6">
cashFunds(item.link); داشبورد مالی
}} </h2>
role="button" <div className="lg:grid lg:grid-cols-3 lg:gap-4">
key={index} {menuItems.map((item, index) => (
tabIndex={0} <div
onKeyDown={(e) => { onClick={() => {
if (e.key === "Enter") {
cashFunds(item.link); cashFunds(item.link);
} }}
}} role="button"
className="border-b flex items-center w-full p-4 bg-WHITE hover:bg-WHITE3 cursor-pointer" key={index}
> tabIndex={0}
<div className="flex flex-col w-full gap-2"> onKeyDown={(e) => {
<div className="flex flex-row w-full gap-2"> if (e.key === "Enter") {
{item.icon} cashFunds(item.link);
<p className="text-R14 font-bold">{item.title}</p> }
}}
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> </div>
<p className="text-R12">{item.description}</p> <ChevronLeft size={16} className="lg:hidden" />
</div> </div>
<ChevronLeft size={16} /> ))}
</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,50 +235,58 @@ export default function Orders() {
}; };
return ( return (
<div className="bg-WHITE"> <div className="bg-WHITE lg:bg-transparent">
{/* Header */} {/* 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"> <div className="py-4">
{/* Tabs */} {/* Tabs + Search row (desktop puts them side by side) */}
<div className="bg-WHITE px-4 mb-4 overflow-hidden"> <div className="lg:flex lg:items-center lg:justify-between lg:gap-4 lg:mb-4">
<div className="overflow-x-auto grid grid-cols-4"> {/* Tabs */}
{orderTabs.map((tab) => ( <div className="bg-WHITE lg:bg-transparent px-4 lg:px-0 mb-4 lg:mb-0 overflow-hidden">
<button <div className="overflow-x-auto grid grid-cols-4 lg:flex lg:gap-2 lg:w-auto">
key={tab.key} {orderTabs.map((tab) => (
onClick={() => setActiveTab(tab.key)} <button
className={`flex-shrink-0 flex flex-col items-center justify-center gap-1 py-1 text-R12 font-medium border-b-2 transition-colors ${ key={tab.key}
activeTab === 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 ${
: "border-transparent text-GRAY hover:text-BLACK2 hover:bg-WHITE3" 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> {tab.icon}
))} {tab.label}
</button>
))}
</div>
</div> </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"
placeholder="جستجو..." placeholder="جستجو..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} 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" 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"> <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Search size={24} className="text-BLACK2" /> <Search size={24} className="text-BLACK2" />
</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>
<p className="text-sm text-gray-500 mt-1">{products.length} محصول</p> <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>
<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,71 +141,92 @@ export default function StoreSetting() {
return ( return (
<div className="flex flex-col bg-WHITE"> <div className="flex flex-col bg-WHITE">
{/* Header */} {/* Header */}
<HeaderWithSupport title="تنظیمات فروشگاه" onBack={handleBack} /> <div className="lg:hidden">
<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> </div>
<h2 className="hidden lg:block text-[24px] font-extrabold mb-6">
تنظیمات فروشگاه
</h2>
{/* Theme Toggle */} {/* Status + Theme cards */}
<div className="p-4 bg-WHITE border-b border-inner-border"> <div className="lg:grid lg:grid-cols-2 lg:gap-4 lg:mb-6">
<div className="flex w-full items-center justify-between"> {/* Store Status */}
<div className="flex flex-col gap-1"> <div className="p-4 bg-WHITE border-b border-inner-border lg:border lg:border-WHITE3 lg:rounded-2xl lg:p-6">
<p className="text-R12"> <div className="flex w-full items-center justify-between">
{initialTheme === "dark" ? "حالت تاریک" : "حالت روشن"} <div className="flex flex-col gap-1 w-full">
</p> <div className="flex w-full gap-1 items-center">
<p className="text-R10 text-GRAY"> <Code size={20} />
تغییر حالت نمایش به تاریک یا روشن <p className="text-R12 lg:text-B16 font-bold">
</p> وضعیت فروشگاه (فعال)
</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> </div>
<SunMoonToggle theme={initialTheme} />
</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) => {
<div const isLogout = item.pageName === "logout";
key={index} return (
className={`flex flex-row gap-2 items-center justify-between px-[16px] active:bg-hover transition cursor-pointer ${ <div
item.hasBorder && "border-t-[2px]" key={index}
} h-[55px] w-full`} className={`flex flex-row gap-2 items-center justify-between px-[16px] active:bg-hover transition cursor-pointer ${
onClick={() => handleItemClick(item)} item.hasBorder && "border-t-[2px]"
role="button" } 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 ${
tabIndex={0} isLogout ? "lg:border-RED/40 lg:text-RED" : ""
onKeyDown={(e) => { }`}
if (e.key === "Enter" || e.key === " ") { onClick={() => handleItemClick(item)}
handleItemClick(item); role="button"
} tabIndex={0}
}} onKeyDown={(e) => {
> if (e.key === "Enter" || e.key === " ") {
<div className="flex flex-row gap-2 items-center"> handleItemClick(item);
{item.image} }
<p className={"text-R12"}>{item.title}</p> }}
>
<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> </div>
<ChevronLeft className="size-4" /> );
</div> })}
))}
</div> </div>
{/* Instagram Sync Drawer */} {/* Instagram Sync Drawer */}