بلاک‌ها · موبایل

سبد و پرداخت (اپ)

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

سبد و پرداخت در اپ: اقلام با عکس و تعداد، نشانی به‌صورتِ ردیفِ لمسی، روشِ پرداخت با کارت‌های رادیویی و نوارِ پایینیِ چسبان با جمع و پرداخت.

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

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