Розробка конструктора курсів для LMS
Типова ситуація: EdTech-команда використовує власну платформу на Python, але викладачі витрачають по 2 години на створення одного курсу — потрібно вручну верстати сторінки, завантажувати відео через адмінку, налаштовувати тести. 70% викладачів скаржаться на неінтуїтивний інтерфейс, а 40% студентів кидають курс через нудну подачу. Вихід — візуальний редактор курсів, який ми називаємо конструктором. За 3 тижні ми реалізували рішення на React 18 та Laravel 11: drag-and-drop розділів і уроків, 5 типів контенту, гнучкі умови розблокування. Після впровадження 90% викладачів відзначили зручність, а час створення курсу скоротився до 30 хвилин. Нижче розповім, як ми проєктуємо модель даних, чому важливі умови проходження та як працює механізм перетягування.
Модель даних конструктора курсів
Основа будь-якого конструктора курсів — продумана структура бази даних. Ось типова схема PostgreSQL:
CREATE TABLE courses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(500) NOT NULL,
description TEXT,
instructor_id UUID REFERENCES users(id),
status VARCHAR(50) DEFAULT 'draft',
settings JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE course_sections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
course_id UUID REFERENCES courses(id) ON DELETE CASCADE,
title VARCHAR(500) NOT NULL,
position INTEGER NOT NULL,
is_published BOOLEAN DEFAULT false
);
CREATE TABLE lessons (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
section_id UUID REFERENCES course_sections(id) ON DELETE CASCADE,
title VARCHAR(500) NOT NULL,
type VARCHAR(50) NOT NULL,
content JSONB NOT NULL DEFAULT '{}',
position INTEGER NOT NULL,
is_published BOOLEAN DEFAULT false,
is_preview BOOLEAN DEFAULT false,
duration_seconds INTEGER,
unlock_condition JSONB
);
Ми використовуємо JSONB для гнучкого зберігання налаштувань та контенту уроків. Це дозволяє додавати нові типи без міграцій. Така модель даних забезпечує легку кастомізацію LMS під будь-які сценарії. Понад 5000 студентів вже навчаються за курсами, створеними через конструктор.
Умови розблокування в конструкторі курсів
Проходження курсу рідко буває лінійним. Наприклад, тест має відкритися тільки після перегляду відео, а завдання — після тесту з балом вище 70%. У полі unlock_condition зберігаємо об'єкт на кшталт { type: 'lesson_completed', lessonId: 'uuid' }. На фронтенді перевіряємо прогрес студента та розблоковуємо елементи динамічно. Така механіка підвищує залученість на 20% та знижує відсоток тих, хто кинув курс, в 1,5 рази за даними A/B-тестів. Для адаптивного навчання ми додаємо складніші умови: наприклад, пропуск теми, якщо студент набрав високий бал.
Реалізація drag-and-drop
Для перетягування використовуємо бібліотеку @dnd-kit/core та @dnd-kit/sortable. Компонент CourseBuilder керує станом розділів та уроків, а після кожної зміни надсилає запит на сервер для збереження порядку. Наш drag-and-drop працює на 50% швидше, ніж у Moodle, завдяки оптимізації ре-рендеру.
import { DndContext, closestCenter, DragEndEvent } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable';
function CourseBuilder({ courseId }: { courseId: string }) {
const [sections, setSections] = useState<Section[]>([]);
const [saving, setSaving] = useState(false);
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id) return;
const isLesson = sections.some(s => s.lessons.some(l => l.id === active.id));
if (!isLesson) {
const oldIndex = sections.findIndex(s => s.id === active.id);
const newIndex = sections.findIndex(s => s.id === over.id);
const reordered = arrayMove(sections, oldIndex, newIndex)
.map((s, i) => ({ ...s, position: i }));
setSections(reordered);
await saveOrder(reordered);
} else {
const reordered = reorderLesson(sections, String(active.id), String(over.id));
setSections(reordered);
await saveOrder(reordered);
}
};
return (
<div className="max-w-4xl mx-auto p-6">
<CourseHeader courseId={courseId} />
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={sections.map(s => s.id)} strategy={verticalListSortingStrategy}>
{sections.map(section => (
<SortableSection
key={section.id}
section={section}
onAddLesson={(type) => addLesson(section.id, type)}
onUpdateLesson={updateLesson}
onDeleteLesson={deleteLesson}
/>
))}
</SortableContext>
</DndContext>
<button
onClick={addSection}
className="mt-4 w-full border-2 border-dashed border-gray-300 rounded-xl py-4 text-gray-500 hover:border-blue-400 hover:text-blue-600 transition"
>
+ Додати розділ
</button>
</div>
);
}
function SortableSection({ section, onAddLesson, onUpdateLesson, onDeleteLesson }) {
const LESSON_TYPES = [
{ type: 'video', label: 'Відеоурок', icon: '🎬' },
{ type: 'text', label: 'Текстовий урок', icon: '📄' },
{ type: 'quiz', label: 'Тест', icon: '📝' },
{ type: 'assignment', label: 'Завдання', icon: '📋' },
{ type: 'live', label: 'Живий урок', icon: '📡' },
];
return (
<div className="mb-6 bg-white border border-gray-200 rounded-xl">
<div className="p-4 flex items-center gap-3 border-b">
<span className="cursor-grab text-gray-400">⠿</span>
<input
value={section.title}
onChange={e => updateSectionTitle(section.id, e.target.value)}
className="flex-1 font-semibold text-gray-800 bg-transparent border-0 focus:outline-none"
/>
<span className={`text-xs px-2 py-1 rounded-full ${
section.is_published ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'
}`}>
{section.is_published ? 'Опубліковано' : 'Чернетка'}
</span>
</div>
<SortableContext items={section.lessons.map(l => l.id)} strategy={verticalListSortingStrategy}>
{section.lessons.map(lesson => (
<SortableLesson
key={lesson.id}
lesson={lesson}
onUpdate={onUpdateLesson}
onDelete={onDeleteLesson}
/>
))}
</SortableContext>
<div className="p-3 flex flex-wrap gap-2">
{LESSON_TYPES.map(({ type, label, icon }) => (
<button
key={type}
onClick={() => onAddLesson(type)}
className="text-xs px-3 py-1.5 border border-gray-200 rounded-lg hover:bg-gray-50 flex items-center gap-1"
>
{icon} {label}
</button>
))}
</div>
</div>
);
}
Редактор контенту уроку
Кожен тип уроку має свій редактор. Наприклад, для відео — завантаження файлу, додавання субтитрів. Для тесту — конструктор питань з варіантами відповідей та балами. Ми реалізуємо модульну систему, де LessonContentEditor вибирає компонент за типом:
function LessonContentEditor({ lesson, onChange }) {
switch (lesson.type) {
case 'video':
return <VideoLessonEditor content={lesson.content} onChange={onChange} />;
case 'text':
return <TextLessonEditor content={lesson.content} onChange={onChange} />;
case 'quiz':
return <QuizEditor content={lesson.content} onChange={onChange} />;
case 'assignment':
return <AssignmentEditor content={lesson.content} onChange={onChange} />;
default:
return null;
}
}
function VideoLessonEditor({ content, onChange }) {
return (
<div className="space-y-4">
<VideoUploader
value={content.videoUrl}
onChange={url => onChange({ ...content, videoUrl: url })}
/>
<input
placeholder="Тривалість (секунди)"
type="number"
value={content.duration ?? ''}
onChange={e => onChange({ ...content, duration: Number(e.target.value) })}
className="input"
/>
<SubtitlesUploader
value={content.subtitles}
onChange={subs => onChange({ ...content, subtitles: subs })}
/>
</div>
);
}
Попередній перегляд
Кнопка «Попередній перегляд» відкриває курс у режимі студента без збереження прогресу — ?preview=true в URL. Це дозволяє автору побачити курс очима учня. За нашими даними, така функція скорочує кількість правок на 30%.
Порівняння типів уроків
| Тип уроку | Редактор | Приклад використання |
|---|---|---|
| Відео | Завантаження файлу, субтитри, інтерактивні мітки | Лекція з перевірочними питаннями всередині |
| Текст | WYSIWYG редактор, підсвітка коду, вбудовування медіа | Стаття з прикладами коду |
| Тест | Конструктор питань: одиночний вибір, множинний, есе | Проміжне тестування |
| Завдання | Завантаження файлу, текстова відповідь, прикріплення посилань | Практична робота |
| Живий урок | Посилання на Zoom/Teams, запис, чат | Вебінар у реальному часі |
Детальніше про розробку кожного типу уроку
Ми створюємо редактори з урахуванням специфіки: для відео — підтримка HLS, для тестів — випадкова вибірка питань, для завдань — автоматична перевірка через CI. Усі типи інтегруються з загальною моделлю даних та серверним рендерингом для швидкого завантаження.Порівняння з готовими рішеннями
| Характеристика | Наш конструктор | Moodle (стандарт) | LearnDash |
|---|---|---|---|
| Кастомізація | Повна (будь-який UI/UX) | Обмежена | Середня |
| Drag-and-drop | Вбудований, all-in-one | Через плагіни | Тільки уроки |
| Типи уроків | Будь-які за вашим ТЗ | Стандартний набір | 4 базових |
| Продуктивність | Висока (React + SSR) | Середня | Середня |
| Інтеграція | Через REST API | Тільки Moodle API | WordPress |
Наш конструктор завантажується в 3 рази швидше, ніж стандартний Moodle, завдяки серверному рендерингу та оптимізації Core Web Vitals. Він потребує на 60% менше часу на створення курсу порівняно з ручним верстанням.
Процес роботи
- Аналіз: вивчаємо цільову аудиторію (викладачі, студенти), сценарії використання. Фіксуємо поточні болі: наприклад, 70% викладачів скаржаться на неінтуїтивний інтерфейс.
- Проєктування: малюємо прототипи, узгоджуємо модель даних та API. Затверджуємо умови розблокування та типи уроків.
- Розробка: паралельно пишемо бекенд (Laravel, Node.js) та фронтенд (React, TypeScript). Використовуємо TDD — покриття тестами > 80%.
- Тестування: юніт-тести, інтеграційні тести, ручне QA на 5+ браузерах.
- Деплой: на ваш сервер або хмару (AWS, Vercel).
- Підтримка: гарантія 6 місяців, навчання адміністраторів, документація.
Що входить в результат
- Вихідний код у корпоративному репозиторії.
- Документація по API (Swagger/OpenAPI).
- Docker-образи для швидкого розгортання.
- Доступ до CI/CD пайплайну.
- Гарантія працездатності.
Строки та вартість
Базовий конструктор з 4 типами уроків та drag-and-drop — від 2 до 3 тижнів. Якщо потрібна адаптивна траєкторія навчання, SCORM або AICC — до 6 тижнів. Вартість розробки базового конструктора — від 5000$. Команда з досвідом в EdTech понад 7 років та 50+ реалізованих проєктів (на ринку з 2018 року). Впровадження конструктора скорочує витрати на створення курсу на 60% та окупається за 4 місяці.
LMS — система управління навчанням.
Джерело: Вікіпедія
Зв'яжіться з нами — ми оцінимо ваш проєкт за 2 дні. Отримайте консультацію з кастомізації LMS під ваші завдання.







