quiz.js

7.43 KB
23/10/2025 01:57
JS
quiz.js
/* eslint-env browser */
// Polished single-question card quiz renderer (choice / true-false only)
const QuizEngine = (function() {
  // Top-level storage key; inside we'll store per-quiz results by quizId
  const ROOT_KEY = 'weblesson_quiz_v1';

  function makeStorage() {
    const raw = localStorage.getItem(ROOT_KEY) || '{}';
    try {return JSON.parse(raw);} catch (e) {return {};}
  }
  function saveStorage(obj) {localStorage.setItem(ROOT_KEY, JSON.stringify(obj));}

  // Render a quiz as a card UI. `quizMeta` may contain id/title; questions array contains {id, question, options[], answer, type}
  function renderQuiz(container, questions, quizMeta = {}) {
    if (!Array.isArray(questions) || questions.length === 0) {
      container.innerHTML = '<div class="quiz-empty">ไม่มีแบบทดสอบ</div>';
      return;
    }

    const quizId = quizMeta.id || quizMeta.src || ('quiz_' + (questions[0].id || Date.now()));

    // ensure only supported types
    const filtered = questions.map(q => ({
      id: q.id || String(Math.random()).slice(2, 8),
      question: q.question || '',
      options: Array.isArray(q.options) ? q.options : [],
      answer: typeof q.answer !== 'undefined' ? q.answer : 0,
      type: q.type === 'true-false' ? 'true-false' : 'choice',
      explanation: q.explanation || ''
    }));

    const total = filtered.length;
    let idx = 0;

    function renderCard() {
      container.innerHTML = '';
      const card = document.createElement('div');
      card.className = 'quiz-card';

      const header = document.createElement('div');
      header.className = 'quiz-header';
      const title = document.createElement('div');
      title.className = 'quiz-title';
      title.textContent = quizMeta.title || 'แบบทดสอบ';
      const progress = document.createElement('div');
      progress.className = 'quiz-progress';
      progress.textContent = `คำถาม ${idx + 1} / ${total}`;
      header.appendChild(title);
      header.appendChild(progress);

      const body = document.createElement('div');
      body.className = 'quiz-body';
      const qobj = filtered[idx];
      const qEl = document.createElement('div');
      qEl.className = 'quiz-question';
      qEl.textContent = qobj.question;

      const optionsWrap = document.createElement('div');
      optionsWrap.className = 'quiz-options';

      const opts = qobj.type === 'true-false' ? ['True', 'False'] : qobj.options;

      opts.forEach((opt, i) => {
        const row = document.createElement('label');
        row.className = 'quiz-option';
        row.setAttribute('data-choice', String(i));

        const radio = document.createElement('input');
        radio.type = 'radio';
        radio.name = 'quiz_opt_' + qobj.id;
        radio.value = String(i);
        // visually hidden, we use the whole label as button
        const span = document.createElement('div');
        span.className = 'label-text';
        span.textContent = opt;

        row.appendChild(radio);
        row.appendChild(span);

        row.addEventListener('click', () => handleSelect(qobj, i, row));
        optionsWrap.appendChild(row);
      });

      body.appendChild(qEl);
      body.appendChild(optionsWrap);

      const actions = document.createElement('div');
      actions.className = 'quiz-actions';
      const skip = document.createElement('button');
      skip.className = 'quiz-btn ghost';
      skip.textContent = idx < total - 1 ? 'ข้าม' : 'ดูผล';
      skip.addEventListener('click', () => {if (idx < total - 1) {idx++; renderCard();} else {showSummary();} });

      const next = document.createElement('button');
      next.className = 'quiz-btn';
      next.textContent = idx < total - 1 ? 'ถัดไป' : 'ส่งคำตอบ';
      next.addEventListener('click', () => {if (idx < total - 1) {idx++; renderCard();} else {showSummary();} });

      actions.appendChild(skip);
      actions.appendChild(next);

      card.appendChild(header);
      card.appendChild(body);
      card.appendChild(actions);

      container.appendChild(card);

      // restore previous answer if present
      const stored = makeStorage();
      const quizData = stored[quizId] || {};
      const saved = quizData[qobj.id];
      if (typeof saved !== 'undefined') {
        const chosen = String(saved.choice);
        const label = card.querySelector('.quiz-option[data-choice="' + chosen + '"]');
        if (label) label.classList.add(saved.correct ? 'correct' : 'wrong');
      }
    }

    function handleSelect(qobj, choiceIndex, rowEl) {
      // disable options for this question
      const card = rowEl.closest('.quiz-card');
      card.querySelectorAll('.quiz-option').forEach(r => r.style.pointerEvents = 'none');

      const correct = Number(qobj.answer) === Number(choiceIndex);
      rowEl.classList.add(correct ? 'correct' : 'wrong');

      // save per-quiz/per-question
      const store = makeStorage();
      store[quizId] = store[quizId] || {};
      store[quizId][qobj.id] = {choice: choiceIndex, correct: !!correct, ts: Date.now()};
      saveStorage(store);

      // show explanation if any
      if (qobj.explanation) {
        const fb = document.createElement('div');
        fb.className = 'quiz-feedback';
        fb.textContent = qobj.explanation;
        const body = card.querySelector('.quiz-body');
        body.appendChild(fb);
      }
    }

    function showSummary() {
      const store = makeStorage();
      const data = (store[quizId]) || {};
      let correctCount = 0;
      filtered.forEach(q => {if (data[q.id] && data[q.id].correct) correctCount++;});

      container.innerHTML = '';
      const card = document.createElement('div');
      card.className = 'quiz-card';

      const header = document.createElement('div');
      header.className = 'quiz-header';
      const title = document.createElement('div');
      title.className = 'quiz-title';
      title.textContent = quizMeta.title || 'ผลการทดสอบ';
      const progress = document.createElement('div');
      progress.className = 'quiz-progress';
      progress.textContent = `ได้ ${correctCount} / ${total}`;
      header.appendChild(title);
      header.appendChild(progress);

      const score = document.createElement('div');
      score.className = 'quiz-score';
      score.innerHTML = `<div class="big">${Math.round((correctCount / total) * 100)}%</div><div class="quiz-small-muted">คำตอบถูก ${correctCount} จาก ${total}</div>`;

      const actions = document.createElement('div');
      actions.className = 'quiz-actions';
      const retry = document.createElement('button');
      retry.className = 'quiz-btn';
      retry.textContent = 'ทำใหม่';
      retry.addEventListener('click', () => {
        // clear quiz results
        const s = makeStorage();
        if (s[quizId]) delete s[quizId];
        saveStorage(s);
        idx = 0; renderCard();
      });
      const close = document.createElement('button');
      close.className = 'quiz-btn ghost';
      close.textContent = 'ปิด';
      close.addEventListener('click', () => {container.innerHTML = '';});

      actions.appendChild(close);
      actions.appendChild(retry);

      card.appendChild(header);
      card.appendChild(score);
      card.appendChild(actions);
      container.appendChild(card);
    }

    renderCard();
  }

  // convenience: load/save helpers exposed for tests
  return {renderQuiz, _makeStorage: makeStorage};
})();

// Expose globally
window.QuizEngine = QuizEngine;