feat(frontend): draft/publish, certificate approval UI, contributors, leaderboard tiers
- admin-studio: chapter/lesson publish toggle with Draft/Published badges; per-chapter accordion - cap/page: remove auto-issue effect; student requests, admin approves - learning-panel: admin issue-certificate action per student - site-settings: contributors editor with avatar upload (ContributorAvatarPicker, TagInput) - community-panel: render contributors section below social links - leaderboard-panel: TierBadge, totalParticipants, tier on entries and self - page.tsx: fix "شروع مسیر" routing to /register instead of /login - navbar: correct icons (Settings, Palette) in user dropdown - types: Contributor, LeaderboardTier, Certificate status field, updated LeaderboardData/Entry - layout: PWA icon metadata (192, 512, apple-touch-icon)
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
Copy,
|
||||
Cpu,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Flag,
|
||||
GitBranch,
|
||||
Globe,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
Loader2,
|
||||
Lock,
|
||||
MapPin,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
Plus,
|
||||
Rocket,
|
||||
@@ -59,6 +61,15 @@ import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -685,7 +696,11 @@ function CurriculumPanel() {
|
||||
|
||||
const [lessonModal, setLessonModal] = useState<Lesson | null>(null);
|
||||
const [chapterModal, setChapterModal] = useState<Chapter | null>(null);
|
||||
const [dragIdx, setDragIdx] = useState<{ chapter: number; from: number; over?: number } | null>(null);
|
||||
const [activeChapter, setActiveChapter] = useState<number | null>(null);
|
||||
const [dragIdx, setDragIdx] = useState<{ chapter: number; from: number } | null>(null);
|
||||
|
||||
const toggleChapter = (id: number) =>
|
||||
setActiveChapter((prev) => (prev === id ? null : id));
|
||||
|
||||
const persistChapterOrder = async (list: Chapter[]) => {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
@@ -772,12 +787,14 @@ function CurriculumPanel() {
|
||||
}
|
||||
};
|
||||
|
||||
const newLesson = (chapterId: number) =>
|
||||
const newLesson = (chapterId: number) => {
|
||||
setActiveChapter(chapterId);
|
||||
setLessonModal({
|
||||
id: 0, chapterId, title: "", description: "", videoKey: "",
|
||||
durationSeconds: 45, completionThresholdPct: 85, requiresLessonId: undefined,
|
||||
x: lessons.filter((l) => l.chapterId === chapterId).length, y: 0, sortOrder: 1, xpReward: 50, isActive: true, isPublished: false,
|
||||
});
|
||||
};
|
||||
|
||||
const duplicateLesson = async (l: Lesson) => {
|
||||
try {
|
||||
@@ -812,6 +829,7 @@ function CurriculumPanel() {
|
||||
}
|
||||
try {
|
||||
await http.del(`/api/admin/chapters/${ch.id}`);
|
||||
if (activeChapter === ch.id) setActiveChapter(null);
|
||||
qc.invalidateQueries({ queryKey: ["admin", "lessons"] });
|
||||
qc.invalidateQueries({ queryKey: ["admin", "stats"] });
|
||||
toast.success("فصل حذف شد");
|
||||
@@ -820,70 +838,159 @@ function CurriculumPanel() {
|
||||
}
|
||||
};
|
||||
|
||||
const publishedChapters = chapters.filter((c) => c.isPublished).length;
|
||||
const publishedLessons = lessons.filter((l) => l.isPublished).length;
|
||||
|
||||
return (
|
||||
<>
|
||||
{dialog}
|
||||
<Card>
|
||||
<CardHeader className="flex-row items-center justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle className="text-base">فصلها و درخت مهارت</CardTitle>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{chapters.length} فصل · {lessons.length} درس · ترتیب با کشیدن یا فلش · موقعیت x,y
|
||||
</p>
|
||||
|
||||
{/* Course overview header */}
|
||||
<div className="mb-4 flex flex-wrap items-start justify-between gap-3 rounded-xl border border-border bg-muted/30 px-4 py-3">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-bold">دوره آموزشی</p>
|
||||
<div className="flex flex-wrap gap-3 text-xs text-muted-foreground">
|
||||
<span>{chapters.length} فصل</span>
|
||||
<span>{lessons.length} درس</span>
|
||||
<span className="text-success">{publishedChapters} فصل منتشر</span>
|
||||
<span className="text-success">{publishedLessons} درس منتشر</span>
|
||||
</div>
|
||||
<Button className="gap-1.5 rounded-2xl" onClick={() => setChapterModal({ id: 0, title: "", description: "", icon: "terminal", sortOrder: chapters.length + 1, isPublished: false })}>
|
||||
<Plus className="h-4 w-4" /> فصل جدید
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{isLoading && <Loader />}
|
||||
{chapters.map((ch, ci) => {
|
||||
const ls = lessons.filter((l) => l.chapterId === ch.id);
|
||||
const ChIcon = chapterIconOf(ch.icon);
|
||||
return (
|
||||
<div key={ch.id} className={cn("rounded-xl border border-border bg-muted/20", dragIdx !== null && dragIdx?.chapter === ch.id && "border-primary/50")}>
|
||||
<div className="flex items-center gap-2 p-3 pb-2">
|
||||
<span className="grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-primary/10 text-primary">
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={() => setChapterModal({ id: 0, title: "", description: "", icon: "terminal", sortOrder: chapters.length + 1, isPublished: false })}
|
||||
>
|
||||
<Plus className="h-4 w-4" /> فصل جدید
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Chapter list */}
|
||||
{isLoading && <Loader />}
|
||||
|
||||
<div className="space-y-2">
|
||||
{chapters.map((ch, ci) => {
|
||||
const ls = lessons.filter((l) => l.chapterId === ch.id);
|
||||
const ChIcon = chapterIconOf(ch.icon);
|
||||
const isOpen = activeChapter === ch.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={ch.id}
|
||||
className={cn(
|
||||
"overflow-hidden rounded-xl border border-border transition-colors",
|
||||
isOpen && "border-primary/40 shadow-sm",
|
||||
)}
|
||||
>
|
||||
{/* Chapter header row */}
|
||||
<div className="flex items-center gap-2 px-3 py-2.5">
|
||||
{/* Expand toggle + identity — takes full remaining width */}
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={isOpen}
|
||||
aria-controls={`chapter-${ch.id}-lessons`}
|
||||
className="flex min-w-0 flex-1 items-center gap-2.5 rounded-md py-0.5 text-start outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
onClick={() => toggleChapter(ch.id)}
|
||||
>
|
||||
<span className="grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-primary/10 text-primary">
|
||||
<ChIcon className="h-4 w-4" />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="truncate font-bold">{ch.title}</h3>
|
||||
{ch.description && <p className="truncate text-xs text-muted-foreground">{ch.description}</p>}
|
||||
<p className="truncate text-sm font-bold">{ch.title}</p>
|
||||
{ch.description && (
|
||||
<p className="truncate text-[11px] text-muted-foreground">{ch.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<Badge variant="outline" className="shrink-0">{ls.length} درس</Badge>
|
||||
<Badge variant={ch.isPublished ? "success" : "secondary"} className="shrink-0 px-1.5 text-[10px]">
|
||||
{ch.isPublished ? "منتشر" : "پیشنویس"}
|
||||
</Badge>
|
||||
<GroupNav
|
||||
onUp={ci > 0 ? () => moveChapter(ci, -1) : undefined}
|
||||
onDown={ci < chapters.length - 1 ? () => moveChapter(ci, 1) : undefined}
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200",
|
||||
isOpen && "rotate-180",
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
<Button size="sm" variant="outline" className="h-7 gap-1 text-xs" onClick={() => setChapterModal(ch)}>
|
||||
<Pencil className="h-3 w-3" /> فصل
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 gap-1 text-xs"
|
||||
onClick={() => publishChapter(ch, !ch.isPublished)}
|
||||
title={ch.isPublished ? "برگشت به پیشنویس" : "انتشار فصل"}
|
||||
>
|
||||
{ch.isPublished ? "برگشت" : "انتشار"}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="h-7 gap-1 text-xs" onClick={() => newLesson(ch.id)}>
|
||||
<Plus className="h-3.5 w-3.5" /> درس
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" className="h-7 w-7 px-0 text-destructive" onClick={() => delChapter(ch)} title="حذف فصل">
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
{ls.length === 0 ? (
|
||||
<p className="px-3 pb-3 text-xs text-muted-foreground">هنوز درسی نیست. «+ درس» را بزنید.</p>
|
||||
) : (
|
||||
<div className="px-3 pb-3">
|
||||
<div className="grid gap-1.5 sm:grid-cols-2">
|
||||
</button>
|
||||
|
||||
{/* Status badges */}
|
||||
<Badge variant="outline" className="shrink-0 px-2 text-[10px]">
|
||||
{ls.length} درس
|
||||
</Badge>
|
||||
<Badge
|
||||
variant={ch.isPublished ? "success" : "secondary"}
|
||||
className="hidden shrink-0 px-1.5 text-[10px] sm:inline-flex"
|
||||
>
|
||||
{ch.isPublished ? "منتشر" : "پیشنویس"}
|
||||
</Badge>
|
||||
|
||||
{/* Chapter actions menu */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 shrink-0 px-0"
|
||||
aria-label={`اقدامات فصل ${ch.title}`}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuLabel>فصل {ci + 1}</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => setChapterModal(ch)}>
|
||||
<Pencil className="h-3.5 w-3.5" /> ویرایش فصل
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setActiveChapter(ch.id);
|
||||
newLesson(ch.id);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" /> درس جدید
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{ci > 0 && (
|
||||
<DropdownMenuItem onClick={() => moveChapter(ci, -1)}>
|
||||
<ChevronUp className="h-3.5 w-3.5" /> جابجا به بالا
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{ci < chapters.length - 1 && (
|
||||
<DropdownMenuItem onClick={() => moveChapter(ci, 1)}>
|
||||
<ChevronDown className="h-3.5 w-3.5" /> جابجا به پایین
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={() => publishChapter(ch, !ch.isPublished)}>
|
||||
{ch.isPublished ? (
|
||||
<><EyeOff className="h-3.5 w-3.5" /> برگشت به پیشنویس</>
|
||||
) : (
|
||||
<><Globe className="h-3.5 w-3.5" /> انتشار فصل</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={() => delChapter(ch)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" /> حذف فصل
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* Lessons panel (progressive disclosure) */}
|
||||
{isOpen && (
|
||||
<div
|
||||
id={`chapter-${ch.id}-lessons`}
|
||||
className="border-t border-border/70"
|
||||
>
|
||||
{ls.length === 0 ? (
|
||||
<div className="px-4 py-5 text-center text-sm text-muted-foreground">
|
||||
هنوز درسی نیست — از دکمه زیر درس اضافه کنید.
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border/60">
|
||||
{ls.map((l, li) => {
|
||||
const cols = ls.filter((o) => o.x === l.x && o.y === l.y && o.id !== l.id);
|
||||
const collision = ls.some(
|
||||
(o) => o.x === l.x && o.y === l.y && o.id !== l.id,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={l.id}
|
||||
@@ -892,80 +999,150 @@ function CurriculumPanel() {
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
setDragIdx((d) => (d ? { ...d, over: li, chapter: ch.id } : d));
|
||||
}}
|
||||
onDrop={() => {
|
||||
if (dragIdx && dragIdx.chapter === ch.id && dragIdx.from !== li) {
|
||||
const idx = dragIdx.from;
|
||||
const next = reorderArray(ls, idx, li);
|
||||
const next = reorderArray(ls, dragIdx.from, li);
|
||||
persistLessonOrder(next).catch(() => {});
|
||||
}
|
||||
setDragIdx(null);
|
||||
}}
|
||||
onDragEnd={() => setDragIdx(null)}
|
||||
className={cn(
|
||||
"group flex cursor-grab items-center gap-2 rounded-lg border border-border bg-card px-3 py-2.5 transition-colors active:cursor-grabbing",
|
||||
"flex items-center gap-2 px-3 py-2.5 transition-colors hover:bg-muted/40",
|
||||
dragIdx?.from === li && dragIdx.chapter === ch.id && "opacity-40",
|
||||
!l.videoKey && "border-dashed"
|
||||
)}
|
||||
>
|
||||
<GripVertical className="h-4 w-4 shrink-0 text-muted-foreground/50" />
|
||||
<span className="shrink-0 text-xs font-bold tabular-nums text-muted-foreground">L{l.id}</span>
|
||||
{/* Grip handle */}
|
||||
<GripVertical className="h-4 w-4 shrink-0 cursor-grab text-muted-foreground/40 active:cursor-grabbing" aria-hidden />
|
||||
|
||||
{/* Lesson identity */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium">{l.title}</p>
|
||||
<div className="flex flex-wrap items-center gap-1.5 text-[11px] text-muted-foreground">
|
||||
<Timer className="h-3 w-3" /> {formatDur(l.durationSeconds)}
|
||||
<Sparkles className="h-3 w-3 text-gold" /> +{formatFaNumber(l.xpReward)}
|
||||
<MapPin className="h-3 w-3" /> {l.x},{l.y}
|
||||
<p className="truncate text-sm font-medium leading-tight">
|
||||
{l.title}
|
||||
</p>
|
||||
<div className="mt-0.5 flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground">
|
||||
<span className="inline-flex items-center gap-0.5">
|
||||
<Timer className="h-3 w-3" />
|
||||
{formatDur(l.durationSeconds)}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-0.5 text-gold">
|
||||
<Sparkles className="h-3 w-3" />
|
||||
+{formatFaNumber(l.xpReward)}
|
||||
</span>
|
||||
{l.videoKey ? (
|
||||
<span className="inline-flex items-center gap-0.5 text-success"><Video className="h-3 w-3" /> ویدیو</span>
|
||||
<span className="inline-flex items-center gap-0.5 text-success">
|
||||
<Video className="h-3 w-3" /> ویدیو
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-warning">بدون ویدیو</span>
|
||||
)}
|
||||
{cols.length > 0 && <Badge variant="destructive" className="px-1 text-[9px]">تداخل</Badge>}
|
||||
{collision && (
|
||||
<Badge variant="destructive" className="px-1 text-[9px]">
|
||||
تداخل موقعیت
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={l.isPublished ? "success" : "secondary"} className="shrink-0 px-1.5 text-[10px]">
|
||||
|
||||
{/* Publish badge */}
|
||||
<Badge
|
||||
variant={l.isPublished ? "success" : "secondary"}
|
||||
className="hidden shrink-0 px-1.5 text-[10px] sm:inline-flex"
|
||||
>
|
||||
{l.isPublished ? "منتشر" : "پیشنویس"}
|
||||
</Badge>
|
||||
<div className="flex shrink-0 gap-0.5">
|
||||
<GroupNav onUp={li > 0 ? () => moveLesson(ch.id, li, -1) : undefined} onDown={li < ls.length - 1 ? () => moveLesson(ch.id, li, 1) : undefined} compact />
|
||||
<Button size="sm" variant="ghost" className="h-7 px-1.5" title="تکثیر" onClick={() => duplicateLesson(l)}>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" className="h-7 px-1.5" onClick={() => setLessonModal(l)} title="ویرایش">
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 px-1.5 text-[10px]"
|
||||
title={l.isPublished ? "برگشت به پیشنویس" : "انتشار درس"}
|
||||
onClick={() => publishLesson(l, !l.isPublished)}
|
||||
>
|
||||
{l.isPublished ? "برگشت" : "انتشار"}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" className="h-7 w-7 px-0 text-destructive opacity-60 hover:opacity-100" onClick={() => delLesson(l)} title="حذف درس">
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Up/down reorder (accessible) */}
|
||||
<GroupNav
|
||||
onUp={li > 0 ? () => moveLesson(ch.id, li, -1) : undefined}
|
||||
onDown={li < ls.length - 1 ? () => moveLesson(ch.id, li, 1) : undefined}
|
||||
compact
|
||||
/>
|
||||
|
||||
{/* Edit (primary action) */}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 shrink-0 px-0"
|
||||
title="ویرایش درس"
|
||||
aria-label={`ویرایش ${l.title}`}
|
||||
onClick={() => setLessonModal(l)}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
|
||||
{/* Secondary actions menu */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 shrink-0 px-0"
|
||||
aria-label={`اقدامات ${l.title}`}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-44">
|
||||
<DropdownMenuLabel className="truncate">L{l.id}</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => duplicateLesson(l)}>
|
||||
<Copy className="h-3.5 w-3.5" /> تکثیر
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => publishLesson(l, !l.isPublished)}>
|
||||
{l.isPublished ? (
|
||||
<><EyeOff className="h-3.5 w-3.5" /> برگشت به پیشنویس</>
|
||||
) : (
|
||||
<><Globe className="h-3.5 w-3.5" /> انتشار درس</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={() => delLesson(l)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" /> حذف درس
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add lesson footer */}
|
||||
<div className="border-t border-border/60 px-3 py-2.5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="gap-1.5"
|
||||
onClick={() => newLesson(ch.id)}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" /> درس جدید در این فصل
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{chapters.length === 0 && !isLoading && (
|
||||
<div className="rounded-xl border border-dashed border-border p-8 text-center">
|
||||
<p className="font-bold">هنوز فصلی نیست</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">با «فصل جدید» شروع کنید؛ درسهای هر فصل روی درخت مهارت هنرجو نمایش داده میشود.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{chapters.length === 0 && !isLoading && (
|
||||
<div className="rounded-xl border border-dashed border-border p-10 text-center">
|
||||
<p className="font-bold">هنوز فصلی نیست</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
با «فصل جدید» شروع کنید؛ درسهای هر فصل روی درخت مهارت هنرجو نمایش داده میشود.
|
||||
</p>
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => setChapterModal({ id: 0, title: "", description: "", icon: "terminal", sortOrder: 1, isPublished: false })}
|
||||
>
|
||||
<Plus className="h-4 w-4" /> فصل جدید
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ChapterDialog chapter={chapterModal} onClose={() => setChapterModal(null)} onSave={saveChapter} />
|
||||
<LessonDialog
|
||||
@@ -1437,11 +1614,11 @@ function EventDialog({ event, onClose, onSave }: { event: AdminEvent | null; onC
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>توضیحات</Label>
|
||||
<textarea
|
||||
<Textarea
|
||||
value={draft.description}
|
||||
onChange={(e) => set({ description: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full resize-y rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
className="resize-y"
|
||||
placeholder="شرح کوتاه رویداد (اختیاری)…"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user