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

تختهٔ ویجت‌ها

پیش‌نمایش تمام‌صفحه

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

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

این بلاک از ۸ آیتم دیگ استفاده می‌کند: 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.json
pnpm 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.json
yarn 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.json
bunx --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>
  )
}