284 lines
15 KiB
JavaScript
284 lines
15 KiB
JavaScript
// Course Builder screen — module/lesson tree with learning path.
|
||
|
||
const TYPE_META = {
|
||
video: { ic: 'play', color: 'oklch(0.72 0.10 30)', fa: 'ویدیو' },
|
||
pdf: { ic: 'pdf', color: 'oklch(0.72 0.10 80)', fa: 'PDF' },
|
||
audio: { ic: 'audio', color: 'oklch(0.72 0.10 200)', fa: 'صوت' },
|
||
task: { ic: 'edit', color: 'oklch(0.72 0.10 140)', fa: 'تمرین' },
|
||
quiz: { ic: 'quiz', color: 'oklch(0.72 0.10 260)', fa: 'آزمون' },
|
||
};
|
||
|
||
function LessonRow({ lesson, selected, onSelect }) {
|
||
const m = TYPE_META[lesson.type];
|
||
return (
|
||
<div onClick={() => onSelect(lesson.id)}
|
||
style={{
|
||
display: 'flex', alignItems: 'center', gap: 12,
|
||
padding: '10px 14px',
|
||
borderRadius: 'var(--r-sm)',
|
||
background: selected ? 'var(--accent-soft)' : 'transparent',
|
||
border: '1px solid ' + (selected ? 'transparent' : 'transparent'),
|
||
cursor: 'pointer',
|
||
transition: 'background .12s',
|
||
}}
|
||
onMouseEnter={(e) => { if (!selected) e.currentTarget.style.background = 'var(--surface-2)'; }}
|
||
onMouseLeave={(e) => { if (!selected) e.currentTarget.style.background = 'transparent'; }}
|
||
>
|
||
<Icon name="drag" className="ic-sm" style={{ color: 'var(--ink-4)' }} />
|
||
<div style={{
|
||
width: 28, height: 28, borderRadius: 7,
|
||
background: 'color-mix(in oklch, ' + m.color + ' 18%, transparent)',
|
||
color: m.color,
|
||
display: 'grid', placeItems: 'center', flexShrink: 0,
|
||
}}>
|
||
<Icon name={m.ic} className="ic-sm" />
|
||
</div>
|
||
<div style={{ flex: 1, minWidth: 0 }}>
|
||
<div style={{ fontSize: 13.5, fontWeight: selected ? 600 : 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||
{lesson.title}
|
||
</div>
|
||
<div className="muted num" style={{ fontSize: 11.5, marginTop: 1 }}>
|
||
{m.fa} · {lesson.duration}
|
||
{lesson.preview && <span> · <span style={{ color: 'var(--accent-ink)' }}>پیشنمایش رایگان</span></span>}
|
||
</div>
|
||
</div>
|
||
{lesson.status === 'draft'
|
||
? <span className="pill warn">پیشنویس</span>
|
||
: <Icon name="check" className="ic-sm" style={{ color: 'oklch(0.62 0.14 155)' }} />
|
||
}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ModuleBlock({ mod, selectedLesson, onSelect, onToggle }) {
|
||
const lessonCount = mod.lessons.length;
|
||
const totalMin = mod.lessons.reduce((s, l) => s + (parseInt(l.duration) || 0), 0);
|
||
return (
|
||
<div style={{ borderTop: '1px solid var(--line-soft)' }}>
|
||
<div onClick={() => onToggle(mod.id)}
|
||
style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '14px 18px', cursor: 'pointer', userSelect: 'none' }}>
|
||
<Icon name="chevron" className="ic-sm flipx"
|
||
style={{ transform: mod.expanded ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform .15s', color: 'var(--ink-3)' }} />
|
||
<Icon name="drag" className="ic-sm" style={{ color: 'var(--ink-4)' }} />
|
||
<div style={{ flex: 1 }}>
|
||
<div style={{ fontSize: 14, fontWeight: 600 }}>{mod.title}</div>
|
||
<div className="muted num" style={{ fontSize: 11.5, marginTop: 2 }}>
|
||
{lessonCount} درس · {totalMin > 0 ? `${totalMin} دقیقه` : 'بدون زمانبندی'}
|
||
</div>
|
||
</div>
|
||
<button className="btn ghost sm" onClick={(e) => e.stopPropagation()}>
|
||
<Icon name="more" className="ic-sm" />
|
||
</button>
|
||
</div>
|
||
{mod.expanded && (
|
||
<div style={{ padding: '0 14px 12px 14px', display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||
{mod.lessons.map(l => (
|
||
<LessonRow key={l.id} lesson={l} selected={l.id === selectedLesson} onSelect={onSelect} />
|
||
))}
|
||
<button className="btn ghost sm" style={{ alignSelf: 'flex-start', marginTop: 4, color: 'var(--ink-3)' }}>
|
||
<Icon name="plus" className="ic-sm" /> افزودن درس
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function LessonInspector({ lesson }) {
|
||
if (!lesson) return null;
|
||
const m = TYPE_META[lesson.type];
|
||
return (
|
||
<div className="col">
|
||
{/* preview frame */}
|
||
<div className="card" style={{ overflow: 'hidden' }}>
|
||
<div style={{ aspectRatio: '16/9', background: 'linear-gradient(135deg, oklch(0.30 0.02 60), oklch(0.22 0.02 60))', position: 'relative', display: 'grid', placeItems: 'center' }}>
|
||
<div style={{
|
||
width: 64, height: 64, borderRadius: '50%',
|
||
background: 'rgba(255,255,255,.95)',
|
||
display: 'grid', placeItems: 'center',
|
||
boxShadow: '0 8px 24px rgba(0,0,0,.3)'
|
||
}}>
|
||
<Icon name="play" style={{ color: 'var(--accent)', width: 26, height: 26 }} />
|
||
</div>
|
||
<div style={{ position: 'absolute', bottom: 14, insetInlineStart: 14, display: 'flex', gap: 8 }}>
|
||
<span className="pill" style={{ background: 'rgba(0,0,0,.5)', color: 'white', borderColor: 'transparent' }}>
|
||
<Icon name="clock" className="ic-sm" /> {lesson.duration}
|
||
</span>
|
||
<span className="pill" style={{ background: 'rgba(0,0,0,.5)', color: 'white', borderColor: 'transparent' }}>
|
||
{m.fa}
|
||
</span>
|
||
</div>
|
||
<div style={{ position: 'absolute', top: 14, insetInlineEnd: 14 }}>
|
||
<button className="btn sm" style={{ background: 'rgba(255,255,255,.92)' }}>
|
||
<Icon name="upload" className="ic-sm" /> جایگزینی فایل
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="card card-pad col">
|
||
<div>
|
||
<label className="muted" style={{ fontSize: 11.5, fontWeight: 500 }}>عنوان درس</label>
|
||
<input className="inp" defaultValue={lesson.title} style={{ marginTop: 6, fontSize: 14, fontWeight: 500 }} />
|
||
</div>
|
||
<div>
|
||
<label className="muted" style={{ fontSize: 11.5, fontWeight: 500 }}>توضیح کوتاه</label>
|
||
<textarea className="inp" rows={3} style={{ marginTop: 6, resize: 'vertical', fontFamily: 'inherit' }}
|
||
defaultValue="در این درس با اصول سلسلهمراتب بصری آشنا میشویم و چند نمونهی واقعی را با هم بررسی میکنیم." />
|
||
</div>
|
||
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--gap-md)' }}>
|
||
<div>
|
||
<label className="muted" style={{ fontSize: 11.5, fontWeight: 500 }}>فایل پیوست</label>
|
||
<div style={{ marginTop: 6, padding: '10px 12px', border: '1px dashed var(--line)', borderRadius: 'var(--r-sm)', display: 'flex', alignItems: 'center', gap: 10 }}>
|
||
<Icon name="filetext" className="ic-sm" style={{ color: 'var(--ink-3)' }} />
|
||
<span style={{ flex: 1, fontSize: 12.5 }}>تمرین-فصل-دوم.pdf</span>
|
||
<button className="btn ghost sm"><Icon name="trash" className="ic-sm" /></button>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label className="muted" style={{ fontSize: 11.5, fontWeight: 500 }}>وضعیت انتشار</label>
|
||
<div style={{ marginTop: 6, display: 'flex', gap: 6 }}>
|
||
<button className="btn sm" style={{ flex: 1, background: lesson.status === 'published' ? 'var(--accent-soft)' : 'var(--surface)', color: lesson.status === 'published' ? 'var(--accent-ink)' : 'var(--ink)', borderColor: lesson.status === 'published' ? 'transparent' : 'var(--line)' }}>منتشر شده</button>
|
||
<button className="btn sm" style={{ flex: 1, background: lesson.status === 'draft' ? 'var(--warn-soft)' : 'var(--surface)', color: lesson.status === 'draft' ? 'oklch(0.46 0.10 75)' : 'var(--ink)', borderColor: lesson.status === 'draft' ? 'transparent' : 'var(--line)' }}>پیشنویس</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="divider" />
|
||
|
||
<div className="between">
|
||
<span style={{ fontSize: 13, fontWeight: 600 }}>پیشنمایش رایگان</span>
|
||
<button className="btn sm" style={{ background: lesson.preview ? 'var(--accent)' : 'var(--surface)', color: lesson.preview ? 'white' : 'var(--ink-2)', borderColor: lesson.preview ? 'transparent' : 'var(--line)' }}>
|
||
{lesson.preview ? 'فعال' : 'غیرفعال'}
|
||
</button>
|
||
</div>
|
||
<div className="between">
|
||
<span style={{ fontSize: 13, fontWeight: 600 }}>دانلود برای دانشجویان</span>
|
||
<button className="btn sm">فعال</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function CoursesScreen() {
|
||
const { COURSES, MODULES } = window.DATA;
|
||
const [modules, setModules] = React.useState(MODULES);
|
||
const [selectedLesson, setSelectedLesson] = React.useState('l4');
|
||
const [tab, setTab] = React.useState('content');
|
||
|
||
const allLessons = modules.flatMap(m => m.lessons);
|
||
const lesson = allLessons.find(l => l.id === selectedLesson);
|
||
|
||
const totalLessons = allLessons.length;
|
||
const publishedLessons = allLessons.filter(l => l.status === 'published').length;
|
||
const totalMin = allLessons.reduce((s, l) => s + (parseInt(l.duration) || 0), 0);
|
||
|
||
const toggleMod = (id) => {
|
||
setModules(modules.map(m => m.id === id ? { ...m, expanded: !m.expanded } : m));
|
||
};
|
||
|
||
const course = COURSES[0];
|
||
|
||
return (
|
||
<div className="page-inner">
|
||
{/* course header */}
|
||
<div className="card card-pad">
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 18 }}>
|
||
<div style={{ width: 64, height: 64, borderRadius: 14, background: course.color, display: 'grid', placeItems: 'center', color: 'white', flexShrink: 0 }}>
|
||
<Icon name="book" className="ic-lg" />
|
||
</div>
|
||
<div style={{ flex: 1, minWidth: 0 }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
||
<span className="pill ok">منتشر شده</span>
|
||
<span className="muted en" style={{ fontSize: 11 }}>{course.titleEn}</span>
|
||
</div>
|
||
<h1 style={{ fontSize: 22, fontWeight: 700, margin: 0, letterSpacing: '-0.01em' }}>{course.title}</h1>
|
||
<div className="muted num" style={{ fontSize: 13, marginTop: 6 }}>
|
||
{course.students} دانشجو · {totalLessons} درس · {totalMin} دقیقه · ⭐ {course.rating}
|
||
</div>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 8 }}>
|
||
<button className="btn"><Icon name="eye" className="ic-sm" /> پیشنمایش</button>
|
||
<button className="btn"><Icon name="globe" className="ic-sm" /> مشاهده در سایت</button>
|
||
<button className="btn primary"><Icon name="check" className="ic-sm" /> ذخیره تغییرات</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="tabs" style={{ marginTop: 24, marginInline: -20, paddingInline: 20 }}>
|
||
{[['content','محتوا و درسها'], ['settings','تنظیمات'], ['pricing','قیمتگذاری'], ['marketing','صفحه فروش'], ['students-c','دانشجویان'], ['analytics-c','تحلیلها']].map(([k, v]) => (
|
||
<button key={k} className="tab" aria-selected={tab === k} onClick={() => setTab(k)}>{v}</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* progress strip */}
|
||
<div className="card card-pad" style={{ display: 'flex', alignItems: 'center', gap: 24, flexWrap: 'wrap' }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||
<div style={{ position: 'relative', width: 56, height: 56 }}>
|
||
<svg width="56" height="56" viewBox="0 0 56 56">
|
||
<circle cx="28" cy="28" r="24" fill="none" stroke="var(--line)" strokeWidth="6" />
|
||
<circle cx="28" cy="28" r="24" fill="none" stroke="var(--accent)" strokeWidth="6"
|
||
strokeDasharray={`${(publishedLessons/totalLessons) * 150.8} 150.8`}
|
||
strokeLinecap="round" transform="rotate(-90 28 28)" />
|
||
</svg>
|
||
<div className="num" style={{ position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', fontSize: 13, fontWeight: 700 }}>
|
||
{Math.round((publishedLessons/totalLessons) * 100)}٪
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style={{ fontSize: 14, fontWeight: 600 }}>کامل بودن دوره</div>
|
||
<div className="muted num" style={{ fontSize: 12, marginTop: 2 }}>
|
||
{publishedLessons} از {totalLessons} درس آماده انتشار است
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style={{ height: 36, width: 1, background: 'var(--line-soft)' }} />
|
||
<div style={{ display: 'flex', gap: 24, flex: 1 }}>
|
||
{[
|
||
{ k: 'محتوا', v: '۹۲٪', good: true },
|
||
{ k: 'صفحه فروش', v: '۱۰۰٪', good: true },
|
||
{ k: 'قیمتگذاری', v: '۱۰۰٪', good: true },
|
||
{ k: 'گواهی پایان', v: 'فعال', good: true },
|
||
].map(s => (
|
||
<div key={s.k}>
|
||
<div className="muted" style={{ fontSize: 11.5 }}>{s.k}</div>
|
||
<div className="num" style={{ fontSize: 14, fontWeight: 600, marginTop: 2, color: s.good ? 'oklch(0.50 0.14 155)' : 'var(--ink)' }}>{s.v}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<button className="btn"><Icon name="trend" className="ic-sm" /> پیشنهاد بهبود</button>
|
||
</div>
|
||
|
||
{/* main: tree + inspector */}
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: 'var(--gap-lg)', alignItems: 'flex-start' }}>
|
||
<div className="card">
|
||
<div className="card-h">
|
||
<div>
|
||
<h3>مسیر یادگیری</h3>
|
||
<div className="muted" style={{ fontSize: 12, marginTop: 2 }}>درسها را بکشید تا ترتیب را تغییر دهید</div>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 6 }}>
|
||
<button className="btn sm"><Icon name="plus" className="ic-sm" /> فصل جدید</button>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
{modules.map(mod => (
|
||
<ModuleBlock key={mod.id} mod={mod} selectedLesson={selectedLesson} onSelect={setSelectedLesson} onToggle={toggleMod} />
|
||
))}
|
||
<div style={{ borderTop: '1px solid var(--line-soft)', padding: 16, display: 'flex', gap: 8, justifyContent: 'center' }}>
|
||
<button className="btn ghost"><Icon name="plus" className="ic-sm" /> افزودن فصل جدید</button>
|
||
<button className="btn ghost"><Icon name="upload" className="ic-sm" /> آپلود گروهی فایل</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<LessonInspector lesson={lesson} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
window.CoursesScreen = CoursesScreen;
|