تسویه با گامهای تاشونده
پیشنمایش تمامصفحهتسویه در یک صفحه: هر گام بعد از تکمیل بسته میشود و خلاصهاش با «ویرایش» میماند؛ فقط گامِ فعال باز، جمع در ستونِ کنار.
۱. نصب کامپوننتها
این بلاک از ۷ آیتم دیگ استفاده میکند: 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.jsonpnpm 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.jsonyarn 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.jsonbunx --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>
)
}