جزئیاتِ سفارش
پیشنمایش تمامصفحهنوارِ خلاصهٔ سفارش، مرسولههای جدا هر کدام با مسیرِ سهمرحلهای، کالاها با اقدامِ وابسته به وضعیت (لغو با تأیید، یا مرجوعی و تعویض)، و ستونِ چسبانِ نشانی و ریزِ پرداخت.
۱. نصب کامپوننتها
این بلاک از ۸ آیتم دیگ استفاده میکند: alert-dialogbadgebreadcrumbbuttoncontrol-sizeiconspersiantoast
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/alert-dialog.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/breadcrumb.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/toast.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/alert-dialog.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/breadcrumb.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/toast.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/alert-dialog.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/breadcrumb.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/toast.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/alert-dialog.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/breadcrumb.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/toast.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
orders-detail.tsx
"use client"
import * as React from "react"
import { CheckIcon, ClockIcon, CopyIcon } from "@/components/icons"
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"
import { Badge } from "@/components/ui/badge"
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { toast } from "@/components/ui/toast"
import { formatCurrency, formatNumber, toFaDigits } from "@/lib/persian"
import { stockImages, type StockImageName } from "@/lib/stock-images"
type Item = { id: string; title: string; variant: string; qty: number; price: number; image: StockImageName; arrive: string }
type Shipment = { id: string; steps: { title: string; time?: string }[]; current: number; delivered: boolean; items: Item[] }
const SHIPMENTS: Shipment[] = [
{
id: "s1",
current: 1,
delivered: false,
steps: [{ title: "تأییدِ سفارش", time: "شنبه، ۱۹ مهر" }, { title: "ارسال شد", time: "دوشنبه، ۲۱ مهر" }, { title: "در انتظارِ تحویل" }],
items: [
{ id: "1", title: "گلدانِ سرامیکی دوقلو", variant: "سفید · مات", qty: 1, price: 990_000, image: "productVase", arrive: "چهارشنبه، ۲۳ مهر" },
{ id: "2", title: "دفترِ یادداشتِ جلدسخت", variant: "A5 · خطدار", qty: 2, price: 120_000, image: "coverNotebook", arrive: "چهارشنبه، ۲۳ مهر" },
],
},
{
id: "s2",
current: 2,
delivered: true,
steps: [{ title: "تأییدِ سفارش", time: "شنبه، ۱۹ مهر" }, { title: "ارسال شد", time: "یکشنبه، ۲۰ مهر" }, { title: "تحویل شد", time: "دوشنبه، ۲۱ مهر" }],
items: [{ id: "3", title: "ست لوازمالتحریرِ رومیزی", variant: "چوبی", qty: 1, price: 450_000, image: "coverOfficeSupplies", arrive: "تحویل شد، ۲۱ مهر" }],
},
]
const ITEMS = SHIPMENTS.flatMap((shipment) => shipment.items)
const SUBTOTAL = ITEMS.reduce((sum, item) => sum + item.qty * item.price, 0)
const COUPON = 150_000
const SHIPPING = 45_000
const TOTAL = SUBTOTAL - COUPON + SHIPPING
/** مسیرِ یک مرسوله: سه مرحله با خطِ رابط؛ انجامشده سبز، آینده خطچین. */
function ShipmentSteps({ shipment }: { shipment: Shipment }) {
return (
<ol className="grid grid-cols-3" aria-label="مراحلِ مرسوله">
{shipment.steps.map((step, index) => {
const reached = index <= shipment.current
return (
<li key={step.title} aria-current={index === shipment.current ? "step" : undefined} className="relative flex flex-col items-center gap-2 text-center">
{index < shipment.steps.length - 1 && (
<span aria-hidden className={`absolute start-1/2 top-3.5 end-[-50%] h-0.5 ${index < shipment.current ? "bg-success" : "h-0 border-t-2 border-dashed border-border"}`} />
)}
<span className={`relative flex size-7 items-center justify-center rounded-full [&>svg]:size-4 ${reached ? "bg-success text-[var(--dig-white)]" : "border-2 border-border bg-card text-muted-foreground"}`}>
{reached ? <CheckIcon aria-label="انجام شد" /> : <ClockIcon aria-hidden />}
</span>
<span className="flex flex-col gap-0.5">
<span className={`text-sm ${reached ? "font-semibold" : "text-muted-foreground"}`}>{step.title}</span>
{step.time && <span className="text-xs text-muted-foreground">{step.time}</span>}
</span>
</li>
)
})}
</ol>
)
}
/**
* جزئیاتِ سفارش برای مشتری: مسیرِ صفحه، نوارِ خلاصه (شمارهٔ سفارش با کپی، تعدادِ کالا، مبلغ و
* صرفهجویی، تاریخ)، مرسولهها جدا با مسیرِ سهمرحلهای و کالاهای هر مرسوله که اقدامش به وضعیت
* بستگی دارد (در راه: لغو با تأیید؛ تحویلشده: مرجوعی یا تعویض)، و ستونِ کنارِ چسبان با نشانی و
* ریزِ پرداخت (تخفیف سبز، جمعِ نهایی درشت).
*/
export default function OrdersDetailBlock() {
const copy = (value: string, label: string) => {
void navigator.clipboard?.writeText(value)
toast.success(`${label} کپی شد.`)
}
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-6xl flex-col gap-6 p-4 sm:p-6">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#">حسابِ من</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="#">سفارشها</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>سفارشِ {toFaDigits(286396)}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
{/* نوارِ خلاصه: سه بخش با جداکننده؛ در موبایل زیرِ هم. */}
<header className="grid grid-cols-1 divide-y divide-border rounded-xl bg-subtle sm:grid-cols-3 sm:divide-x sm:divide-y-0 rtl:sm:divide-x-reverse">
<div className="flex items-center justify-between gap-2 p-5">
<span className="flex flex-col gap-0.5">
<h1 className="text-lg font-black">
سفارشِ <bdi className="tabular-nums">{toFaDigits(286396)}</bdi>
</h1>
<span className="text-sm text-muted-foreground">{toFaDigits(ITEMS.length)} کالا در {toFaDigits(SHIPMENTS.length)} مرسوله</span>
</span>
<Button variant="ghost" size="icon" aria-label="کپیِ شمارهٔ سفارش" onClick={() => copy("286396", "شمارهٔ سفارش")}>
<CopyIcon />
</Button>
</div>
<div className="flex flex-col gap-0.5 p-5">
<span className="text-lg font-black tabular-nums">{formatCurrency(TOTAL)}</span>
<span className="text-sm font-semibold text-success">{formatCurrency(COUPON)} صرفهجویی کردی</span>
</div>
<div className="flex flex-col gap-0.5 p-5">
<span className="text-sm text-muted-foreground">تاریخِ ثبت</span>
<span className="font-semibold">شنبه، ۱۹ مهر ۱۴۰۵</span>
</div>
</header>
<div className="grid grid-cols-1 items-start gap-8 lg:grid-cols-[minmax(0,1fr)_22rem]">
<section aria-labelledby="items-title" className="flex flex-col gap-6">
<h2 id="items-title" className="text-lg font-bold">
کالاها و ارسال
</h2>
{SHIPMENTS.map((shipment, index) => (
<div key={shipment.id} className="flex flex-col gap-5">
<div className="flex items-center justify-between gap-3">
<h3 className="font-semibold">مرسولهٔ {toFaDigits(index + 1)}</h3>
<Badge variant="soft" color={shipment.delivered ? "success" : "warning"}>
{shipment.delivered ? "تحویل شد" : "در راه"}
</Badge>
</div>
<ShipmentSteps shipment={shipment} />
<ul className="flex flex-col gap-3">
{shipment.items.map((item) => (
<li key={item.id} className="overflow-hidden rounded-xl border border-border bg-card">
<div className="flex gap-4 p-4">
{/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
<img src={stockImages[item.image].src} alt="" className="size-20 shrink-0 rounded-lg object-cover" />
<div className="flex min-w-0 flex-1 flex-col gap-1">
<span className="font-semibold">{item.title}</span>
<span className="text-sm text-muted-foreground">
{item.variant} · {toFaDigits(item.qty)} عدد
</span>
<span className="font-bold tabular-nums">{formatCurrency(item.qty * item.price)}</span>
</div>
<span className="hidden shrink-0 flex-col items-end gap-1 text-end text-xs text-muted-foreground sm:flex">
<span className={`font-semibold ${shipment.delivered ? "text-success" : "text-[var(--dig-amber-700)] dark:text-[var(--dig-amber-300)]"}`}>{shipment.delivered ? "تحویل شد" : "در راه"}</span>
{shipment.delivered ? item.arrive : `میرسد ${item.arrive}`}
</span>
</div>
{/* اقدامها به وضعیت بستگی دارد: پیش از تحویل لغو، بعد از تحویل مرجوعی یا تعویض. */}
<div className="grid auto-cols-fr grid-flow-col divide-x divide-border border-t border-border rtl:divide-x-reverse">
{shipment.delivered ? (
<>
<Button variant="ghost" className="rounded-none" onClick={() => toast.success("درخواستِ مرجوعی ثبت شد.")}>
مرجوعی
</Button>
<Button variant="ghost" className="rounded-none" onClick={() => toast.success("درخواستِ تعویض ثبت شد.")}>
تعویض
</Button>
</>
) : (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" className="rounded-none text-destructive hover:text-destructive">
لغوِ این کالا
</Button>
</AlertDialogTrigger>
<ControlSizeProvider size="xl" asChild>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>«{item.title}» لغو شود؟</AlertDialogTitle>
<AlertDialogDescription>این کالا در راه است؛ پیک آن را برمیگرداند و مبلغش تا ۳ روزِ کاری به حسابت برمیگردد. باقیِ مرسوله ارسال میشود.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>منصرف شدم</AlertDialogCancel>
<AlertDialogAction onClick={() => toast.success("درخواستِ لغو ثبت شد.")}>بله، لغو کن</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</ControlSizeProvider>
</AlertDialog>
)}
</div>
</li>
))}
</ul>
</div>
))}
</section>
<aside className="flex flex-col gap-6 lg:sticky lg:top-6">
<section aria-labelledby="address-title" className="flex flex-col gap-3">
<h2 id="address-title" className="text-lg font-bold">
نشانیِ تحویل
</h2>
<div className="flex flex-col gap-1 rounded-xl border border-border bg-card p-4 text-sm leading-6">
<span className="text-muted-foreground">خانه</span>
<span className="font-semibold">آرمیتا رستگار · <bdi dir="ltr">۰۹۱۲ ۳۴۵ ۶۷۸۹</bdi></span>
<span>اهواز، خیابانِ کیانپارس، کوچهٔ نرگس، پلاکِ ۳۲</span>
<span className="text-muted-foreground">کدپستی <bdi className="tabular-nums">{toFaDigits("6634271458")}</bdi></span>
</div>
</section>
<section aria-labelledby="payment-title" className="flex flex-col gap-3">
<h2 id="payment-title" className="text-lg font-bold">
ریزِ پرداخت
</h2>
<dl className="flex flex-col gap-3 rounded-xl bg-[color-mix(in_oklab,var(--dig-violet-500)_6%,var(--card))] p-4 text-sm">
{ITEMS.map((item) => (
<div key={item.id} className="flex justify-between gap-4">
<dt className="text-muted-foreground">{item.title}</dt>
<dd className="shrink-0 tabular-nums">{formatCurrency(item.qty * item.price)}</dd>
</div>
))}
<div className="flex justify-between gap-4 font-semibold text-success">
<dt>کدِ تخفیف</dt>
<dd className="shrink-0 tabular-nums">
<bdi dir="ltr">−{formatNumber(COUPON)}</bdi> تومان
</dd>
</div>
<div className="flex justify-between gap-4">
<dt className="text-muted-foreground">ارسال</dt>
<dd className="shrink-0 tabular-nums">{formatCurrency(SHIPPING)}</dd>
</div>
<div className="flex justify-between gap-4 border-t border-dashed border-border pt-3 text-base">
<dt className="font-bold">جمعِ پرداخت</dt>
<dd className="shrink-0 font-black tabular-nums">{formatCurrency(TOTAL)}</dd>
</div>
</dl>
</section>
</aside>
</div>
</main>
</ControlSizeProvider>
)
}