diff --git a/public/js/learningHub.js b/public/js/learningHub.js
index 6244363..9726848 100644
--- a/public/js/learningHub.js
+++ b/public/js/learningHub.js
@@ -10,7 +10,16 @@ import {
} from './learningHub/tiptapEditor.js';
import { esc, sanitizeHtml } from './learningHub/sanitize.js';
import { deleteJson, getJson, sendJson } from './learningHub/api.js';
+import {
+ renderCmsCategoryList,
+ renderCmsContentEmpty,
+ renderCmsContentRow,
+ renderOptionRowShell,
+ renderQuestionBlockShell
+} from './learningHub/cmsRenderer.js';
import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearchHeader } from './learningHub/feedRenderer.js';
+import { renderQuizQuestions, renderQuizResultExplanations } from './learningHub/quizRenderer.js';
+import { buildViewerMeta, renderPresentationCard, renderProgressList } from './learningHub/viewerRenderer.js';
var loaded = false;
var cmsLoaded = false;
@@ -126,6 +135,10 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch
var delCat = e.target.closest('.lh-cms-del-cat');
if (delCat) { deleteCategory(delCat.dataset.id); return; }
+ // CMS sidebar category filter
+ var cmsCat = e.target.closest('.lh-cms-cat-filter');
+ if (cmsCat && cmsCat.dataset.id) { filterCmsContentByCategory(cmsCat.dataset.id); return; }
+
// Remove question block
var rmQ = e.target.closest('.lh-rm-question');
if (rmQ) { var qb = rmQ.closest('.lh-question-block'); if (qb) { destroyQuestionBlockEditors(qb); qb.remove(); } return; }
@@ -278,14 +291,7 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch
var progressSection = document.getElementById('lh-progress-section');
if (titleEl) titleEl.textContent = item.title;
- if (metaEl) {
- var parts = [];
- if (item.category_name) parts.push(item.category_name);
- if (item.subject) parts.push(item.subject);
- if (item.author_name) parts.push('by ' + item.author_name);
- if (item.created_at) parts.push(new Date(item.created_at).toLocaleDateString());
- metaEl.textContent = parts.join(' | ');
- }
+ if (metaEl) metaEl.textContent = buildViewerMeta(item);
var presViewer = document.getElementById('lh-presentation-viewer');
var bodyCard = bodyEl ? bodyEl.closest('.card') : null;
@@ -293,17 +299,8 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch
// Hide body card, show presentation card with View Slides button
if (bodyCard) bodyCard.style.display = 'none';
if (presViewer) {
- var slideCount = (item.body || '').split(/\n---\n/).length;
presViewer.className = ''; // remove hidden
- presViewer.innerHTML =
- '
' +
- '
๐
' +
- '
' + esc(item.title) + '
' +
- '
' + slideCount + ' slides' + (item.subject ? ' ยท ' + esc(item.subject) : '') + '
' +
- '
' +
- '
';
+ presViewer.innerHTML = renderPresentationCard(item);
// Auto-load and open slides
document.getElementById('btn-lh-view-slides').addEventListener('click', function() {
@@ -341,15 +338,7 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch
// Progress
if (progressSection && item.progress && item.progress.length > 0) {
progressSection.classList.remove('hidden');
- var progHtml = item.progress.map(function(p) {
- var pct = p.total > 0 ? Math.round((p.score / p.total) * 100) : 0;
- var date = new Date(p.completed_at).toLocaleDateString();
- return '' +
- '' + date + '' +
- '' + p.score + '/' + p.total + ' (' + pct + '%)' +
- '
';
- }).join('');
- document.getElementById('lh-progress-list').innerHTML = progHtml;
+ document.getElementById('lh-progress-list').innerHTML = renderProgressList(item.progress);
} else if (progressSection) {
progressSection.classList.add('hidden');
}
@@ -361,32 +350,7 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch
if (!container) return;
if (countEl) countEl.textContent = questions.length + ' question' + (questions.length !== 1 ? 's' : '');
- container.innerHTML = questions.map(function(q, idx) {
- var isMulti = q.question_type === 'multi';
- var typeLabel = q.question_type === 'true_false' ? 'True / False'
- : isMulti ? 'Multiple Select' : 'Single Choice';
- var inputType = isMulti ? 'checkbox' : 'radio';
- var nameAttr = isMulti ? 'lh-qm-' + q.id : 'lh-q-' + q.id;
-
- var optionsHtml = (q.options || []).map(function(opt) {
- return '';
- }).join('');
-
- var hint = isMulti ? 'Select all that apply
' : '';
-
- return '' +
- '' +
- '
' + sanitizeHtml(q.question_text) + '
' +
- hint +
- '
' + optionsHtml + '
' +
- '
';
- }).join('');
+ container.innerHTML = renderQuizQuestions(questions);
}
function submitQuiz() {
@@ -432,24 +396,7 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch
}
if (explEl) {
- explEl.innerHTML = data.results.map(function(r, idx) {
- var icon = r.isCorrect ? '' : '';
- var explHtml = '';
- if (!r.isCorrect && r.selectedExplanation) {
- explHtml += 'Why incorrect: ' + esc(r.selectedExplanation) + '
';
- }
- if (!r.isCorrect) {
- explHtml += 'Correct answer: ' + esc(r.correctOptionText) + '
';
- }
- if (r.generalExplanation) {
- explHtml += 'Explanation: ' + sanitizeHtml(r.generalExplanation) + '
';
- }
-
- return '' +
- '' +
- explHtml +
- '
';
- }).join('');
+ explEl.innerHTML = renderQuizResultExplanations(data.results);
}
// Highlight correct/incorrect in quiz
@@ -882,7 +829,7 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch
opt.textContent = categoryName;
sel.appendChild(opt);
sel.value = String(data.id);
- loadCmsCategories();
+ loadCmsCategories(String(data.id));
loadCategories();
})
.catch(function(err) {
@@ -943,43 +890,46 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch
});
}
- function loadCmsCategories() {
+ function loadCmsCategories(editorCategoryId, filterCategoryId) {
getJson('/api/admin/learning/categories')
.then(function(data) {
if (!data.success) return;
var container = document.getElementById('lh-cms-categories');
if (!container) return;
- if (data.categories.length === 0) {
- container.innerHTML = 'No categories yet
';
- } else {
- container.innerHTML = data.categories.map(function(c) {
- return '' +
- '' + esc(c.name) + '' +
- '' + c.content_count + '' +
- '' +
- '
';
- }).join('');
- }
+ container.innerHTML = renderCmsCategoryList(data.categories);
// Update category dropdowns (editor + filter)
var sel = document.getElementById('lh-cms-edit-category');
if (sel) {
+ var selectedEditorId = editorCategoryId !== undefined ? editorCategoryId : sel.value;
sel.innerHTML = '';
data.categories.forEach(function(c) {
sel.innerHTML += '';
});
+ if (selectedEditorId && hasSelectValue(sel, selectedEditorId)) sel.value = selectedEditorId;
}
var filterSel = document.getElementById('cms-filter-category');
if (filterSel) {
+ var selectedFilterId = filterCategoryId !== undefined ? filterCategoryId : filterSel.value;
filterSel.innerHTML = '';
data.categories.forEach(function(c) {
filterSel.innerHTML += '';
});
+ if (selectedFilterId && hasSelectValue(filterSel, selectedFilterId)) filterSel.value = selectedFilterId;
}
+ setActiveCmsCat((document.getElementById('cms-filter-category') || {}).value || 'all');
});
}
+ function hasSelectValue(sel, value) {
+ value = String(value);
+ for (var i = 0; i < sel.options.length; i++) {
+ if (sel.options[i].value === value) return true;
+ }
+ return false;
+ }
+
function addCategory() {
var input = document.getElementById('lh-cms-cat-name');
if (!input || !input.value.trim()) { showToast('Enter category name', 'error'); return; }
@@ -1032,27 +982,11 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch
var container = document.getElementById('lh-cms-content-list');
if (!container) return;
if (!items || items.length === 0) {
- container.innerHTML = 'No content yet. Click "New Content" to get started.
';
+ container.innerHTML = renderCmsContentEmpty();
return;
}
- container.innerHTML = items.map(function(item) {
- var statusBadge = item.published
- ? 'Published'
- : 'Draft';
- var typeLabel = item.content_type === 'quiz' ? 'Quiz' : item.content_type === 'pearl' ? 'Pearl' : item.content_type === 'presentation' ? 'Slides' : 'Article';
- var qBadge = item.question_count > 0 ? ' ' + item.question_count + 'Q' : '';
- var date = item.updated_at ? new Date(item.updated_at).toLocaleDateString() : '';
-
- return '' +
- '' + esc(item.title) + qBadge + '
' + esc(item.subject || '') + '' +
- '' + esc(item.category_name || 'Uncategorized') + '' +
- '' + typeLabel + '' +
- '' + statusBadge + '' +
- '' + date + '' +
- '' +
- '
';
- }).join('');
+ container.innerHTML = items.map(renderCmsContentRow).join('');
}
// CMS search & filter
@@ -1060,9 +994,26 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch
if (e.target.id === 'cms-search') filterCmsContent();
});
document.addEventListener('change', function(e) {
- if (e.target.id === 'cms-filter-status' || e.target.id === 'cms-filter-category') filterCmsContent();
+ if (e.target.id === 'cms-filter-status' || e.target.id === 'cms-filter-category') {
+ if (e.target.id === 'cms-filter-category') setActiveCmsCat(e.target.value || 'all');
+ filterCmsContent();
+ }
});
+ function filterCmsContentByCategory(categoryId) {
+ var filterSel = document.getElementById('cms-filter-category');
+ if (filterSel) filterSel.value = String(categoryId);
+ setActiveCmsCat(categoryId);
+ filterCmsContent();
+ }
+
+ function setActiveCmsCat(categoryId) {
+ categoryId = String(categoryId || 'all');
+ document.querySelectorAll('.lh-cms-cat-filter').forEach(function(item) {
+ item.classList.toggle('active', categoryId !== 'all' && item.dataset.id === categoryId);
+ });
+ }
+
function filterCmsContent() {
var items = window._cmsContentData || [];
var search = (document.getElementById('cms-search') || {}).value || '';
@@ -1314,6 +1265,7 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch
showToast('Content saved', 'success');
closeEditor();
loadCmsContent();
+ loadCmsCategories();
loadCmsStats();
loaded = false; // force refresh user feed
});
@@ -1386,6 +1338,7 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch
showToast('Content deleted', 'info');
closeEditor();
loadCmsContent();
+ loadCmsCategories();
loadCmsStats();
loaded = false;
} else showToast(data.error || 'Failed', 'error');
@@ -1430,6 +1383,7 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch
}
function openSlideModal(slides, css) {
+ closeSlideModal();
_previewSlides = slides || [];
_previewIdx = 0;
@@ -1437,7 +1391,7 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch
var cssEl = document.getElementById('slide-preview-css');
var dotsEl = document.getElementById('slide-preview-dots');
- if (!modal) return;
+ if (!modal || !_previewSlides.length) { showToast('No slides to display', 'error'); return; }
// Inject Marp CSS once (scoped to preview frame)
if (cssEl) cssEl.textContent = css || '';
@@ -1499,6 +1453,14 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch
document.removeEventListener('keydown', document._slideKeyHandler);
document._slideKeyHandler = null;
}
+ if (modal && modal._touchStart) {
+ modal.removeEventListener('touchstart', modal._touchStart);
+ modal._touchStart = null;
+ }
+ if (modal && modal._touchEnd) {
+ modal.removeEventListener('touchend', modal._touchEnd);
+ modal._touchEnd = null;
+ }
}
// slide modal nav is wired via the main document click delegation below
@@ -1543,29 +1505,7 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch
if (existingQ && existingQ.id) block.dataset.questionId = existingQ.id;
var qNum = container.querySelectorAll('.lh-question-block').length + 1;
- block.innerHTML =
- '' +
- '
' +
- 'Q' + qNum + '' +
- '' +
- '' +
- '
' +
- '
' +
- '
' +
- '' +
- '' +
- '' +
- '
' +
- '' +
- '' +
- '' +
- '' +
- '' +
- '
';
+ block.innerHTML = renderQuestionBlockShell(existingQ, qNum);
container.appendChild(block);
@@ -1589,16 +1529,7 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch
if (!container) return;
var row = document.createElement('div');
row.className = 'lh-option-row';
- row.innerHTML =
- '' +
- '' +
- '' +
- '' +
- '
' +
- '' +
- '';
+ row.innerHTML = renderOptionRowShell(opt);
container.appendChild(row);
// Set option text value after insertion (can't use value="" on nested input via innerHTML)
row.querySelector('.lh-opt-text').value = opt ? opt.option_text || '' : '';
diff --git a/public/js/learningHub/cmsRenderer.js b/public/js/learningHub/cmsRenderer.js
new file mode 100644
index 0000000..c769901
--- /dev/null
+++ b/public/js/learningHub/cmsRenderer.js
@@ -0,0 +1,109 @@
+import { esc } from './sanitize.js';
+
+export function renderCmsCategoryList(categories) {
+ if (!categories || categories.length === 0) {
+ return 'No categories yet
';
+ }
+
+ return categories.map(function(c) {
+ return '' +
+ '' +
+ '' +
+ esc(c.name) +
+ '' +
+ '' + c.content_count + '' +
+ '' +
+ '
';
+ }).join('');
+}
+
+export function renderCmsContentEmpty() {
+ return '' +
+ '' +
+ 'No content yet. Click "New Content" to get started.' +
+ '
';
+}
+
+export function renderCmsContentRow(item) {
+ var statusBadge = item.published
+ ? 'Published'
+ : 'Draft';
+ var typeLabel = item.content_type === 'quiz' ? 'Quiz'
+ : item.content_type === 'pearl' ? 'Pearl'
+ : item.content_type === 'presentation' ? 'Slides'
+ : 'Article';
+ var qBadge = item.question_count > 0
+ ? ' ' + item.question_count + 'Q'
+ : '';
+ var date = item.updated_at ? new Date(item.updated_at).toLocaleDateString() : '';
+
+ return '' +
+ '' + esc(item.title) + qBadge +
+ '
' + esc(item.subject || '') + '' +
+ '' +
+ '' + esc(item.category_name || 'Uncategorized') + '' +
+ '' + typeLabel + '' +
+ '' + statusBadge + '' +
+ '' + date + '' +
+ '' +
+ '
';
+}
+
+export function renderQuestionBlockShell(existingQ, qNum) {
+ var qBadgeStyle = 'background:linear-gradient(135deg,var(--blue),var(--purple));color:white;' +
+ 'font-size:12px;font-weight:700;padding:3px 10px;border-radius:6px;';
+ var qTextStyle = 'width:100%;font-size:14px;font-weight:500;padding:10px;' +
+ 'border:1.5px solid var(--g300);border-radius:8px;box-sizing:border-box;' +
+ 'margin-bottom:8px;resize:vertical;font-family:inherit;';
+ var qExplStyle = 'width:100%;font-size:12px;padding:6px 10px;border:1px solid var(--g300);' +
+ 'border-radius:6px;box-sizing:border-box;resize:vertical;font-family:inherit;';
+
+ return '' +
+ '
' +
+ 'Q' + qNum + '' +
+ '' +
+ '' +
+ '
' +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '
';
+}
+
+export function renderOptionRowShell(opt) {
+ var optTextStyle = 'width:100%;font-size:13px;padding:6px 10px;border:1.5px solid var(--g300);' +
+ 'border-radius:6px;box-sizing:border-box;';
+ var optExplStyle = 'flex:1;font-size:12px;padding:6px 10px;border:1px solid var(--g300);' +
+ 'border-radius:6px;color:var(--g600);';
+
+ return '' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '' +
+ '';
+}
+
+function selectedQuestionType(existingQ, type) {
+ return existingQ && existingQ.question_type === type ? ' selected' : '';
+}
diff --git a/public/js/learningHub/feedRenderer.js b/public/js/learningHub/feedRenderer.js
index 9625bd7..97981ae 100644
--- a/public/js/learningHub/feedRenderer.js
+++ b/public/js/learningHub/feedRenderer.js
@@ -11,7 +11,10 @@ export function renderCategoryPills(categories) {
export function renderFeed(items, feedEl) {
if (!feedEl) return;
if (!items || items.length === 0) {
- feedEl.innerHTML = 'No content yet. Check back soon!
';
+ feedEl.innerHTML = '' +
+ '' +
+ 'No content yet. Check back soon!' +
+ '
';
return;
}
@@ -37,8 +40,14 @@ export function renderEmptySearchMessage() {
}
function renderFeedItem(item) {
- var typeIcon = item.content_type === 'quiz' ? 'fa-clipboard-question' : item.content_type === 'pearl' ? 'fa-gem' : item.content_type === 'presentation' ? 'fa-presentation-screen' : 'fa-file-alt';
- var typeColor = item.content_type === 'quiz' ? 'var(--amber)' : item.content_type === 'pearl' ? 'var(--purple, #8b5cf6)' : item.content_type === 'presentation' ? '#065f46' : 'var(--blue)';
+ var typeIcon = item.content_type === 'quiz' ? 'fa-clipboard-question'
+ : item.content_type === 'pearl' ? 'fa-gem'
+ : item.content_type === 'presentation' ? 'fa-presentation-screen'
+ : 'fa-file-alt';
+ var typeColor = item.content_type === 'quiz' ? 'var(--amber)'
+ : item.content_type === 'pearl' ? 'var(--purple, #8b5cf6)'
+ : item.content_type === 'presentation' ? '#065f46'
+ : 'var(--blue)';
var quizBadge = item.question_count > 0 ? ' ' + item.question_count + ' Q' : '';
var catBadge = item.category_name ? '' + esc(item.category_name) + '' : 'Uncategorized';
var date = item.created_at ? new Date(item.created_at).toLocaleDateString() : '';
diff --git a/public/js/learningHub/quizRenderer.js b/public/js/learningHub/quizRenderer.js
new file mode 100644
index 0000000..0df9607
--- /dev/null
+++ b/public/js/learningHub/quizRenderer.js
@@ -0,0 +1,63 @@
+import { esc, sanitizeHtml } from './sanitize.js';
+
+export function renderQuizQuestions(questions) {
+ return (questions || []).map(function(q, idx) {
+ var isMulti = q.question_type === 'multi';
+ var typeLabel = q.question_type === 'true_false' ? 'True / False'
+ : isMulti ? 'Multiple Select'
+ : 'Single Choice';
+ var inputType = isMulti ? 'checkbox' : 'radio';
+ var nameAttr = isMulti ? 'lh-qm-' + q.id : 'lh-q-' + q.id;
+ var hint = isMulti ? 'Select all that apply
' : '';
+
+ return '' +
+ '' +
+ '
' + sanitizeHtml(q.question_text) + '
' +
+ hint +
+ '
' + renderQuizOptions(q.options, inputType, nameAttr) + '
' +
+ '
';
+ }).join('');
+}
+
+export function renderQuizResultExplanations(results) {
+ return (results || []).map(function(r, idx) {
+ var icon = r.isCorrect
+ ? ''
+ : '';
+ var explHtml = '';
+ if (!r.isCorrect && r.selectedExplanation) {
+ explHtml += 'Why incorrect: ' +
+ esc(r.selectedExplanation) +
+ '
';
+ }
+ if (!r.isCorrect) {
+ explHtml += 'Correct answer: ' +
+ esc(r.correctOptionText) +
+ '
';
+ }
+ if (r.generalExplanation) {
+ explHtml += 'Explanation: ' +
+ sanitizeHtml(r.generalExplanation) +
+ '
';
+ }
+
+ return '' +
+ '' +
+ explHtml +
+ '
';
+ }).join('');
+}
+
+function renderQuizOptions(options, inputType, nameAttr) {
+ return (options || []).map(function(opt) {
+ return '';
+ }).join('');
+}
diff --git a/public/js/learningHub/viewerRenderer.js b/public/js/learningHub/viewerRenderer.js
new file mode 100644
index 0000000..7e2c53a
--- /dev/null
+++ b/public/js/learningHub/viewerRenderer.js
@@ -0,0 +1,42 @@
+import { esc } from './sanitize.js';
+
+export function buildViewerMeta(item) {
+ var parts = [];
+ if (item.category_name) parts.push(item.category_name);
+ if (item.subject) parts.push(item.subject);
+ if (item.author_name) parts.push('by ' + item.author_name);
+ if (item.created_at) parts.push(new Date(item.created_at).toLocaleDateString());
+ return parts.join(' | ');
+}
+
+export function renderPresentationCard(item) {
+ var slideCount = (item.body || '').split(/\n---\n/).length;
+ var slideButtonStyle = 'padding:11px 28px;font-size:14px;border-radius:10px;border:none;' +
+ 'cursor:pointer;background:var(--blue);color:white;font-weight:600;';
+
+ return '' +
+ '
๐
' +
+ '
' + esc(item.title) + '
' +
+ '
' +
+ slideCount + ' slides' + (item.subject ? ' ยท ' + esc(item.subject) : '') +
+ '
' +
+ '
' +
+ '
';
+}
+
+export function renderProgressList(progress) {
+ return (progress || []).map(function(p) {
+ var pct = p.total > 0 ? Math.round((p.score / p.total) * 100) : 0;
+ var date = new Date(p.completed_at).toLocaleDateString();
+ var color = pct >= 70 ? 'var(--green)' : 'var(--amber)';
+
+ return '' +
+ '' + date + '' +
+ '' +
+ p.score + '/' + p.total + ' (' + pct + '%)' +
+ '' +
+ '
';
+ }).join('');
+}
diff --git a/test/learning-hub-editor.test.js b/test/learning-hub-editor.test.js
index 93f37c6..4bd2776 100644
--- a/test/learning-hub-editor.test.js
+++ b/test/learning-hub-editor.test.js
@@ -9,9 +9,12 @@ function read(relativePath) {
const source = read('public/js/learningHub.js');
const apiModule = read('public/js/learningHub/api.js');
+const cmsRendererModule = read('public/js/learningHub/cmsRenderer.js');
const editorModule = read('public/js/learningHub/tiptapEditor.js');
const feedRendererModule = read('public/js/learningHub/feedRenderer.js');
+const quizRendererModule = read('public/js/learningHub/quizRenderer.js');
const sanitizeModule = read('public/js/learningHub/sanitize.js');
+const viewerRendererModule = read('public/js/learningHub/viewerRenderer.js');
test('Learning Hub destroys embedded Tiptap editors before removing CMS rows', () => {
assert.match(source, /from '\.\/learningHub\/tiptapEditor\.js'/);
@@ -57,3 +60,58 @@ test('Learning Hub feed renderers are isolated from API and editor flows', () =>
assert.doesNotMatch(feedRendererModule, /fetch\(|getJson|sendJson|deleteJson|ai-generate|FormData|window\.Tiptap/);
assert.doesNotMatch(source, /function renderFeed\(items, feedEl\)/);
});
+
+test('Learning Hub CMS renderers are isolated from API and editor flows', () => {
+ assert.match(source, /from '\.\/learningHub\/cmsRenderer\.js'/);
+ assert.match(cmsRendererModule, /export function renderCmsCategoryList\(categories\)/);
+ assert.match(cmsRendererModule, /export function renderCmsContentRow\(item\)/);
+ assert.match(cmsRendererModule, /export function renderQuestionBlockShell\(existingQ, qNum\)/);
+ assert.match(cmsRendererModule, /export function renderOptionRowShell\(opt\)/);
+ assert.match(cmsRendererModule, /lh-cms-cat-filter/);
+ assert.match(cmsRendererModule, /selectedQuestionType\(existingQ, type\)/);
+ assert.doesNotMatch(cmsRendererModule, /fetch\(|getJson|sendJson|deleteJson|ai-generate|FormData|window\.Tiptap/);
+});
+
+test('Learning Hub preserves AI-created category selection before save', () => {
+ assert.match(source, /function loadCmsCategories\(editorCategoryId, filterCategoryId\)/);
+ assert.match(source, /var selectedEditorId = editorCategoryId !== undefined \? editorCategoryId : sel\.value/);
+ assert.match(source, /if \(selectedEditorId && hasSelectValue\(sel, selectedEditorId\)\) sel\.value = selectedEditorId/);
+ assert.match(source, /loadCmsCategories\(String\(data\.id\)\)/);
+});
+
+test('Learning Hub CMS sidebar categories filter content list', () => {
+ assert.match(source, /filterCmsContentByCategory\(cmsCat\.dataset\.id\)/);
+ assert.match(source, /function filterCmsContentByCategory\(categoryId\)/);
+ assert.match(source, /function setActiveCmsCat\(categoryId\)/);
+ assert.match(source, /item\.classList\.toggle\('active'/);
+});
+
+test('Learning Hub refreshes CMS category counts after content changes', () => {
+ assert.match(source, /saveQuestions\(contentId, questions, 0, function\(\) \{[\s\S]*?loadCmsContent\(\);[\s\S]*?loadCmsCategories\(\);[\s\S]*?loadCmsStats\(\);/);
+ assert.match(source, /showToast\('Content deleted', 'info'\);[\s\S]*?loadCmsContent\(\);[\s\S]*?loadCmsCategories\(\);[\s\S]*?loadCmsStats\(\);/);
+});
+
+test('Learning Hub slide modal cleans up reusable input handlers', () => {
+ assert.match(source, /function openSlideModal\(slides, css\) \{\s*closeSlideModal\(\);/);
+ assert.match(source, /if \(!modal \|\| !_previewSlides\.length\) \{ showToast\('No slides to display', 'error'\); return; \}/);
+ assert.match(source, /modal\.removeEventListener\('touchstart', modal\._touchStart\)/);
+ assert.match(source, /modal\.removeEventListener\('touchend', modal\._touchEnd\)/);
+});
+
+test('Learning Hub quiz renderers are isolated from API and submit flow', () => {
+ assert.match(source, /from '\.\/learningHub\/quizRenderer\.js'/);
+ assert.match(quizRendererModule, /export function renderQuizQuestions\(questions\)/);
+ assert.match(quizRendererModule, /export function renderQuizResultExplanations\(results\)/);
+ assert.match(quizRendererModule, /sanitizeHtml\(q\.question_text\)/);
+ assert.match(quizRendererModule, /sanitizeHtml\(r\.generalExplanation\)/);
+ assert.doesNotMatch(quizRendererModule, /fetch\(|getJson|sendJson|deleteJson|submitQuiz|showLoading|currentContent/);
+});
+
+test('Learning Hub viewer renderers are isolated from API and slide loading', () => {
+ assert.match(source, /from '\.\/learningHub\/viewerRenderer\.js'/);
+ assert.match(viewerRendererModule, /export function buildViewerMeta\(item\)/);
+ assert.match(viewerRendererModule, /export function renderPresentationCard\(item\)/);
+ assert.match(viewerRendererModule, /export function renderProgressList\(progress\)/);
+ assert.match(viewerRendererModule, /btn-lh-view-slides/);
+ assert.doesNotMatch(viewerRendererModule, /fetch\(|getJson|sendJson|deleteJson|openSlidesFromSlug|showViewer/);
+});