// ============================================================ // LEARNING HUB — User content feed, viewer, quizzes + Admin CMS // ============================================================ (function() { var loaded = false; var cmsLoaded = false; var currentContent = null; var currentView = 'feed'; // 'feed' | 'category' | 'viewer' // ── Load when tab activated ──────────────────────────────── document.addEventListener('tabChanged', function(e) { if (e.detail && e.detail.tab === 'learning') { if (!loaded) { loadFeed(); loadCategories(); loaded = true; } } // Load CMS when Content Manager tab is opened if (e.detail && e.detail.tab === 'cms') { if (!cmsLoaded) { loadCms(); cmsLoaded = true; } } }); // ── Event delegation ─────────────────────────────────────── document.addEventListener('click', function(e) { // Feed item click var feedItem = e.target.closest('.lh-feed-item'); if (feedItem && feedItem.dataset.slug) { loadContent(feedItem.dataset.slug); return; } // Category pill click var catPill = e.target.closest('.lh-cat-pill'); if (catPill && catPill.dataset.slug) { if (catPill.dataset.slug === 'all') { loadFeed(); setActiveCatPill('all'); } else { loadCategoryContent(catPill.dataset.slug); setActiveCatPill(catPill.dataset.slug); } return; } // Back button if (e.target.closest('#lh-back')) { showFeed(); return; } // Submit quiz if (e.target.closest('#lh-submit-quiz')) { submitQuiz(); return; } // ── CMS events ───────────────────────────── if (e.target.closest('#btn-lh-add-cat')) { addCategory(); return; } if (e.target.closest('#btn-lh-new-content')) { openEditor(null, 'article'); return; } if (e.target.closest('#btn-lh-new-quiz')) { openEditor(null, 'quiz'); return; } if (e.target.closest('#btn-lh-new-pearl')) { openEditor(null, 'pearl'); return; } if (e.target.closest('#btn-lh-close-editor')) { closeEditor(); return; } if (e.target.closest('#btn-lh-save-content')) { saveContent(); return; } if (e.target.closest('#btn-lh-delete-content')) { deleteContent(); return; } if (e.target.closest('#btn-lh-add-question')) { addQuestionBlock(); return; } // Editor toolbar buttons var tbBtn = e.target.closest('.cms-tb-btn'); if (tbBtn && tbBtn.dataset.insert) { insertTag(tbBtn.dataset.insert); return; } // CMS content item edit var cmsItem = e.target.closest('.lh-cms-content-item'); if (cmsItem && cmsItem.dataset.id) { openEditor(cmsItem.dataset.id); return; } // CMS delete category var delCat = e.target.closest('.lh-cms-del-cat'); if (delCat) { deleteCategory(delCat.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) qb.remove(); return; } // Add option to question var addOpt = e.target.closest('.lh-add-option'); if (addOpt) { var qBlock = addOpt.closest('.lh-question-block'); if (qBlock) addOptionRow(qBlock.querySelector('.lh-options-list'), null); return; } // Remove option var rmOpt = e.target.closest('.lh-rm-option'); if (rmOpt) { var row = rmOpt.closest('.lh-option-row'); if (row) row.remove(); return; } }); // Search var searchTimeout = null; var searchEl = document.getElementById('lh-search'); if (searchEl) { searchEl.addEventListener('input', function() { clearTimeout(searchTimeout); var q = searchEl.value.trim(); if (!q) { loadFeed(); return; } searchTimeout = setTimeout(function() { searchContent(q); }, 300); }); } // ============================================================ // USER-FACING: FEED, CATEGORIES, CONTENT, QUIZZES // ============================================================ function loadFeed() { var feedEl = document.getElementById('lh-feed'); if (!feedEl) return; feedEl.innerHTML = '

Loading...

'; showFeed(); fetch('/api/learning/feed', { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) { feedEl.innerHTML = '

Failed to load

'; return; } renderFeed(data.content, feedEl); }) .catch(function() { feedEl.innerHTML = '

Connection error

