upsc_quiz-1.0.x-dev/js/upsc-quiz.js

js/upsc-quiz.js
/**
 * @file
 * UPSC Quiz JavaScript functionality.
 */

(function ($, Drupal, drupalSettings) {
  'use strict';

  /**
   * upsc Quiz Application Class
   */
  class UPSCQuizApp {
    constructor() {
      this.currentSection = '';
      this.currentQuestionIndex = 0;
      this.questions = [];
      this.userAnswers = {};
      this.timeRemaining = 0;
      this.timerInterval = null;
      this.startTime = null;
      this.settings = drupalSettings.upscQuiz || {};
      
      this.init();
    }

    /**
     * Initialize the quiz application
     */
    init() {
      this.bindEvents();
      this.initializeWelcomeScreen();
    }

    /**
     * Bind event listeners
     */
    bindEvents() {
      $(document).on('click', '.section-card', this.selectSection.bind(this));
      $(document).on('click', '.start-quiz-btn', this.startQuiz.bind(this));
      $(document).on('click', '.option-btn', this.selectAnswer.bind(this));
      $(document).on('click', '.next-question', this.nextQuestion.bind(this));
      $(document).on('click', '.prev-question', this.prevQuestion.bind(this));
      $(document).on('click', '.finish-quiz', this.finishQuiz.bind(this));
      $(document).on('click', '.retake-quiz', this.retakeQuiz.bind(this));
      $(document).on('click', '.review-answer', this.reviewAnswer.bind(this));
      $(document).on('click', '.back-to-sections', this.backToSections.bind(this));
    }

    /**
     * Initialize welcome screen
     */
    initializeWelcomeScreen() {
      this.showScreen('welcome');
    }

    /**
     * Show specific screen
     */
    showScreen(screenName) {
      $('.screen').removeClass('active');
      $(`.${screenName}-screen`).addClass('active');
    }

    /**
     * Select a quiz section
     */
    selectSection(event) {
      const sectionCard = $(event.currentTarget);
      this.currentSection = sectionCard.data('section');
      
      // Update UI
      $('.section-card').removeClass('selected');
      sectionCard.addClass('selected');
      $('.start-quiz-btn').prop('disabled', false);
    }

    /**
     * Start the quiz for selected section
     */
    async startQuiz() {
      if (!this.currentSection) {
        this.showMessage(Drupal.t('Please select a section first.'), 'error');
        return;
      }

      try {
        // Load questions from server
        await this.loadQuestions();
        
        // Initialize quiz state
        this.currentQuestionIndex = 0;
        this.userAnswers = {};
        this.startTime = Date.now();
        
        // Set up timer if enabled
        if (this.settings.timeLimit > 0) {
          this.timeRemaining = this.settings.timeLimit * 60; // Convert to seconds
          this.startTimer();
        }
        
        // Show quiz screen and first question
        this.showScreen('quiz');
        this.displayQuestion();
        this.updateProgress();
        
      } catch (error) {
        this.showMessage(Drupal.t('Failed to load quiz questions. Please try again.'), 'error');
        console.error('Quiz load error:', error);
      }
    }

    /**
     * Load questions from server
     */
    async loadQuestions() {
      const response = await fetch(`/upsc-quiz/api/questions/${this.currentSection}`);
      if (!response.ok) {
        throw new Error('Failed to load questions');
      }
      
      const data = await response.json();
      this.questions = data.questions;
      
      if (this.questions.length === 0) {
        throw new Error('No questions available for this section');
      }
    }

    /**
     * Display current question
     */
    displayQuestion() {
      const question = this.questions[this.currentQuestionIndex];
      const questionContainer = $('.question-container');
      
      questionContainer.html(`
        <div class="question-header">
          <h3>${Drupal.t('Question')} ${this.currentQuestionIndex + 1}</h3>
          <div class="question-info">
            <span class="section-name">${this.currentSection}</span>
            ${question.difficulty ? `<span class="difficulty level-${question.difficulty}">${this.getDifficultyText(question.difficulty)}</span>` : ''}
          </div>
        </div>
        <div class="question-text">
          ${question.question}
        </div>
        <div class="options">
          ${this.renderOptions(question)}
        </div>
      `);

      // Highlight previously selected answer if any
      const userAnswer = this.userAnswers[this.currentQuestionIndex];
      if (userAnswer) {
        $(`.option-btn[data-value="${userAnswer}"]`).addClass('selected');
      }

      // Update navigation buttons
      this.updateNavigationButtons();
    }

    /**
     * Render question options
     */
    renderOptions(question) {
      const options = ['A', 'B', 'C', 'D'];
      return options.map(option => `
        <button class="option-btn" data-value="${option}">
          <span class="option-letter">${option}</span>
          <span class="option-text">${question[`option_${option.toLowerCase()}`]}</span>
        </button>
      `).join('');
    }

    /**
     * Get difficulty text
     */
    getDifficultyText(level) {
      const difficulties = {
        1: Drupal.t('Easy'),
        2: Drupal.t('Medium'),
        3: Drupal.t('Hard'),
        4: Drupal.t('Very Hard'),
        5: Drupal.t('Expert')
      };
      return difficulties[level] || Drupal.t('Unknown');
    }

    /**
     * Select an answer
     */
    selectAnswer(event) {
      const optionBtn = $(event.currentTarget);
      const value = optionBtn.data('value');
      
      // Remove previous selection
      $('.option-btn').removeClass('selected');
      
      // Add selection to current option
      optionBtn.addClass('selected');
      
      // Store answer
      this.userAnswers[this.currentQuestionIndex] = value;
      
      // Update progress
      this.updateProgress();
    }

    /**
     * Go to next question
     */
    nextQuestion() {
      if (this.currentQuestionIndex < this.questions.length - 1) {
        this.currentQuestionIndex++;
        this.displayQuestion();
        this.updateProgress();
      }
    }

    /**
     * Go to previous question
     */
    prevQuestion() {
      if (this.currentQuestionIndex > 0) {
        this.currentQuestionIndex--;
        this.displayQuestion();
        this.updateProgress();
      }
    }

    /**
     * Update navigation buttons
     */
    updateNavigationButtons() {
      const prevBtn = $('.prev-question');
      const nextBtn = $('.next-question');
      const finishBtn = $('.finish-quiz');
      
      // Previous button
      prevBtn.prop('disabled', this.currentQuestionIndex === 0);
      
      // Next/Finish buttons
      if (this.currentQuestionIndex === this.questions.length - 1) {
        nextBtn.hide();
        finishBtn.show();
      } else {
        nextBtn.show();
        finishBtn.hide();
      }
    }

    /**
     * Update progress indicators
     */
    updateProgress() {
      const totalQuestions = this.questions.length;
      const answeredQuestions = Object.keys(this.userAnswers).length;
      const progressPercentage = (answeredQuestions / totalQuestions) * 100;
      
      // Update progress bar
      $('.progress-fill').css('width', `${progressPercentage}%`);
      
      // Update progress text
      $('.progress-text').text(`${answeredQuestions}/${totalQuestions} ${Drupal.t('answered')}`);
      
      // Update question indicator
      $('.question-indicator').html(`
        ${Drupal.t('Question')} ${this.currentQuestionIndex + 1} ${Drupal.t('of')} ${totalQuestions}
      `);
    }

    /**
     * Start timer
     */
    startTimer() {
      this.timerInterval = setInterval(() => {
        this.timeRemaining--;
        
        if (this.timeRemaining <= 0) {
          this.timeUp();
          return;
        }
        
        this.updateTimerDisplay();
        
        // Warning at 5 minutes
        if (this.timeRemaining === 300) {
          this.showMessage(Drupal.t('5 minutes remaining!'), 'warning');
        }
        
        // Warning at 1 minute
        if (this.timeRemaining === 60) {
          this.showMessage(Drupal.t('1 minute remaining!'), 'warning');
        }
        
      }, 1000);
    }

    /**
     * Update timer display
     */
    updateTimerDisplay() {
      const minutes = Math.floor(this.timeRemaining / 60);
      const seconds = this.timeRemaining % 60;
      const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
      
      $('.timer').text(timeString);
      
      // Add warning class when time is low
      if (this.timeRemaining <= 300) { // 5 minutes
        $('.timer').addClass('warning');
      }
      if (this.timeRemaining <= 60) { // 1 minute
        $('.timer').addClass('critical');
      }
    }

    /**
     * Handle time up
     */
    timeUp() {
      this.clearTimer();
      this.showMessage(Drupal.t('Time is up! The quiz will be submitted automatically.'), 'error');
      setTimeout(() => {
        this.finishQuiz();
      }, 2000);
    }

    /**
     * Clear timer
     */
    clearTimer() {
      if (this.timerInterval) {
        clearInterval(this.timerInterval);
        this.timerInterval = null;
      }
    }

    /**
     * Finish quiz and calculate results
     */
    async finishQuiz() {
      this.clearTimer();
      
      try {
        // Calculate time taken
        const timeTaken = this.startTime ? Math.floor((Date.now() - this.startTime) / 1000) : 0;
        
        // Prepare submission data
        const submissionData = {
          section: this.currentSection,
          answers: this.userAnswers,
          timeTaken: timeTaken,
          totalQuestions: this.questions.length
        };
        
        // Submit to server
        const response = await fetch('/upsc-quiz/api/submit', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
          },
          body: JSON.stringify(submissionData)
        });
        
        if (!response.ok) {
          throw new Error('Failed to submit quiz');
        }
        
        const results = await response.json();
        this.displayResults(results);
        
      } catch (error) {
        this.showMessage(Drupal.t('Failed to submit quiz. Please try again.'), 'error');
        console.error('Quiz submission error:', error);
      }
    }

    /**
     * Display quiz results
     */
    displayResults(results) {
      const resultsContainer = $('.results-container');
      const percentage = Math.round((results.score / results.totalQuestions) * 100);
      
      // Determine performance level
      let performanceClass = 'poor';
      let performanceText = Drupal.t('Needs Improvement');
      
      if (percentage >= 80) {
        performanceClass = 'excellent';
        performanceText = Drupal.t('Excellent');
      } else if (percentage >= 60) {
        performanceClass = 'good';
        performanceText = Drupal.t('Good');
      } else if (percentage >= 40) {
        performanceClass = 'average';
        performanceText = Drupal.t('Average');
      }
      
      resultsContainer.html(`
        <div class="results-header">
          <h2>${Drupal.t('Quiz Completed!')}</h2>
          <div class="section-name">${this.currentSection}</div>
        </div>
        
        <div class="score-display ${performanceClass}">
          <div class="score-circle">
            <span class="percentage">${percentage}%</span>
          </div>
          <div class="performance-text">${performanceText}</div>
        </div>
        
        <div class="results-details">
          <div class="result-item">
            <span class="label">${Drupal.t('Correct Answers:')}</span>
            <span class="value">${results.score} / ${results.totalQuestions}</span>
          </div>
          <div class="result-item">
            <span class="label">${Drupal.t('Time Taken:')}</span>
            <span class="value">${this.formatTime(results.timeTaken)}</span>
          </div>
          <div class="result-item">
            <span class="label">${Drupal.t('Accuracy:')}</span>
            <span class="value">${percentage}%</span>
          </div>
        </div>
        
        <div class="results-actions">
          ${this.settings.allowRetake ? '<button class="retake-quiz btn btn-primary">' + Drupal.t('Retake Quiz') + '</button>' : ''}
          ${this.settings.showCorrectAnswers ? '<button class="review-answers btn btn-secondary">' + Drupal.t('Review Answers') + '</button>' : ''}
          <button class="back-to-sections btn btn-outline">${Drupal.t('Back to Sections')}</button>
        </div>
      `);
      
      // Store results for review
      this.lastResults = results;
      
      // Show results screen
      this.showScreen('results');
    }

    /**
     * Review answers
     */
    reviewAnswer() {
      if (!this.lastResults) {
        this.showMessage(Drupal.t('No results available for review.'), 'error');
        return;
      }
      
      this.showReviewScreen();
    }

    /**
     * Show review screen
     */
    showReviewScreen() {
      const reviewContainer = $('.review-container');
      let reviewHTML = `
        <div class="review-header">
          <h2>${Drupal.t('Answer Review')}</h2>
          <div class="section-name">${this.currentSection}</div>
        </div>
        <div class="review-questions">
      `;
      
      this.questions.forEach((question, index) => {
        const userAnswer = this.userAnswers[index];
        const correctAnswer = question.correct_answer;
        const isCorrect = userAnswer === correctAnswer;
        
        reviewHTML += `
          <div class="review-question ${isCorrect ? 'correct' : 'incorrect'}">
            <div class="question-number">
              ${Drupal.t('Question')} ${index + 1}
              <span class="result-icon ${isCorrect ? 'correct' : 'incorrect'}">
                ${isCorrect ? '✓' : '✗'}
              </span>
            </div>
            <div class="question-text">${question.question}</div>
            <div class="answer-review">
              <div class="user-answer">
                <strong>${Drupal.t('Your Answer:')}</strong> 
                ${userAnswer ? `${userAnswer}) ${question[`option_${userAnswer.toLowerCase()}`]}` : Drupal.t('Not answered')}
              </div>
              <div class="correct-answer">
                <strong>${Drupal.t('Correct Answer:')}</strong> 
                ${correctAnswer}) ${question[`option_${correctAnswer.toLowerCase()}`]}
              </div>
              ${question.explanation ? `<div class="explanation"><strong>${Drupal.t('Explanation:')}</strong> ${question.explanation}</div>` : ''}
            </div>
          </div>
        `;
      });
      
      reviewHTML += `
        </div>
        <div class="review-actions">
          <button class="back-to-results btn btn-primary">${Drupal.t('Back to Results')}</button>
          <button class="back-to-sections btn btn-outline">${Drupal.t('Back to Sections')}</button>
        </div>
      `;
      
      reviewContainer.html(reviewHTML);
      this.showScreen('review');
      
      // Bind back to results button
      $('.back-to-results').on('click', () => {
        this.showScreen('results');
      });
    }

    /**
     * Retake quiz
     */
    retakeQuiz() {
      this.resetQuiz();
      this.startQuiz();
    }

    /**
     * Back to sections
     */
    backToSections() {
      this.resetQuiz();
      this.showScreen('welcome');
    }

    /**
     * Reset quiz state
     */
    resetQuiz() {
      this.currentSection = '';
      this.currentQuestionIndex = 0;
      this.questions = [];
      this.userAnswers = {};
      this.lastResults = null;
      this.clearTimer();
      
      // Reset UI
      $('.section-card').removeClass('selected');
      $('.start-quiz-btn').prop('disabled', true);
      $('.timer').removeClass('warning critical');
    }

    /**
     * Format time in human readable format
     */
    formatTime(seconds) {
      const minutes = Math.floor(seconds / 60);
      const remainingSeconds = seconds % 60;
      
      if (minutes > 0) {
        return `${minutes}m ${remainingSeconds}s`;
      }
      return `${remainingSeconds}s`;
    }

    /**
     * Show message to user
     */
    showMessage(message, type = 'info') {
      // Remove existing messages
      $('.quiz-message').remove();
      
      // Create message element
      const messageEl = $(`
        <div class="quiz-message alert alert-${type}">
          ${message}
        </div>
      `);
      
      // Add to page
      $('.active.screen').prepend(messageEl);
      
      // Auto-remove after 5 seconds
      setTimeout(() => {
        messageEl.fadeOut(() => messageEl.remove());
      }, 5000);
    }
  }

  /**
   * Initialize quiz when DOM is ready
   */
  Drupal.behaviors.upscQuiz = {
    attach: function (context, settings) {
      $('.upsc-quiz-container', context).once('upsc-quiz').each(function () {
        new UPSCQuizApp();
      });
    }
  };

})(jQuery, Drupal, drupalSettings);

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc