|
| 1 | +(() => { |
| 2 | + // Exam State |
| 3 | + const examState = { |
| 4 | + userAnswers: {}, // { questionId: selectedOptionIndex } |
| 5 | + shuffledQuestions: [], |
| 6 | + isSubmitted: false |
| 7 | + }; |
| 8 | + |
| 9 | + // Initialize exam on page load |
| 10 | + function initExam() { |
| 11 | + // Shuffle only the options for each question, not the questions themselves |
| 12 | + examState.shuffledQuestions = examQuestions.map(q => { |
| 13 | + // Create array of option indices |
| 14 | + const optionIndices = q.options.map((_, index) => index); |
| 15 | + const shuffledIndices = shuffleArray(optionIndices); |
| 16 | + |
| 17 | + // Create shuffled options with mapping to original index |
| 18 | + const shuffledOptions = shuffledIndices.map(originalIndex => ({ |
| 19 | + text: q.options[originalIndex], |
| 20 | + originalIndex: originalIndex |
| 21 | + })); |
| 22 | + |
| 23 | + return { |
| 24 | + ...q, |
| 25 | + shuffledOptions: shuffledOptions, |
| 26 | + // Track which shuffled index corresponds to the correct answer |
| 27 | + correctShuffledIndex: shuffledIndices.indexOf(q.correct) |
| 28 | + }; |
| 29 | + }); |
| 30 | + |
| 31 | + renderQuestions(); |
| 32 | + attachEventListeners(); |
| 33 | + } |
| 34 | + |
| 35 | + // Render all questions |
| 36 | + function renderQuestions() { |
| 37 | + const container = document.getElementById('questionsContainer'); |
| 38 | + if (!container) return; |
| 39 | + |
| 40 | + container.innerHTML = ''; |
| 41 | + |
| 42 | + examState.shuffledQuestions.forEach((q, index) => { |
| 43 | + const questionCard = document.createElement('div'); |
| 44 | + questionCard.className = 'question-card'; |
| 45 | + questionCard.id = `question-${q.id}`; |
| 46 | + |
| 47 | + const questionHeader = document.createElement('div'); |
| 48 | + questionHeader.className = 'question-header'; |
| 49 | + questionHeader.innerHTML = ` |
| 50 | + <span class="question-number">Question ${index + 1} of ${examState.shuffledQuestions.length}</span> |
| 51 | + <span class="question-topic">${q.lesson} - ${q.topic}</span> |
| 52 | + `; |
| 53 | + |
| 54 | + const questionText = document.createElement('div'); |
| 55 | + questionText.className = 'question-text'; |
| 56 | + questionText.textContent = q.question; |
| 57 | + |
| 58 | + const optionsContainer = document.createElement('div'); |
| 59 | + optionsContainer.className = 'options-container'; |
| 60 | + |
| 61 | + q.shuffledOptions.forEach((option, optionIndex) => { |
| 62 | + const optionDiv = document.createElement('div'); |
| 63 | + optionDiv.className = 'option-item'; |
| 64 | + |
| 65 | + const input = document.createElement('input'); |
| 66 | + input.type = 'radio'; |
| 67 | + input.name = `question-${q.id}`; |
| 68 | + input.id = `q${q.id}-opt${optionIndex}`; |
| 69 | + input.value = optionIndex; |
| 70 | + input.dataset.questionId = q.id; |
| 71 | + |
| 72 | + // Check if this option was previously selected |
| 73 | + if (examState.userAnswers[q.id] === optionIndex) { |
| 74 | + input.checked = true; |
| 75 | + } |
| 76 | + |
| 77 | + const label = document.createElement('label'); |
| 78 | + label.htmlFor = `q${q.id}-opt${optionIndex}`; |
| 79 | + label.textContent = option.text; |
| 80 | + |
| 81 | + optionDiv.appendChild(input); |
| 82 | + optionDiv.appendChild(label); |
| 83 | + optionsContainer.appendChild(optionDiv); |
| 84 | + }); |
| 85 | + |
| 86 | + questionCard.appendChild(questionHeader); |
| 87 | + questionCard.appendChild(questionText); |
| 88 | + questionCard.appendChild(optionsContainer); |
| 89 | + container.appendChild(questionCard); |
| 90 | + }); |
| 91 | + |
| 92 | + updateProgress(); |
| 93 | + } |
| 94 | + |
| 95 | + // Attach event listeners |
| 96 | + function attachEventListeners() { |
| 97 | + // Listen for answer selections |
| 98 | + const container = document.getElementById('questionsContainer'); |
| 99 | + if (container) { |
| 100 | + container.addEventListener('change', (e) => { |
| 101 | + if (e.target.type === 'radio') { |
| 102 | + const questionId = parseInt(e.target.dataset.questionId); |
| 103 | + const selectedOption = parseInt(e.target.value); |
| 104 | + examState.userAnswers[questionId] = selectedOption; |
| 105 | + updateProgress(); |
| 106 | + |
| 107 | + // Scroll smoothly to next question |
| 108 | + const currentQuestion = document.getElementById(`question-${questionId}`); |
| 109 | + const nextQuestion = currentQuestion?.nextElementSibling; |
| 110 | + if (nextQuestion) { |
| 111 | + setTimeout(() => { |
| 112 | + nextQuestion.scrollIntoView({ behavior: 'smooth', block: 'start' }); |
| 113 | + }, 200); |
| 114 | + } |
| 115 | + } |
| 116 | + }); |
| 117 | + } |
| 118 | + |
| 119 | + // Submit buttons |
| 120 | + const submitBtn = document.getElementById('submitExamBtn'); |
| 121 | + const submitBtn2 = document.getElementById('submitExamBtn2'); |
| 122 | + if (submitBtn) submitBtn.addEventListener('click', handleSubmit); |
| 123 | + if (submitBtn2) submitBtn2.addEventListener('click', handleSubmit); |
| 124 | + |
| 125 | + // Reset button |
| 126 | + const resetBtn = document.getElementById('resetBtn'); |
| 127 | + if (resetBtn) resetBtn.addEventListener('click', handleReset); |
| 128 | + |
| 129 | + // Scroll to top button |
| 130 | + const scrollTopBtn = document.getElementById('scrollTopBtn'); |
| 131 | + if (scrollTopBtn) { |
| 132 | + scrollTopBtn.addEventListener('click', () => { |
| 133 | + window.scrollTo({ top: 0, behavior: 'smooth' }); |
| 134 | + }); |
| 135 | + } |
| 136 | + |
| 137 | + // Retake button |
| 138 | + const retakeBtn = document.getElementById('retakeBtn'); |
| 139 | + if (retakeBtn) retakeBtn.addEventListener('click', handleRetake); |
| 140 | + } |
| 141 | + |
| 142 | + // Update progress display |
| 143 | + function updateProgress() { |
| 144 | + const answeredCount = Object.keys(examState.userAnswers).length; |
| 145 | + const totalCount = examState.shuffledQuestions.length; |
| 146 | + const progressText = document.getElementById('progressText'); |
| 147 | + |
| 148 | + if (progressText) { |
| 149 | + progressText.textContent = `${answeredCount} / ${totalCount} answered`; |
| 150 | + } |
| 151 | + } |
| 152 | + |
| 153 | + // Handle exam submission |
| 154 | + function handleSubmit() { |
| 155 | + const answeredCount = Object.keys(examState.userAnswers).length; |
| 156 | + const totalCount = examState.shuffledQuestions.length; |
| 157 | + |
| 158 | + if (answeredCount < totalCount) { |
| 159 | + const unanswered = totalCount - answeredCount; |
| 160 | + const confirmSubmit = confirm( |
| 161 | + `You have ${unanswered} unanswered question(s). Are you sure you want to submit?` |
| 162 | + ); |
| 163 | + if (!confirmSubmit) return; |
| 164 | + } |
| 165 | + |
| 166 | + examState.isSubmitted = true; |
| 167 | + calculateAndShowResults(); |
| 168 | + } |
| 169 | + |
| 170 | + // Calculate results and show results screen |
| 171 | + function calculateAndShowResults() { |
| 172 | + let correctCount = 0; |
| 173 | + let wrongCount = 0; |
| 174 | + let unansweredCount = 0; |
| 175 | + |
| 176 | + const results = examState.shuffledQuestions.map(q => { |
| 177 | + const userAnswer = examState.userAnswers[q.id]; |
| 178 | + const isCorrect = userAnswer === q.correctShuffledIndex; |
| 179 | + const isAnswered = userAnswer !== undefined; |
| 180 | + |
| 181 | + if (isAnswered) { |
| 182 | + if (isCorrect) correctCount++; |
| 183 | + else wrongCount++; |
| 184 | + } else { |
| 185 | + unansweredCount++; |
| 186 | + } |
| 187 | + |
| 188 | + return { |
| 189 | + question: q, |
| 190 | + userAnswer: userAnswer, |
| 191 | + isCorrect: isCorrect, |
| 192 | + isAnswered: isAnswered |
| 193 | + }; |
| 194 | + }); |
| 195 | + |
| 196 | + // Update results display |
| 197 | + document.getElementById('scoreValue').textContent = correctCount; |
| 198 | + document.getElementById('correctCount').textContent = correctCount; |
| 199 | + document.getElementById('wrongCount').textContent = wrongCount; |
| 200 | + document.getElementById('unansweredCount').textContent = unansweredCount; |
| 201 | + |
| 202 | + const percentage = ((correctCount / examState.shuffledQuestions.length) * 100).toFixed(1); |
| 203 | + document.getElementById('scorePercentage').textContent = `${percentage}%`; |
| 204 | + |
| 205 | + // Add color class based on percentage |
| 206 | + const scoreCircle = document.querySelector('.score-circle'); |
| 207 | + if (scoreCircle) { |
| 208 | + scoreCircle.className = 'score-circle'; |
| 209 | + if (percentage >= 90) scoreCircle.classList.add('excellent'); |
| 210 | + else if (percentage >= 75) scoreCircle.classList.add('good'); |
| 211 | + else if (percentage >= 60) scoreCircle.classList.add('fair'); |
| 212 | + else scoreCircle.classList.add('poor'); |
| 213 | + } |
| 214 | + |
| 215 | + // Render detailed results |
| 216 | + renderDetailedResults(results); |
| 217 | + |
| 218 | + // Switch screens |
| 219 | + document.getElementById('examScreen').hidden = true; |
| 220 | + document.getElementById('resultsScreen').hidden = false; |
| 221 | + window.scrollTo({ top: 0, behavior: 'smooth' }); |
| 222 | + } |
| 223 | + |
| 224 | + // Render detailed results with all questions and answers |
| 225 | + function renderDetailedResults(results) { |
| 226 | + const container = document.getElementById('detailedResults'); |
| 227 | + if (!container) return; |
| 228 | + |
| 229 | + container.innerHTML = ''; |
| 230 | + |
| 231 | + results.forEach((result, index) => { |
| 232 | + const q = result.question; |
| 233 | + const resultCard = document.createElement('div'); |
| 234 | + resultCard.className = 'result-card'; |
| 235 | + |
| 236 | + if (result.isCorrect) { |
| 237 | + resultCard.classList.add('correct'); |
| 238 | + } else if (result.isAnswered) { |
| 239 | + resultCard.classList.add('wrong'); |
| 240 | + } else { |
| 241 | + resultCard.classList.add('unanswered'); |
| 242 | + } |
| 243 | + |
| 244 | + // Question header |
| 245 | + const header = document.createElement('div'); |
| 246 | + header.className = 'result-header'; |
| 247 | + |
| 248 | + const statusIcon = result.isCorrect ? '✓' : result.isAnswered ? '✗' : '—'; |
| 249 | + const statusClass = result.isCorrect ? 'correct' : result.isAnswered ? 'wrong' : 'unanswered'; |
| 250 | + const statusText = result.isCorrect ? 'Correct' : result.isAnswered ? 'Wrong' : 'Unanswered'; |
| 251 | + |
| 252 | + header.innerHTML = ` |
| 253 | + <div class="result-status"> |
| 254 | + <span class="status-icon ${statusClass}">${statusIcon}</span> |
| 255 | + <span class="status-text ${statusClass}">${statusText}</span> |
| 256 | + </div> |
| 257 | + <span class="result-number">Question ${index + 1}</span> |
| 258 | + `; |
| 259 | + |
| 260 | + // Question content |
| 261 | + const questionDiv = document.createElement('div'); |
| 262 | + questionDiv.className = 'result-question'; |
| 263 | + questionDiv.innerHTML = ` |
| 264 | + <div class="result-topic">${q.lesson} - ${q.topic}</div> |
| 265 | + <div class="result-question-text">${q.question}</div> |
| 266 | + `; |
| 267 | + |
| 268 | + // Answer options |
| 269 | + const answersDiv = document.createElement('div'); |
| 270 | + answersDiv.className = 'result-answers'; |
| 271 | + |
| 272 | + q.shuffledOptions.forEach((option, optionIndex) => { |
| 273 | + const answerDiv = document.createElement('div'); |
| 274 | + answerDiv.className = 'result-answer-option'; |
| 275 | + |
| 276 | + const isUserAnswer = result.userAnswer === optionIndex; |
| 277 | + const isCorrectAnswer = optionIndex === q.correctShuffledIndex; |
| 278 | + |
| 279 | + if (isCorrectAnswer) { |
| 280 | + answerDiv.classList.add('correct-answer'); |
| 281 | + } |
| 282 | + if (isUserAnswer && !isCorrectAnswer) { |
| 283 | + answerDiv.classList.add('user-wrong-answer'); |
| 284 | + } |
| 285 | + if (isUserAnswer && isCorrectAnswer) { |
| 286 | + answerDiv.classList.add('user-correct-answer'); |
| 287 | + } |
| 288 | + |
| 289 | + let prefix = ''; |
| 290 | + if (isCorrectAnswer) { |
| 291 | + prefix = '<span class="answer-icon correct">✓</span>'; |
| 292 | + } else if (isUserAnswer) { |
| 293 | + prefix = '<span class="answer-icon wrong">✗</span>'; |
| 294 | + } |
| 295 | + |
| 296 | + let label = ''; |
| 297 | + if (isCorrectAnswer && isUserAnswer) { |
| 298 | + label = '<span class="answer-label correct">Your Answer (Correct)</span>'; |
| 299 | + } else if (isCorrectAnswer) { |
| 300 | + label = '<span class="answer-label correct">Correct Answer</span>'; |
| 301 | + } else if (isUserAnswer) { |
| 302 | + label = '<span class="answer-label wrong">Your Answer</span>'; |
| 303 | + } |
| 304 | + |
| 305 | + answerDiv.innerHTML = ` |
| 306 | + ${prefix} |
| 307 | + <span class="answer-text">${option.text}</span> |
| 308 | + ${label} |
| 309 | + `; |
| 310 | + |
| 311 | + answersDiv.appendChild(answerDiv); |
| 312 | + }); |
| 313 | + |
| 314 | + // Explanation |
| 315 | + const explanationDiv = document.createElement('div'); |
| 316 | + explanationDiv.className = 'result-explanation'; |
| 317 | + explanationDiv.innerHTML = ` |
| 318 | + <strong>Explanation:</strong> ${q.explanation} |
| 319 | + `; |
| 320 | + |
| 321 | + resultCard.appendChild(header); |
| 322 | + resultCard.appendChild(questionDiv); |
| 323 | + resultCard.appendChild(answersDiv); |
| 324 | + resultCard.appendChild(explanationDiv); |
| 325 | + container.appendChild(resultCard); |
| 326 | + }); |
| 327 | + } |
| 328 | + |
| 329 | + // Handle reset |
| 330 | + function handleReset() { |
| 331 | + const confirmReset = confirm('Are you sure you want to reset the exam? All your answers will be lost.'); |
| 332 | + if (!confirmReset) return; |
| 333 | + |
| 334 | + examState.userAnswers = {}; |
| 335 | + renderQuestions(); |
| 336 | + window.scrollTo({ top: 0, behavior: 'smooth' }); |
| 337 | + } |
| 338 | + |
| 339 | + // Handle retake |
| 340 | + function handleRetake() { |
| 341 | + examState.userAnswers = {}; |
| 342 | + examState.isSubmitted = false; |
| 343 | + |
| 344 | + // Re-shuffle options |
| 345 | + examState.shuffledQuestions = examQuestions.map(q => { |
| 346 | + const optionIndices = q.options.map((_, index) => index); |
| 347 | + const shuffledIndices = shuffleArray(optionIndices); |
| 348 | + |
| 349 | + const shuffledOptions = shuffledIndices.map(originalIndex => ({ |
| 350 | + text: q.options[originalIndex], |
| 351 | + originalIndex: originalIndex |
| 352 | + })); |
| 353 | + |
| 354 | + return { |
| 355 | + ...q, |
| 356 | + shuffledOptions: shuffledOptions, |
| 357 | + correctShuffledIndex: shuffledIndices.indexOf(q.correct) |
| 358 | + }; |
| 359 | + }); |
| 360 | + |
| 361 | + renderQuestions(); |
| 362 | + |
| 363 | + document.getElementById('examScreen').hidden = false; |
| 364 | + document.getElementById('resultsScreen').hidden = true; |
| 365 | + window.scrollTo({ top: 0, behavior: 'smooth' }); |
| 366 | + } |
| 367 | + |
| 368 | + // Initialize when DOM is ready |
| 369 | + if (document.readyState === 'loading') { |
| 370 | + document.addEventListener('DOMContentLoaded', initExam); |
| 371 | + } else { |
| 372 | + initExam(); |
| 373 | + } |
| 374 | +})(); |
0 commit comments