تحلیلِ هزینهها
پیشنمایش تمامصفحههزینهٔ ماه با مقایسه، نوارِ سهمِ دستهها و فهرستِ دستهها با بودجه؛ عبور از بودجه با متنِ صریح و رنگ، نه فقط رنگ.
۱. نصب کامپوننتها
این بلاک از ۶ آیتم دیگ استفاده میکند: buttoncontrol-sizeiconspersianprogresstoggle-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/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.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/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.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/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.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/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/toggle-group.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
transactions-insights.tsx
"use client"
import * as React from "react"
import { ReceiptIcon, ShopIcon, TruckIcon, WalletIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Progress } from "@/components/ui/progress"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { formatCurrency, toFaDigits } from "@/lib/persian"
const CATEGORIES = [
{ key: "food", label: "خوراک و کافه", icon: <ShopIcon />, spent: 4_850_000, budget: 5_000_000, count: 23 },
{ key: "transport", label: "رفتوآمد", icon: <TruckIcon />, spent: 1_920_000, budget: 1_500_000, count: 31 },
{ key: "bills", label: "قبض و اشتراک", icon: <ReceiptIcon />, spent: 1_240_000, budget: 2_000_000, count: 6 },
{ key: "other", label: "سایر", icon: <WalletIcon />, spent: 2_310_000, budget: 3_000_000, count: 12 },
]
/**
* تحلیلِ هزینهها: جمعِ ماه با مقایسه با ماهِ قبل، نوارِ سهمِ هر دسته در یک خط، و فهرستِ
* دستهها با بودجه؛ دستهای که از بودجه رد شده با رنگ و متنِ صریح («۴۲۰ هزار بیشتر») نشان
* داده میشود، نه فقط رنگ. لمسِ هر دسته فهرستِ تراکنشهایش را باز میکند.
*/
export default function TransactionsInsightsBlock() {
const [month, setMonth] = React.useState("this")
const total = CATEGORIES.reduce((sum, item) => sum + item.spent, 0)
const colors = ["var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)"]
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-3xl 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">کجا خرج کردی و چقدر تا سقفِ هر دسته مانده.</p>
</div>
<ToggleGroup type="single" variant="segmented" value={month} onValueChange={(value) => value && setMonth(value as string)} aria-label="ماه">
<ToggleGroupItem value="last">شهریور</ToggleGroupItem>
<ToggleGroupItem value="this">مهر</ToggleGroupItem>
</ToggleGroup>
</header>
<section aria-label="جمعِ هزینه" className="flex flex-col gap-5 rounded-xl bg-subtle p-6">
<div className="flex flex-wrap items-end justify-between gap-3">
<div className="flex flex-col gap-1">
<span className="text-sm text-muted-foreground">جمعِ هزینهٔ {month === "this" ? "مهر" : "شهریور"}</span>
<span className="text-3xl font-black tabular-nums">{formatCurrency(total)}</span>
</div>
<span className="text-sm text-muted-foreground">٪{toFaDigits(8)} کمتر از ماهِ قبل</span>
</div>
<div className="flex h-3 gap-0.5 overflow-hidden rounded-full" role="img" aria-label={CATEGORIES.map((item) => `${item.label} ٪${toFaDigits(Math.round((item.spent / total) * 100))}`).join("، ")}>
{CATEGORIES.map((item, index) => (
<span key={item.key} style={{ width: `${(item.spent / total) * 100}%`, background: colors[index] }} />
))}
</div>
</section>
<ul className="flex flex-col gap-3">
{CATEGORIES.map((item, index) => {
const over = item.spent - item.budget
// نزدیکِ سقف (۹۰٪ به بالا) پیش از عبور هشدار میگیرد تا کاربر فرصتِ ترمز داشته باشد.
const near = over <= 0 && item.spent / item.budget >= 0.9
return (
<li key={item.key}>
<Button variant="outline" className="h-auto w-full flex-col items-stretch gap-3 p-4 text-start font-normal">
<span className="flex items-center gap-3">
<span aria-hidden className="flex size-10 items-center justify-center rounded-lg [&>svg]:size-5" style={{ background: `color-mix(in oklab, ${colors[index]} 15%, var(--card))`, color: colors[index] }}>
{item.icon}
</span>
<span className="flex flex-1 flex-col">
<span className="font-semibold">{item.label}</span>
<span className="text-xs text-muted-foreground">{toFaDigits(item.count)} تراکنش</span>
</span>
<span className="flex flex-col items-end">
<span className="font-bold tabular-nums">{formatCurrency(item.spent)}</span>
<span className={`text-xs ${over > 0 ? "font-medium text-destructive" : near ? "font-medium text-warning" : "text-muted-foreground"}`}>
{over > 0 ? `${formatCurrency(over)} بیشتر از بودجه` : near ? `نزدیکِ سقف؛ ${formatCurrency(-over)} مانده` : `${formatCurrency(-over)} مانده`}
</span>
</span>
</span>
<Progress value={Math.min(100, (item.spent / item.budget) * 100)} aria-label={`مصرفِ بودجهٔ ${item.label}`} color={over > 0 ? "destructive" : near ? "warning" : undefined}
// رنگِ عادیِ هر دسته همان رنگِ بخشش در نوارِ بالاست تا راهنما و ردیف یکی خوانده شوند.
style={over > 0 || near ? undefined : ({ "--progress": colors[index] } as React.CSSProperties)}
/>
</Button>
</li>
)
})}
</ul>
</main>
</ControlSizeProvider>
)
}