تختهٔ ویجتها
پیشنمایش تمامصفحهداشبوردِ ویجتی با یک رنگِ تأکید: پروفایل با مدارِ همکاران، بازدید، تبدیلِ ارز، تیم، ستونهای درآمد، دو کاشیِ اقدام، نمودارِ موجودی، شبکهٔ نقطهایِ تراکنشها و فروش.
۱. نصب کامپوننتها
این بلاک از ۸ آیتم دیگ استفاده میکند: avatarbadgebuttonchart-presetscontrol-sizeiconsinputpersian
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/chart-presets.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/persian.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/chart-presets.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/persian.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/chart-presets.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/persian.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/chart-presets.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/persian.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
dashboard-widgets.tsx
"use client"
import * as React from "react"
import { ArrowUpIcon, ChevronsUpDownIcon, FileTextIcon, MoreHorizontalIcon, PlusIcon, SparklesIcon } from "@/components/icons"
import { Avatar, AvatarGroup } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { LineChart } from "@/components/ui/chart-presets"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Input } from "@/components/ui/input"
import { formatNumber, toEnDigits, toFaDigits } from "@/lib/persian"
import { cn } from "@/lib/utils"
import { stockImages } from "@/lib/stock-images"
const ORANGE = "var(--dig-orange-500)"
const VISITS = [32, 48, 40, 70, 54, 92, 66]
const DAYS = ["ش", "ی", "د", "س", "چ", "پ", "ج"]
const REVENUE = Array.from({ length: 36 }, (_, index) => 30 + Math.round(Math.abs(Math.sin(index * 1.7)) * 60 + (index % 5) * 4))
const BALANCE = [
{ m: "فروردین", v: 18 },
{ m: "اردیبهشت", v: 24 },
{ m: "خرداد", v: 21 },
{ m: "تیر", v: 33 },
{ m: "مرداد", v: 29 },
{ m: "شهریور", v: 41 },
{ m: "مهر", v: 46 },
]
const TEAM = [stockImages.personWoman1.src, stockImages.personMan2.src, stockImages.personWoman3.src, stockImages.personMan1.src]
/**
* تختهٔ ویجتها: گریدِ کاشیهای هماندازه روی سطحِ subtle با یک رنگِ تأکیدِ نارنجی؛ پروفایل با
* مدارِ همکاران، ستونهای بازدید با روزِ برتر، تبدیلِ ارز، تیم، ستونهای درآمد، دو کاشیِ اقدامِ
* پررنگ، نمودارِ موجودی، شبکهٔ نقطهایِ تراکنشها و آمارِ فروش. هر کاشی تیتر و یک عدد دارد.
*/
export default function DashboardWidgetsBlock() {
const [amount, setAmount] = React.useState("۲۵۰۰")
return (
<ControlSizeProvider size="xl" asChild>
<main className="w-full bg-subtle p-4 sm:p-6">
<div className="mx-auto grid w-full max-w-6xl grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Tile title="پروفایلِ من">
<div aria-hidden className="relative mx-auto flex size-40 items-center justify-center">
<span className="absolute inset-0 rounded-full border border-dashed border-border" />
<span className="absolute inset-6 rounded-full border border-border" />
<Avatar src={stockImages.personWoman2.src} alt="" size="lg" className="size-14 ring-4 ring-[var(--dig-orange-200)]" />
{TEAM.slice(0, 3).map((src, index) => (
<span key={src} className="absolute" style={{ transform: `rotate(${index * 120 + 30}deg) translateX(80px) rotate(${-(index * 120 + 30)}deg)` }}>
<Avatar src={src} alt="" size="sm" />
</span>
))}
</div>
<p className="text-center text-sm text-muted-foreground">{toFaDigits(12)} همکارِ فعال در این هفته</p>
</Tile>
<Tile title="بازدیدها" action={<span className="text-sm text-muted-foreground">هفته</span>}>
<div className="flex h-36 items-end justify-between gap-2">
{VISITS.map((value, index) => {
const top = value === Math.max(...VISITS)
return (
<div key={DAYS[index]} className="flex flex-1 flex-col items-center gap-2">
{top && <span className="rounded-full bg-[var(--dig-orange-500)] px-2 py-0.5 text-xs font-bold text-[var(--dig-white)] tabular-nums">{toFaDigits(value)}</span>}
<span aria-hidden className={cn("w-1.5 rounded-full", top ? "bg-[var(--dig-orange-500)]" : "bg-foreground")} style={{ height: `${value}%` }} />
<span className="text-xs text-muted-foreground">{DAYS[index]}</span>
</div>
)
})}
</div>
</Tile>
<Tile title="تبدیلِ سریع">
<div className="flex flex-col gap-2">
<Input aria-label="مبلغِ دلار" dir="ltr" faDigits groupDigits value={amount} onValueChange={setAmount} endContent={<span className="text-sm font-semibold text-muted-foreground">USD</span>} />
<div className="flex justify-center">
<span aria-hidden className="flex size-8 items-center justify-center rounded-full bg-muted">
<ChevronsUpDownIcon className="size-4" />
</span>
</div>
<Input aria-label="معادلِ تومان" dir="ltr" readOnly value={formatNumber(Number(toEnDigits(amount).replace(/\D/g, "") || 0) * 82_400)} endContent={<span className="text-sm font-semibold text-muted-foreground">IRT</span>} />
</div>
<div className="grid grid-cols-2 gap-2">
<Button variant="outline">لغو</Button>
<Button className="[--primary-foreground:var(--dig-white)] [--primary:var(--dig-orange-600)]">تبدیل</Button>
</div>
</Tile>
<Tile title="تیمِ من" action={<Badge color="orange" variant="soft">{toFaDigits(4)} نفر</Badge>}>
<ul className="flex flex-col gap-3">
{[
["نگار رحیمی", "طراح"],
["آرش کریمی", "فروش"],
["سارا نیکپور", "محصول"],
].map(([name, role], index) => (
<li key={name} className="flex items-center gap-3">
<Avatar src={TEAM[index]} alt="" size="sm" />
<span className="flex flex-1 flex-col">
<span className="text-sm font-semibold">{name}</span>
<span className="text-xs text-muted-foreground">{role}</span>
</span>
</li>
))}
</ul>
<Button fullWidth className="[--primary-foreground:var(--dig-white)] [--primary:var(--dig-orange-600)]">
<PlusIcon />
دعوتِ همکار
</Button>
</Tile>
<Tile title="درآمد" className="lg:col-span-2" action={<Button variant="ghost" size="icon" aria-label="گزینههای درآمد"><MoreHorizontalIcon /></Button>}>
<div className="flex items-baseline gap-3">
<span className="text-3xl font-black tabular-nums">۲۰٫۱۸ میلیارد</span>
<bdi dir="ltr" className="text-sm font-semibold text-success">+٪۱۲</bdi>
</div>
<div aria-hidden className="flex h-28 items-end gap-1">
{REVENUE.map((value, index) => (
<span key={index} className={cn("flex-1 rounded-sm", index % 7 === 3 ? "bg-[var(--dig-orange-500)]" : "bg-[var(--dig-orange-200)] dark:bg-[var(--dig-orange-900)]")} style={{ height: `${value}%` }} />
))}
</div>
</Tile>
<div className="grid grid-cols-2 gap-4 sm:col-span-2 lg:col-span-1 lg:grid-cols-1">
<a href="#" className="group flex min-h-32 flex-col justify-between rounded-xl bg-[var(--dig-orange-500)] p-5 text-[var(--dig-white)] outline-none transition-colors hover:bg-[var(--dig-orange-600)] focus-visible:ring-4 focus-visible:ring-[var(--dig-orange-200)]">
<span aria-hidden className="flex size-10 items-center justify-center rounded-full bg-[var(--dig-orange-400)]">
<SparklesIcon className="size-5" />
</span>
<span className="font-bold">کمپینِ تازه</span>
</a>
<a href="#" className="group flex min-h-32 flex-col justify-between rounded-xl bg-[var(--dig-graphite-950)] p-5 text-[var(--dig-white)] outline-none transition-colors hover:bg-[var(--dig-graphite-800)] focus-visible:ring-4 focus-visible:ring-[var(--dig-orange-200)]">
<span aria-hidden className="flex size-10 items-center justify-center rounded-full bg-[var(--dig-graphite-800)]">
<FileTextIcon className="size-5" />
</span>
<span className="font-bold">گزارشِ ماه</span>
</a>
</div>
<Tile title="موجودیِ من">
<span className="text-3xl font-black tabular-nums">۴۶٫۲ میلیون</span>
<LineChart className="h-28" data={BALANCE} categoryKey="m" series={[{ key: "v", label: "موجودی", color: ORANGE }]} curve="smooth" showPoints={false} hideValueAxis showGrid={false} />
</Tile>
<Tile title="تراکنشها" className="lg:col-span-2" action={<span className="text-sm text-muted-foreground">۳۰ روزِ اخیر</span>}>
<div className="flex flex-wrap items-baseline gap-x-8 gap-y-2">
{[
["موفق", "۱٬۲۴۰", "bg-[var(--dig-orange-500)]"],
["در انتظار", "۸۶", "bg-[var(--dig-orange-300)]"],
["ناموفق", "۱۲", "bg-foreground"],
].map(([label, value, dot]) => (
<span key={label} className="flex items-center gap-2">
<span aria-hidden className={cn("size-2.5 rounded-full", dot)} />
<span className="text-sm text-muted-foreground">{label}</span>
<span className="font-bold tabular-nums">{value}</span>
</span>
))}
</div>
<div aria-hidden className="grid grid-cols-[repeat(30,minmax(0,1fr))] gap-1">
{Array.from({ length: 120 }, (_, index) => {
const level = (index * 7 + Math.floor(index / 30) * 3) % 10
return <span key={index} className={cn("aspect-square rounded-full", level > 7 ? "bg-[var(--dig-orange-500)]" : level > 4 ? "bg-[var(--dig-orange-300)]" : level > 0 ? "bg-[var(--dig-orange-100)] dark:bg-[var(--dig-orange-950)]" : "bg-foreground")} />
})}
</div>
</Tile>
<Tile title="فروشِ ماه" className="sm:col-span-2" action={<AvatarGroup max={3} total={9} size="sm">{TEAM.map((src) => <Avatar key={src} src={src} alt="" />)}</AvatarGroup>}>
<dl className="grid grid-cols-3 gap-4">
{[
["سفارش", "۸۴۲"],
["میانگینِ سبد", "۲٫۴ م"],
["بازگشتی", "٪۳"],
].map(([label, value]) => (
<div key={label} className="flex flex-col gap-1 rounded-lg bg-muted p-3">
<dt className="text-xs text-muted-foreground">{label}</dt>
<dd className="text-xl font-black tabular-nums">{value}</dd>
</div>
))}
</dl>
<a href="#" className="flex items-center gap-1 self-start text-sm font-semibold text-[var(--dig-orange-600)] hover:underline dark:text-[var(--dig-orange-400)]">
گزارشِ کامل
<ArrowUpIcon aria-hidden className="size-4 -rotate-45" />
</a>
</Tile>
</div>
</main>
</ControlSizeProvider>
)
}
function Tile({ title, action, className, children }: { title: string; action?: React.ReactNode; className?: string; children: React.ReactNode }) {
return (
<section aria-label={title} className={cn("flex flex-col gap-4 rounded-xl bg-card p-5 shadow-sm", className)}>
<header className="flex min-h-8 items-center justify-between gap-2">
<h2 className="font-bold">{title}</h2>
{action}
</header>
{children}
</section>
)
}