Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 58 additions & 5 deletions projects/quiz/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,67 @@ <h1>Quiz</h1>
<div class="controls">
<button id="theme-toggle" aria-pressed="false" title="Toggle theme">🌙</button>
<div class="timer">
Time Left: <span id="time">15</span>s
Time Left: <span id="time">20</span>s
</div>
</div>
</div>
<div id="q"></div>
<div id="answers"></div>
<div id="result"></div>
<p class="notes">Add question sets, scoring, and categories.</p>

<!-- Settings Panel -->
<div id="settings" class="settings-panel">
<div class="setting-group">
<label for="category">Category:</label>
<select id="category">
<option value="any">Any Category</option>
<option value="9">General Knowledge</option>
<option value="17">Science & Nature</option>
<option value="18">Computers</option>
<option value="19">Mathematics</option>
<option value="22">Geography</option>
<option value="23">History</option>
</select>
</div>

<div class="setting-group">
<label>Difficulty:</label>
<div class="radio-group">
<label><input type="radio" name="difficulty" value="any" checked> Any</label>
<label><input type="radio" name="difficulty" value="easy"> Easy</label>
<label><input type="radio" name="difficulty" value="medium"> Medium</label>
<label><input type="radio" name="difficulty" value="hard"> Hard</label>
</div>
</div>

<div class="setting-group">
<label for="question-count">Questions:</label>
<select id="question-count">
<option value="5">5 Questions</option>
<option value="10">10 Questions</option>
<option value="15">15 Questions</option>
<option value="20">20 Questions</option>
</select>
</div>

<div class="setting-group">
<label><input type="checkbox" id="shuffle-answers" checked> Shuffle Answers</label>
<label><input type="checkbox" id="shuffle-questions" checked> Shuffle Questions</label>
</div>

<button id="start-quiz" class="start-btn">Start Quiz</button>
</div>

<!-- Quiz Content -->
<div id="quiz-content" class="hidden">
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div class="progress-text" id="progress-text">Question 1 of 5</div>
</div>

<div id="q"></div>
<div id="answers"></div>
<div id="result"></div>
</div>
</main>
<script type="module" src="./main.js"></script>
</body>
Expand Down
199 changes: 158 additions & 41 deletions projects/quiz/main.js
Original file line number Diff line number Diff line change
@@ -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'),
Expand Down Expand Up @@ -35,38 +42,95 @@ 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');
txt.innerHTML = 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 */
Expand All @@ -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 = '';
Expand All @@ -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"';
Loading