تاریخچهٔ سفارشها
پیشنمایش تمامصفحهپنلِ حسابِ مشتری: ستونِ کنار با کیف پول و منو، زبانههای وضعیت با شمار، جستوجوی کدِ سفارش، کارتِ هر سفارش با عکسِ کالاها و فاکتور، و حالتِ خالیِ راهنما با قدمِ بعد.
۱. نصب کامپوننتها
این بلاک از ۷ آیتم دیگ استفاده میکند: badgebuttoncontrol-sizeiconspersiansearch-fieldtoggle-group
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/toggle-group.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/toggle-group.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/toggle-group.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/toggle-group.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
account-orders.tsx
"use client"
import * as React from "react"
import { ChatIcon, CheckIcon, ChevronLeftIcon, ClockIcon, DashboardIcon, EditIcon, HeartIcon, LocationIcon, LogoutIcon, ReceiptIcon, SearchIcon, ShopIcon, UserIcon, WalletIcon } from "@/components/icons"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { SearchField } from "@/components/ui/search-field"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { formatCurrency, toFaDigits } from "@/lib/persian"
import { stockImages, type StockImageName } from "@/lib/stock-images"
type Status = "current" | "delivered" | "returned" | "canceled"
const TABS: { id: Status; label: string }[] = [
{ id: "current", label: "جاری" },
{ id: "delivered", label: "تحویل شده" },
{ id: "returned", label: "مرجوع شده" },
{ id: "canceled", label: "لغو شده" },
]
const ORDERS: { id: string; status: Status; date: string; amount: number; items: StockImageName[] }[] = [
{ id: "1159260945", status: "delivered", date: "۳ مرداد ۱۴۰۵", amount: 2_684_000, items: ["coverNotebook", "coverOfficeSupplies", "productVase"] },
{ id: "5264036488", status: "delivered", date: "۵ فروردین ۱۴۰۵", amount: 7_890_380, items: ["coverMinimalRoom", "coverWorkspace", "coverCalculator", "productVase"] },
{ id: "4026928258", status: "delivered", date: "۹ مهر ۱۴۰۴", amount: 319_110, items: ["studentStudy"] },
{ id: "4011317730", status: "returned", date: "۱۵ تیر ۱۴۰۴", amount: 789_460, items: ["coverCalculator", "projectCafeBrand"] },
{ id: "3684817827", status: "canceled", date: "۲ تیر ۱۴۰۴", amount: 2_083_880, items: ["coverOfficeSupplies"] },
]
const STATUS: Record<Status, { label: string; color: "info" | "success" | "warning" | "default" }> = {
current: { label: "در جریان", color: "info" },
delivered: { label: "تحویل شده", color: "success" },
returned: { label: "مرجوع شده", color: "warning" },
canceled: { label: "لغو شده", color: "default" },
}
const NAV = [
{ id: "overview", label: "خلاصهٔ فعالیتها", icon: <DashboardIcon /> },
{ id: "orders", label: "سفارشها", icon: <ShopIcon /> },
{ id: "lists", label: "لیستهای من", icon: <HeartIcon /> },
{ id: "reviews", label: "دیدگاهها و پرسشها", icon: <ChatIcon /> },
{ id: "addresses", label: "نشانیها", icon: <LocationIcon /> },
{ id: "account", label: "اطلاعاتِ حساب", icon: <UserIcon /> },
]
/** ستونِ کنارِ پنلِ حساب: نام و موبایل، کیف پول با افزایشِ موجودی، و منوی بخشها. */
function AccountNav({ active }: { active: string }) {
return (
<aside className="flex flex-col gap-2 rounded-xl border border-border bg-card p-4 lg:sticky lg:top-6">
<div className="flex items-start justify-between gap-2 px-2 pt-1 pb-4">
<span className="flex flex-col gap-0.5">
<span className="font-bold">آرمیتا رستگار</span>
<bdi dir="ltr" className="text-sm text-muted-foreground tabular-nums">
{toFaDigits("0912 345 6789")}
</bdi>
</span>
<Button variant="ghost" size="icon" aria-label="ویرایشِ اطلاعات" className="-me-2 -mt-2">
<EditIcon />
</Button>
</div>
<div className="flex items-center justify-between gap-2 rounded-lg bg-subtle p-3">
<span className="flex items-center gap-2 text-sm [&>svg]:size-4 [&>svg]:text-muted-foreground">
<WalletIcon aria-hidden />
کیف پول
</span>
<span className="font-bold tabular-nums">{formatCurrency(150_129)}</span>
</div>
<a href="#" className="flex items-center gap-1 self-end px-2 pb-2 text-sm font-medium text-brand-secondary hover:underline">
افزایشِ موجودی
<ChevronLeftIcon aria-hidden className="size-4" />
</a>
<nav aria-label="حسابِ کاربری" className="flex flex-col border-t border-border pt-2">
{NAV.map((item) => (
<a
key={item.id}
href="#"
aria-current={item.id === active ? "page" : undefined}
className={`flex min-h-12 items-center gap-3 rounded-lg px-3 text-sm [&>svg]:size-5 ${item.id === active ? "bg-primary-subtle font-bold" : "text-muted-foreground hover:bg-subtle hover:text-foreground"}`}
>
{item.icon}
{item.label}
</a>
))}
<a href="#" className="flex min-h-12 items-center gap-3 rounded-lg px-3 text-sm text-destructive hover:bg-destructive-subtle [&>svg]:size-5">
<LogoutIcon aria-hidden />
خروج از حساب
</a>
</nav>
</aside>
)
}
/**
* تاریخچهٔ سفارشها در پنلِ حسابِ مشتری: ستونِ کنار با کیف پول و منو، زبانههای وضعیت با شمار، جستوجو،
* کارتِ هر سفارش (وضعیت، تاریخ، کدِ سفارش، مبلغ، عکسِ کالاها، فاکتور) و حالتِ خالیِ راهنما که میگوید
* اینجا چه میآید و قدمِ بعد چیست، نه فقط «سفارشی ندارید».
*/
export default function AccountOrdersBlock() {
const [tab, setTab] = React.useState<Status>("delivered")
const [query, setQuery] = React.useState("")
const [searching, setSearching] = React.useState(false)
const rows = ORDERS.filter((order) => order.status === tab && (!query || order.id.includes(query.replace(/[۰-۹]/g, (d) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(d))))))
const count = (status: Status) => ORDERS.filter((order) => order.status === status).length
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto grid w-full max-w-6xl grid-cols-1 items-start gap-6 p-4 sm:p-6 lg:grid-cols-[17rem_minmax(0,1fr)]">
<div className="hidden lg:block">
<AccountNav active="orders" />
</div>
<section aria-labelledby="orders-title" className="flex min-w-0 flex-col rounded-xl border border-border bg-card">
<header className="flex items-center justify-between gap-3 px-5 pt-5">
<h1 id="orders-title" className="text-xl font-black">
تاریخچهٔ سفارشها
</h1>
<Button variant="ghost" size="icon" aria-label="جستوجو در سفارشها" aria-pressed={searching} onClick={() => setSearching((value) => !value)}>
<SearchIcon />
</Button>
</header>
{searching && (
<div className="px-5 pt-4">
<SearchField aria-label="کدِ سفارش" placeholder="کدِ سفارش" inputMode="numeric" value={query} onValueChange={setQuery} autoFocus />
</div>
)}
<div className="overflow-x-auto border-b border-border px-5 pt-4 [scrollbar-width:none]">
<ToggleGroup type="single" value={tab} onValueChange={(value) => value && setTab(value as Status)} aria-label="وضعیتِ سفارش" className="w-max gap-1">
{TABS.map((item) => (
<ToggleGroupItem
key={item.id}
value={item.id}
className="gap-2 rounded-none rounded-t-lg border-b-2 border-transparent px-3 bg-card hover:bg-subtle data-[state=on]:border-primary data-[state=on]:bg-card data-[state=on]:font-bold"
>
{item.label}
<span className="flex min-w-6 items-center justify-center rounded-md bg-muted px-1.5 text-xs tabular-nums">{toFaDigits(count(item.id))}</span>
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
{rows.length === 0 ? (
// حالتِ خالیِ راهنما: چه میآید اینجا و قدمِ بعدی، نه فقط «چیزی نیست».
<div className="flex flex-col items-center gap-5 px-6 py-14 text-center">
<span aria-hidden className="relative flex size-28 items-center justify-center rounded-full bg-[color-mix(in_oklab,var(--dig-violet-500)_10%,var(--card))]">
<span className="flex size-16 rotate-[-6deg] items-center justify-center rounded-xl bg-card text-[var(--dig-violet-600)] shadow-card [&>svg]:size-8">
<ReceiptIcon />
</span>
<span className="absolute end-3 top-4 size-3 rounded-full bg-[var(--dig-amber-400)]" />
<span className="absolute start-4 bottom-5 size-2 rounded-full bg-[var(--dig-violet-400)]" />
</span>
<div className="flex flex-col gap-2">
<h2 className="text-lg font-bold">{query ? "سفارشی با این کد پیدا نشد" : tab === "current" ? "سفارشهای در جریانت اینجا میآیند" : "هنوز چیزی اینجا نیست"}</h2>
<p className="max-w-sm text-sm leading-6 text-muted-foreground">
{query ? "کدِ سفارش را از پیامکِ ثبتِ سفارش دوباره نگاه کن." : "بعد از خرید، وضعیتِ ارسال و تحویلِ هر سفارش را همینجا دنبال میکنی."}
</p>
</div>
{query ? (
<Button variant="outline" onClick={() => setQuery("")}>
پاک کردنِ جستوجو
</Button>
) : (
<Button>شروعِ خرید</Button>
)}
</div>
) : (
<ul className="flex flex-col gap-4 p-5">
{rows.map((order) => (
<li key={order.id} className="overflow-hidden rounded-xl border border-border">
<a href="#" className="flex items-start justify-between gap-3 p-4 hover:bg-subtle">
<span className="flex min-w-0 flex-col gap-2">
<Badge variant="soft" color={STATUS[order.status].color} className="w-fit gap-1">
{order.status === "delivered" ? <CheckIcon aria-hidden /> : <ClockIcon aria-hidden />}
{STATUS[order.status].label}
</Badge>
<span className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm">
<span>{order.date}</span>
<span aria-hidden className="text-muted-foreground">·</span>
<span className="text-muted-foreground">
کدِ سفارش <bdi className="font-medium text-foreground tabular-nums">{toFaDigits(order.id)}</bdi>
</span>
<span aria-hidden className="text-muted-foreground">·</span>
<span className="text-muted-foreground">
مبلغ <span className="font-bold text-foreground tabular-nums">{formatCurrency(order.amount)}</span>
</span>
</span>
</span>
<ChevronLeftIcon aria-hidden className="mt-1 size-5 shrink-0 text-muted-foreground" />
<span className="sr-only">جزئیاتِ سفارش</span>
</a>
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-border p-4">
<ul aria-label="کالاها" className="flex gap-2">
{order.items.map((image) => (
<li key={image}>
{/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
<img src={stockImages[image].src} alt={stockImages[image].alt} loading="lazy" className="size-14 rounded-lg object-cover" />
</li>
))}
</ul>
<ControlSizeProvider size="lg" asChild>
<Button variant="ghost" className="gap-1.5 text-brand-secondary">
<ReceiptIcon />
مشاهدهٔ فاکتور
</Button>
</ControlSizeProvider>
</div>
</li>
))}
</ul>
)}
</section>
</main>
</ControlSizeProvider>
)
}