داشبوردِ پرداده
پیشنمایش تمامصفحهنگاهِ کلی با سه کارتِ شاخص (بازهٔ جدا برای هر کارت، رشدِ رنگی)، نمودارِ درآمدِ ماهانه با جمعِ سال، امتیازها در برابرِ میانگین، و جدولِ درخواستها با فیلترِ وضعیت، جستوجو و اقدامِ هر ردیف.
۱. نصب کامپوننتها
این بلاک از ۱۲ آیتم دیگ استفاده میکند: 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.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/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.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/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.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/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>
)
}