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

داشبوردِ یادگیری

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

بنرِ گرادیانیِ طراحی‌شدهٔ دوره‌ها، سه مسیرِ یادگیری با شمارِ درس، «ادامهٔ تماشا» با کارتِ تصویری و پیشرفت و منتور، و ستونِ کنار با حلقهٔ پیشرفتِ کاربر، نمودارِ ساعتِ مطالعه و دنبال‌کردنِ منتورها.

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

این بلاک از ۹ آیتم دیگ استفاده می‌کند: avatarbadgebuttoncardchart-presetscontrol-sizeiconspersianprogress

پکیج‌منیجر پیش‌فرض 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/progress.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/progress.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/progress.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/progress.json

۲. کد صفحه

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

dashboard-learning.tsx
"use client"

import * as React from "react"

import { ChevronLeftIcon, ChevronRightIcon, FileTextIcon, GridIcon, HeartIcon, PlayIcon, PlusIcon, SparklesIcon } 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 } from "@/components/ui/chart-presets"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Progress } from "@/components/ui/progress"
import { toFaDigits } from "@/lib/persian"
import { avatarPhoto, stockImages, type StockImageName } from "@/lib/stock-images"

const TRACKS = [
  { id: "ux", title: "طراحیِ تجربهٔ کاربر", watched: 2, total: 8, icon: <SparklesIcon />, tone: "violet" },
  { id: "brand", title: "برندینگ", watched: 3, total: 8, icon: <GridIcon />, tone: "pink" },
  { id: "front", title: "فرانت‌اند", watched: 6, total: 12, icon: <FileTextIcon />, tone: "cyan" },
] as const
const TONE = {
  violet: "bg-[color-mix(in_oklab,var(--dig-violet-500)_14%,var(--card))] text-[var(--dig-violet-600)] dark:text-[var(--dig-violet-300)]",
  pink: "bg-[color-mix(in_oklab,var(--dig-pink-500)_14%,var(--card))] text-[var(--dig-pink-600)] dark:text-[var(--dig-pink-300)]",
  cyan: "bg-[color-mix(in_oklab,var(--dig-cyan-500)_14%,var(--card))] text-[var(--dig-cyan-700)] dark:text-[var(--dig-cyan-300)]",
}
const LESSONS: { id: string; track: (typeof TRACKS)[number]["id"]; title: string; progress: number; mentor: string; gender: "male" | "female"; image: StockImageName }[] = [
  { id: "l1", track: "front", title: "راهنمای شروع؛ تا فرانت‌اند کارِ حرفه‌ای", progress: 62, mentor: "امیر رضایی", gender: "male", image: "coverWorkspace" },
  { id: "l2", track: "ux", title: "بهبودِ تجربهٔ کاربر با پژوهشِ سریع", progress: 38, mentor: "نگار احمدی", gender: "female", image: "projectMobileApp" },
  { id: "l3", track: "brand", title: "بازطراحیِ هویتِ یک کافهٔ محلی", progress: 80, mentor: "سارا محمدی", gender: "female", image: "projectCafeBrand" },
]
const WEEK = [
  { label: "۱–۱۰ مهر", hours: 22 },
  { label: "۱۱–۲۰", hours: 41 },
  { label: "۲۱–۳۰", hours: 26 },
  { label: "۱–۱۰ آبان", hours: 58 },
  { label: "۱۱–۲۰", hours: 24 },
]
const MENTORS = [
  { name: "امیر رضایی", gender: "male" as const, role: "منتورِ فرانت‌اند" },
  { name: "نگار احمدی", gender: "female" as const, role: "منتورِ تجربهٔ کاربر" },
  { name: "علی کاظمی", gender: "male" as const, role: "منتورِ برندینگ" },
]

/**
 * داشبوردِ آموزشِ آنلاین: بنرِ طراحی‌شده با تیترِ درشت و «ثبت‌نام»، سه مسیرِ در حالِ یادگیری
 * با شمارِ درس‌های دیده‌شده، «ادامهٔ تماشا» با کارتِ تصویری، پیشرفت و منتور (اسکرولِ افقی با
 * دکمه‌های قبلی/بعدی)، و ستونِ کنار با حلقهٔ پیشرفتِ کاربر، نمودارِ ساعتِ مطالعه و دنبال‌کردنِ منتورها.
 */
