سبد و پرداخت (اپ)
پیشنمایش تمامصفحهسبد و پرداخت در اپ: اقلام با عکس و تعداد، نشانی بهصورتِ ردیفِ لمسی، روشِ پرداخت با کارتهای رادیویی و نوارِ پایینیِ چسبان با جمع و پرداخت.
۱. نصب کامپوننتها
این بلاک از ۹ آیتم دیگ استفاده میکند: app-barbuttoncontrol-sizeiconslabelnumber-fieldpersianradio-grouptoast
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/app-bar.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/label.json https://docs.digdesign.ir/r/number-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/toast.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/app-bar.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/label.json https://docs.digdesign.ir/r/number-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/toast.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/app-bar.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/label.json https://docs.digdesign.ir/r/number-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/toast.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/app-bar.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/label.json https://docs.digdesign.ir/r/number-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/toast.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
mobile-checkout.tsx
"use client"
import * as React from "react"
import { ChevronLeftIcon, ChevronRightIcon, CreditCardIcon, TrashIcon, WalletIcon } from "@/components/icons"
import { AppBar, AppBarLeading, AppBarTitle } from "@/components/ui/app-bar"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Label } from "@/components/ui/label"
import { NumberField } from "@/components/ui/number-field"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { toast } from "@/components/ui/toast"
import { formatCurrency, toFaDigits } from "@/lib/persian"
import { stockImages, type StockImageName } from "@/lib/stock-images"
const INITIAL: { id: string; title: string; image: StockImageName; price: number; quantity: number }[] = [
{ id: "1", title: "گلدانِ سرامیکی دوقلو", image: "productVase", price: 1_290_000, quantity: 1 },
{ id: "2", title: "دفترِ یادداشتِ جلدسخت", image: "coverNotebook", price: 180_000, quantity: 2 },
]
const optionCard =
"flex min-h-16 items-center gap-3 rounded-xl border border-border bg-card p-4 transition-colors has-[[data-state=checked]]:border-primary has-[[data-state=checked]]:bg-primary-subtle"
/**
* سبد و پرداخت در اپ: اقلام با عکس و تعداد، نشانی بهصورتِ ردیفِ لمسی برای تغییر، روشِ
* پرداخت با کارتهای رادیویی و نوارِ پایینی چسبان با جمع و «پرداخت» بالای ناحیهٔ امن.
*/
export default function MobileCheckoutBlock() {
const [items, setItems] = React.useState(INITIAL)
const [payment, setPayment] = React.useState("card")
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
return (
<ControlSizeProvider size="xl" asChild>
<div className="mx-auto flex min-h-svh w-full max-w-md flex-col bg-subtle">
<AppBar sticky className="bg-subtle">
<AppBarLeading>
<Button variant="ghost" size="icon" aria-label="بازگشت">
<ChevronRightIcon className="ltr:rotate-180" />
</Button>
</AppBarLeading>
<AppBarTitle>سبد خرید ({toFaDigits(items.length)})</AppBarTitle>
</AppBar>
<main className="flex flex-1 flex-col gap-6 p-4">
<ul className="flex flex-col divide-y divide-border rounded-xl bg-card px-4">
{items.map((item) => (
<li key={item.id} className="flex gap-3 py-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 flex-1 flex-col gap-2">
<div className="flex items-start justify-between gap-2">
<span className="font-medium">{item.title}</span>
<Button variant="ghost" size="icon" aria-label={`حذفِ ${item.title}`} onClick={(() => {
const index = items.findIndex((row) => row.id === item.id)
setItems((current) => current.filter((row) => row.id !== item.id))
toast(`«${item.title}» از سبد حذف شد.`, { action: { label: "برگردان", onClick: () => setItems((current) => (current.some((row) => row.id === item.id) ? current : [...current.slice(0, index), item, ...current.slice(index)])) } })
})}>
<TrashIcon />
</Button>
</div>
<div className="flex items-center justify-between gap-2">
<NumberField aria-label={`تعداد ${item.title}`} value={item.quantity} onValueChange={(quantity) => setItems((list) => list.map((entry) => (entry.id === item.id ? { ...entry, quantity } : entry)))} min={1} max={10} className="w-28" />
<span className="font-bold tabular-nums">{formatCurrency(item.price * item.quantity)}</span>
</div>
</div>
</li>
))}
</ul>
<a href="#" className="flex min-h-16 items-center gap-3 rounded-xl bg-card p-4">
<span className="flex flex-1 flex-col gap-0.5">
<span className="text-sm text-muted-foreground">ارسال به خانه</span>
<span className="line-clamp-1">تهران، ولیعصر، کوچهٔ نسترن، پلاک ۱۲</span>
</span>
<ChevronLeftIcon aria-hidden className="size-5 text-muted-foreground ltr:rotate-180" />
</a>
<section aria-labelledby="mc-pay" className="flex flex-col gap-3">
<h2 id="mc-pay" className="px-1 font-bold">
روشِ پرداخت
</h2>
<RadioGroup value={payment} onValueChange={setPayment} aria-labelledby="mc-pay" className="gap-3">
{[
{ value: "card", label: "درگاهِ بانکی", icon: <CreditCardIcon /> },
{ value: "wallet", label: "کیف پول · موجودی ۴۸ میلیون", icon: <WalletIcon /> },
].map((item) => (
<div key={item.value} className={optionCard}>
<RadioGroupItem id={`mc-${item.value}`} value={item.value} />
<span aria-hidden className="text-muted-foreground [&>svg]:size-5">
{item.icon}
</span>
<Label htmlFor={`mc-${item.value}`} className="flex-1">
{item.label}
</Label>
</div>
))}
</RadioGroup>
</section>
</main>
<footer className="sticky bottom-0 flex items-center gap-4 border-t border-border bg-background p-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
<span className="flex flex-col">
<span className="text-xs text-muted-foreground">مبلغِ قابلِ پرداخت</span>
<span className="text-lg font-black tabular-nums">{formatCurrency(total)}</span>
</span>
<Button className="flex-1" disabled={items.length === 0} onClick={() => toast.success("به درگاه منتقل میشوید…")}>
پرداخت
</Button>
</footer>
</div>
</ControlSizeProvider>
)
}