From 7858484cb0532e1ff76a647fda5fcfcb64ab9f5f Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 8 May 2026 15:47:10 +0200 Subject: [PATCH] refactor learning hub renderers --- public/js/learningHub.js | 205 ++++++++---------------- public/js/learningHub/cmsRenderer.js | 109 +++++++++++++ public/js/learningHub/feedRenderer.js | 15 +- public/js/learningHub/quizRenderer.js | 63 ++++++++ public/js/learningHub/viewerRenderer.js | 42 +++++ test/learning-hub-editor.test.js | 58 +++++++ 6 files changed, 352 insertions(+), 140 deletions(-) create mode 100644 public/js/learningHub/cmsRenderer.js create mode 100644 public/js/learningHub/quizRenderer.js create mode 100644 public/js/learningHub/viewerRenderer.js 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 '
' + - '
' + - 'Q' + (idx + 1) + '' + - '' + typeLabel + '' + - '
' + - '

' + 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 '
' + - '
' + icon + ' Q' + (idx + 1) + ': ' + sanitizeHtml(r.questionText) + '
' + - 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 '
' + + '
' + + 'Q' + (idx + 1) + '' + + '' + typeLabel + '' + + '
' + + '

' + 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 '
' + + '
' + + icon + ' Q' + (idx + 1) + ': ' + sanitizeHtml(r.questionText) + + '
' + + 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/); +});