Loading...
Loading...
State machines in action. Timer, scoring, progress, and a polished flow.
Module 11 · JavaScript Projects
Theory becomes skill through building. These guided projects — calculator, todo app, weather app, and more — combine everything you've learned into real applications you can extend, share, and put in a portfolio.
Learn Quiz App in JavaScript — covering State Machine, Question Flow, Timer, Scoring with interactive visual examples and step-by-step explanations.
Reading about JavaScript is not the same as building with it. These guided projects combine variables, functions, DOM manipulation, async fetch, and state management into applications you can run, extend, and showcase.
Each project walks through architecture decisions, common pitfalls, and incremental steps. By the end you will have portfolio-ready apps and the confidence to start your own projects from scratch.
Understanding quiz app is essential for every JavaScript developer. It shows up in frontend UI code, backend APIs, and framework internals — and getting it wrong leads to bugs that are hard to trace. This chapter builds intuition with visuals so the behavior sticks.
The quiz has distinct screens: start, playing, results.
// Quiz states: "start" → "playing" → "results" // Each state shows different UI. const state = { screen: "start", // "start" | "playing" | "results" questions: [], currentIndex: 0, score: 0, answers: [], // user's answers for review timeRemaining: 0, timerId: null, }; // Question data structure: const questions = [ { id: 1, question: "What does 'typeof null' return in JavaScript?", options: ["null", "undefined", "object", "number"], correctIndex: 2, explanation: "This is a known bug in JS. typeof null returns 'object' due to how types were originally represented in binary.", }, { id: 2, question: "Which method creates a new array from calling a function on every element?", options: ["forEach()", "map()", "filter()", "reduce()"], correctIndex: 1, explanation: "map() returns a new array. forEach() returns undefined.", }, // ... more questions ]; // Shuffle questions for variety: function shuffleArray(array) { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; }
Display questions one at a time with answer feedback.
// Start the quiz: function startQuiz() { state.screen = "playing"; state.questions = shuffleArray(questions).slice(0, 10); // 10 questions state.currentIndex = 0; state.score = 0; state.answers = []; state.timeRemaining = 30; // seconds per question render(); startTimer(); } // Render current question: function renderQuestion() { const q = state.questions[state.currentIndex]; const total = state.questions.length; document.querySelector(".quiz-area").innerHTML = ` <div class="progress"> <div class="progress-bar" style="width: ${((state.currentIndex) / total) * 100}%"></div> <span>${state.currentIndex + 1} / ${total}</span> </div> <div class="timer">${state.timeRemaining}s</div> <h2 class="question">${escapeHtml(q.question)}</h2> <div class="options"> ${q.options.map((opt, i) => ` <button class="option" data-index="${i}"> <span class="letter">${String.fromCharCode(65 + i)}</span> ${escapeHtml(opt)} </button> `).join("")} </div> `; }
Each question shows a progress bar, timer, question text, and multiple choice options.
Countdown per question — auto-skip when time runs out.
// Timer logic: function startTimer() { state.timeRemaining = 30; updateTimerDisplay(); state.timerId = setInterval(() => { state.timeRemaining--; updateTimerDisplay(); if (state.timeRemaining <= 0) { timeUp(); } }, 1000); } function stopTimer() { clearInterval(state.timerId); state.timerId = null; } function timeUp() { stopTimer(); const q = state.questions[state.currentIndex]; // Record as unanswered: state.answers.push({ question: q.question, selected: -1, // no answer correct: q.correctIndex, isCorrect: false, }); // Show correct answer: document.querySelectorAll(".option").forEach(opt => { opt.disabled = true; if (parseInt(opt.dataset.index) === q.correctIndex) { opt.classList.add("correct"); } }); // Flash timer red: const timerEl = document.querySelector(".timer"); timerEl.classList.add("expired"); timerEl.textContent = "Time's up!"; setTimeout(() => nextQuestion(), 2000); } function updateTimerDisplay() { const timerEl = document.querySelector(".timer"); timerEl.textContent = `${state.timeRemaining}s`; // Visual urgency when low: timerEl.classList.toggle("warning", state.timeRemaining <= 10); timerEl.classList.toggle("danger", state.timeRemaining <= 5); } function nextQuestion() { state.currentIndex++; if (state.currentIndex >= state.questions.length) { endQuiz(); } else { state.timeRemaining = 30; renderQuestion(); startTimer(); } }
Track correct answers and calculate final percentage.
// Score calculation: function getScorePercentage() { return Math.round((state.score / state.questions.length) * 100); } function getGrade(percentage) { if (percentage >= 90) return { grade: "A+", message: "Outstanding!", emoji: "🏆" }; if (percentage >= 80) return { grade: "A", message: "Excellent!", emoji: "🌟" }; if (percentage >= 70) return { grade: "B", message: "Good job!", emoji: "👍" }; if (percentage >= 60) return { grade: "C", message: "Not bad!", emoji: "📚" }; return { grade: "D", message: "Keep practicing!", emoji: "💪" }; } // Track stats over multiple attempts: function saveStats() { const stats = JSON.parse(localStorage.getItem("quiz-stats") || "[]"); stats.push({ date: Date.now(), score: state.score, total: state.questions.length, percentage: getScorePercentage(), }); // Keep last 20 attempts: if (stats.length > 20) stats.shift(); localStorage.setItem("quiz-stats", JSON.stringify(stats)); } // Streak tracking: function getStreak() { let streak = 0; for (const answer of state.answers) { if (answer.isCorrect) streak++; else streak = 0; } return streak; } function getBestStreak() { let best = 0, current = 0; for (const answer of state.answers) { if (answer.isCorrect) { current++; best = Math.max(best, current); } else current = 0; } return best; }
Summary screen with score, review, and play-again option.
function endQuiz() { stopTimer(); state.screen = "results"; saveStats(); renderResults(); } function renderResults() { const percentage = getScorePercentage(); const { grade, message, emoji } = getGrade(percentage); const bestStreak = getBestStreak(); document.querySelector(".quiz-area").innerHTML = ` <div class="results"> <div class="score-circle"> <span class="emoji">${emoji}</span> <span class="percentage">${percentage}%</span> <span class="grade">${grade}</span> </div> <h2>${message}</h2> <p class="score-text"> ${state.score} of ${state.questions.length} correct </p> <p class="streak">Best streak: ${bestStreak} in a row 🔥</p> <div class="review"> <h3>Review Answers</h3> ${state.answers.map((a, i) => ` <div class="review-item ${a.isCorrect ? "correct" : "wrong"}"> <span class="icon">${a.isCorrect ? "✓" : "✗"}</span> <span class="q-text">${escapeHtml(a.question)}</span> </div> `).join("")} </div> <div class="actions"> <button class="play-again">Play Again</button> <button class="share-btn">Share Score</button> </div> </div> `; } // Play again: document.querySelector(".quiz-area").addEventListener("click", (e) => { if (e.target.closest(".play-again")) startQuiz(); if (e.target.closest(".share-btn")) shareScore(); }); // Share via Web Share API: async function shareScore() { const text = `I scored ${getScorePercentage()}% on the JS Quiz! 🎯`; if (navigator.share) { await navigator.share({ title: "Quiz Score", text }); } else { await navigator.clipboard.writeText(text); alert("Score copied to clipboard!"); } } // Key patterns in this project: // ✓ State machine (start → playing → results) // ✓ Timer with setInterval + cleanup // ✓ Event delegation for dynamic buttons // ✓ Score tracking + localStorage stats // ✓ Shuffled questions for replayability // ✓ Visual feedback (colors, animations) // ✓ Web Share API for native sharing
Common questions about quiz app.