Add Quill rich text editor, multi-select questions, per-field rich text toggle

- Replace textarea+toolbar with Quill.js (free, MIT) for body editor
  - Full WYSIWYG: headings, bold/italic, lists, blockquote, code, link (inline, no popup)
- Rich Text toggle button on each question text field and option text field
  - Plain textarea by default; toggle activates mini Quill editor per field
- New 'Multiple Select' question type: checkboxes, all correct must be chosen
  - Backend scoring: allCorrectChosen && noWrongChosen
  - Results show all correct options highlighted, wrong selections flagged
- quiz display uses sanitizeHtml for question/option text (supports formatted HTML)
- sanitizeHtml allows Quill class attrs (ql-indent, etc.)
This commit is contained in:
Daniel Onyejesi 2026-03-23 22:37:29 -04:00
parent 6c0c911dbd
commit d4e57177e4
5 changed files with 248 additions and 83 deletions

View file

@ -112,27 +112,10 @@
</div>
</div>
<!-- Body editor with toolbar -->
<!-- Body editor (Quill rich text) -->
<div class="cms-body-section">
<label class="cms-label">Body</label>
<div class="cms-editor-toolbar">
<button type="button" class="cms-tb-btn" data-insert="b" title="Bold"><i class="fas fa-bold"></i></button>
<button type="button" class="cms-tb-btn" data-insert="i" title="Italic"><i class="fas fa-italic"></i></button>
<button type="button" class="cms-tb-btn" data-insert="u" title="Underline"><i class="fas fa-underline"></i></button>
<span class="cms-tb-sep"></span>
<button type="button" class="cms-tb-btn" data-insert="h2" title="Heading 2">H2</button>
<button type="button" class="cms-tb-btn" data-insert="h3" title="Heading 3">H3</button>
<span class="cms-tb-sep"></span>
<button type="button" class="cms-tb-btn" data-insert="ul" title="Bullet List"><i class="fas fa-list-ul"></i></button>
<button type="button" class="cms-tb-btn" data-insert="ol" title="Numbered List"><i class="fas fa-list-ol"></i></button>
<button type="button" class="cms-tb-btn" data-insert="blockquote" title="Quote"><i class="fas fa-quote-left"></i></button>
<span class="cms-tb-sep"></span>
<button type="button" class="cms-tb-btn" data-insert="a" title="Link"><i class="fas fa-link"></i></button>
<button type="button" class="cms-tb-btn" data-insert="code" title="Code"><i class="fas fa-code"></i></button>
<button type="button" class="cms-tb-btn" data-insert="table" title="Table"><i class="fas fa-table"></i></button>
<button type="button" class="cms-tb-btn" data-insert="hr" title="Horizontal Rule"><i class="fas fa-minus"></i></button>
</div>
<textarea id="lh-cms-edit-body" class="cms-body-editor" rows="16" placeholder="Write your content here..."></textarea>
<div id="lh-body-editor" class="cms-quill-body"></div>
</div>
<!-- Quiz builder -->

View file

