Ilo/design/screen-courses.jsx
2026-05-02 20:01:30 +03:30

284 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;