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

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

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

این بلاک از ۹ آیتم دیگ استفاده می‌کند: buttoncontrol-sizedialogiconspersianprogresstext-fieldtoasttoggle-group

پکیج‌منیجر پیش‌فرض 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/dialog.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.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/button.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/progress.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/button.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/progress.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/button.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/progress.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-savings.tsx
"use client"

import * as React from "react"

import { PlusIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Progress } from "@/components/ui/progress"
import { TextField } from "@/components/ui/text-field"
import { toast } from "@/components/ui/toast"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { formatCurrency } from "@/lib/persian"
import { stockImages, type StockImageName } from "@/lib/stock-images"

type Goal = { id: string; title: string; image: StockImageName; saved: number; target: number; due: string }
const GOALS: Goal[] = [
  { id: "1", title: "سفرِ نوروز", image: "planeWindow", saved: 18_400_000, target: 30_000_000, due: "۵ ماهِ دیگر" },
  { id: "2", title: "لپ‌تاپِ کار", image: "coverWorkspace", saved: 41_000_000, target: 45_000_000, due: "۱ ماهِ دیگر" },
  { id: "3", title: "صندوقِ اضطراری", image: "coverFinanceDesk", saved: 12_000_000, target: 60_000_000, due: "بدونِ موعد" },
]
const QUICK = [
  { value: 500_000, label: "۵۰۰ هزار" },
  { value: 1_000_000, label: "۱ میلیون" },
  { value: 5_000_000, label: "۵ میلیون" },
]

/**
 * قلک‌ها (هدف‌های پس‌انداز): جمعِ پس‌انداز، کارتِ هر هدف با عکس، مبلغِ جمع‌شده از هدف و
 * نوارِ پیشرفت، و «واریز» با مبلغ‌های آماده در دیالوگ؛ هدفِ نزدیک به تکمیل تشویق می‌شود.
 */
export default function WalletSavingsBlock() {
  const [goals, setGoals] = React.useState(GOALS)
  const [open, setOpen] = React.useState<Goal | null>(null)
  const [amount, setAmount] = React.useState("1000000")
  const total = goals.reduce((sum, goal) => sum + goal.saved, 0)

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto flex w-full max-w-4xl flex-col gap-8 p-4 sm:p-6">
        <header className="flex flex-wrap items-end justify-between gap-x-4 gap-y-6">
          <div className="flex flex-col gap-1">
            <h1 className="text-2xl font-bold">قلک‌های من</h1>
            <p className="text-muted-foreground">
              جمعِ پس‌انداز: <span className="font-bold text-foreground tabular-nums">{formatCurrency(total)}</span>
            </p>
          </div>
          <Button>
            <PlusIcon />
            قلکِ تازه
          </Button>
        </header>
        <ul className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
          {goals.map((goal) => {
            const ratio = goal.saved / goal.target
            return (
              <li key={goal.id} className="flex flex-col overflow-hidden rounded-xl border border-border bg-card">
                {/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
                <img src={stockImages[goal.image].src} alt="" className="aspect-[16/9] w-full object-cover" />
                <div className="flex flex-1 flex-col gap-4 p-4">
                  <div className="flex items-start justify-between gap-2">
                    <span className="font-bold">{goal.title}</span>
                    <span className="text-xs text-muted-foreground">{goal.due}</span>
                  </div>
                  <div className="flex flex-col gap-2">
                    <span className="text-sm">
                      <span className="font-bold tabular-nums">{formatCurrency(goal.saved)}</span> <span className="text-muted-foreground">از {formatCurrency(goal.target)}</span>
                    </span>
                    <Progress value={ratio * 100} aria-label={`پیشرفتِ ${goal.title}`} color={ratio >= 0.9 ? "success" : "primary"} />
                    {ratio >= 0.9 && <span className="text-xs font-medium text-success">فقط {formatCurrency(goal.target - goal.saved)} مانده!</span>}
                  </div>
                  <Button variant="outline" className="mt-auto" onClick={() => setOpen(goal)}>
                    واریز
                  </Button>
                </div>
              </li>
            )
          })}
        </ul>

        <Dialog open={open !== null} onOpenChange={(value) => !value && setOpen(null)}>
          <ControlSizeProvider size="xl" asChild>
            <DialogContent>
              <DialogHeader>
                <DialogTitle>واریز به «{open?.title}»</DialogTitle>
                <DialogDescription>از موجودی کیف پول برداشته می‌شود.</DialogDescription>
              </DialogHeader>
              <div className="flex flex-col gap-4">
                <TextField label="مبلغ" inputMode="numeric" dir="rtl" groupDigits endContent="تومان" value={amount} onValueChange={setAmount} />
                <ToggleGroup type="single" variant="segmented" value={amount} onValueChange={(value) => value && setAmount(value as string)} aria-label="مبلغ‌های آماده" className="w-full">
                  {QUICK.map((item) => (
                    <ToggleGroupItem key={item.value} value={String(item.value)} className="flex-1">
                      {item.label}
                    </ToggleGroupItem>
                  ))}
                </ToggleGroup>
              </div>
              <DialogFooter>
                <Button variant="outline" onClick={() => setOpen(null)}>
                  انصراف
                </Button>
                <Button
                  disabled={!Number(amount)}
                  onClick={() => {
                    setGoals((list) => list.map((goal) => (goal.id === open?.id ? { ...goal, saved: Math.min(goal.target, goal.saved + Number(amount)) } : goal)))
                    toast.success(`${formatCurrency(Number(amount))} به قلک رفت.`)
                    setOpen(null)
                  }}
                >
                  واریز
                </Button>
              </DialogFooter>
            </DialogContent>
          </ControlSizeProvider>
        </Dialog>
      </main>
    </ControlSizeProvider>
  )
}