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);