diff --git a/projects/quiz/index.html b/projects/quiz/index.html index d4528fd..018144b 100644 --- a/projects/quiz/index.html +++ b/projects/quiz/index.html @@ -15,14 +15,67 @@

Quiz

- Time Left: 15s + Time Left: 20s
-
-
-
-

Add question sets, scoring, and categories.

+ + +
+
+ + +
+ +
+ +
+ + + + +
+
+ +
+ + +
+ +
+ + +
+ + +
+ + + diff --git a/projects/quiz/main.js b/projects/quiz/main.js index d13e2e1..916e9fe 100644 --- a/projects/quiz/main.js +++ b/projects/quiz/main.js @@ -1,11 +1,18 @@ -const TIME_LIMIT = 15; // seconds per question +const TIME_LIMIT = 20; // seconds per question let timeLeft = TIME_LIMIT; let timerInterval = null; const timerElement = document.getElementById("time"); +const settingsPanel = document.getElementById("settings"); +const quizContent = document.getElementById("quiz-content"); +const progressFill = document.getElementById("progress-fill"); +const progressText = document.getElementById("progress-text"); +const startButton = document.getElementById("start-quiz"); let questions = []; // API-loaded questions +let currentQuestions = []; // Questions for current session let i = 0, score = 0; +let totalQuestions = 5; const q = document.getElementById('q'), answers = document.getElementById('answers'), @@ -35,6 +42,16 @@ const stored = safeGet('quiz-theme'); const prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches; applyTheme(stored ? stored : (prefersLight ? 'light' : 'dark')); +/** Fisher-Yates shuffle algorithm */ +function shuffleArray(array) { + const newArray = [...array]; + for (let i = newArray.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [newArray[i], newArray[j]] = [newArray[j], newArray[i]]; + } + return newArray; +} + /** Decode HTML entities from API */ function decodeHTML(str) { const txt = document.createElement('textarea'); @@ -42,31 +59,78 @@ function decodeHTML(str) { return txt.value; } -/** Shuffle array */ -function shuffle(arr) { - return arr.sort(() => Math.random() - 0.5); +/** Get settings from UI */ +function getSettings() { + const category = document.getElementById('category').value; + const difficulty = document.querySelector('input[name="difficulty"]:checked').value; + const questionCount = parseInt(document.getElementById('question-count').value); + const shuffleAnswers = document.getElementById('shuffle-answers').checked; + const shuffleQuestions = document.getElementById('shuffle-questions').checked; + + return { category, difficulty, questionCount, shuffleAnswers, shuffleQuestions }; } -/** Fetch questions from Open Trivia DB API */ +/** Fetch questions from Open Trivia DB API with filters */ async function loadQuestions() { try { - const res = await fetch('https://opentdb.com/api.php?amount=5&type=multiple'); + const settings = getSettings(); + totalQuestions = settings.questionCount; + + let apiUrl = `https://opentdb.com/api.php?amount=20&type=multiple`; + if (settings.category !== 'any') { + apiUrl += `&category=${settings.category}`; + } + if (settings.difficulty !== 'any') { + apiUrl += `&difficulty=${settings.difficulty}`; + } + + const res = await fetch(apiUrl); const data = await res.json(); + + if (data.response_code !== 0 || !data.results.length) { + throw new Error('No questions available with selected filters'); + } + questions = data.results.map(q => ({ q: decodeHTML(q.question), - a: shuffle([decodeHTML(q.correct_answer), ...q.incorrect_answers.map(decodeHTML)]), - c: null, // correct answer index - correctAnswer: decodeHTML(q.correct_answer) + a: [decodeHTML(q.correct_answer), ...q.incorrect_answers.map(decodeHTML)], + c: 0, // correct answer index will be set after shuffling + correctAnswer: decodeHTML(q.correct_answer), + difficulty: q.difficulty, + category: q.category })); - // Compute correct answer index - questions.forEach(qObj => { - qObj.c = qObj.a.findIndex(ans => ans === qObj.correctAnswer); + + // Prepare questions for current session + currentQuestions = settings.shuffleQuestions ? + shuffleArray(questions).slice(0, totalQuestions) : + questions.slice(0, totalQuestions); + + // Process each question + currentQuestions.forEach(qObj => { + if (settings.shuffleAnswers) { + const correctIndex = qObj.a.findIndex(ans => ans === qObj.correctAnswer); + const shuffledAnswers = shuffleArray(qObj.a); + qObj.a = shuffledAnswers; + qObj.c = shuffledAnswers.findIndex(ans => ans === qObj.correctAnswer); + } else { + qObj.c = 0; // Correct answer is always first if not shuffled + } }); + } catch (err) { console.error('Failed to load questions', err); - q.textContent = 'Failed to load questions 😢'; + q.textContent = 'Failed to load questions. Please try different filters. 😢'; answers.innerHTML = ''; + return false; } + return true; +} + +/** Update progress bar */ +function updateProgress() { + const progress = ((i + 1) / currentQuestions.length) * 100; + progressFill.style.width = `${progress}%`; + progressText.textContent = `Question ${i + 1} of ${currentQuestions.length}`; } /** Start timer for each question */ @@ -87,33 +151,73 @@ function startTimer() { if (timeLeft <= 0) { clearInterval(timerInterval); - handleNextQuestion(); + handleTimeUp(); } }, 1000); } +/** Handle when time runs out */ +function handleTimeUp() { + const currentQuestion = currentQuestions[i]; + Array.from(answers.children).forEach(btn => { + btn.disabled = true; + if (parseInt(btn.dataset.index) === currentQuestion.c) { + btn.classList.add('correct'); + } + }); + + result.textContent = 'Time\'s up!'; + + setTimeout(() => { + handleNextQuestion(); + }, 1500); +} + /** Move to next question */ function handleNextQuestion() { i++; - render(); + if (i < currentQuestions.length) { + render(); + } else { + endQuiz(); + } +} + +/** End quiz and show results */ +function endQuiz() { + clearInterval(timerInterval); + timerElement.parentElement.style.display = 'none'; + q.textContent = '🎉 Quiz Complete!'; + answers.innerHTML = ''; + result.textContent = `Final Score: ${score}/${currentQuestions.length}`; + + // Add restart button + const restartBtn = document.createElement('button'); + restartBtn.textContent = 'Try Again'; + restartBtn.className = 'start-btn'; + restartBtn.style.marginTop = '1rem'; + restartBtn.onclick = resetQuiz; + result.appendChild(restartBtn); +} + +/** Reset quiz to settings screen */ +function resetQuiz() { + i = 0; + score = 0; + currentQuestions = []; + settingsPanel.classList.remove('hidden'); + quizContent.classList.add('hidden'); + result.textContent = ''; } /** Render current question */ function render() { - if (!questions.length) return; - - if (i >= questions.length) { - clearInterval(timerInterval); - timerElement.parentElement.style.display = 'none'; - q.textContent = '🎉 Quiz Complete!'; - answers.innerHTML = ''; - result.textContent = `Score: ${score}/${questions.length}`; - return; - } + if (!currentQuestions.length) return; + updateProgress(); startTimer(); - const cur = questions[i]; + const cur = currentQuestions[i]; q.textContent = cur.q; answers.innerHTML = ''; result.textContent = ''; @@ -122,36 +226,49 @@ function render() { const b = document.createElement('button'); b.textContent = ans; b.className = 'answer-btn'; + b.dataset.index = idx; b.addEventListener('click', () => { - // prevent double clicks if (b.disabled) return; clearInterval(timerInterval); - // mark selected - Array.from(answers.children).forEach(x=>x.classList.remove('selected')); + + Array.from(answers.children).forEach(x => x.classList.remove('selected')); b.classList.add('selected'); - // mark correct/incorrect + if (idx === cur.c){ b.classList.add('correct'); score++; + result.textContent = 'Correct! 🎉'; } else { b.classList.add('incorrect'); - // reveal the correct one const correctBtn = answers.children[cur.c]; if (correctBtn) correctBtn.classList.add('correct'); + result.textContent = 'Incorrect 😞'; } - // disable all to avoid extra clicks - Array.from(answers.children).forEach(x=>x.disabled=true); - // short delay to show feedback - setTimeout(()=>{ + + Array.from(answers.children).forEach(x => x.disabled = true); + + setTimeout(() => { handleNextQuestion(); - }, 700); + }, 1500); }); answers.appendChild(b); }); } -(async function init() { - result.textContent = 'Loading questions...'; - await loadQuestions(); - render(); -})(); +/** Start quiz */ +async function startQuiz() { + const success = await loadQuestions(); + if (success && currentQuestions.length > 0) { + i = 0; + score = 0; + settingsPanel.classList.add('hidden'); + quizContent.classList.remove('hidden'); + render(); + } +} + +// Event Listeners +startButton.addEventListener('click', startQuiz); + +// Initialize +result.textContent = 'Configure your quiz settings and click "Start Quiz"'; \ No newline at end of file diff --git a/projects/quiz/styles.css b/projects/quiz/styles.css index 09ff40b..2e34d24 100644 --- a/projects/quiz/styles.css +++ b/projects/quiz/styles.css @@ -53,6 +53,100 @@ main{ .quiz-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem} .controls{display:flex;gap:.5rem;align-items:center} +/* Settings Panel */ +.settings-panel { + display: grid; + gap: 1.5rem; + margin-bottom: 1rem; +} + +.setting-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.setting-group label { + font-weight: 600; + color: var(--text); + font-size: 0.95rem; +} + +.setting-group select { + padding: 0.75rem; + border-radius: 8px; + border: 1px solid rgba(255,255,255,0.1); + background: var(--card); + color: var(--text); + font-size: 1rem; +} + +.radio-group { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.radio-group label { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: normal; + cursor: pointer; +} + +.radio-group input[type="radio"] { + margin: 0; +} + +.start-btn { + background: linear-gradient(135deg, var(--primary), var(--accent)); + color: white; + border: none; + padding: 1rem 2rem; + border-radius: 10px; + font-weight: 600; + font-size: 1.1rem; + cursor: pointer; + transition: all 0.2s ease; + margin-top: 0.5rem; +} + +.start-btn:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(96,165,250,0.3); +} + +/* Progress Bar */ +.progress-container { + margin-bottom: 1.5rem; +} + +.progress-bar { + width: 100%; + height: 6px; + background: rgba(255,255,255,0.1); + border-radius: 3px; + overflow: hidden; + margin-bottom: 0.5rem; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary), var(--accent)); + border-radius: 3px; + transition: width 0.3s ease; + width: 0%; +} + +.progress-text { + font-size: 0.9rem; + color: var(--muted); + text-align: center; + font-weight: 600; +} + +/* Quiz Content */ #q{font-size:1.125rem;line-height:1.4;margin:0 0 1rem 0} #answers{display:flex;flex-direction:column;gap:.5rem} @@ -73,7 +167,13 @@ button#theme-toggle{background:transparent;border:1px solid rgba(255,255,255,0.0 .timer{font-weight:700;color:var(--muted);font-size:.95rem} .timer.warning{color:var(--danger)} +/* Utility Classes */ +.hidden { display: none !important; } +.visible { display: block !important; } + @media (max-width:520px){ main{padding:1rem;border-radius:12px} #q{font-size:1rem} + .radio-group { gap: 0.5rem; } + .setting-group select, .start-btn { font-size: 0.9rem; } } \ No newline at end of file