داشبوردِ پروژهها
پیشنمایش تمامصفحهداشبوردِ کارِ روزانه: سلامِ درشت، قرصِ سه آمار، جدولِ پروژهها با مسئول و وضعیتِ رنگی، برنامهٔ روز با نوارِ روزهای هفته و رویدادِ رنگی، و یادداشتها با تیک.
۱. نصب کامپوننتها
این بلاک از ۱۰ آیتم دیگ استفاده میکند: avatarbadgebuttoncardcheckboxcontrol-sizeiconslabelpersiantoggle-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/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.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/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.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/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.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/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/toggle-group.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
dashboard-projects.tsx
"use client"
import * as React from "react"
import { CheckCircleIcon, ChatIcon, ClockIcon, LoaderIcon, PaperclipIcon, PlusIcon, ShareIcon } from "@/components/icons"
import { Avatar, AvatarGroup } 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 { Checkbox } from "@/components/ui/checkbox"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Label } from "@/components/ui/label"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { toFaDigits } from "@/lib/persian"
import { avatarPhoto } from "@/lib/stock-images"
type Status = "progress" | "pending" | "done"
const STATUS: Record<Status, { label: string; color: "info" | "warning" | "success" }> = {
progress: { label: "در جریان", color: "info" },
pending: { label: "در انتظار", color: "warning" },
done: { label: "انجام شد", color: "success" },
}
const PROJECTS: { id: string; name: string; comments: number; files: number; owner: { name: string; gender: "male" | "female" }; status: Status }[] = [
{ id: "p1", name: "جذبِ مشتریِ تازه برای کافه رست", comments: 7, files: 2, owner: { name: "نگار احمدی", gender: "female" }, status: "progress" },
{ id: "p2", name: "اپِ بانکیِ نو، فازِ دوم", comments: 10, files: 3, owner: { name: "امیر رضایی", gender: "male" }, status: "pending" },
{ id: "p3", name: "هویتِ بصریِ فروشگاهِ بوم", comments: 5, files: 8, owner: { name: "سارا محمدی", gender: "female" }, status: "done" },
]
const DAYS = [
{ value: "12", short: "ش", name: "شنبه", day: 12 },
{ value: "13", short: "ی", name: "یکشنبه", day: 13 },
{ value: "14", short: "د", name: "دوشنبه", day: 14 },
{ value: "15", short: "س", name: "سهشنبه", day: 15 },
{ value: "16", short: "چ", name: "چهارشنبه", day: 16 },
{ value: "17", short: "پ", name: "پنجشنبه", day: 17 },
]
const EVENTS = [
{ id: "e1", title: "جلسهٔ شروعِ پروژه با بانک", time: "۱۱:۰۰ تا ۱۲:۳۰", tone: "var(--dig-green-500)", people: ["سارا محمدی", "امیر رضایی"] },
{ id: "e2", title: "طراحیِ صفحهٔ ثبتنامِ رویداد", time: "۱۴:۰۰ تا ۱۶:۰۰", tone: "var(--dig-blue-500)", people: ["نگار احمدی", "علی کاظمی"] },
{ id: "e3", title: "جریانِ کاربرِ رزروِ هتل", time: "۱۷:۰۰ تا ۱۸:۰۰", tone: "var(--dig-pink-500)", people: ["آرمیتا رستگار", "امیر رضایی"] },
]
const NOTES = [
{ id: "n1", title: "صفحهٔ فرود برای کافه", text: "هدفِ صفحه را دقیقتر بنویسیم: جذبِ رزرو یا معرفیِ منو؟" },
{ id: "n2", title: "آیکونها روی زمینهٔ تیره", text: "آیکونهای ساده و آشنا؛ از جزئیاتِ زیاد پرهیز کنیم." },
{ id: "n3", title: "بازبینیِ جریانِ پرداخت", text: "دکمهٔ تأیید روی موبایل زیرِ کیبورد نرود." },
]
/**
* داشبوردِ کارِ روزانه: تاریخ و سلامِ درشت با «اشتراک» و «کارِ تازه»، نوارِ قرصیِ سه آمار، جدولِ
* «پروژههای من» با نظر و فایل، مسئول و وضعیتِ رنگی (در موبایل کارتِ ردیفی)، و زیرش برنامهٔ روز با
* نوارِ روزهای هفته و رویدادهای رنگی، و یادداشتها با تیک.
*/
export default function DashboardProjectsBlock() {
const [day, setDay] = React.useState("14")
const [done, setDone] = React.useState<string[]>(["n3"])
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-wrap items-end justify-between gap-x-4 gap-y-6">
<div className="flex flex-col gap-1">
<span className="text-muted-foreground">دوشنبه، ۱۴ مهر</span>
<h1 className="text-3xl leading-snug font-black sm:text-4xl">عصر بخیر، آرمیتا</h1>
</div>
<div className="flex gap-3">
<Button variant="outline">
<ShareIcon />
اشتراک
</Button>
<Button>
<PlusIcon />
کارِ تازه
</Button>
</div>
</header>
{/* سه آمار در یک قرص؛ در موبایل زیرِ هم با جداکننده. */}
<ul className="flex w-full flex-col divide-y divide-border rounded-xl border border-border bg-subtle sm:w-fit sm:flex-row sm:divide-x sm:divide-y-0 sm:rounded-full rtl:sm:divide-x-reverse">
{[
{ icon: <ClockIcon />, value: "۱۲ ساعت", label: "زمانِ صرفهجوییشده" },
{ icon: <CheckCircleIcon />, value: toFaDigits(24), label: "پروژهٔ انجامشده" },
{ icon: <LoaderIcon />, value: toFaDigits(7), label: "پروژهٔ در جریان" },
].map((stat) => (
<li key={stat.label} className="flex items-center gap-2.5 px-5 py-3.5 [&>svg]:size-5 [&>svg]:text-muted-foreground">
{stat.icon}
<span className="text-xl font-black tabular-nums">{stat.value}</span>
<span className="text-sm text-muted-foreground">{stat.label}</span>
</li>
))}
</ul>
<Card className="gap-0 py-0">
<CardHeader className="border-b border-border py-5">
<CardTitle className="text-lg">پروژههای من</CardTitle>
<CardAction>
<Button variant="ghost">همه</Button>
</CardAction>
</CardHeader>
<div className="hidden grid-cols-[minmax(0,1fr)_14rem_9rem] gap-4 border-b border-border bg-subtle px-6 py-3 text-sm font-medium text-muted-foreground md:grid">
<span>نامِ کار</span>
<span>مسئول</span>
<span>وضعیت</span>
</div>
<ul className="flex flex-col divide-y divide-border">
{PROJECTS.map((project) => (
<li key={project.id} className="grid grid-cols-1 gap-3 px-6 py-4 md:grid-cols-[minmax(0,1fr)_14rem_9rem] md:items-center md:gap-4">
<div className="flex min-w-0 items-center justify-between gap-3">
<a href="#" className="truncate font-semibold hover:underline">
{project.name}
</a>
<span className="flex shrink-0 items-center gap-3 text-sm text-muted-foreground [&_svg]:size-4">
<span className="flex items-center gap-1" aria-label={`${toFaDigits(project.comments)} نظر`}>
<ChatIcon aria-hidden />
{toFaDigits(project.comments)}
</span>
<span className="flex items-center gap-1" aria-label={`${toFaDigits(project.files)} فایل`}>
<PaperclipIcon aria-hidden />
{toFaDigits(project.files)}
</span>
</span>
</div>
<div className="flex items-center justify-between gap-3 md:contents">
<span className="flex items-center gap-2 text-sm">
<Avatar size="sm" alt={project.owner.name} src={avatarPhoto(project.owner.name, project.owner.gender)} />
{project.owner.name}
</span>
<Badge variant="soft" color={STATUS[project.status].color} className="w-fit">
{STATUS[project.status].label}
</Badge>
</div>
</li>
))}
</ul>
</Card>
<div className="grid grid-cols-1 items-start gap-6 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-lg">برنامهٔ روز</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-5">
<div>
<ToggleGroup type="single" separated value={day} onValueChange={(value) => value && setDay(value as string)} aria-label="روزِ هفته" className="grid w-full grid-cols-6 gap-1.5">
{DAYS.map((item) => (
<ToggleGroupItem
key={item.value}
value={item.value}
aria-label={`${item.name} ${toFaDigits(item.day)} مهر`}
className="h-auto w-full flex-col gap-0.5 rounded-lg px-0 py-2 data-[state=on]:bg-[color-mix(in_oklab,var(--dig-violet-500)_14%,var(--card))] data-[state=on]:text-[var(--dig-violet-700)] dark:data-[state=on]:text-[var(--dig-violet-300)]"
>
<span className="text-xs">{item.short}</span>
<span className="text-lg font-bold tabular-nums">{toFaDigits(item.day)}</span>
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<ul className="flex flex-col divide-y divide-border">
{EVENTS.map((event) => (
<li key={event.id} className="flex items-center gap-3 py-3.5 first:pt-0 last:pb-0">
<span aria-hidden className="h-10 w-1 shrink-0 rounded-full" style={{ background: event.tone }} />
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="truncate font-semibold">{event.title}</span>
<span className="text-sm text-muted-foreground tabular-nums">{event.time}</span>
</span>
<AvatarGroup>
{event.people.map((name) => (
<Avatar key={name} size="sm" alt={name} src={avatarPhoto(name, name.startsWith("امیر") || name.startsWith("علی") ? "male" : "female")} className="ring-2 ring-card" />
))}
</AvatarGroup>
</li>
))}
</ul>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">یادداشتها</CardTitle>
<CardAction>
<span className="text-sm text-muted-foreground">
{toFaDigits(NOTES.length - done.length)} مانده
</span>
</CardAction>
</CardHeader>
<CardContent>
<ul className="flex flex-col divide-y divide-border">
{NOTES.map((note) => {
const checked = done.includes(note.id)
return (
<li key={note.id} className="flex items-start gap-3 py-3.5 first:pt-0 last:pb-0">
<Checkbox id={note.id} checked={checked} onCheckedChange={(value) => setDone((list) => (value ? [...list, note.id] : list.filter((id) => id !== note.id)))} className="mt-1 rounded-full" />
<Label htmlFor={note.id} className="flex flex-col items-start gap-1 font-normal">
<span className={`font-semibold ${checked ? "text-muted-foreground line-through" : ""}`}>{note.title}</span>
<span className="text-sm leading-6 text-muted-foreground">{note.text}</span>
</Label>
</li>
)
})}
</ul>
</CardContent>
</Card>
</div>
</main>
</ControlSizeProvider>
)
}