@ -651,7 +651,7 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--g50);color:var(--
.cms-stat{padding:8px 10px;}
.cms-stat-value{font-size:18px;}
}
.lh-option-row{display:flex;align-items:center;gap:8px;margin-bottom:6px;padding:6px 8px;border-radius:6px;background:var(--g50);}
.lh-option-row{display:flex;align-items:flex-start;gap:8px;margin-bottom:6px;padding:6px 8px;border-radius:6px;background:var(--g50);}
@media(max-width:640px){
.lh-feed-item-header{flex-direction:column;gap:6px;}
@ -659,3 +659,32 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--g50);color:var(--
.lh-option-row{flex-wrap:wrap;}
.lh-option-row .lh-opt-expl{width:100%;}
}
/* ── Quill editor overrides ───────────────────────────────── */
.cms-quill-body{border:1px solid var(--g300);border-radius:8px;overflow:hidden;background:white;}
.cms-quill-body .ql-toolbar{border:none;border-bottom:1px solid var(--g200);background:var(--g50);border-radius:0;padding:8px;}
.cms-quill-body .ql-container{border:none;font-family:'Inter',system-ui,sans-serif;font-size:14px;}
.cms-quill-body .ql-editor{min-height:280px;padding:16px;}
.cms-quill-body .ql-editor p{margin-bottom:8px;}
.cms-quill-body .ql-editor h2{font-size:20px;font-weight:700;margin:16px 0 8px;}
.cms-quill-body .ql-editor h3{font-size:16px;font-weight:600;margin:12px 0 6px;}
.cms-quill-body .ql-editor blockquote{border-left:4px solid var(--blue);padding-left:12px;color:var(--g600);margin:10px 0;}
.cms-quill-body .ql-editor pre{background:var(--g900);color:#e5e7eb;padding:12px;border-radius:6px;font-size:13px;overflow-x:auto;}
/* Mini quill on questions/options */
.lh-q-richtext-wrap{border:1.5px solid var(--g300);border-radius:8px;overflow:hidden;margin-bottom:8px;}
.lh-q-richtext-wrap .ql-toolbar{border:none;border-bottom:1px solid var(--g200);background:var(--g50);padding:4px 6px;}
.lh-q-richtext-wrap .ql-container{border:none;font-size:14px;font-family:'Inter',system-ui,sans-serif;}
.lh-q-richtext-wrap .ql-editor{min-height:60px;padding:10px;}
.lh-opt-richtext-wrap{border:1.5px solid var(--g300);border-radius:6px;overflow:hidden;flex:2;}
.lh-opt-richtext-wrap .ql-toolbar{border:none;border-bottom:1px solid var(--g200);background:var(--g50);padding:2px 4px;}
.lh-opt-richtext-wrap .ql-container{border:none;font-size:13px;font-family:'Inter',system-ui,sans-serif;}
.lh-opt-richtext-wrap .ql-editor{min-height:40px;padding:6px 8px;}
/* Rich text toggle button */
.lh-richtext-btn{font-size:11px;padding:3px 8px;border:1px solid var(--g300);border-radius:4px;background:white;cursor:pointer;color:var(--g500);white-space:nowrap;}
.lh-richtext-btn.active{background:var(--blue-light);border-color:var(--blue);color:var(--blue);font-weight:600;}
/* Multi-select quiz options */
.lh-quiz-option input[type="checkbox"]{accent-color:var(--blue);width:18px;height:18px;flex-shrink:0;}
.lh-quiz-multi-hint{font-size:12px;color:var(--g500);margin-bottom:10px;font-style:italic;}

View file

@ -10,6 +10,7 @@
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<link rel="stylesheet" href="https://cdn.quilljs.com/1.3.7/quill.snow.css">
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#2563eb">
<meta name="apple-mobile-web-app-capable" content="yes">
@ -265,6 +266,7 @@
<div id="toast-container" class="toast-container"></div>
<!-- SCRIPTS (defer for faster initial paint) -->
<script src="https://cdn.quilljs.com/1.3.7/quill.min.js"></script>
<script defer src="/js/milestonesData.js"></script>
<script defer src="/js/pediatricScheduleData.js"></script>
<script defer src="/js/app.js"></script>

View file

@ -8,6 +8,7 @@
var cmsLoaded = false;
var currentContent = null;
var currentView = 'feed'; // 'feed' | 'category' | 'viewer'
var _bodyQuill = null; // Quill instance for body editor
// ── Load when tab activated ────────────────────────────────
document.addEventListener('tabChanged', function(e) {
@ -64,9 +65,9 @@
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; }
// Rich text toggle for question or option
var rtBtn = e.target.closest('.lh-richtext-btn');
if (rtBtn) { toggleRichText(rtBtn); return; }
// CMS content item edit
var cmsItem = e.target.closest('.lh-cms-content-item');
@ -286,20 +287,28 @@
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 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 '<label class="lh-quiz-option">' +
'<input type="radio" name="lh-q-' + q.id + '" value="' + opt.id + '">' +
'<span>' + esc(opt.option_text) + '</span>' +
'<input type="' + inputType + '" name="' + nameAttr + '" value="' + opt.id + '">' +
'<span>' + sanitizeHtml(opt.option_text) + '</span>' +
'</label>';
}).join('');
return '<div class="lh-quiz-q" data-qid="' + q.id + '">' +
var hint = isMulti ? '<div class="lh-quiz-multi-hint">Select all that apply</div>' : '';
return '<div class="lh-quiz-q" data-qid="' + q.id + '" data-qtype="' + q.question_type + '">' +
'<div class="lh-quiz-q-header">' +
'<span class="lh-quiz-q-num">Q' + (idx + 1) + '</span>' +
'<span class="lh-quiz-q-type">' + typeLabel + '</span>' +
'</div>' +
'<p class="lh-quiz-q-text">' + sanitizeHtml(q.question_text) + '</p>' +
hint +
'<div class="lh-quiz-options">' + optionsHtml + '</div>' +
'</div>';
}).join('');
@ -310,11 +319,15 @@
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
});
if (q.question_type === 'multi') {
var checkedEls = document.querySelectorAll('input[name="lh-qm-' + q.id + '"]:checked');
var optionIds = [];
checkedEls.forEach(function(el) { optionIds.push(parseInt(el.value)); });
answers.push({ questionId: q.id, optionIds: optionIds });
} else {
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...');
@ -373,12 +386,20 @@
data.results.forEach(function(r) {
var qDiv = document.querySelector('.lh-quiz-q[data-qid="' + r.questionId + '"]');
if (!qDiv) return;
var isMulti = r.questionType === 'multi';
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');
if (isMulti) {
var correctIds = r.correctOptionIds || [];
var selectedIds = r.selectedOptionIds || [];
if (correctIds.indexOf(optId) !== -1) label.classList.add('lh-opt-correct');
if (selectedIds.indexOf(optId) !== -1 && correctIds.indexOf(optId) === -1) label.classList.add('lh-opt-wrong');
} else {
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;
});
});
@ -560,30 +581,131 @@
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') + '</' + tag + '>'; break;
case 'a':
var url = prompt('Enter URL:');
if (!url) return;
insert = '<a href="' + url + '">' + (sel || 'link text') + '</a>'; break;
case 'ul': insert = '<ul>\n <li>' + (sel || 'item') + '</li>\n <li></li>\n</ul>'; break;
case 'ol': insert = '<ol>\n <li>' + (sel || 'item') + '</li>\n <li></li>\n</ol>'; break;
case 'blockquote': insert = '<blockquote>' + (sel || 'quote') + '</blockquote>'; break;
case 'table': insert = '<table>\n<tr><th>Header 1</th><th>Header 2</th></tr>\n<tr><td>' + (sel || 'data') + '</td><td></td></tr>\n</table>'; break;
case 'hr': insert = '<hr>'; break;
default: return;
// ── Quill body editor ─────────────────────────────────────
function initBodyEditor() {
var container = document.getElementById('lh-body-editor');
if (!container || _bodyQuill) return;
_bodyQuill = new Quill(container, {
theme: 'snow',
modules: {
toolbar: [
[{ header: [2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }],
['blockquote', 'code-block'],
['link'],
[{ color: [] }],
['clean']
]
}
});
}
// Mini Quill for question text or option text
function makeMinQuill(container, existingHtml, isOption) {
var toolbar = isOption
? [['bold', 'italic'], ['link']]
: [['bold', 'italic'], [{ list: 'ordered' }, { list: 'bullet' }], ['link'], ['clean']];
var q = new Quill(container, {
theme: 'snow',
modules: { toolbar: toolbar }
});
if (existingHtml) q.clipboard.dangerouslyPasteHTML(existingHtml);
return q;
}
// Toggle rich text on a question text area or option text area
function toggleRichText(btn) {
var isActive = btn.classList.contains('active');
var target = btn.dataset.target; // 'question' or 'option'
var block = btn.closest(target === 'question' ? '.lh-question-block' : '.lh-option-row');
if (!block) return;
if (target === 'question') {
var ta = block.querySelector('.lh-q-text');
var wrap = block.querySelector('.lh-q-richtext-wrap');
if (!isActive) {
// Enable: hide textarea, show quill
var currentVal = ta ? ta.value : '';
btn.classList.add('active');
btn.textContent = 'Plain Text';
if (ta) ta.style.display = 'none';
if (!wrap) {
wrap = document.createElement('div');
wrap.className = 'lh-q-richtext-wrap';
ta.parentNode.insertBefore(wrap, ta.nextSibling);
}
wrap.style.display = '';
if (!block._questionQuill) {
block._questionQuill = makeMinQuill(wrap, currentVal, false);
} else {
wrap.style.display = '';
}
} else {
// Disable: get HTML from quill, restore textarea
btn.classList.remove('active');
btn.textContent = 'Rich Text';
if (block._questionQuill) {
var html = block._questionQuill.root.innerHTML;
if (html === '<p><br></p>') html = '';
if (ta) { ta.value = html; ta.style.display = ''; }
}
if (wrap) wrap.style.display = 'none';
}
} else {
// option
var row = block;
var optInput = row.querySelector('.lh-opt-text');
var optWrap = row.querySelector('.lh-opt-richtext-wrap');
if (!isActive) {
var curVal = optInput ? optInput.value : '';
btn.classList.add('active');
btn.textContent = 'Plain';
if (optInput) optInput.style.display = 'none';
if (!optWrap) {
optWrap = document.createElement('div');
optWrap.className = 'lh-opt-richtext-wrap';
optInput.parentNode.insertBefore(optWrap, optInput.nextSibling);
}
optWrap.style.display = '';
if (!row._optQuill) {
row._optQuill = makeMinQuill(optWrap, curVal, true);
}
} else {
btn.classList.remove('active');
btn.textContent = 'Rich';
if (row._optQuill) {
var optHtml = row._optQuill.root.innerHTML;
if (optHtml === '<p><br></p>') optHtml = '';
if (optInput) { optInput.value = optHtml; optInput.style.display = ''; }
}
if (optWrap) optWrap.style.display = 'none';
}
}
ta.value = ta.value.substring(0, start) + insert + ta.value.substring(end);
ta.focus();
ta.selectionStart = ta.selectionEnd = start + insert.length;
}
// Read question text whether in rich text mode or plain
function getQText(block) {
if (block._questionQuill && block.querySelector('.lh-q-richtext-wrap') &&
block.querySelector('.lh-q-richtext-wrap').style.display !== 'none') {
var html = block._questionQuill.root.innerHTML;
return html === '<p><br></p>' ? '' : html;
}
var ta = block.querySelector('.lh-q-text');
return ta ? ta.value.trim() : '';
}
// Read option text whether in rich text mode or plain
function getOptText(row) {
if (row._optQuill && row.querySelector('.lh-opt-richtext-wrap') &&
row.querySelector('.lh-opt-richtext-wrap').style.display !== 'none') {
var html = row._optQuill.root.innerHTML;
return html === '<p><br></p>' ? '' : html;
}
var inp = row.querySelector('.lh-opt-text');
return inp ? inp.value.trim() : '';
}
function openEditor(contentId, contentType) {
@ -597,12 +719,13 @@
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');
initBodyEditor();
if (_bodyQuill) _bodyQuill.setContents([{ insert: '\n' }]);
// For quiz type, add a starter question
if (contentType === 'quiz' && !contentId) {
@ -620,7 +743,7 @@
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 || '';
if (_bodyQuill) _bodyQuill.clipboard.dangerouslyPasteHTML(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';
@ -648,7 +771,7 @@
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 body = _bodyQuill ? (_bodyQuill.root.innerHTML === '<p><br></p>' ? '' : _bodyQuill.root.innerHTML) : '';
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';
@ -661,7 +784,7 @@
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();
var qText = getQText(qBlock);
if (!qText) continue;
var qType = qBlock.querySelector('.lh-q-type').value;
var qExpl = qBlock.querySelector('.lh-q-explanation').value.trim();
@ -669,7 +792,7 @@
var options = [];
qBlock.querySelectorAll('.lh-option-row').forEach(function(row) {
var optText = row.querySelector('.lh-opt-text').value.trim();
var optText = getOptText(row);
if (!optText) return;
options.push({
option_text: optText,
@ -792,8 +915,10 @@
'<span style="background:linear-gradient(135deg,var(--blue),var(--purple));color:white;font-size:12px;font-weight:700;padding:3px 10px;border-radius:6px;">Q' + qNum + '</span>' +
'<select class="lh-q-type" style="font-size:12px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;">' +
'<option value="mcq"' + (existingQ && existingQ.question_type === 'mcq' ? ' selected' : '') + '>Single Choice</option>' +
'<option value="multi"' + (existingQ && existingQ.question_type === 'multi' ? ' selected' : '') + '>Multiple Select</option>' +
'<option value="true_false"' + (existingQ && existingQ.question_type === 'true_false' ? ' selected' : '') + '>True / False</option>' +
'</select>' +
'<button type="button" class="lh-richtext-btn" data-target="question">Rich Text</button>' +
'</div>' +
'<button class="btn-sm btn-ghost lh-rm-question" style="padding:2px 8px;font-size:12px;color:var(--red);"><i class="fas fa-trash"></i></button>' +
'</div>' +
@ -831,13 +956,18 @@
var row = document.createElement('div');
row.className = 'lh-option-row';
row.innerHTML =
'<label style="display:flex;align-items:center;gap:4px;cursor:pointer;min-width:28px;" title="Mark as correct answer">' +
'<label style="display:flex;align-items:center;gap:4px;cursor:pointer;min-width:28px;padding-top:6px;" title="Mark as correct answer">' +
'<input type="checkbox" class="lh-opt-correct"' + (opt && opt.is_correct ? ' checked' : '') + ' style="accent-color:var(--green);width:16px;height:16px;">' +
'</label>' +
'<input type="text" class="lh-opt-text" placeholder="Option text" value="' + esc(opt ? opt.option_text : '') + '" style="flex:2;font-size:13px;padding:6px 10px;border:1.5px solid var(--g300);border-radius:6px;">' +
'<div style="flex:2;display:flex;flex-direction:column;gap:3px;">' +
'<input type="text" class="lh-opt-text" placeholder="Option text" style="width:100%;font-size:13px;padding:6px 10px;border:1.5px solid var(--g300);border-radius:6px;box-sizing:border-box;">' +
'<button type="button" class="lh-richtext-btn" data-target="option" style="align-self:flex-start;">Rich</button>' +
'</div>' +
'<input type="text" class="lh-opt-expl" placeholder="Explanation if wrong" value="' + esc(opt ? opt.explanation || '' : '') + '" style="flex:1;font-size:12px;padding:6px 10px;border:1px solid var(--g300);border-radius:6px;color:var(--g600);">' +
'<button class="btn-sm btn-ghost lh-rm-option" style="padding:2px 6px;font-size:12px;color:var(--g400);"><i class="fas fa-times"></i></button>';
'<button class="btn-sm btn-ghost lh-rm-option" style="padding:2px 6px;font-size:12px;color:var(--g400);margin-top:4px;"><i class="fas fa-times"></i></button>';
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 || '' : '';
}
// ── Helpers ──────────────────────────────────────────────────
@ -851,7 +981,7 @@
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 ALLOWED_ATTRS = { 'a': ['href'], 'td': ['colspan','rowspan'], 'th': ['colspan','rowspan'], 'li': ['class'], 'ol': ['class'], 'ul': ['class'], 'span': ['class'], 'p': ['class'] };
var div = document.createElement('div');
div.innerHTML = html;

View file

@ -127,9 +127,9 @@ router.post('/submit-quiz', async function(req, res) {
return res.status(400).json({ error: 'contentId and answers array required' });
}
// Get all questions for this content
// Get all questions for this content (include question_type)
var questions = await db.all(
'SELECT id, question_text, explanation as general_explanation FROM learning_questions WHERE content_id = ? ORDER BY sort_order ASC',
'SELECT id, question_text, question_type, explanation as general_explanation FROM learning_questions WHERE content_id = ? ORDER BY sort_order ASC',
[contentId]
);
@ -139,7 +139,6 @@ router.post('/submit-quiz', async function(req, res) {
for (var i = 0; i < questions.length; i++) {
var q = questions[i];
var userAnswer = answers.find(function(a) { return a.questionId === q.id; });
var selectedOptionId = userAnswer ? userAnswer.optionId : null;
// Get all options with correct flag
var options = await db.all(
@ -147,21 +146,43 @@ router.post('/submit-quiz', async function(req, res) {
[q.id]
);
var correctOption = options.find(function(o) { return o.is_correct; });
var selectedOption = selectedOptionId ? options.find(function(o) { return o.id === selectedOptionId; }) : null;
var isCorrect = selectedOption ? selectedOption.is_correct : false;
if (isCorrect) score++;
var isCorrect, resultEntry;
results.push({
questionId: q.id,
questionText: q.question_text,
selectedOptionId: selectedOptionId,
isCorrect: isCorrect,
correctOptionId: correctOption ? correctOption.id : null,
correctOptionText: correctOption ? correctOption.option_text : '',
selectedExplanation: selectedOption && !isCorrect ? selectedOption.explanation : '',
generalExplanation: q.general_explanation || ''
});
if (q.question_type === 'multi') {
var selectedIds = (userAnswer && Array.isArray(userAnswer.optionIds)) ? userAnswer.optionIds.map(Number) : [];
var correctIds = options.filter(function(o) { return o.is_correct; }).map(function(o) { return o.id; });
var allCorrectChosen = correctIds.length > 0 && correctIds.every(function(id) { return selectedIds.indexOf(id) !== -1; });
var noWrongChosen = selectedIds.every(function(id) { return correctIds.indexOf(id) !== -1; });
isCorrect = allCorrectChosen && noWrongChosen && selectedIds.length > 0;
if (isCorrect) score++;
resultEntry = {
questionId: q.id,
questionType: 'multi',
questionText: q.question_text,
selectedOptionIds: selectedIds,
correctOptionIds: correctIds,
isCorrect: isCorrect,
generalExplanation: q.general_explanation || ''
};
} else {
var selectedOptionId = userAnswer ? userAnswer.optionId : null;
var correctOption = options.find(function(o) { return o.is_correct; });
var selectedOption = selectedOptionId ? options.find(function(o) { return o.id === selectedOptionId; }) : null;
isCorrect = selectedOption ? selectedOption.is_correct : false;
if (isCorrect) score++;
resultEntry = {
questionId: q.id,
questionType: q.question_type,
questionText: q.question_text,
selectedOptionId: selectedOptionId,
isCorrect: isCorrect,
correctOptionId: correctOption ? correctOption.id : null,
correctOptionText: correctOption ? correctOption.option_text : '',
selectedExplanation: selectedOption && !isCorrect ? selectedOption.explanation : '',
generalExplanation: q.general_explanation || ''
};
}
results.push(resultEntry);
}
// Save progress