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

داشبوردِ پرداده

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

نگاهِ کلی با سه کارتِ شاخص (بازهٔ جدا برای هر کارت، رشدِ رنگی)، نمودارِ درآمدِ ماهانه با جمعِ سال، امتیازها در برابرِ میانگین، و جدولِ درخواست‌ها با فیلترِ وضعیت، جست‌وجو و اقدامِ هر ردیف.

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

این بلاک از ۱۲ آیتم دیگ استفاده می‌کند: avatarbadgebuttoncardchart-presetscontrol-sizeiconspersiansearch-fieldselecttabletoggle-group

پکیج‌منیجر پیش‌فرض 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/card.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/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.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/card.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/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.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/card.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/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.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/card.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/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.json

۲. کد صفحه

کد را در app/<route>/page.tsx بگذارید و متن‌ها، داده و هندلرها را با محصول خودتان عوض کنید.

dashboard-dense.tsx
"use client"

import * as React from "react"

import { ClockIcon, StarIcon, UsersIcon, WalletIcon } from "@/components/icons"
import { Avatar } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { BarChart, LineChart } from "@/components/ui/chart-presets"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { SearchField } from "@/components/ui/search-field"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { formatCurrency, toFaDigits, toSearchKey } from "@/lib/persian"
import { avatarPhoto } from "@/lib/stock-images"

const KPIS = [
  { icon: <ClockIcon />, label: "ساعتِ کلاس", value: 158, unit: "ساعت", delta: 5.27, tone: "blue" },
  { icon: <UsersIcon />, label: "تعدادِ کلاس", value: 95, unit: "کلاس", delta: 2.27, tone: "amber" },
  { icon: <WalletIcon />, label: "زبان‌آموزان", value: 350, unit: "نفر", delta: -1.34, tone: "green" },
] as const
const TONE = {
  blue: "bg-[color-mix(in_oklab,var(--dig-blue-500)_14%,var(--card))] text-[var(--dig-blue-600)] dark:text-[var(--dig-blue-300)]",
  amber: "bg-[color-mix(in_oklab,var(--dig-amber-500)_16%,var(--card))] text-[var(--dig-amber-700)] dark:text-[var(--dig-amber-300)]",
  green: "bg-[color-mix(in_oklab,var(--dig-green-500)_14%,var(--card))] text-[var(--dig-green-700)] dark:text-[var(--dig-green-300)]",
}
const INCOME = [
  { label: "فروردین", value: 3 },
  { label: "اردیبهشت", value: 4 },
  { label: "خرداد", value: 3 },
  { label: "تیر", value: 4.6 },
  { label: "مرداد", value: 4 },
  { label: "شهریور", value: 5.8 },
  { label: "مهر", value: 3.1 },
  { label: "آبان", value: 4.7 },
]
const SCORES = [
  { label: "مفید بودن", value: 4.8 },
  { label: "مکالمه", value: 4.4 },
  { label: "مسئولیت", value: 3.2 },
  { label: "تلفظ", value: 4.5 },
]
type Status = "waiting" | "canceled" | "done"
const STATUS: Record<Status, { label: string; color: "warning" | "destructive" | "success" }> = {
  waiting: { label: "در انتظارِ شما", color: "warning" },
  canceled: { label: "لغو شده", color: "destructive" },
  done: { label: "تمام شده", color: "success" },
}
const REQUESTS: { id: string; name: string; gender: "male" | "female"; city: string; level: string; score: number; time: string; status: Status }[] = [
  { id: "1", name: "نگار حسینی", gender: "female", city: "تهران", level: "B1", score: 3.5, time: "۵ دقیقه پیش", status: "waiting" },
  { id: "2", name: "امیر رضایی", gender: "male", city: "شیراز", level: "A2", score: 4.1, time: "۲۰ دقیقه پیش", status: "waiting" },
  { id: "3", name: "سارا محمدی", gender: "female", city: "اصفهان", level: "C1", score: 4.8, time: "دیروز", status: "done" },
  { id: "4", name: "علی کاظمی", gender: "male", city: "تبریز", level: "B2", score: 3.9, time: "۲ روز پیش", status: "canceled" },
]

/** انتخابِ بازهٔ هر کارت؛ همان ظاهرِ کنترل‌های دیگ در اندازهٔ کوچک‌ترِ کارت. */
function PeriodSelect({ label }: { label: string }) {
  return (
    <ControlSizeProvider size="lg" asChild>
      <Select defaultValue="month">
        <SelectTrigger aria-label={`بازهٔ ${label}`} className="w-auto gap-2 text-sm">
          <SelectValue />
        </SelectTrigger>
        <ControlSizeProvider size="xl" asChild>
          <SelectContent>
            <SelectItem value="week">هفتهٔ جاری</SelectItem>
            <SelectItem value="month">ماهِ جاری</SelectItem>
            <SelectItem value="year">سالِ جاری</SelectItem>
          </SelectContent>
        </ControlSizeProvider>
      </Select>
    </ControlSizeProvider>
  )
}

