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

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

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

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

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

این بلاک از ۱۰ آیتم دیگ استفاده می‌کند: 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.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/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
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/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
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/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>
  )
}