'; }); } function loadCategories() { fetch('/api/learning/categories', { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) return; var bar = document.getElementById('lh-categories'); if (!bar) return; var html = ''; data.categories.forEach(function(c) { html += ''; }); bar.innerHTML = html; }); } function loadCategoryContent(slug) { var feedEl = document.getElementById('lh-feed'); if (!feedEl) return; feedEl.innerHTML = '

Loading...

'; showFeed(); fetch('/api/learning/category/' + encodeURIComponent(slug), { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) { feedEl.innerHTML = '

Category not found

'; return; } renderFeed(data.content, feedEl); }); } function searchContent(q) { var feedEl = document.getElementById('lh-feed'); if (!feedEl) return; showFeed(); fetch('/api/learning/search?q=' + encodeURIComponent(q), { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) return; renderFeed(data.content, feedEl); }); } function renderFeed(items, feedEl) { if (!items || items.length === 0) { feedEl.innerHTML = '
No content yet. Check back soon!
'; return; } feedEl.innerHTML = items.map(function(item) { var typeIcon = item.content_type === 'quiz' ? 'fa-clipboard-question' : item.content_type === 'pearl' ? 'fa-gem' : 'fa-file-alt'; var typeColor = item.content_type === 'quiz' ? 'var(--amber)' : item.content_type === 'pearl' ? 'var(--purple, #8b5cf6)' : '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() : ''; return '
' + '
' + '' + '
' + '
' + esc(item.title) + '
' + '
' + (item.subject ? '' + esc(item.subject) + '' : '') + (item.author_name ? 'by ' + esc(item.author_name) + '' : '') + '' + date + '' + '
' + '
' + '
' + catBadge + quizBadge + '
' + '
' + '
'; }).join(''); } function loadContent(slug) { fetch('/api/learning/content/' + encodeURIComponent(slug), { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) { showToast(data.error || 'Content not found', 'error'); return; } currentContent = data.content; showViewer(data.content); }) .catch(function() { showToast('Failed to load content', 'error'); }); } function showFeed() { currentView = 'feed'; var feedEl = document.getElementById('lh-feed'); var viewerEl = document.getElementById('lh-viewer'); var catsEl = document.getElementById('lh-categories'); var searchWrap = document.getElementById('lh-search'); if (feedEl) feedEl.classList.remove('hidden'); if (viewerEl) viewerEl.classList.add('hidden'); if (catsEl) catsEl.classList.remove('hidden'); if (searchWrap) searchWrap.closest('.card').classList.remove('hidden'); } function showViewer(item) { currentView = 'viewer'; var feedEl = document.getElementById('lh-feed'); var viewerEl = document.getElementById('lh-viewer'); var catsEl = document.getElementById('lh-categories'); var searchWrap = document.getElementById('lh-search'); if (feedEl) feedEl.classList.add('hidden'); if (viewerEl) viewerEl.classList.remove('hidden'); if (catsEl) catsEl.classList.add('hidden'); if (searchWrap) searchWrap.closest('.card').classList.add('hidden'); var titleEl = document.getElementById('lh-viewer-title'); var metaEl = document.getElementById('lh-viewer-meta'); var bodyEl = document.getElementById('lh-viewer-body'); var quizSection = document.getElementById('lh-quiz-section'); var resultsSection = document.getElementById('lh-quiz-results'); 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 (bodyEl) { // Render body as HTML (admin creates it) bodyEl.innerHTML = sanitizeHtml(item.body || ''); } // Quiz if (quizSection) { if (item.questions && item.questions.length > 0) { quizSection.classList.remove('hidden'); renderQuiz(item.questions); } else { quizSection.classList.add('hidden'); } } if (resultsSection) resultsSection.classList.add('hidden'); // 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; } else if (progressSection) { progressSection.classList.add('hidden'); } } function renderQuiz(questions) { var container = document.getElementById('lh-quiz-questions'); var countEl = document.getElementById('lh-quiz-count'); if (!container) return; if (countEl) countEl.textContent = questions.length + ' question' + (questions.length !== 1 ? 's' : ''); container.innerHTML = questions.map(function(q, idx) { var typeLabel = q.question_type === 'true_false' ? 'True / False' : 'Multiple Choice'; var optionsHtml = (q.options || []).map(function(opt) { return ''; }).join(''); return '
' + '
' + 'Q' + (idx + 1) + '' + '' + typeLabel + '' + '
' + '

' + esc(q.question_text) + '

' + '
' + optionsHtml + '
' + '
'; }).join(''); } function submitQuiz() { if (!currentContent || !currentContent.questions) return; var answers = []; currentContent.questions.forEach(function(q) { var selected = document.querySelector('input[name="lh-q-' + q.id + '"]:checked'); answers.push({ questionId: q.id, optionId: selected ? parseInt(selected.value) : null }); }); showLoading('Submitting quiz...'); fetch('/api/learning/submit-quiz', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ contentId: currentContent.id, answers: answers }) }) .then(function(r) { return r.json(); }) .then(function(data) { hideLoading(); if (!data.success) { showToast(data.error || 'Submit failed', 'error'); return; } showQuizResults(data); }) .catch(function() { hideLoading(); showToast('Submit failed', 'error'); }); } function showQuizResults(data) { var resultsEl = document.getElementById('lh-quiz-results'); var scoreEl = document.getElementById('lh-quiz-score'); var explEl = document.getElementById('lh-quiz-explanations'); if (!resultsEl) return; resultsEl.classList.remove('hidden'); var pct = data.percentage; var color = pct >= 80 ? 'var(--green)' : pct >= 50 ? 'var(--amber)' : 'var(--red)'; if (scoreEl) { scoreEl.textContent = data.score + '/' + data.total + ' (' + pct + '%)'; scoreEl.style.background = color; scoreEl.style.color = 'white'; } 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: ' + esc(r.generalExplanation) + '
'; } return '
' + '
' + icon + ' Q' + (idx + 1) + ': ' + esc(r.questionText) + '
' + explHtml + '
'; }).join(''); } // Highlight correct/incorrect in quiz data.results.forEach(function(r) { var qDiv = document.querySelector('.lh-quiz-q[data-qid="' + r.questionId + '"]'); if (!qDiv) return; qDiv.querySelectorAll('.lh-quiz-option').forEach(function(label) { var input = label.querySelector('input'); if (!input) return; var optId = parseInt(input.value); if (optId === r.correctOptionId) label.classList.add('lh-opt-correct'); if (optId === r.selectedOptionId && !r.isCorrect) label.classList.add('lh-opt-wrong'); input.disabled = true; }); }); // Disable submit button var submitBtn = document.getElementById('lh-submit-quiz'); if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = ' Submitted'; } resultsEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); } function setActiveCatPill(slug) { document.querySelectorAll('.lh-cat-pill').forEach(function(p) { p.classList.toggle('active', p.dataset.slug === slug); }); } // ============================================================ // ADMIN CMS // ============================================================ function loadCms() { loadCmsCategories(); loadCmsContent(); loadCmsStats(); } function loadCmsStats() { fetch('/api/admin/learning/stats', { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) return; var s = data.stats; var set = function(id, val) { var el = document.getElementById(id); if (el) el.textContent = val; }; set('cms-stat-published', s.publishedContent); set('cms-stat-drafts', s.totalContent - s.publishedContent); set('cms-stat-categories', s.totalCategories); set('cms-stat-quizzes', s.totalQuizzes); set('cms-stat-attempts', s.totalAttempts); }); } function loadCmsCategories() { fetch('/api/admin/learning/categories', { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .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(''); } // Update category dropdowns (editor + filter) var sel = document.getElementById('lh-cms-edit-category'); if (sel) { sel.innerHTML = ''; data.categories.forEach(function(c) { sel.innerHTML += ''; }); } var filterSel = document.getElementById('cms-filter-category'); if (filterSel) { filterSel.innerHTML = ''; data.categories.forEach(function(c) { filterSel.innerHTML += ''; }); } }); } function addCategory() { var input = document.getElementById('lh-cms-cat-name'); if (!input || !input.value.trim()) { showToast('Enter category name', 'error'); return; } fetch('/api/admin/learning/categories', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ name: input.value.trim() }) }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { input.value = ''; loadCmsCategories(); loadCategories(); // refresh user-facing pills too showToast('Category created', 'success'); } else showToast(data.error || 'Failed', 'error'); }) .catch(function() { showToast('Request failed', 'error'); }); } function deleteCategory(id) { if (!confirm('Delete this category? Content will become uncategorized.')) return; fetch('/api/admin/learning/categories/' + id, { method: 'DELETE', headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { loadCmsCategories(); loadCategories(); showToast('Category deleted', 'info'); } else showToast(data.error || 'Failed', 'error'); }); } function loadCmsContent() { var container = document.getElementById('lh-cms-content-list'); if (!container) return; container.innerHTML = '

Loading...

'; fetch('/api/admin/learning/content', { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) return; if (data.content.length === 0) { container.innerHTML = '

No content yet. Click "New Content" to create.

'; return; } window._cmsContentData = data.content; renderCmsContentList(data.content); }); } function renderCmsContentList(items) { 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.
'; 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' : '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(''); } // CMS search & filter document.addEventListener('input', function(e) { 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(); }); function filterCmsContent() { var items = window._cmsContentData || []; var search = (document.getElementById('cms-search') || {}).value || ''; var statusFilter = (document.getElementById('cms-filter-status') || {}).value || 'all'; var catFilter = (document.getElementById('cms-filter-category') || {}).value || 'all'; search = search.toLowerCase().trim(); var filtered = items.filter(function(item) { if (search && item.title.toLowerCase().indexOf(search) === -1 && (item.subject || '').toLowerCase().indexOf(search) === -1) return false; if (statusFilter === 'published' && !item.published) return false; if (statusFilter === 'draft' && item.published) return false; if (catFilter !== 'all' && String(item.category_id) !== catFilter) return false; return true; }); renderCmsContentList(filtered); } // Editor toolbar — insert HTML tags at cursor in textarea function insertTag(tag) { var ta = document.getElementById('lh-cms-edit-body'); if (!ta) return; var start = ta.selectionStart, end = ta.selectionEnd; var sel = ta.value.substring(start, end); var insert = ''; switch(tag) { case 'b': case 'i': case 'u': case 'code': case 'h2': case 'h3': insert = '<' + tag + '>' + (sel || 'text') + ''; break; case 'a': var url = prompt('Enter URL:'); if (!url) return; insert = '' + (sel || 'link text') + ''; break; case 'ul': insert = ''; break; case 'ol': insert = '
    \n
  1. ' + (sel || 'item') + '
  2. \n
  3. \n
'; break; case 'blockquote': insert = '
' + (sel || 'quote') + '
'; break; case 'table': insert = '\n\n\n
Header 1Header 2
' + (sel || 'data') + '
'; break; case 'hr': insert = '
'; break; default: return; } ta.value = ta.value.substring(0, start) + insert + ta.value.substring(end); ta.focus(); ta.selectionStart = ta.selectionEnd = start + insert.length; } function openEditor(contentId, contentType) { var editor = document.getElementById('lh-cms-editor'); var listView = document.getElementById('cms-list-view'); if (!editor) return; editor.classList.remove('hidden'); if (listView) listView.classList.add('hidden'); // Reset document.getElementById('lh-cms-edit-id').value = ''; document.getElementById('lh-cms-edit-title').value = ''; document.getElementById('lh-cms-edit-subject').value = ''; document.getElementById('lh-cms-edit-body').value = ''; document.getElementById('lh-cms-edit-category').value = ''; document.getElementById('lh-cms-edit-type').value = contentType || 'article'; document.getElementById('lh-cms-edit-published').value = 'false'; document.getElementById('lh-cms-questions').innerHTML = ''; document.getElementById('btn-lh-delete-content').classList.add('hidden'); // For quiz type, add a starter question if (contentType === 'quiz' && !contentId) { addQuestionBlock(); } if (!contentId) return; // Load existing content fetch('/api/admin/learning/content/' + contentId, { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) { showToast('Failed to load', 'error'); return; } var c = data.content; document.getElementById('lh-cms-edit-id').value = c.id; document.getElementById('lh-cms-edit-title').value = c.title; document.getElementById('lh-cms-edit-subject').value = c.subject || ''; document.getElementById('lh-cms-edit-body').value = c.body || ''; document.getElementById('lh-cms-edit-category').value = c.category_id || ''; document.getElementById('lh-cms-edit-type').value = c.content_type || 'article'; document.getElementById('lh-cms-edit-published').value = c.published ? 'true' : 'false'; document.getElementById('btn-lh-delete-content').classList.remove('hidden'); // Load questions var qContainer = document.getElementById('lh-cms-questions'); qContainer.innerHTML = ''; if (c.questions && c.questions.length > 0) { c.questions.forEach(function(q) { addQuestionBlock(q); }); } editor.scrollIntoView({ behavior: 'smooth' }); }); } function closeEditor() { var editor = document.getElementById('lh-cms-editor'); var listView = document.getElementById('cms-list-view'); if (editor) editor.classList.add('hidden'); if (listView) listView.classList.remove('hidden'); } function saveContent() { var id = document.getElementById('lh-cms-edit-id').value; var title = document.getElementById('lh-cms-edit-title').value.trim(); var subject = document.getElementById('lh-cms-edit-subject').value.trim(); var body = document.getElementById('lh-cms-edit-body').value; var category_id = document.getElementById('lh-cms-edit-category').value || null; var content_type = document.getElementById('lh-cms-edit-type').value; var published = document.getElementById('lh-cms-edit-published').value === 'true'; if (!title) { showToast('Title required', 'error'); return; } // Gather questions with validation var questions = []; var validationError = false; var qBlocks = document.querySelectorAll('#lh-cms-questions .lh-question-block'); for (var qi = 0; qi < qBlocks.length; qi++) { var qBlock = qBlocks[qi]; var qText = qBlock.querySelector('.lh-q-text').value.trim(); if (!qText) continue; var qType = qBlock.querySelector('.lh-q-type').value; var qExpl = qBlock.querySelector('.lh-q-explanation').value.trim(); var qId = qBlock.dataset.questionId || null; var options = []; qBlock.querySelectorAll('.lh-option-row').forEach(function(row) { var optText = row.querySelector('.lh-opt-text').value.trim(); if (!optText) return; options.push({ option_text: optText, is_correct: row.querySelector('.lh-opt-correct').checked, explanation: row.querySelector('.lh-opt-expl').value.trim() }); }); // Validate: must have a correct answer marked var hasCorrect = options.some(function(o) { return o.is_correct; }); if (options.length > 0 && !hasCorrect) { showToast('Question "' + qText.substring(0, 50) + '..." needs a correct answer marked', 'error'); qBlock.style.outline = '2px solid var(--red)'; qBlock.scrollIntoView({ behavior: 'smooth', block: 'center' }); validationError = true; break; } qBlock.style.outline = ''; questions.push({ id: qId, question_text: qText, question_type: qType, explanation: qExpl, options: options }); } if (validationError) return; var payload = { title: title, body: body, category_id: category_id, subject: subject, content_type: content_type, published: published }; var method = id ? 'PUT' : 'POST'; var url = id ? '/api/admin/learning/content/' + id : '/api/admin/learning/content'; showLoading('Saving...'); fetch(url, { method: method, headers: getAuthHeaders(), body: JSON.stringify(payload) }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) { hideLoading(); showToast(data.error || 'Save failed', 'error'); return; } var contentId = id || data.id; // Save questions sequentially saveQuestions(contentId, questions, 0, function() { hideLoading(); showToast('Content saved', 'success'); closeEditor(); loadCmsContent(); loadCmsStats(); loaded = false; // force refresh user feed }); }) .catch(function() { hideLoading(); showToast('Save failed', 'error'); }); } function saveQuestions(contentId, questions, index, callback) { if (index >= questions.length) { callback(); return; } var q = questions[index]; var qId = q.id; var method, url; if (qId) { method = 'PUT'; url = '/api/admin/learning/questions/' + qId; } else { method = 'POST'; url = '/api/admin/learning/content/' + contentId + '/questions'; } fetch(url, { method: method, headers: getAuthHeaders(), body: JSON.stringify({ question_text: q.question_text, question_type: q.question_type, explanation: q.explanation, options: q.options }) }) .then(function(r) { return r.json(); }) .then(function() { saveQuestions(contentId, questions, index + 1, callback); }) .catch(function() { saveQuestions(contentId, questions, index + 1, callback); }); } function deleteContent() { var id = document.getElementById('lh-cms-edit-id').value; if (!id) return; if (!confirm('Delete this content and all its questions? This cannot be undone.')) return; fetch('/api/admin/learning/content/' + id, { method: 'DELETE', headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { showToast('Content deleted', 'info'); closeEditor(); loadCmsContent(); loadCmsStats(); loaded = false; } else showToast(data.error || 'Failed', 'error'); }); } function addQuestionBlock(existingQ) { var container = document.getElementById('lh-cms-questions'); if (!container) return; var block = document.createElement('div'); block.className = 'lh-question-block'; if (existingQ && existingQ.id) block.dataset.questionId = existingQ.id; var qNum = container.querySelectorAll('.lh-question-block').length + 1; block.innerHTML = '
' + '
' + 'Q' + qNum + '' + '' + '
' + '' + '
' + '' + '
' + '' + '
' + '
' + '' + '
' + '' + '' + '
'; container.appendChild(block); var optList = block.querySelector('.lh-options-list'); if (existingQ && existingQ.options && existingQ.options.length > 0) { existingQ.options.forEach(function(opt) { addOptionRow(optList, opt); }); } else if (existingQ && existingQ.question_type === 'true_false') { addOptionRow(optList, { option_text: 'True', is_correct: false, explanation: '' }); addOptionRow(optList, { option_text: 'False', is_correct: false, explanation: '' }); } else { // Add 4 blank MCQ options for (var i = 0; i < 4; i++) addOptionRow(optList, null); } } function addOptionRow(container, opt) { if (!container) return; var row = document.createElement('div'); row.className = 'lh-option-row'; row.innerHTML = '' + '' + '' + ''; container.appendChild(row); } // ── Helpers ────────────────────────────────────────────────── function esc(str) { if (!str) return ''; return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function sanitizeHtml(html) { // Allowlist-based sanitizer: only safe tags and no event handlers var ALLOWED_TAGS = ['p','br','b','strong','i','em','u','s','h1','h2','h3','h4','h5','h6', 'ul','ol','li','a','blockquote','code','pre','table','thead','tbody','tr','th','td', 'hr','div','span','sub','sup','dl','dt','dd']; var ALLOWED_ATTRS = { 'a': ['href'], 'td': ['colspan','rowspan'], 'th': ['colspan','rowspan'] }; var div = document.createElement('div'); div.innerHTML = html; function clean(node) { var children = Array.prototype.slice.call(node.childNodes); for (var i = 0; i < children.length; i++) { var child = children[i]; if (child.nodeType === 3) continue; // text nodes OK if (child.nodeType !== 1) { child.remove(); continue; } // remove comments etc var tag = child.tagName.toLowerCase(); if (ALLOWED_TAGS.indexOf(tag) === -1) { // Replace with text content var text = document.createTextNode(child.textContent); node.replaceChild(text, child); continue; } // Remove all attributes except explicitly allowed ones var allowed = ALLOWED_ATTRS[tag] || []; var attrs = Array.prototype.slice.call(child.attributes); for (var j = 0; j < attrs.length; j++) { if (allowed.indexOf(attrs[j].name) === -1) { child.removeAttribute(attrs[j].name); } } // For tags, validate href is not javascript: if (tag === 'a') { var href = (child.getAttribute('href') || '').trim().toLowerCase(); if (href.indexOf('javascript:') === 0 || href.indexOf('data:') === 0 || href.indexOf('vbscript:') === 0) { child.setAttribute('href', '#'); } child.setAttribute('rel', 'noopener noreferrer'); child.setAttribute('target', '_blank'); } clean(child); } } clean(div); return div.innerHTML; } })();