feat: textbooks — модуль учебников + чтение как ДЗ (3 фазы)

Фаза 1 — структура и каталог:
  - frontend/textbooks/chemistry_9.html (Шиманович, 60 §) + physics_9.html (Исаченкова, 38 §)
  - frontend/textbooks.html — каталог в стиле LearnSpace (карточки с обложками)
  - Маршруты: /textbooks (каталог), /textbook/<slug> (полноэкранный учебник)
  - Сайдбар: пункт «Учебники» (book-open-text)
  - Feature flag feature_textbooks_enabled, hideDisabledFeatures map

Фаза 2 — прогресс в localStorage + UI чтения:
  - frontend/js/textbook-tracker.js — инжектится в каждый учебник:
    - «← Учебники» overlay-кнопка (top-left, semi-transparent)
    - «Прочитано» чекбокс рядом с каждым §-заголовком
    - Зелёный dot на pill уже прочитанных параграфов
    - Авто-открытие последнего параграфа при возврате
  - Каталог показывает прогресс-бар «X из Y прочитано» + кнопку «Продолжить»

Фаза 3 — серверный прогресс + назначение чтения как ДЗ:
  - Таблица textbooks (slug, subject, grade, title, author, color, ...)
  - Таблица textbook_progress (user_id, textbook_id, JSON read[], last_para)
  - Колонки assignments.textbook_id + textbook_paragraphs
  - API: GET /api/textbooks (с прогрессом), GET /:slug, POST /:slug/progress,
    GET /:slug/class-progress (учитель)
  - tracker.js синхронизирует прогресс через POST /progress (если залогинен)
  - На каталоге у учителей кнопка «Назначить чтение» — модалка с выбором
    классов + параграфы («1-5» или «1,3,5») + deadline
  - bulkCreateAssignment расширен: принимает textbook_slug, резолвит в id

Миграция 004 идемпотентная; сиды двух учебников включены.
This commit is contained in:
Maxim Dolgolyov
2026-05-16 14:05:19 +03:00
parent 31a51956b6
commit e8018d85c1
10 changed files with 23974 additions and 4 deletions
+19
View File
@@ -51,6 +51,7 @@ const collectionRoutes = require('./routes/collection');
const redBookRoutes = require('./routes/red-book');
const parentRoutes = require('./routes/parent');
const exam9Routes = require('./routes/exam9');
const textbookRoutes = require('./routes/textbooks');
const { requestId, errorHandler } = require('./middleware/errorHandler');
@@ -168,6 +169,7 @@ app.use('/api/red-book', redBookRoutes);
app.use('/api/biochem', require('./routes/biochem'));
app.use('/api/parent', parentRoutes);
app.use('/api/exam9', exam9Routes);
app.use('/api/textbooks', textbookRoutes);
/* ── Public features endpoint (merges global + per-class for authenticated students) ── */
const _featDb = require('./db/db');
@@ -318,6 +320,23 @@ app.use((req, res, next) => {
next();
});
// Clean URL for textbooks: /textbook/<slug> → frontend/textbooks/<html_path>
const _textbookDb = require('./db/db');
const _stmtTextbookPath = _textbookDb.prepare('SELECT html_path FROM textbooks WHERE slug=? AND is_active=1');
app.get('/textbook/:slug', (req, res, next) => {
const row = _stmtTextbookPath.get(req.params.slug);
if (!row) return next();
const filePath = path.join(frontendDir, 'textbooks', row.html_path);
if (!isProd) res.setHeader('Cache-Control', 'no-store');
res.sendFile(filePath, err => { if (err) next(); });
});
// Catalog: /textbooks → frontend/textbooks.html (explicit to avoid conflict with /textbooks/ directory)
app.get('/textbooks', (_req, res) => {
if (!isProd) res.setHeader('Cache-Control', 'no-store');
res.sendFile(path.join(frontendDir, 'textbooks.html'));
});
// Serve HTML files without extension (/dashboard → dashboard.html)
// In dev: disable cache so edits are always picked up immediately
const htmlCacheOpts = isProd ? { extensions: ['html'] } : {