بلاک‌ها · فروشگاهی

تسویه با گام‌های تاشونده

پیش‌نمایش تمام‌صفحه

تسویه در یک صفحه: هر گام بعد از تکمیل بسته می‌شود و خلاصه‌اش با «ویرایش» می‌ماند؛ فقط گامِ فعال باز، جمع در ستونِ کنار.

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

این بلاک از ۷ آیتم دیگ استفاده می‌کند: buttoncontrol-sizeiconslabelpersianradio-grouptext-field

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add 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/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/text-field.json
pnpm dlx digdesign@latest add 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/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/text-field.json
yarn dlx digdesign@latest add 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/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/text-field.json
bunx --bun digdesign@latest add 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/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/text-field.json

۲. کد صفحه

کد را در app/<route>/page.tsx بگذارید و متن‌ها، داده و هندلرها را با محصول خودتان عوض کنید.

checkout-steps.tsx
"use client"

import * as React from "react"

import { CheckIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { TextField } from "@/components/ui/text-field"
import { formatCurrency, isValidMobile, toFaDigits } from "@/lib/persian"

const STEPS = ["اطلاعاتِ تماس", "نشانی", "ارسال", "پرداخت"]
const SHIPPING = [
  { value: "post", label: "پستِ پیشتاز", note: "۳ تا ۵ روزِ کاری", price: 45_000 },
  { value: "express", label: "پیکِ امروز", note: "فقط تهران", price: 90_000 },
]

const optionCard =
  "flex items-center gap-3 rounded-xl border border-border p-4 transition-colors has-[[data-state=checked]]:border-primary has-[[data-state=checked]]:bg-primary-subtle"

/**
 * تسویه در یک صفحه با گام‌های تاشونده: هر گام بعد از تکمیل بسته می‌شود و خلاصه‌اش با
 * «ویرایش» می‌ماند؛ فقط گامِ فعال باز است تا کاربر گم نشود. جمع در ستونِ کنار.
 */
export default function CheckoutStepsBlock() {
  const [step, setStep] = React.useState(1)
  const [mobile, setMobile] = React.useState("09121234567")
  const [shipping, setShipping] = React.useState("post")
  const ship = SHIPPING.find((item) => item.value === shipping) ?? SHIPPING[0]
  const summary = [`${toFaDigits(mobile)}`, "تهران، ولیعصر، کوچهٔ نسترن، پلاک ۱۲", `${ship.label} · ${formatCurrency(ship.price)}`, ""]
  const subtotal = 1_650_000

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto grid w-full max-w-5xl grid-cols-1 items-start gap-8 p-4 sm:p-6 lg:grid-cols-[minmax(0,1fr)_20rem]">
        <h1 className="sr-only">تسویه‌حساب</h1>
        <ol className="flex flex-col gap-3">
          {STEPS.map((title, index) => {
            const state = index < step ? "done" : index === step ? "open" : "next"
            return (
              <li key={title} className={`rounded-xl border bg-card ${state === "open" ? "border-primary" : "border-border"}`}>
                <div className="flex min-h-16 items-center gap-4 px-5">
                  <span
                    aria-hidden
                    className={`flex size-8 shrink-0 items-center justify-center rounded-full text-sm font-bold [&>svg]:size-4 ${state === "done" ? "bg-success text-[var(--dig-white)]" : state === "open" ? "bg-foreground text-background" : "border border-input text-muted-foreground"}`}
                  >
                    {state === "done" ? <CheckIcon /> : toFaDigits(index + 1)}
                  </span>
                  <span className="flex flex-1 flex-col">
                    <span className={state === "next" ? "font-medium text-muted-foreground" : "font-semibold"}>{title}</span>
                    {state === "done" && <span className="text-sm text-muted-foreground">{summary[index]}</span>}
                  </span>
                  {state === "done" && (
                    <Button variant="ghost" onClick={() => setStep(index)}>
                      ویرایش
                    </Button>
                  )}
                </div>
                {state === "open" && (
                  <form
                    className="flex flex-col gap-5 border-t border-border p-5"
                    onSubmit={(event) => {
                      event.preventDefault()
                      setStep(index + 1)
                    }}
                  >
                    {index === 0 && <TextField label="شماره موبایل" type="tel" dir="ltr" maxLength={11} value={mobile} onValueChange={setMobile} required validate={(value) => (isValidMobile(value) ? undefined : "شماره موبایل باید با ۰۹ شروع شود و ۱۱ رقم باشد.")} />}
                    {index === 1 && (
                      <div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
                        <TextField label="نشانی" defaultValue="تهران، ولیعصر، کوچهٔ نسترن" required className="sm:col-span-2" />
                        <TextField label="پلاک" defaultValue="12" dir="ltr" required />
                        <TextField label="کد پستی" defaultValue="1234567890" dir="ltr" maxLength={10} required />
                      </div>
                    )}
                    {index === 2 && (
                      <RadioGroup value={shipping} onValueChange={setShipping} aria-label="روشِ ارسال" className="gap-3">
                        {SHIPPING.map((item) => (
                          <div key={item.value} className={optionCard}>
                            <RadioGroupItem id={`cs-${item.value}`} value={item.value} />
                            <Label htmlFor={`cs-${item.value}`} className="flex flex-1 flex-col items-start gap-1">
                              <span>{item.label}</span>
                              <span className="text-xs font-normal text-muted-foreground">{item.note}</span>
                            </Label>
                            <span className="text-sm tabular-nums">{formatCurrency(item.price)}</span>
                          </div>
                        ))}
                      </RadioGroup>
                    )}
                    {index === 3 && <p className="text-muted-foreground">بعد از «پرداخت» به درگاهِ امنِ بانک می‌روی و دوباره به همین صفحه برمی‌گردی.</p>}
                    <Button type="submit" className="mt-3 self-start">
                      {index === STEPS.length - 1 ? `پرداختِ ${formatCurrency(subtotal + ship.price)}` : "ادامه"}
                    </Button>
                  </form>
                )}
              </li>
            )
          })}
        </ol>
        <aside className="flex flex-col gap-3 rounded-xl bg-subtle p-6 text-sm lg:sticky lg:top-6">
          <div className="flex justify-between">
            <span className="text-muted-foreground">جمعِ اقلام</span>
            <span className="tabular-nums">{formatCurrency(subtotal)}</span>
          </div>
          <div className="flex justify-between">
            <span className="text-muted-foreground">ارسال</span>
            <span className="tabular-nums">{formatCurrency(ship.price)}</span>
          </div>
          <div className="flex items-baseline justify-between border-t border-border pt-3">
            <span className="font-semibold">قابلِ پرداخت</span>
            <span className="text-xl font-black tabular-nums">{formatCurrency(subtotal + ship.price)}</span>
          </div>
        </aside>
      </main>
    </ControlSizeProvider>
  )
}