export default function DashboardLearningBlock() {
  const railRef = React.useRef<HTMLDivElement>(null)
  const [liked, setLiked] = React.useState<string[]>(["l2"])
  const [following, setFollowing] = React.useState<string[]>([])

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto grid w-full max-w-6xl grid-cols-1 items-start gap-6 p-4 sm:p-6 lg:grid-cols-[minmax(0,1fr)_20rem]">
        <h1 className="sr-only">داشبوردِ یادگیری</h1>
        <div className="flex min-w-0 flex-col gap-6">
          {/* بنر: گرادیانِ برند با درخشش و ستاره‌های تزئینی. */}
          <section aria-labelledby="promo-title" className="relative isolate flex flex-col gap-6 overflow-hidden rounded-xl bg-[linear-gradient(135deg,var(--dig-violet-600),var(--dig-indigo-700))] p-6 text-[var(--dig-white)] sm:p-8">
            <span aria-hidden className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(circle_at_85%_20%,var(--dig-violet-400),transparent_45%)]" />
            <svg aria-hidden viewBox="0 0 100 100" className="pointer-events-none absolute end-10 top-6 -z-10 size-28 text-[color-mix(in_oklab,var(--dig-white)_55%,transparent)] sm:size-36">
              <path d="M50 0 C53 40 60 47 100 50 C60 53 53 60 50 100 C47 60 40 53 0 50 C40 47 47 40 50 0Z" fill="currentColor" />
            </svg>
            <svg aria-hidden viewBox="0 0 100 100" className="pointer-events-none absolute end-44 bottom-4 -z-10 size-12 text-[color-mix(in_oklab,var(--dig-white)_30%,transparent)]">
              <path d="M50 0 C53 40 60 47 100 50 C60 53 53 60 50 100 C47 60 40 53 0 50 C40 47 47 40 50 0Z" fill="currentColor" />
            </svg>
            <div className="flex max-w-md flex-col gap-2">
              <span className="text-xs font-bold text-[var(--dig-violet-100)]">دوره‌های آنلاین</span>
              <h2 id="promo-title" className="text-2xl leading-snug font-black text-balance sm:text-3xl">
                مهارتت را با دوره‌های حرفه‌ای تیز کن
              </h2>
            </div>
            <Button className="w-fit gap-3 rounded-full bg-[var(--dig-slate-900)] ps-5 pe-2 text-[var(--dig-white)] hover:bg-[var(--dig-slate-800)]">
              ثبت‌نام
              <span className="flex size-8 items-center justify-center rounded-full bg-[var(--dig-white)] text-[var(--dig-slate-900)] [&>svg]:size-4">
                <ChevronLeftIcon />
              </span>
            </Button>
          </section>

          <ul className="grid grid-cols-1 gap-3 sm:grid-cols-3">
            {TRACKS.map((track) => (
              <li key={track.id} className="flex items-center gap-3 rounded-xl border border-border bg-card p-3 shadow-card">
                <span aria-hidden className={`flex size-11 shrink-0 items-center justify-center rounded-full [&>svg]:size-5 ${TONE[track.tone]}`}>
                  {track.icon}
                </span>
                <span className="flex min-w-0 flex-col">
                  <span className="text-xs text-muted-foreground tabular-nums">
                    {toFaDigits(track.watched)} از {toFaDigits(track.total)} درس
                  </span>
                  <span className="truncate font-bold">{track.title}</span>
                </span>
              </li>
            ))}
          </ul>

          <section aria-labelledby="continue-title" className="flex flex-col gap-4">
            <div className="flex items-center justify-between">
              <h2 id="continue-title" className="text-lg font-bold">
                ادامهٔ تماشا
              </h2>
              <div className="flex gap-2">
                <Button variant="outline" size="icon" aria-label="قبلی" className="rounded-full" onClick={() => railRef.current?.scrollBy({ left: 300, behavior: "smooth" })}>
                  <ChevronRightIcon />
                </Button>
                <Button size="icon" aria-label="بعدی" className="rounded-full" onClick={() => railRef.current?.scrollBy({ left: -300, behavior: "smooth" })}>
                  <ChevronLeftIcon />
                </Button>
              </div>
            </div>
            <div ref={railRef} className="flex snap-x snap-mandatory gap-4 overflow-x-auto pb-1 [scrollbar-width:none]">
              {LESSONS.map((lesson) => {
                const track = TRACKS.find((item) => item.id === lesson.track)!
                const isLiked = liked.includes(lesson.id)
                return (
                  <article key={lesson.id} className="relative flex w-72 shrink-0 snap-start flex-col gap-3 rounded-xl border border-border bg-card p-3 shadow-card">
                    <div className="relative">
                      {/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
                      <img src={stockImages[lesson.image].src} alt="" loading="lazy" className="aspect-[16/10] w-full rounded-lg object-cover" />
                      <span aria-hidden className="absolute inset-0 m-auto flex size-11 items-center justify-center rounded-full bg-[var(--dig-white)] text-[var(--dig-slate-900)] shadow-card [&>svg]:size-5">
                        <PlayIcon />
                      </span>
                      <ControlSizeProvider size="lg" asChild>
                        <Button
                          variant="outline"
                          size="icon"
                          aria-pressed={isLiked}
                          aria-label={isLiked ? "برداشتن از علاقه‌مندی‌ها" : "افزودن به علاقه‌مندی‌ها"}
                          onClick={() => setLiked((list) => (isLiked ? list.filter((id) => id !== lesson.id) : [...list, lesson.id]))}
                          className={`absolute end-2 top-2 z-10 rounded-full bg-card ${isLiked ? "text-[var(--dig-pink-600)]" : ""}`}
                        >
                          <HeartIcon filled={isLiked} />
                        </Button>
                      </ControlSizeProvider>
                    </div>
                    <Badge variant="soft" className={`w-fit ${TONE[track.tone]}`}>
                      {track.title}
                    </Badge>
                    <h3 className="line-clamp-2 min-h-12 leading-6 font-bold">
                      <a href="#" className="after:absolute after:inset-0">
                        {lesson.title}
                      </a>
                    </h3>
                    <Progress value={lesson.progress} aria-label={`پیشرفت ${toFaDigits(lesson.progress)} درصد`} className="[--progress:var(--dig-violet-500)]" />
                    <span className="flex items-center gap-2 text-sm">
                      <Avatar size="sm" alt={lesson.mentor} src={avatarPhoto(lesson.mentor, lesson.gender)} />
                      <span className="flex flex-col leading-tight">
                        <span className="font-medium">{lesson.mentor}</span>
                        <span className="text-xs text-muted-foreground">منتور</span>
                      </span>
                    </span>
                  </article>
                )
              })}
            </div>
          </section>
        </div>

        <aside className="flex flex-col gap-6">
          <Card>
            <CardContent className="flex flex-col items-center gap-4 text-center">
              <div className="relative">
                <svg aria-hidden viewBox="0 0 120 120" className="size-32 -rotate-90">
                  <circle cx="60" cy="60" r="54" fill="none" strokeWidth="5" className="stroke-muted" />
                  <circle cx="60" cy="60" r="54" fill="none" strokeWidth="5" strokeLinecap="round" stroke="var(--dig-violet-500)" strokeDasharray={2 * Math.PI * 54} strokeDashoffset={2 * Math.PI * 54 * (1 - 0.32)} />
                </svg>
                <Avatar alt="آرمیتا رستگار" src={stockImages.personWoman1.src} className="absolute inset-3 size-26" />
                <span className="absolute -end-1 top-2 rounded-full bg-[var(--dig-violet-600)] px-2 py-0.5 text-xs font-bold text-[var(--dig-white)]">٪۳۲</span>
              </div>
              <div className="flex flex-col gap-1">
                <span className="text-lg font-black">صبح بخیر، آرمیتا</span>
                <span className="text-sm text-muted-foreground">ادامه بده تا به هدفِ این ماه برسی.</span>
              </div>
              <div className="w-full rounded-xl bg-subtle p-3">
                <BarChart className="h-40" data={WEEK} categoryKey="label" series={[{ key: "hours", label: "ساعت مطالعه", color: "var(--dig-violet-500)" }]} valueFormatter={(value) => `${toFaDigits(value)} ساعت`} />
              </div>
            </CardContent>
          </Card>

          <Card>
            <CardHeader>
              <CardTitle className="text-lg">منتورهای تو</CardTitle>
              <CardAction>
                <Button variant="outline" size="icon" aria-label="افزودنِ منتور" className="rounded-full">
                  <PlusIcon />
                </Button>
              </CardAction>
            </CardHeader>
            <CardContent>
              <ul className="flex flex-col divide-y divide-dashed divide-border">
                {MENTORS.map((mentor) => {
                  const isFollowing = following.includes(mentor.name)
                  return (
                    <li key={mentor.name} className="flex items-center gap-3 py-3 first:pt-0 last:pb-0">
                      <Avatar alt={mentor.name} src={avatarPhoto(mentor.name, mentor.gender)} className="size-11" />
                      <span className="flex min-w-0 flex-1 flex-col">
                        <span className="text-sm font-bold">{mentor.name}</span>
                        <span className="truncate text-xs text-muted-foreground">{mentor.role}</span>
                      </span>
                      <ControlSizeProvider size="lg" asChild>
                        <Button
                          variant={isFollowing ? "secondary" : "outline"}
                          aria-pressed={isFollowing}
                          onClick={() => setFollowing((list) => (isFollowing ? list.filter((name) => name !== mentor.name) : [...list, mentor.name]))}
                          className="rounded-full px-3 text-sm"
                        >
                          {isFollowing ? "دنبال شد" : "دنبال کن"}
                        </Button>
                      </ControlSizeProvider>
                    </li>
                  )
                })}
              </ul>
            </CardContent>
          </Card>
        </aside>
      </main>
    </ControlSizeProvider>
  )
}