تسویه حساب
پیشنمایش تمامصفحهمراحل خرید، روش ارسال، روش پرداخت و خلاصهٔ سفارش.
۱. نصب کامپوننتها
این بلاک از ۱۱ آیتم دیگ استفاده میکند: buttoncardcontrol-sizeiconslabelpersianradio-groupseparatorsteppertext-fieldtoast
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.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/separator.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.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/separator.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.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/separator.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.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/separator.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
checkout.tsx
"use client"
import * as React from "react"
import { CheckIcon, CreditCardIcon, ExpressIcon, TruckIcon, WalletIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Separator } from "@/components/ui/separator"
import { TextField } from "@/components/ui/text-field"
import { toast } from "@/components/ui/toast"
import { Stepper, StepperItem, StepperTitle } from "@/components/ui/stepper"
import { formatCurrency, toFaDigits } from "@/lib/persian"
import { stockImages, type StockImageName } from "@/lib/stock-images"
const ITEMS: { id: string; title: string; quantity: number; price: number; image: StockImageName }[] = [
{ id: "1", title: "گلدانِ سرامیکی دوقلو", quantity: 1, price: 3_490_000, image: "productVase" },
{ id: "2", title: "دفترِ یادداشتِ جلدسخت", quantity: 2, price: 180_000, image: "coverNotebook" },
]
const WALLET_BALANCE = 1_345_000
const SHIPPING = [
{ value: "post", label: "پست پیشتاز", note: "۳ تا ۵ روز کاری", price: 45_000, icon: <TruckIcon /> },
{ value: "express", label: "پیک سریع", note: "همان روز در تهران", price: 90_000, icon: <ExpressIcon /> },
]
/**
* تسویهحساب: مراحل، نشانی انتخابشده با «تغییر»، روشِ ارسال و پرداخت با کارتهای رادیویی
* (کیف پولِ ناکافی غیرفعال با دلیل)، و خلاصهٔ چسبان با عکسِ اقلام و کدِ تخفیف. نتیجهٔ
* پرداخت صفحهٔ خودش را دارد.
*/
export default function CheckoutBlock() {
const [shipping, setShipping] = React.useState("post")
const [payment, setPayment] = React.useState("card")
const [paying, setPaying] = React.useState(false)
const [paid, setPaid] = React.useState(false)
const subtotal = ITEMS.reduce((sum, item) => sum + item.price * item.quantity, 0)
const shippingItem = SHIPPING.find((item) => item.value === shipping)!
const total = subtotal + shippingItem.price
// کیف پولی که موجودیاش به مبلغِ سفارش نمیرسد قابل انتخاب نیست، نه اینکه پرداخت بعداً خطا بدهد.
const walletShort = WALLET_BALANCE < total
const payments = [
{ value: "card", label: "درگاه بانکی", note: "همهٔ کارتهای عضو شتاب", icon: <CreditCardIcon />, disabled: false },
{
value: "wallet",
label: "کیف پول",
note: walletShort
? `موجودی ${formatCurrency(WALLET_BALANCE)}؛ برای این سفارش کافی نیست`
: `موجودی ${formatCurrency(WALLET_BALANCE)}`,
icon: <WalletIcon />,
disabled: walletShort,
},
]
async function pay() {
setPaying(true)
await new Promise((resolve) => setTimeout(resolve, 900))
setPaying(false)
// نتیجهٔ پرداخت صفحهٔ خودش را میخواهد؛ اعلانِ گوشهٔ صفحه ممکن است دیده نشود.
setPaid(true)
window.scrollTo({ top: 0 })
}
if (paid) {
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-lg flex-col items-center gap-6 p-4 py-12 text-center sm:p-6">
<span
aria-hidden
className="flex size-16 items-center justify-center rounded-xl bg-success-subtle text-success [&_svg]:size-8"
>
<CheckIcon />
</span>
<div className="flex flex-col gap-2" role="status">
<h1 className="text-2xl font-bold">پرداخت موفق بود</h1>
<p className="text-muted-foreground">سفارش شما ثبت شد و پیامکِ تأیید برایتان ارسال شد.</p>
</div>
<Card className="w-full text-start">
<CardContent className="flex flex-col gap-3 text-sm">
<div className="flex justify-between gap-2">
<span className="text-muted-foreground">کد پیگیری</span>
<bdi dir="ltr" className="font-semibold tracking-wider">
{toFaDigits("8821042")}
</bdi>
</div>
<div className="flex justify-between gap-2">
<span className="text-muted-foreground">مبلغ پرداختشده</span>
<span className="font-semibold">{formatCurrency(total)}</span>
</div>
<div className="flex justify-between gap-2">
<span className="text-muted-foreground">روش ارسال</span>
<span>
{shippingItem.label} · {shippingItem.note}
</span>
</div>
</CardContent>
</Card>
<div className="flex w-full flex-col gap-2 sm:flex-row">
<Button className="flex-1">پیگیری سفارش</Button>
<Button variant="outline" className="flex-1" onClick={() => setPaid(false)}>
بازگشت به فروشگاه
</Button>
</div>
</main>
</ControlSizeProvider>
)
}
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-5xl flex-col gap-8 p-4 sm:p-6">
<h1 className="sr-only">تسویهحساب</h1>
<Stepper value={2} className="mx-auto w-full max-w-xl">
<StepperItem>
<StepperTitle>سبد خرید</StepperTitle>
</StepperItem>
<StepperItem>
<StepperTitle>آدرس</StepperTitle>
</StepperItem>
<StepperItem>
<StepperTitle>پرداخت</StepperTitle>
</StepperItem>
</Stepper>
<div className="grid grid-cols-1 items-start gap-6 lg:grid-cols-[minmax(0,1fr)_22rem]">
<div className="flex flex-col gap-6">
<Card>
<CardContent className="flex items-start gap-4">
<div className="flex flex-1 flex-col gap-1">
<span className="text-sm text-muted-foreground">ارسال به</span>
<span className="font-semibold">خانه · مهسا کریمی</span>
<span className="text-sm leading-6 text-muted-foreground">تهران، خیابان ولیعصر، کوچهٔ نسترن، پلاک ۱۲، واحد ۴</span>
</div>
<Button variant="outline">تغییر</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">روش ارسال</CardTitle>
</CardHeader>
<CardContent>
<RadioGroup name="shipping" value={shipping} onValueChange={setShipping} className="gap-3" aria-label="روش ارسال">
{SHIPPING.map((item) => (
<div key={item.value} className="group 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">
<RadioGroupItem id={`ship-${item.value}`} value={item.value} />
<OptionIcon>{item.icon}</OptionIcon>
<Label htmlFor={`ship-${item.value}`} className="flex flex-1 flex-col items-start gap-1">
<span>{item.label}</span>
<span className="text-xs leading-5 font-normal text-muted-foreground">{item.note}</span>
</Label>
<span className="text-sm tabular-nums">{formatCurrency(item.price)}</span>
</div>
))}
</RadioGroup>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">روش پرداخت</CardTitle>
</CardHeader>
<CardContent>
<RadioGroup name="payment" value={payment} onValueChange={setPayment} className="gap-3" aria-label="روش پرداخت">
{payments.map((item) => (
<div key={item.value} className="group 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">
<RadioGroupItem id={`pay-${item.value}`} value={item.value} disabled={item.disabled} />
<OptionIcon>{item.icon}</OptionIcon>
<Label htmlFor={`pay-${item.value}`} className="flex flex-1 flex-col items-start gap-1">
<span>{item.label}</span>
<span className={item.disabled ? "text-xs font-normal text-destructive" : "text-xs font-normal text-muted-foreground"}>
{item.note}
</span>
</Label>
</div>
))}
</RadioGroup>
</CardContent>
</Card>
</div>
<section aria-labelledby="checkout-summary" className="flex flex-col gap-5 rounded-xl bg-subtle p-6 lg:sticky lg:top-6">
<h2 id="checkout-summary" className="font-bold">
خلاصهٔ سفارش
</h2>
<ul className="flex flex-col gap-4">
{ITEMS.map((item) => (
<li key={item.id} className="flex items-center gap-3 text-sm">
{/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
<img src={stockImages[item.image].src} alt={stockImages[item.image].alt} className="size-14 rounded-lg object-cover" />
<span className="flex flex-1 flex-col gap-0.5">
<span className="font-medium">{item.title}</span>
<span className="text-muted-foreground">{toFaDigits(item.quantity)} عدد</span>
</span>
<span className="tabular-nums">{formatCurrency(item.price * item.quantity)}</span>
</li>
))}
</ul>
<form
className="flex items-end gap-2"
onSubmit={(event) => {
event.preventDefault()
toast.error("این کد تخفیف معتبر نیست.")
}}
>
<TextField label="کد تخفیف" name="coupon" dir="ltr" className="flex-1" />
<Button type="submit" variant="outline">
اعمال
</Button>
</form>
<Separator />
<dl className="flex flex-col gap-3 text-sm">
<div className="flex justify-between gap-2 text-muted-foreground">
<dt>جمع اقلام</dt>
<dd className="tabular-nums">{formatCurrency(subtotal)}</dd>
</div>
<div className="flex justify-between gap-2 text-muted-foreground">
<dt>هزینهٔ ارسال</dt>
<dd className="tabular-nums">{formatCurrency(shippingItem.price)}</dd>
</div>
<div className="flex items-baseline justify-between gap-2">
<dt className="font-semibold">مبلغ قابل پرداخت</dt>
<dd className="text-xl font-black tabular-nums">{formatCurrency(total)}</dd>
</div>
</dl>
<Button fullWidth loading={paying} onClick={pay} className="mt-3">
پرداخت {formatCurrency(total)}
</Button>
</section>
</div>
</main>
</ControlSizeProvider>
)
}
function OptionIcon({ children }: { children: React.ReactNode }) {
return (
<span
aria-hidden
className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-subtle text-foreground group-has-[[data-state=checked]]:bg-background [&_svg]:size-5"
>
{children}
</span>
)
}