بلاک‌ها · بانکی و پرداخت

موجودی، افزایش اعتبار و آخرین تراکنش‌های کیف پول.

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

این بلاک از ۱۲ آیتم دیگ استفاده می‌کند: badgebank-logobuttoncardcontrol-sizedialogiconspersianstat-tiletext-fieldtoasttoggle-group

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

۲. کد صفحه

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

wallet.tsx
"use client"

import * as React from "react"

import { BankLogo, getBankBySheba } from "@/components/ui/bank-logo"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Badge } from "@/components/ui/badge"
import { WalletIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog"
import { StatTile, StatTileHeader, StatTileIcon, StatTileLabel, StatTileUnit, StatTileValue } from "@/components/ui/stat-tile"
import { TextField } from "@/components/ui/text-field"
import { toast } from "@/components/ui/toast"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { formatCurrency, formatJalaliDateTime, formatNumber } from "@/lib/persian"

type Entry = { id: number; title: string; date: string; amount: number }

const INITIAL: Entry[] = [
  { id: 1, title: "افزایش موجودی", date: "2026-09-29T10:20:00", amount: 500_000 },
  { id: 2, title: "خرید از فروشگاه دیگ", date: "2026-09-28T18:05:00", amount: -245_000 },
  { id: 3, title: "بازگشت وجه سفارش ۱۰۴۰", date: "2026-09-26T09:41:00", amount: 120_000 },
  { id: 4, title: "خرید شارژ", date: "2026-09-25T21:12:00", amount: -50_000 },
]
const PRESETS = [100_000, 200_000, 500_000]
const MIN_AMOUNT = 10_000

/** مبلغِ علامت‌دار: عدد و علامت یک تکهٔ چپ‌به‌راست‌اند تا علامت به آن سرِ عدد نپرد. */
function SignedAmount({ value }: { value: number }) {
  return (
    <span>
      <bdi dir="ltr">
        {value > 0 ? "+" : "−"}
        {formatNumber(Math.abs(value))}
      </bdi>{" "}
      تومان
    </span>
  )
}

export default function WalletBlock() {
  const [balance, setBalance] = React.useState(1_345_000)
  const [history, setHistory] = React.useState(INITIAL)
  const [dialog, setDialog] = React.useState<"charge" | "withdraw" | null>(null)
  const [charge, setCharge] = React.useState("200000")
  const [withdraw, setWithdraw] = React.useState("")
  const [sheba, setSheba] = React.useState("")

  const withdrawValue = Number(withdraw || 0)
  const overBalance = withdrawValue > balance
  const shebaBank = getBankBySheba(sheba)
  const canWithdraw = withdrawValue >= MIN_AMOUNT && !overBalance && sheba.length === 24 && Boolean(shebaBank)

  function record(title: string, amount: number) {
    setBalance((value) => value + amount)
    setHistory((list) => [{ id: Date.now(), title, date: new Date().toISOString(), amount }, ...list])
  }

  return (
    <ControlSizeProvider size="xl" asChild>
    <main className="mx-auto flex w-full max-w-2xl flex-col gap-4 p-4 sm:p-6">
      <h1 className="sr-only">کیف پول</h1>
      <StatTile>
        <StatTileHeader>
          <StatTileIcon tone="accent">
            <WalletIcon />
          </StatTileIcon>
          <StatTileLabel>موجودی کیف پول</StatTileLabel>
        </StatTileHeader>
        <StatTileValue className="text-4xl">
          {formatNumber(balance)}
          <StatTileUnit>تومان</StatTileUnit>
        </StatTileValue>
        <div className="flex gap-2">
          <Button onClick={() => setDialog("charge")}>افزایش موجودی</Button>
          <Button variant="outline" onClick={() => setDialog("withdraw")} disabled={balance < MIN_AMOUNT}>
            برداشت به حساب
          </Button>
        </div>
      </StatTile>

      <Card>
        <CardHeader>
          <CardTitle>تراکنش‌های اخیر</CardTitle>
        </CardHeader>
        <CardContent className="flex flex-col divide-y divide-border">
          {history.map((item) => (
            <div key={item.id} className="flex items-center justify-between gap-3 py-3">
              <div className="flex min-w-0 flex-col gap-1">
                <span className="truncate text-sm font-medium">{item.title}</span>
                <span className="text-xs text-muted-foreground">{formatJalaliDateTime(item.date)}</span>
              </div>
              <Badge variant="soft" color={item.amount > 0 ? "success" : "default"} className="shrink-0">
                <SignedAmount value={item.amount} />
              </Badge>
            </div>
          ))}
        </CardContent>
      </Card>

      <Dialog open={dialog === "charge"} onOpenChange={(open) => !open && setDialog(null)}>
        <ControlSizeProvider size="xl" asChild>
          <DialogContent>
            <DialogHeader>
              <DialogTitle>افزایش موجودی</DialogTitle>
              <DialogDescription>مبلغ از درگاه بانکی پرداخت می‌شود.</DialogDescription>
            </DialogHeader>
            <ToggleGroup
              type="single"
              variant="segmented"
              fullWidth
              value={charge}
              onValueChange={(value) => value && setCharge(value as string)}
              aria-label="مبلغ پیشنهادی"
            >
              {PRESETS.map((value) => (
                <ToggleGroupItem key={value} value={String(value)}>
                  {formatNumber(value)}
                </ToggleGroupItem>
              ))}
            </ToggleGroup>
            <TextField
              label="مبلغ"
              inputMode="numeric" dir="rtl"
              groupDigits
              endContent="تومان"
              value={charge}
              onValueChange={setCharge}
            />
            <DialogFooter>
              <Button
                fullWidth
                disabled={Number(charge) < MIN_AMOUNT}
                onClick={() => {
                  record("افزایش موجودی", Number(charge))
                  setDialog(null)
                  toast.success("موجودی افزایش یافت.", {
                    description: `${formatCurrency(Number(charge))} به کیف پول اضافه شد.`,
                  })
                }}
              >
                پرداخت {formatCurrency(Number(charge || 0))}
              </Button>
            </DialogFooter>
          </DialogContent>
        </ControlSizeProvider>
      </Dialog>

      <Dialog open={dialog === "withdraw"} onOpenChange={(open) => !open && setDialog(null)}>
        <ControlSizeProvider size="xl" asChild>
          <DialogContent>
            <DialogHeader>
              <DialogTitle>برداشت به حساب</DialogTitle>
              <DialogDescription>مبلغ تا پایان روز کاری بعد به حساب شما واریز می‌شود.</DialogDescription>
            </DialogHeader>
            <TextField
              label="مبلغ"
              inputMode="numeric" dir="rtl"
              groupDigits
              endContent="تومان"
              value={withdraw}
              onValueChange={setWithdraw}
              invalid={overBalance}
              errorMessage="مبلغ از موجودی کیف پول بیشتر است."
              description={`موجودی: ${formatCurrency(balance)}`}
            />
            <TextField
              label="شماره شبا"
              inputMode="numeric"
              dir="ltr"
              startContent="IR"
              digitGroups={[2, 4, 4, 4, 4, 4, 2]}
              value={sheba}
              onValueChange={setSheba}
              endContent={<BankLogo sheba={sheba} size="sm" showName />}
              invalid={sheba.length >= 5 && !shebaBank}
              errorMessage="کد بانک در این شبا شناخته نشد."
            />
            <DialogFooter>
              <Button
                fullWidth
                disabled={!canWithdraw}
                onClick={() => {
                  record(`برداشت به ${shebaBank?.name ?? "حساب"}`, -withdrawValue)
                  setDialog(null)
                  setWithdraw("")
                  setSheba("")
                  toast.success("درخواست برداشت ثبت شد.", { description: "تا پایان روز کاری بعد واریز می‌شود." })
                }}
              >
                برداشت {formatCurrency(withdrawValue)}
              </Button>
            </DialogFooter>
          </DialogContent>
        </ControlSizeProvider>
      </Dialog>
    </main>
    </ControlSizeProvider>
  )
}