قلکها
پیشنمایش تمامصفحههدفهای پسانداز: جمعِ پسانداز، کارتِ هر هدف با عکس، مبلغ از هدف و نوارِ پیشرفت، تشویقِ نزدیک به تکمیل و واریز با مبلغهای آماده.
۱. نصب کامپوننتها
این بلاک از ۹ آیتم دیگ استفاده میکند: 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.jsonpnpm 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.jsonyarn 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.jsonbunx --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>
)
}