Newer
Older

Leon
committed
// 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}

Leon
committed
*/
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'),

Leon
committed
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 };

Leon
committed
}
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) + '%';

Leon
committed
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) {

Leon
committed
return 'Question ID was not provided!';
}

Leon
committed
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!';
}

Leon
committed
return 'Question label was not provided!';
}

Leon
committed
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) {

Leon
committed
return `Answer in slot ${i} has no ID!`;
} else if (typeof data['answers'][i]['id'] !== 'number') {
return `Answer in slot ${i} has invalid ID!`;
}

Leon
committed
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') + ')';

Leon
committed
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');

Leon
committed
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');
}

Leon
committed
label.setAttribute('for', `answer_${i}`);
label.innerText = data['answers'][i]['label'];
container.append(input);
container.append(label);
answersContainer.append(container);

Leon
committed
}
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'),

Leon
committed
checked: elem.checked || false

Leon
committed
});
});
let request = new XMLHttpRequest();
request.overrideMimeType("application/json");
request.open("POST", dataBlock.dataset.evalpath, true);

Leon
committed
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),

Leon
committed
nxtBtn = document.createElement('button'),
resultMap = {
wrong: 'That\'s wrong.',
correct: 'Correct!'
};
if (!hasProp(result, 'message') || !['correct', 'wrong'].includes(result['message'])) {

Leon
committed
alert('Invalid or missing message');
return;
}
questionBlock.removeChild(evalBtn);
resultP.setAttribute('class', result['message']);

Leon
committed
resultP.innerText = resultMap[result['message']];
if (result['message'] === 'correct') {
dataset.c += 1;
} else {
dataset.w += 1;
}

Leon
committed
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 = '';

Leon
committed
let request = new XMLHttpRequest();
request.overrideMimeType("application/json");
request.open("POST", dataBlock.dataset.nextpath, true);

Leon
committed
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
request.onload = function() {
if (this.readyState === 4 && this.status === 200) {

Leon
committed
constructQuestion(JSON.parse(request.responseText));
} else if (this.status === 404) {
evaluate();

Leon
committed
} 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');
});

Leon
committed
let msgSpan = q('p.wrong span[data-target="close"]');
if (null !== msgSpan) {
msgSpan.addEventListener('click', function () {
this.parentElement.parentElement.removeChild(this.parentElement);
});
}

Leon
committed
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');
});