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