Разработка конструктора курсов (Course Builder) на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка конструктора курсов (Course Builder) на сайте
Сложная
~2-4 недели
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Разработка конструктора курсов для LMS

Конструктор курсов — это интерфейс для преподавателей, где они создают структуру курса: добавляют разделы, уроки разных типов (видео, текст, тест, задание), настраивают порядок прохождения, условия разблокировки следующего урока. Ключевая задача — сделать это интуитивно без технических знаний.

Модель данных

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',  -- 'draft' | 'published' | 'archived'
  settings JSONB DEFAULT '{}',
  -- settings: { passingScore, allowSkip, sequentialMode, certificate }
  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,
  -- 'video' | 'text' | 'quiz' | 'assignment' | 'scorm' | 'live'
  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  -- { type: 'lesson_completed', lessonId: '...' }
);

API управления курсом

// Создание урока
app.post('/api/courses/:courseId/sections/:sectionId/lessons', authenticate, async (req, res) => {
  const { title, type, position } = req.body;

  // Дефолтный контент по типу урока
  const defaultContent: Record<string, unknown> = {
    video: { videoUrl: '', duration: 0, subtitles: [] },
    text: { body: '' },
    quiz: { questions: [], passingScore: 70, timeLimit: null },
    assignment: { description: '', maxScore: 100, dueDate: null, rubric: [] },
  };

  const lesson = await db.lessons.create({
    sectionId: req.params.sectionId,
    title,
    type,
    content: defaultContent[type] ?? {},
    position: position ?? await getNextPosition(req.params.sectionId),
  });

  res.json(lesson);
});

// Переупорядочивание уроков (drag-and-drop)
app.patch('/api/courses/:courseId/reorder', authenticate, async (req, res) => {
  const { sections } = req.body;
  // sections: [{ id, position, lessons: [{ id, position }] }]

  await db.transaction(async (trx) => {
    for (const section of sections) {
      await trx.query(
        'UPDATE course_sections SET position = $1 WHERE id = $2',
        [section.position, section.id]
      );
      for (const lesson of section.lessons) {
        await trx.query(
          'UPDATE lessons SET position = $1, section_id = $2 WHERE id = $3',
          [lesson.position, section.id, lesson.id]  // перемещение между разделами
        );
      }
    }
  });

  res.json({ ok: true });
});

React компонент конструктора

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>
  );
}

Редактор контента урока

Каждый тип урока имеет свой редактор:

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.

Сроки

Конструктор курсов с drag-and-drop, типами уроков и редакторами контента — 2–3 недели.