/**
 * داشبوردِ پرداده (پنلِ مدرس): «نگاهِ کلی» با سه کارتِ شاخص (عدد و واحد، آیکونِ رنگی، رشد نسبت به
 * ماهِ قبل با علامت و رنگ، انتخابِ بازه برای هر کارت)، نمودارِ درآمدِ ماهانه با جمعِ سال، امتیازها در
 * برابرِ میانگینِ هدف، و جدولِ درخواست‌ها با فیلترِ وضعیت، جست‌وجو و اقدامِ هر ردیف. جدول در موبایل
 * اسکرولِ افقیِ خودش را دارد و ستونِ نام چسبان است.
 */
export default function DashboardDenseBlock() {
  const [filter, setFilter] = React.useState<"all" | Status>("all")
  const [query, setQuery] = React.useState("")
  const rows = REQUESTS.filter((row) => (filter === "all" || row.status === filter) && toSearchKey(row.name).includes(toSearchKey(query)))

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto flex w-full max-w-6xl flex-col gap-8 p-4 sm:p-6">
        <header className="flex flex-col gap-1">
          <h1 className="text-2xl leading-snug font-black">نگاهِ کلی</h1>
          <p className="text-muted-foreground">آمار و ارقامِ کلاس‌های تو در یک صفحه.</p>
        </header>

        <section aria-label="شاخص‌ها" className="grid grid-cols-1 gap-4 md:grid-cols-3">
          {KPIS.map((kpi) => (
            <Card key={kpi.label} className="gap-4 py-5">
              <CardHeader className="px-5">
                <CardTitle className="text-base">{kpi.label}</CardTitle>
                <CardAction>
                  <PeriodSelect label={kpi.label} />
                </CardAction>
              </CardHeader>
              <CardContent className="flex items-center justify-between gap-4 px-5">
                <span className="flex flex-col gap-2">
                  <span className="flex items-baseline gap-1.5">
                    <span className="text-3xl font-black tabular-nums">{toFaDigits(kpi.value)}</span>
                    <span className="text-sm text-muted-foreground">{kpi.unit}</span>
                  </span>
                  <span className="text-xs text-muted-foreground">
                    <bdi dir="ltr" className={`font-bold ${kpi.delta > 0 ? "text-success" : "text-destructive"}`}>
                      {kpi.delta > 0 ? "+" : "−"}٪{toFaDigits(String(Math.abs(kpi.delta)).replace(".", "٫"))}
                    </bdi>{" "}
                    نسبت به ماهِ قبل
                  </span>
                </span>
                <span aria-hidden className={`flex size-14 shrink-0 items-center justify-center rounded-full [&>svg]:size-6 ${TONE[kpi.tone]}`}>
                  {kpi.icon}
                </span>
              </CardContent>
            </Card>
          ))}
        </section>

        <div className="grid grid-cols-1 items-start gap-4 lg:grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)]">
          <Card>
            <CardHeader>
              <CardTitle className="text-base">درآمد (میلیون تومان)</CardTitle>
              <CardAction>
                <PeriodSelect label="درآمد" />
              </CardAction>
            </CardHeader>
            <CardContent className="flex flex-col gap-3">
              <span className="text-sm text-muted-foreground">
                دریافتیِ امسال: <span className="font-bold text-foreground tabular-nums">{formatCurrency(33_521_000)}</span>
              </span>
              <LineChart className="h-60" data={INCOME} categoryKey="label" series={[{ key: "value", label: "درآمد", color: "var(--dig-violet-600)" }]} valueFormatter={(value) => `${toFaDigits(String(value).replace(".", "٫"))} میلیون`} />
            </CardContent>
          </Card>
          <Card>
            <CardHeader>
              <CardTitle className="text-base">امتیازِ تو</CardTitle>
              <CardAction>
                <span className="flex items-center gap-1 text-sm text-muted-foreground [&>svg]:size-4 [&>svg]:text-[var(--dig-amber-500)]">
                  <StarIcon aria-hidden />
                  {toFaDigits(269)} امتیاز
                </span>
              </CardAction>
            </CardHeader>
            <CardContent className="flex flex-col gap-3">
              <span className="text-sm text-muted-foreground">
                میانگین: <span className="font-bold text-foreground">۴٫۵ از ۵</span>
              </span>
              <BarChart className="h-60" data={SCORES} categoryKey="label" series={[{ key: "value", label: "امتیاز", color: "var(--dig-violet-400)" }]} valueFormatter={(value) => toFaDigits(String(value).replace(".", "٫"))} />
            </CardContent>
          </Card>
        </div>

        <section aria-labelledby="requests-title" className="flex flex-col gap-4">
          <div className="flex flex-col gap-1">
            <h2 id="requests-title" className="text-lg font-bold">
              درخواست‌های کلاس
            </h2>
            <p className="text-sm text-muted-foreground">درخواست‌های فعالِ کلاس‌های تو.</p>
          </div>
          <div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-3">
            <div className="max-w-full overflow-x-auto [scrollbar-width:none]">
              <ToggleGroup type="single" variant="segmented" value={filter} onValueChange={(value) => value && setFilter(value as typeof filter)} aria-label="فیلترِ وضعیت" className="w-max">
                <ToggleGroupItem value="all">همه</ToggleGroupItem>
                <ToggleGroupItem value="waiting">در انتظار</ToggleGroupItem>
                <ToggleGroupItem value="canceled">لغو شده</ToggleGroupItem>
                <ToggleGroupItem value="done">تمام شده</ToggleGroupItem>
              </ToggleGroup>
            </div>
            <SearchField aria-label="جست‌وجو در نام‌ها" placeholder="جست‌وجو در نام‌ها" value={query} onValueChange={setQuery} className="w-full sm:w-64" />
          </div>
          {/* موبایل: هر درخواست یک کارت با وضعیت و اقدام، بی‌اسکرولِ افقی. */}
          <ul className="flex flex-col divide-y divide-border rounded-xl border border-border bg-card md:hidden">
            {rows.length === 0 ? (
              <li className="p-8 text-center text-muted-foreground">درخواستی پیدا نشد؛ فیلتر را روی «همه» بگذار.</li>
            ) : (
              rows.map((row) => (
                <li key={row.id} className="flex flex-col gap-3 p-4">
                  <div className="flex items-center gap-3">
                    <Avatar alt={row.name} src={avatarPhoto(row.name, row.gender)} className="size-10" />
                    <span className="flex min-w-0 flex-1 flex-col gap-0.5">
                      <span className="font-bold">{row.name}</span>
                      <span className="text-sm text-muted-foreground">
                        {row.city} · سطحِ {row.level} · نمره {toFaDigits(String(row.score).replace(".", "٫"))}
                      </span>
                    </span>
                    <Badge variant="soft" color={STATUS[row.status].color} className="shrink-0">
                      {STATUS[row.status].label}
                    </Badge>
                  </div>
                  <div className="flex items-center justify-between gap-3">
                    <span className="text-sm text-muted-foreground">{row.time}</span>
                    <ControlSizeProvider size="lg" asChild>
                      <Button variant={row.status === "waiting" ? "default" : "outline"}>{row.status === "waiting" ? "شروعِ کلاس" : "مشاهده"}</Button>
                    </ControlSizeProvider>
                  </div>
                </li>
              ))
            )}
          </ul>
          <div className="hidden rounded-xl border border-border bg-card md:block">
            <Table className="min-w-[46rem]">
              <TableHeader>
                <TableRow className="hover:bg-card">
                  <TableHead pin="start">زبان‌آموز</TableHead>
                  <TableHead>شهر</TableHead>
                  <TableHead>سطح</TableHead>
                  <TableHead>میانگینِ نمره</TableHead>
                  <TableHead>زمانِ درخواست</TableHead>
                  <TableHead>وضعیت</TableHead>
                  <TableHead>
                    <span className="sr-only">اقدام</span>
                  </TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {rows.length === 0 ? (
                  <TableRow className="hover:bg-card">
                    <TableCell colSpan={7} className="py-10 text-center text-muted-foreground">
                      درخواستی پیدا نشد؛ فیلتر را روی «همه» بگذار.
                    </TableCell>
                  </TableRow>
                ) : (
                  rows.map((row) => (
                    <TableRow key={row.id}>
                      <TableCell pin="start">
                        <span className="flex items-center gap-3">
                          <Avatar alt={row.name} src={avatarPhoto(row.name, row.gender)} className="size-9" />
                          <span className="font-semibold">{row.name}</span>
                        </span>
                      </TableCell>
                      <TableCell>{row.city}</TableCell>
                      <TableCell>
                        <Badge variant="outline">{row.level}</Badge>
                      </TableCell>
                      <TableCell className="tabular-nums">{toFaDigits(String(row.score).replace(".", "٫"))}</TableCell>
                      <TableCell className="text-muted-foreground">{row.time}</TableCell>
                      <TableCell>
                        <Badge variant="soft" color={STATUS[row.status].color}>
                          {STATUS[row.status].label}
                        </Badge>
                      </TableCell>
                      <TableCell>
                        <ControlSizeProvider size="lg" asChild>
                          <Button variant={row.status === "waiting" ? "default" : "outline"} className="whitespace-nowrap">
                            {row.status === "waiting" ? "شروعِ کلاس" : "مشاهده"}
                          </Button>
                        </ControlSizeProvider>
                      </TableCell>
                    </TableRow>
                  ))
                )}
              </TableBody>
            </Table>
          </div>
        </section>
      </main>
    </ControlSizeProvider>
  )
}