-
-
Save imhamad/46dea504946f287300e3b197e59d33b3 to your computer and use it in GitHub Desktop.
One page JS Quiz App
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Quiz built in JS</title> | |
<style> | |
body { | |
font-family: Verdana, sans-serif; | |
font-size: 16px; | |
line-height: 1.6; | |
} | |
.container {width: 80%; max-width: 900px; margin: 0 auto} | |
section { padding: 10px 20px; } | |
button { background: #3cf; padding: 5px 8px; border: none; font-size: 1.2em; margin: 5px 8px 15px 0; min-width: 120px; } | |
button#start { background: #dedede; width: 320px; padding: 8px 13px;} | |
button#submit { background: #69f; } | |
.hidden { display: none; } | |
.inactive { background: #dedede; color: #555; } | |
.success { background: #0c6; } | |
.error {background: #f97;} | |
input {margin-bottom: 15px;} | |
#next {float: right;} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<h1>The best little JavaScript Quiz</h1> | |
<p>(Questions are from this <a href="http://dmitrysoshnikov.com/ecmascript/the-quiz/">post</a> by Dimitry Soshnikov) | |
<p> </p> | |
<section id="message"></section> | |
<button id="start" >Start the Quiz</button> | |
<h2 id="titleEl">Welcome to my little JavaScript Quiz</h2> | |
<section id="question"></section> | |
<section> | |
<form id="answers" action=""></form> | |
</section> | |
<button id="submit" type="submit" class="hidden">Submit</button><br /> | |
<button id="prev" tabindex="0" class="inactive">« Previous</button> | |
<button id="next" tabindex="0">Next »</button> | |
</div> | |
<script> | |
const quiz = [ | |
{question: "What's the result of \"typeof typeof(null)\"?", | |
choices: ["\"undefined\"","SyntaxError","\"string\"","\"object\"","TypeError"], correctAnswer:2}, | |
{question: "Are the algorithms of the following checks completely equivalent?: \"typeof foo == 'undefined'\" and \"typeof foo === 'undefined'\"", | |
choices: ["Yes","No"], correctAnswer:0}, | |
{question: "What's the result of \"100['toString']['length']\"?", | |
choices: ["100","3","1","8","0"], correctAnswer:2}, | |
{question: "What's the value of a, when \"var a = (1,5 - 1) * 2\"?", | |
choices: ["0.99999999","1","0.5","8","-0.5","4"], correctAnswer:3}, | |
{question: "What's the result of \"1..z\"?", | |
choices: ["SyntaxError","New Range object (equivalent to new Range(1, 'z')) including all numbers and letters","undefined","Error of Range object (incompatible types for range: number and string)","ReferenceError \"z\" is not defined"], correctAnswer:2} | |
] | |
// check for valid page querystring | |
const validQs = [...Array(quiz.length + 2).keys()] | |
const validParams = validQs.map(i => i.toString()) | |
const validP = () => { | |
const urlParams = new URLSearchParams(window.location.search); | |
const qparam = urlParams.get('page'); | |
return (qparam && validParams.includes(qparam)) ? qparam : false; | |
} | |
// baseURL | |
const base = window.location.href.split('/').slice(0, -1).join('/') | |
const setDOMprops = (type, page) => { | |
const domFuncs = { | |
start: () => { | |
titleEl.innerHTML = Qz.msgstrings.welcome; | |
start.classList.remove("hidden"); | |
submitbtn.classList.add("hidden"); | |
prev.classList.add("inactive"); | |
next.classList.remove("inactive"); | |
}, | |
questions: page => { | |
titleEl.innerHTML = `Question ${page}`; | |
start.classList.add("hidden"); | |
message.className = ""; | |
message.innerHTML =""; | |
submitbtn.classList.remove("hidden"); | |
prev.classList.remove("inactive"); | |
next.classList.remove("inactive"); | |
}, | |
end: () => { | |
titleEl.innerHTML = `End of Quiz`; | |
start.classList.add("hidden"); | |
prev.classList.remove("inactive"); | |
next.classList.add("inactive"); | |
}, | |
resetQA: () => { | |
questionEl.innerHTML = ""; | |
answersEl.innerHTML = ""; | |
}, | |
resetMsg: () => { | |
message.className = ""; | |
message.innerHTML = ""; | |
} | |
} | |
return domFuncs[type] | |
} | |
const showPage = page => { | |
page = parseInt(page) | |
if (page === 0){ | |
Qz.setDOMprops("resetQA")(); | |
Qz.setDOMprops("resetMsg")(); | |
Qz.setDOMprops("start")(); | |
} else if (page > quiz.length) { | |
Qz.setDOMprops("resetQA")(); | |
Qz.setDOMprops("resetMsg")(); | |
Qz.setDOMprops("end")(); | |
//console.log("final answer count", Qz.submittedAnswers) | |
Qz.getFinishingMsg() | |
} else { | |
Qz.setDOMprops("resetQA")(); | |
Qz.setDOMprops("resetMsg")(); | |
Qz.setDOMprops("questions")(page); | |
} | |
if (page > 0 && page < quiz.length + 1) { | |
q = (page >= 1) ? page-1 : page; | |
if(q < quiz.length) { | |
const { question, choices, correctAnswer } = quiz[q]; | |
questionEl.innerHTML = question; | |
Qz.displayAndHandleAnswers(choices, correctAnswer); | |
} | |
} | |
} | |
const displayAndHandleAnswers = (choices, correct) => { | |
const addRadioBtn = (acc, choice, index) => { | |
return `${acc}<input type="radio" name="answer" value="${index}" tabindex="0">${choice}<br />\n` | |
} | |
const answersStr = choices.reduce(addRadioBtn, "") | |
answersEl.innerHTML = answersStr; | |
const handler = e => { | |
e.preventDefault() | |
let selected = false; | |
const q = Qz.page-1; | |
const answers = document.querySelectorAll("input"); | |
answers.forEach(answer => { | |
if(answer.checked){ | |
selected = answer.value; | |
} | |
}) | |
if(!selected){ | |
message.className = "error"; | |
message.innerHTML = Qz.msgstrings.noselect; | |
} else { | |
let output = (correct === parseInt(selected)) | |
message.className = (output) ? "success" : "error"; | |
message.innerHTML = (output) ? `This is the correct answer to question ${Qz.page}! Click 'Next' to go to the next question.` : Qz.msgstrings.incorrect; | |
Qz.submittedAnswers = {...Qz.submittedAnswers, [`${q}`]: output} | |
} | |
} | |
submitbtn.addEventListener("click", handler); | |
answersEl.addEventListener("keydown", e => {if (e.keyCode === 13) {handler(e)} }) | |
} | |
const getFinishingMsg = () => { | |
const entries = Object.entries(Qz.submittedAnswers) | |
let counterCorrect = 0; | |
let counterIncorrect = 0; | |
let counterUnanswered = 0; | |
let unanswered = []; | |
entries.forEach(entry => { | |
[key, value] = entry; | |
if (value === true){ ++counterCorrect } | |
if (value === false){ ++counterIncorrect } | |
if (value === "unanswered"){ | |
++counterUnanswered; | |
unanswered.push(key) | |
} | |
}) | |
const unansweredQs = unanswered.map(q => parseInt(q) + 1); | |
const unansweredStr = unansweredQs.slice().join(", ") | |
let msg = ""; | |
if (counterCorrect === quiz.length){ | |
msg += "Congratulations, you answered all questions correctly!"; | |
} else { | |
if(counterCorrect){ | |
msg += `You answered ${counterCorrect} out of ${quiz.length} questions correctly.` | |
} | |
if(counterUnanswered){ | |
msg += `You did not answer the following questions: ${unansweredStr}.` | |
} | |
} | |
answersEl.innerHTML = msg; | |
} | |
const Qz = { | |
// initialise variables | |
submittedAnswers: {}, | |
page: 0, | |
msgstrings: { | |
welcome: `Welcome to my little JavaScript Quiz`, | |
noselect: `You have not selected an answer yet`, | |
incorrect: `This answer is not correct. Try again or click 'Next' for the next question.` | |
}, | |
// functions | |
setDOMprops: setDOMprops, | |
showPage: showPage, | |
displayAndHandleAnswers: displayAndHandleAnswers, | |
getFinishingMsg: getFinishingMsg | |
} | |
const start = document.getElementById("start"); | |
const titleEl = document.getElementById("titleEl"); | |
const questionEl = document.getElementById("question"); | |
const submitbtn = document.getElementById("submit"); | |
const message = document.getElementById("message"); | |
const answersEl = document.getElementById("answers"); | |
const prev = document.getElementById("prev"); | |
const next = document.getElementById("next"); | |
if (validP()){ | |
Qz.page = parseInt(validP()) | |
Qz.showPage(Qz.page) | |
} | |
start.addEventListener("click", () => { | |
Qz.page = 1 | |
Qz.showPage(Qz.page); | |
history.pushState({page: Qz.page}, `Page ${Qz.page}`, `${base}/index.html?page=${Qz.page}`) | |
}) | |
next.addEventListener("click", () => { | |
if(Qz.page < quiz.length + 1){ | |
if(Qz.page > 0){ | |
console.log("type", typeof(Qz.page)) | |
q = Qz.page - 1 | |
if(Qz.submittedAnswers[`${q}`] === undefined){ | |
Qz.submittedAnswers = {...Qz.submittedAnswers, [`${q}`]:"unanswered"} | |
} | |
console.log("answers after check", Qz.submittedAnswers) | |
} | |
Qz.page += 1; | |
Qz.showPage(Qz.page) | |
} | |
history.pushState({page: Qz.page}, `Page ${Qz.page}`, `${base}/index.html?page=${Qz.page}`) | |
}) | |
prev.addEventListener("click", () => { | |
Qz.page = (Qz.page > 1) ? Qz.page-1 : 0; | |
showPage(Qz.page) | |
history.pushState({page: Qz.page}, `Page ${Qz.page}`, `${base}/index.html?page=${Qz.page}`) | |
}) | |
window.onpopstate = () => { | |
if (history.state && history.state.page){ | |
Qz.page = history.state.page; | |
Qz.showPage(Qz.page) | |
} else { | |
Qz.showPage(0) | |
} | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment