// any CSS you import will output into a single css file (app.css in this case) import '../scss/app.scss'; const SINGLE_CHOICE = 'scq'; const MULTIPLE_CHOICE = 'mcq'; const MULTIPLE_CHOICE_RANDOM_WRONGS = 'mcqrw'; const VALID_QS_TYPES = [SINGLE_CHOICE, MULTIPLE_CHOICE, MULTIPLE_CHOICE_RANDOM_WRONGS]; let dataset = { t: null, ids: [], a: 0, c: 0, w: 0 }; /** * @param {string} slctr * @return {Element|null} */ function q(slctr) { return document.querySelector(slctr); } /** * @param {string} slctr * @return {NodeListOf<Element>} */ function qa(slctr) { return document.querySelectorAll(slctr); } /** * @param {string} id * @return {HTMLElement|null} */ function id(id) { return document.getElementById(id); } /** * @param {object} obj * @param {string} pName * @return {boolean} */ function hasProp(obj, pName) { return obj.hasOwnProperty(pName); } let dataBlock = id('data-block'), loginForm = q('.admin'), startBlock = q('.startblock'), questionBlock = q('.questionblock'), evalBlock = q('.evalblock'), allQuestions = id('all_questions'), correctAnswers = id('correct_answers'), wrongAnswers = id('wrong_answers'), percentage = id('percentage'), resultMessage = id('result_message') ; function resetDataset() { dataset = { t: null, ids: [], a: 0, c: 0, w: 0 }; } function resetEvalBlock() { allQuestions.innerText = ''; correctAnswers.innerText = ''; wrongAnswers.innerText = ''; percentage.innerText = ''; resultMessage.innerText = ''; } function evaluate() { let percentageVal = (dataset.c / dataset.a) * 100, msgMap = { 0: "Ouf. Get a book!", 25: 'Try again!', 50: 'I wouldn\'t be satisfied with that.', 75: 'Looks good, aim for something more', 90: 'That\'s actually impressive!', 100: 'I think you\'re ready. Go get \'em, champ!' }, msgKeys = Object.keys(msgMap), i = 1, msg = msgMap['0'] ; allQuestions.innerText = dataset.a.toString(10); correctAnswers.innerText = dataset.c.toString(10); wrongAnswers.innerText = dataset.w.toString(10); percentage.innerText = percentageVal.toString(10) + '%'; for (i; i < msgKeys.length; i += 1) { if (parseInt(msgKeys[i], 10) < percentageVal) { msg = msgMap[msgKeys[i]]; } else { i = msgKeys.length; } } resultMessage.innerText = msg; startBlock.setAttribute('style', 'display:none'); questionBlock.setAttribute('style', 'display:none'); evalBlock.removeAttribute('style'); } /** * @param {object} data * @return {string} */ function validateQuestionData(data) { if (!hasProp(data, 'id')) { return 'Question ID was not provided!'; } if (!hasProp(data, 'type')) { return 'Question structure type was not provided!'; } else if (typeof data['type'] !== 'string' || !VALID_QS_TYPES.includes(data['type'])) { return 'Question structure type is invalid!'; } if (!hasProp(data,'label')) { return 'Question label was not provided!'; } if (!hasProp(data, 'answers')) { return 'No answers provided!'; } else if (typeof data['answers'] !== 'object') { return 'Answers dataset invalid!'; } else if (data['answers'].length < 5) { return 'Insufficient amount of answers provided!'; } for (let i = 0; i < data['answers'].length; i += 1) { if (!hasProp(data['answers'][i], 'id')) { return `Answer in slot ${i} has no ID!`; } else if (typeof data['answers'][i]['id'] !== 'number') { return `Answer in slot ${i} has invalid ID!`; } if (!hasProp(data['answers'][i], 'label')) { return `Answer in slot ${i} has no label!`; } else if (typeof data['answers'][i]['label'] !== 'string') { return `Answer in slot ${i} has invalid label!`; } } return ''; } function constructQuestion(data) { let validation = validateQuestionData(data); if (validation.length > 0) { alert(`Error while reading question data: ${validation}`); } let label = document.createElement('p'), answersContainer = document.createElement('div'), evalBtn = document.createElement('button'), resultP = document.createElement('p'), isScq = data['type'] === SINGLE_CHOICE ; dataset.ids.push(data['id']); label.innerText = data['label'] + ' (' + (isScq ? 'Single choice' : 'Multiple choice') + ')'; answersContainer.setAttribute('id', 'answers_container'); questionBlock.append(label); for (let i = 0; i < data['answers'].length; i += 1) { let container = document.createElement('div'), input = document.createElement('input'), label = document.createElement('label') ; container.setAttribute('class', 'input-container'); input.setAttribute('type', isScq ? 'radio' : 'checkbox'); input.setAttribute('id', `answer_${i}`); input.setAttribute('answerId', `${data['answers'][i]['id']}`); if (isScq) { input.setAttribute('name', 'scq-answer'); } label.setAttribute('for', `answer_${i}`); label.innerText = data['answers'][i]['label']; container.append(input); container.append(label); answersContainer.append(container); } questionBlock.append(answersContainer); questionBlock.append(resultP); evalBtn.addEventListener('click', function () { let answerData = []; qa('#answers_container input').forEach(function (elem) { answerData.push({ id: elem.getAttribute('answerId'), checked: elem.checked || false }); }); let request = new XMLHttpRequest(); request.overrideMimeType("application/json"); request.open("POST", dataBlock.dataset.evalpath, true); request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); request.onload = function() { if (this.readyState === 4 && this.status === 200) { let result = JSON.parse(request.responseText), nxtBtn = document.createElement('button'), resultMap = { wrong: 'That\'s wrong.', correct: 'Correct!' }; if (!hasProp(result, 'message') || !['correct', 'wrong'].includes(result['message'])) { alert('Invalid or missing message'); return; } questionBlock.removeChild(evalBtn); resultP.setAttribute('class', result['message']); resultP.innerText = resultMap[result['message']]; if (result['message'] === 'correct') { dataset.c += 1; } else { dataset.w += 1; } nxtBtn.addEventListener('click', getNextQuestion); nxtBtn.innerText = 'Next question'; questionBlock.append(nxtBtn); } else { alert(`Error! Returned status ${this.status.toString(10)}`); } }; request.send(JSON.stringify({ question: data['id'], answers: answerData })); }); evalBtn.innerText = 'Send'; questionBlock.append(evalBtn); } function getNextQuestion() { if ((dataset.a + 1) >= 10) { evaluate(); return; } questionBlock.innerHTML = ''; let request = new XMLHttpRequest(); request.overrideMimeType("application/json"); request.open("POST", dataBlock.dataset.nextpath, true); request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); request.onload = function() { if (this.readyState === 4 && this.status === 200) { dataset.a += 1; constructQuestion(JSON.parse(request.responseText)); } else if (this.status === 404) { evaluate(); } else { alert('Error! Returned status ' + this.status.toString(10)); } }; request.send(JSON.stringify({ t: dataset.t, ids: dataset.ids })); } function startQuiz() { dataset.t = id('topic').value; startBlock.setAttribute('style', 'display:none'); questionBlock.removeAttribute('style'); getNextQuestion(); } q('header nav a[data-target="admin"]').addEventListener('click', function () { loginForm.classList.add('open'); }); q('.admin > a[data-target="close"]').addEventListener('click', function () { loginForm.classList.remove('open'); }); let msgSpan = q('p.wrong span[data-target="close"]'); if (null !== msgSpan) { msgSpan.addEventListener('click', function () { this.parentElement.parentElement.removeChild(this.parentElement); }); } id('start').addEventListener('click', startQuiz); id('retry').addEventListener('click', function () { resetEvalBlock(); resetDataset(); questionBlock.setAttribute('style', 'display:none'); evalBlock.setAttribute('style', 'display:none'); id('topic').value = 'all topics'; startBlock.removeAttribute('style'); });