Quiz Application
State machines in action. Timer, scoring, progress, and a polished flow.
State Machine
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;
}Question Flow
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.
1 / 2
Timer
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();
}
}Scoring
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;
}Results
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 sharingFAQ
Common questions about this project.