بلاک‌ها · پنل مدیریت

نوارِ خلاصهٔ سفارش، مرسوله‌های جدا هر کدام با مسیرِ سه‌مرحله‌ای، کالاها با اقدامِ وابسته به وضعیت (لغو با تأیید، یا مرجوعی و تعویض)، و ستونِ چسبانِ نشانی و ریزِ پرداخت.

۱. نصب کامپوننت‌ها

این بلاک از ۸ آیتم دیگ استفاده می‌کند: 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.json
pnpm 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.json
yarn 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.json
bunx --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>
  )
}