initial commit

This commit is contained in:
2025-10-26 23:39:49 -05:00
commit 5fb0909e8d
120 changed files with 11279 additions and 0 deletions

338
static/js/app.js Normal file
View File

@@ -0,0 +1,338 @@
// Funciones globales para la aplicación del Balotario
// Configuración global
const APP_CONFIG = {
ANIMATION_DURATION: 300,
CONFETTI_DURATION: 3000,
SOUND_ENABLED: true,
DARK_MODE: false
};
// Utilidades generales
class Utils {
static showNotification(message, type = 'info', duration = 3000) {
const notification = document.createElement('div');
notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
notification.style.cssText = `
top: 20px;
right: 20px;
z-index: 9999;
min-width: 300px;
animation: slideInRight 0.3s ease-out;
`;
notification.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, duration);
}
static playSound(type) {
if (!APP_CONFIG.SOUND_ENABLED) return;
// Crear sonidos usando Web Audio API
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
const frequencies = {
correct: 800,
incorrect: 300,
click: 600,
complete: 1000
};
oscillator.frequency.setValueAtTime(frequencies[type] || 600, audioContext.currentTime);
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.1);
}
static createConfetti() {
const confettiContainer = document.createElement('div');
confettiContainer.className = 'confetti';
document.body.appendChild(confettiContainer);
const colors = ['#f39c12', '#e74c3c', '#3498db', '#27ae60', '#9b59b6'];
for (let i = 0; i < 50; i++) {
const confettiPiece = document.createElement('div');
confettiPiece.className = 'confetti-piece';
confettiPiece.style.cssText = `
left: ${Math.random() * 100}%;
background: ${colors[Math.floor(Math.random() * colors.length)]};
animation-delay: ${Math.random() * 3}s;
animation-duration: ${3 + Math.random() * 2}s;
`;
confettiContainer.appendChild(confettiPiece);
}
setTimeout(() => {
confettiContainer.remove();
}, APP_CONFIG.CONFETTI_DURATION);
}
static formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
static saveToLocalStorage(key, data) {
try {
localStorage.setItem(key, JSON.stringify(data));
} catch (e) {
console.warn('No se pudo guardar en localStorage:', e);
}
}
static loadFromLocalStorage(key, defaultValue = null) {
try {
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : defaultValue;
} catch (e) {
console.warn('No se pudo cargar de localStorage:', e);
return defaultValue;
}
}
}
// Clase para manejar estadísticas
class StatsManager {
constructor() {
this.stats = Utils.loadFromLocalStorage('balotario_stats', {
totalAnswered: 0,
correctAnswers: 0,
incorrectAnswers: 0,
accuracy: 0,
studyTime: 0,
examsTaken: 0,
bestScore: 0,
streak: 0,
lastActivity: null
});
}
updateStats(isCorrect) {
this.stats.totalAnswered++;
if (isCorrect) {
this.stats.correctAnswers++;
this.stats.streak++;
} else {
this.stats.incorrectAnswers++;
this.stats.streak = 0;
}
this.stats.accuracy = this.stats.totalAnswered > 0 ?
Math.round((this.stats.correctAnswers / this.stats.totalAnswered) * 100) : 0;
this.stats.lastActivity = new Date().toISOString();
this.saveStats();
return this.stats;
}
updateExamScore(score) {
this.stats.examsTaken++;
if (score > this.stats.bestScore) {
this.stats.bestScore = score;
}
this.saveStats();
}
addStudyTime(minutes) {
this.stats.studyTime += minutes;
this.saveStats();
}
getStats() {
return { ...this.stats };
}
saveStats() {
Utils.saveToLocalStorage('balotario_stats', this.stats);
}
resetStats() {
this.stats = {
totalAnswered: 0,
correctAnswers: 0,
incorrectAnswers: 0,
accuracy: 0,
studyTime: 0,
examsTaken: 0,
bestScore: 0,
streak: 0,
lastActivity: null
};
this.saveStats();
}
}
// Clase para manejar el progreso circular
class CircularProgress {
constructor(element, options = {}) {
this.element = element;
this.options = {
size: 120,
strokeWidth: 8,
color: '#3498db',
backgroundColor: 'rgba(255,255,255,0.2)',
...options
};
this.init();
}
init() {
const { size, strokeWidth } = this.options;
const radius = (size - strokeWidth) / 2;
const circumference = radius * 2 * Math.PI;
this.element.innerHTML = `
<svg width="${size}" height="${size}" class="progress-ring">
<circle class="bg" cx="${size/2}" cy="${size/2}" r="${radius}"
stroke="${this.options.backgroundColor}" stroke-width="${strokeWidth}"/>
<circle class="progress" cx="${size/2}" cy="${size/2}" r="${radius}"
stroke="${this.options.color}" stroke-width="${strokeWidth}"
stroke-dasharray="${circumference}" stroke-dashoffset="${circumference}"/>
</svg>
<div class="position-absolute top-50 start-50 translate-middle text-center">
<div class="progress-text fw-bold"></div>
<div class="progress-label small"></div>
</div>
`;
this.progressCircle = this.element.querySelector('.progress');
this.progressText = this.element.querySelector('.progress-text');
this.progressLabel = this.element.querySelector('.progress-label');
this.circumference = circumference;
}
setProgress(percentage, text = '', label = '') {
const offset = this.circumference - (percentage / 100) * this.circumference;
this.progressCircle.style.strokeDashoffset = offset;
this.progressText.textContent = text || `${percentage}%`;
this.progressLabel.textContent = label;
}
}
// Clase para manejar temas y preferencias
class ThemeManager {
constructor() {
this.currentTheme = Utils.loadFromLocalStorage('theme', 'light');
this.applyTheme();
}
toggleTheme() {
this.currentTheme = this.currentTheme === 'light' ? 'dark' : 'light';
this.applyTheme();
Utils.saveToLocalStorage('theme', this.currentTheme);
}
applyTheme() {
document.documentElement.setAttribute('data-theme', this.currentTheme);
document.body.setAttribute('data-theme', this.currentTheme);
APP_CONFIG.DARK_MODE = this.currentTheme === 'dark';
// Forzar actualización de estilos
document.body.style.display = 'none';
document.body.offsetHeight; // Trigger reflow
document.body.style.display = '';
}
}
// Clase para manejar atajos de teclado
class KeyboardManager {
constructor() {
this.shortcuts = new Map();
this.init();
}
init() {
document.addEventListener('keydown', (e) => {
const key = this.getKeyString(e);
if (this.shortcuts.has(key)) {
e.preventDefault();
this.shortcuts.get(key)();
}
});
}
getKeyString(event) {
const parts = [];
if (event.ctrlKey) parts.push('ctrl');
if (event.altKey) parts.push('alt');
if (event.shiftKey) parts.push('shift');
parts.push(event.key.toLowerCase());
return parts.join('+');
}
addShortcut(keys, callback) {
this.shortcuts.set(keys, callback);
}
removeShortcut(keys) {
this.shortcuts.delete(keys);
}
}
// Inicialización global
document.addEventListener('DOMContentLoaded', function() {
// Inicializar managers globales
window.statsManager = new StatsManager();
window.themeManager = new ThemeManager();
window.keyboardManager = new KeyboardManager();
// Agregar atajos de teclado globales
window.keyboardManager.addShortcut('ctrl+d', () => {
window.themeManager.toggleTheme();
Utils.showNotification('Tema cambiado', 'info');
});
// Mejorar la experiencia de navegación
const links = document.querySelectorAll('a[href^="/"]');
links.forEach(link => {
link.addEventListener('click', function(e) {
Utils.playSound('click');
});
});
// Agregar efectos de hover a las tarjetas
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.classList.add('card-hover');
});
// Inicializar tooltips de Bootstrap
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Mostrar mensaje de bienvenida
const isFirstVisit = !Utils.loadFromLocalStorage('hasVisited', false);
if (isFirstVisit) {
setTimeout(() => {
Utils.showNotification('¡Bienvenido al Balotario! Usa Ctrl+D para cambiar el tema.', 'info', 5000);
Utils.saveToLocalStorage('hasVisited', true);
}, 1000);
}
});
// Exportar para uso global
window.Utils = Utils;
window.StatsManager = StatsManager;
window.CircularProgress = CircularProgress;

139
static/js/modules/base.js Normal file
View File

@@ -0,0 +1,139 @@
// Base module for common functionality
class BaseModule {
constructor() {
this.questions = [];
this.currentIndex = 0;
this.loading = false;
}
// Common DOM utilities
static getElementById(id) {
return document.getElementById(id);
}
static show(element) {
if (typeof element === 'string') {
element = document.getElementById(element);
}
if (element) element.style.display = 'block';
}
static hide(element) {
if (typeof element === 'string') {
element = document.getElementById(element);
}
if (element) element.style.display = 'none';
}
static showFlex(element) {
if (typeof element === 'string') {
element = document.getElementById(element);
}
if (element) element.style.display = 'flex';
}
// Common API calls
async fetchQuestions(params = {}) {
const url = new URL('/api/questions', window.location.origin);
Object.keys(params).forEach(key => {
if (params[key] !== undefined && params[key] !== null) {
url.searchParams.append(key, params[key]);
}
});
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Error fetching questions:', error);
this.showNotification('Error al cargar las preguntas', 'danger');
throw error;
}
}
// Common notification wrapper
showNotification(message, type = 'info', duration = 3000) {
if (window.Utils) {
Utils.showNotification(message, type, duration);
} else {
alert(message);
}
}
// Common sound wrapper
playSound(type) {
if (window.Utils) {
Utils.playSound(type);
}
}
// Common loading states
showLoading(loadingId = 'loading') {
BaseModule.showFlex(loadingId);
}
hideLoading(loadingId = 'loading') {
BaseModule.hide(loadingId);
}
// Common question display utilities
createQuestionHTML(question, currentIndex, totalQuestions) {
let html = `
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">Pregunta ${question.id}</h5>
<span class="badge bg-primary">${currentIndex + 1} de ${totalQuestions}</span>
</div>
`;
if (question.has_image && question.image) {
html += `<img src="${question.image}" class="question-image" alt="Imagen de la pregunta">`;
}
html += `<p class="lead mb-4">${question.question}</p>`;
return html;
}
// Common option creation
createOptionsHTML(options, correctAnswer, optionClass = 'option-btn') {
let html = '<div class="options-container">';
options.forEach((option, index) => {
const letter = String.fromCharCode(97 + index); // a, b, c, d
const isCorrect = letter === correctAnswer;
html += `
<div class="${optionClass} mb-2 p-3 border rounded"
data-correct="${isCorrect}" data-letter="${letter}">
<strong>${option}</strong>
<i class="fas fa-check-circle text-success ms-2 answer-indicator" style="display: none;"></i>
</div>
`;
});
html += '</div>';
return html;
}
// Common navigation update
updateNavigation(prevBtnId, nextBtnId, currentQuestionId) {
const prevBtn = BaseModule.getElementById(prevBtnId);
const nextBtn = BaseModule.getElementById(nextBtnId);
const currentQuestionEl = BaseModule.getElementById(currentQuestionId);
if (prevBtn) prevBtn.disabled = this.currentIndex === 0;
if (nextBtn) nextBtn.disabled = this.currentIndex === this.questions.length - 1;
if (currentQuestionEl) currentQuestionEl.textContent = this.currentIndex + 1;
}
// Common progress update
updateProgress(progressBarId) {
const progressBar = BaseModule.getElementById(progressBarId);
if (progressBar && this.questions.length > 0) {
const progress = ((this.currentIndex + 1) / this.questions.length) * 100;
progressBar.style.width = progress + '%';
}
}
}
// Export for global use
window.BaseModule = BaseModule;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,600 @@
// Exam Mode Module
class ExamMode extends BaseModule {
constructor() {
super();
this.examAnswers = {};
this.examTimer = null;
this.examTimeLimit = 30; // minutes
this.examStartTime = null;
this.navigator = null;
this.init();
}
init() {
this.setupEventListeners();
}
setupEventListeners() {
document.getElementById('start-exam')?.addEventListener('click', () => {
this.startExam();
});
document.getElementById('prev-exam-btn')?.addEventListener('click', () => {
this.previousQuestion();
});
document.getElementById('next-exam-btn')?.addEventListener('click', () => {
this.nextQuestion();
});
document.getElementById('finish-exam')?.addEventListener('click', () => {
this.showFinishConfirmation();
});
document.getElementById('retake-exam')?.addEventListener('click', () => {
this.resetExam();
});
document.getElementById('review-answers')?.addEventListener('click', () => {
this.showReviewMode();
});
// Keyboard navigation
document.addEventListener('keydown', (e) => {
this.handleKeyboard(e);
});
// Modal event listeners
this.setupModalListeners();
}
setupModalListeners() {
document.addEventListener('click', (e) => {
if (e.target.id === 'confirm-finish') {
this.finishExam();
this.hideConfirmModal();
} else if (e.target.id === 'confirmModalCancel' || e.target.id === 'confirmModalClose') {
this.hideConfirmModal();
} else if (e.target.id === 'confirmBackdrop') {
this.hideConfirmModal();
}
});
}
async startExam() {
const questionCount = parseInt(document.getElementById('exam-questions').value);
this.examTimeLimit = parseInt(document.getElementById('exam-time').value);
try {
const data = await this.fetchQuestions({
mode: 'random',
count: questionCount
});
this.questions = data;
this.examAnswers = {};
this.currentIndex = 0;
this.examStartTime = new Date();
this.initializeNavigator();
this.displayQuestion();
this.startTimer();
BaseModule.hide('exam-setup');
BaseModule.show('exam-content');
this.updateExamProgress();
} catch (error) {
console.error('Error starting exam:', error);
}
}
initializeNavigator() {
if (!this.navigator) {
this.navigator = new QuestionNavigator('question-navigator', {
onQuestionSelect: (index) => {
this.currentIndex = index;
this.displayQuestion();
},
storagePrefix: 'exam_temp' // Don't persist exam navigation
});
}
this.navigator.setQuestions(this.questions);
}
displayQuestion() {
if (this.currentIndex >= this.questions.length) return;
const question = this.questions[this.currentIndex];
let html = this.createQuestionHTML(question, this.currentIndex, this.questions.length);
html += this.createExamOptionsHTML(question.options);
document.getElementById('exam-question-container').innerHTML = html;
document.getElementById('question-progress').textContent = `${this.currentIndex + 1} de ${this.questions.length}`;
// Setup option click handlers
this.setupExamOptionHandlers(question);
this.updateExamNavigation();
if (this.navigator) {
this.navigator.setCurrentIndex(this.currentIndex);
}
}
setupExamOptionHandlers(question) {
const optionButtons = document.querySelectorAll('.option-enhanced');
optionButtons.forEach(button => {
button.addEventListener('click', () => {
this.playSound('click');
optionButtons.forEach(btn => btn.classList.remove('selected'));
button.classList.add('selected');
const selectedAnswer = button.getAttribute('data-answer');
this.examAnswers[question.id] = selectedAnswer;
if (this.navigator) {
this.navigator.updateNavigator();
}
this.updateExamProgress();
});
});
// Pre-select if already answered
if (this.examAnswers[question.id]) {
const selectedButton = document.querySelector(`[data-answer="${this.examAnswers[question.id]}"]`);
if (selectedButton) {
selectedButton.classList.add('selected');
}
}
}
previousQuestion() {
if (this.currentIndex > 0) {
this.currentIndex--;
this.displayQuestion();
}
}
nextQuestion() {
if (this.currentIndex < this.questions.length - 1) {
this.currentIndex++;
this.displayQuestion();
}
}
updateExamNavigation() {
document.getElementById('prev-exam-btn').disabled = this.currentIndex === 0;
document.getElementById('next-exam-btn').disabled = this.currentIndex === this.questions.length - 1;
}
updateExamProgress() {
const answeredCount = Object.keys(this.examAnswers).length;
const remainingCount = this.questions.length - answeredCount;
document.getElementById('answered-count').textContent = answeredCount;
document.getElementById('remaining-count').textContent = remainingCount;
}
startTimer() {
const endTime = new Date(this.examStartTime.getTime() + this.examTimeLimit * 60000);
this.examTimer = setInterval(() => {
const now = new Date();
const timeLeft = endTime - now;
if (timeLeft <= 0) {
clearInterval(this.examTimer);
this.finishExam();
return;
}
const minutes = Math.floor(timeLeft / 60000);
const seconds = Math.floor((timeLeft % 60000) / 1000);
document.getElementById('timer').textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
// Change color when time is running out
const timerElement = document.getElementById('timer');
if (minutes < 5) {
timerElement.classList.remove('text-warning');
timerElement.classList.add('text-danger');
}
}, 1000);
}
showFinishConfirmation() {
const unansweredCount = this.questions.length - Object.keys(this.examAnswers).length;
document.getElementById('unanswered-warning').textContent = unansweredCount;
document.getElementById('confirmModal').style.display = 'block';
document.body.classList.add('modal-open');
}
hideConfirmModal() {
document.getElementById('confirmModal').style.display = 'none';
document.body.classList.remove('modal-open');
}
finishExam() {
clearInterval(this.examTimer);
let correct = 0;
let incorrect = 0;
let unanswered = 0;
this.questions.forEach(question => {
if (this.examAnswers.hasOwnProperty(question.id)) {
if (this.examAnswers[question.id] === question.correct) {
correct++;
} else {
incorrect++;
}
} else {
unanswered++;
}
});
const score = Math.round((correct / this.questions.length) * 100);
const passed = score >= 70; // 70% to pass
// Update global stats
if (window.statsManager) {
for (let i = 0; i < correct; i++) {
window.statsManager.updateStats(true);
}
for (let i = 0; i < incorrect; i++) {
window.statsManager.updateStats(false);
}
window.statsManager.updateExamScore(score);
}
// Show results
this.showExamResults(correct, incorrect, unanswered, score, passed);
}
showExamResults(correct, incorrect, unanswered, score, passed) {
document.getElementById('exam-correct').textContent = correct;
document.getElementById('exam-incorrect').textContent = incorrect;
document.getElementById('exam-unanswered').textContent = unanswered;
document.getElementById('exam-score').textContent = score + '%';
const progressBar = document.getElementById('exam-progress-bar');
progressBar.style.width = score + '%';
const resultIcon = document.getElementById('result-icon');
const resultTitle = document.getElementById('result-title');
const passMessage = document.getElementById('pass-message');
if (passed) {
progressBar.classList.remove('bg-danger');
progressBar.classList.add('bg-success');
resultIcon.innerHTML = '<i class="fas fa-trophy fa-4x text-success"></i>';
resultTitle.textContent = '¡Felicitaciones! Has Aprobado';
passMessage.classList.add('alert-success');
passMessage.textContent = 'Has obtenido una calificación aprobatoria. ¡Buen trabajo!';
passMessage.style.display = 'block';
if (window.Utils) {
Utils.createConfetti();
Utils.playSound('complete');
}
} else {
progressBar.classList.remove('bg-success');
progressBar.classList.add('bg-danger');
resultIcon.innerHTML = '<i class="fas fa-times-circle fa-4x text-danger"></i>';
resultTitle.textContent = 'Examen No Aprobado';
passMessage.classList.add('alert-danger');
passMessage.textContent = 'Necesitas al menos 70% para aprobar. ¡Sigue estudiando y vuelve a intentarlo!';
passMessage.style.display = 'block';
}
BaseModule.hide('exam-content');
BaseModule.show('exam-results');
}
resetExam() {
this.examAnswers = {};
this.currentIndex = 0;
clearInterval(this.examTimer);
BaseModule.hide('exam-results');
BaseModule.show('exam-setup');
}
createExamOptionsHTML(options) {
let html = '<div class="options-container">';
options.forEach((option, index) => {
const letter = String.fromCharCode(97 + index); // a, b, c, d
html += `
<button class="option-enhanced" data-answer="${letter}">
<div class="d-flex align-items-center">
<span class="badge bg-primary me-3">${letter.toUpperCase()}</span>
<span>${option.replace(/^[a-d]\)\s*/, '')}</span>
</div>
</button>
`;
});
html += '</div>';
return html;
}
showReviewMode() {
this.reviewIndex = 0;
this.setupReviewEventListeners();
this.displayReviewQuestion();
this.createReviewNavigator();
BaseModule.hide('exam-results');
BaseModule.show('exam-review');
}
setupReviewEventListeners() {
// Remove existing listeners to avoid duplicates
document.getElementById('prev-review-btn')?.removeEventListener('click', this.prevReviewHandler);
document.getElementById('next-review-btn')?.removeEventListener('click', this.nextReviewHandler);
document.getElementById('back-to-results')?.removeEventListener('click', this.backToResultsHandler);
// Create bound handlers
this.prevReviewHandler = () => this.previousReviewQuestion();
this.nextReviewHandler = () => this.nextReviewQuestion();
this.backToResultsHandler = () => this.backToResults();
// Add event listeners
document.getElementById('prev-review-btn')?.addEventListener('click', this.prevReviewHandler);
document.getElementById('next-review-btn')?.addEventListener('click', this.nextReviewHandler);
document.getElementById('back-to-results')?.addEventListener('click', this.backToResultsHandler);
}
displayReviewQuestion() {
if (this.reviewIndex >= this.questions.length) return;
const question = this.questions[this.reviewIndex];
const userAnswer = this.examAnswers[question.id];
const isCorrect = userAnswer === question.correct;
const wasAnswered = userAnswer !== undefined;
let html = `
<div class="question-card-enhanced fade-in">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0 text-gradient">Pregunta ${question.id}</h5>
<div>
<span class="badge bg-primary me-2">${this.reviewIndex + 1} de ${this.questions.length}</span>
${wasAnswered ?
(isCorrect ?
'<span class="badge bg-success"><i class="fas fa-check me-1"></i>Correcta</span>' :
'<span class="badge bg-danger"><i class="fas fa-times me-1"></i>Incorrecta</span>'
) :
'<span class="badge bg-secondary"><i class="fas fa-question me-1"></i>Sin responder</span>'
}
</div>
</div>
`;
if (question.has_image && question.image) {
html += `<img src="${question.image}" class="question-image" alt="Imagen de la pregunta">`;
}
html += `<p class="lead mb-4">${question.question}</p>`;
html += '<div class="options-container">';
question.options.forEach((option, index) => {
const letter = String.fromCharCode(97 + index); // a, b, c, d
const isUserAnswer = letter === userAnswer;
const isCorrectAnswer = letter === question.correct;
let optionClass = 'option-enhanced';
let badgeClass = 'bg-primary';
let iconHtml = '';
if (isCorrectAnswer) {
optionClass += ' correct';
badgeClass = 'bg-success';
iconHtml = '<i class="fas fa-check ms-2 text-success"></i>';
} else if (isUserAnswer && !isCorrect) {
optionClass += ' incorrect';
badgeClass = 'bg-danger';
iconHtml = '<i class="fas fa-times ms-2 text-danger"></i>';
} else if (isUserAnswer) {
optionClass += ' selected';
}
html += `
<div class="${optionClass}">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center">
<span class="badge ${badgeClass} me-3">${letter.toUpperCase()}</span>
<span>${option.replace(/^[a-d]\)\s*/, '')}</span>
</div>
${iconHtml}
</div>
</div>
`;
});
html += '</div>';
// Add explanation if available
if (!wasAnswered) {
html += `
<div class="alert alert-warning mt-3">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>No respondida:</strong> Esta pregunta no fue respondida durante el examen.
</div>
`;
} else if (!isCorrect) {
html += `
<div class="alert alert-info mt-3">
<i class="fas fa-info-circle me-2"></i>
<strong>Tu respuesta:</strong> ${userAnswer?.toUpperCase()} |
<strong>Respuesta correcta:</strong> ${question.correct.toUpperCase()}
</div>
`;
} else {
html += `
<div class="alert alert-success mt-3">
<i class="fas fa-check-circle me-2"></i>
<strong>¡Correcto!</strong> Seleccionaste la respuesta correcta.
</div>
`;
}
html += '</div>';
document.getElementById('review-question-container').innerHTML = html;
document.getElementById('review-progress').textContent = `${this.reviewIndex + 1} de ${this.questions.length}`;
this.updateReviewNavigation();
this.updateReviewStats();
}
createReviewNavigator() {
const navigator = document.getElementById('review-navigator');
let html = '';
this.questions.forEach((question, index) => {
const userAnswer = this.examAnswers[question.id];
const isCorrect = userAnswer === question.correct;
const wasAnswered = userAnswer !== undefined;
let buttonClass = 'btn btn-sm me-1 mb-1';
let iconHtml = '';
if (wasAnswered) {
if (isCorrect) {
buttonClass += ' btn-success';
iconHtml = '<i class="fas fa-check"></i>';
} else {
buttonClass += ' btn-danger';
iconHtml = '<i class="fas fa-times"></i>';
}
} else {
buttonClass += ' btn-secondary';
iconHtml = '<i class="fas fa-question"></i>';
}
if (index === this.reviewIndex) {
buttonClass += ' active';
}
html += `
<button class="${buttonClass}" data-review-index="${index}">
${index + 1} ${iconHtml}
</button>
`;
});
navigator.innerHTML = html;
// Add click handlers for navigator buttons
navigator.querySelectorAll('button[data-review-index]').forEach(button => {
button.addEventListener('click', () => {
this.reviewIndex = parseInt(button.getAttribute('data-review-index'));
this.displayReviewQuestion();
this.createReviewNavigator();
});
});
}
updateReviewNavigation() {
document.getElementById('prev-review-btn').disabled = this.reviewIndex === 0;
document.getElementById('next-review-btn').disabled = this.reviewIndex === this.questions.length - 1;
}
updateReviewStats() {
let correct = 0, incorrect = 0, unanswered = 0;
this.questions.forEach(question => {
const userAnswer = this.examAnswers[question.id];
if (userAnswer === undefined) {
unanswered++;
} else if (userAnswer === question.correct) {
correct++;
} else {
incorrect++;
}
});
document.getElementById('review-correct-count').textContent = correct;
document.getElementById('review-incorrect-count').textContent = incorrect;
document.getElementById('review-unanswered-count').textContent = unanswered;
}
previousReviewQuestion() {
if (this.reviewIndex > 0) {
this.reviewIndex--;
this.displayReviewQuestion();
this.createReviewNavigator();
}
}
nextReviewQuestion() {
if (this.reviewIndex < this.questions.length - 1) {
this.reviewIndex++;
this.displayReviewQuestion();
this.createReviewNavigator();
}
}
backToResults() {
BaseModule.hide('exam-review');
BaseModule.show('exam-results');
}
handleKeyboard(e) {
// Handle keyboard shortcuts during exam
if (document.getElementById('exam-content') && document.getElementById('exam-content').style.display !== 'none') {
const key = e.key.toLowerCase();
// Handle option selection (a, b, c, d)
if (['a', 'b', 'c', 'd'].includes(key)) {
e.preventDefault();
const optionButton = document.querySelector(`.option-enhanced[data-answer="${key}"]`);
if (optionButton) {
// Remove previous selection
document.querySelectorAll('.option-enhanced').forEach(btn => btn.classList.remove('selected'));
// Select the new option
optionButton.classList.add('selected');
// Save the answer
const question = this.questions[this.currentIndex];
this.examAnswers[question.id] = key;
if (this.navigator) {
this.navigator.updateNavigator();
}
this.updateExamProgress();
this.playSound('click');
}
}
// Navigation keys
else if (e.key === 'ArrowLeft' && this.currentIndex > 0) {
this.previousQuestion();
} else if (e.key === 'ArrowRight' && this.currentIndex < this.questions.length - 1) {
this.nextQuestion();
}
}
// Handle keyboard shortcuts during review
else if (document.getElementById('exam-review') && document.getElementById('exam-review').style.display !== 'none') {
if (e.key === 'ArrowLeft' && this.reviewIndex > 0) {
this.previousReviewQuestion();
} else if (e.key === 'ArrowRight' && this.reviewIndex < this.questions.length - 1) {
this.nextReviewQuestion();
} else if (e.key === 'Escape') {
this.backToResults();
}
}
}
}
// Auto-initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
if (document.getElementById('exam-setup')) {
window.examMode = new ExamMode();
}
});
// Export for global use
window.ExamMode = ExamMode;

View File

@@ -0,0 +1,326 @@
// Practice Mode Module
class PracticeMode extends BaseModule {
constructor() {
super();
this.sessionStats = {
correct: 0,
incorrect: 0,
total: 0
};
this.answered = false;
this.init();
}
init() {
this.setupEventListeners();
}
setupEventListeners() {
document.getElementById('start-practice')?.addEventListener('click', () => {
this.startPractice();
});
document.getElementById('new-session')?.addEventListener('click', () => {
this.resetSession();
});
document.getElementById('skip-question')?.addEventListener('click', () => {
this.nextQuestion();
});
document.getElementById('next-question')?.addEventListener('click', () => {
this.nextQuestion();
});
document.getElementById('practice-again')?.addEventListener('click', () => {
this.resetSession();
this.startPractice();
});
}
async startPractice() {
const count = parseInt(document.getElementById('question-count').value);
this.showLoading();
BaseModule.hide('welcome-message');
BaseModule.hide('practice-content');
BaseModule.hide('practice-complete');
try {
const data = await this.fetchQuestions({
mode: 'random',
count: count
});
this.questions = data;
this.currentIndex = 0;
this.resetSession();
this.displayQuestion();
this.hideLoading();
BaseModule.show('practice-content');
} catch (error) {
this.hideLoading();
console.error('Error in startPractice:', error);
}
}
displayQuestion() {
if (this.currentIndex >= this.questions.length) {
this.showResults();
return;
}
const question = this.questions[this.currentIndex];
this.answered = false;
let html = `
<div class="question-card-enhanced fade-in">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0 text-gradient">Pregunta ${question.id}</h5>
<span class="badge bg-success">${this.currentIndex + 1} de ${this.questions.length}</span>
</div>
`;
if (question.has_image && question.image) {
html += `<img src="${question.image}" class="question-image" alt="Imagen de la pregunta">`;
}
html += `<p class="lead mb-4">${question.question}</p>`;
html += '<div class="options-container">';
question.options.forEach((option, index) => {
const letter = String.fromCharCode(97 + index); // a, b, c, d
html += `
<button class="option-enhanced" data-answer="${letter}">
<div class="d-flex align-items-center">
<span class="badge bg-primary me-3">${letter.toUpperCase()}</span>
<span>${option.replace(/^[a-d]\)\s*/, '')}</span>
</div>
</button>
`;
});
html += '</div>';
html += '<div id="feedback" class="mt-3" style="display: none;"></div>';
html += '</div>';
document.getElementById('question-container').innerHTML = html;
document.getElementById('question-counter').textContent = `${this.currentIndex + 1} de ${this.questions.length}`;
// Setup option click handlers
this.setupOptionHandlers(question);
BaseModule.show('skip-question');
BaseModule.hide('next-question');
// Setup keyboard shortcuts
this.setupKeyboardHandlers();
}
setupOptionHandlers(question) {
const optionButtons = document.querySelectorAll('.option-enhanced');
optionButtons.forEach(button => {
button.addEventListener('click', () => {
if (this.answered) return;
this.playSound('click');
button.classList.add('selected');
const selectedAnswer = button.getAttribute('data-answer');
this.checkAnswer(selectedAnswer, question);
});
});
}
setupKeyboardHandlers() {
document.removeEventListener('keydown', this.practiceKeyHandler);
document.addEventListener('keydown', this.practiceKeyHandler.bind(this));
}
practiceKeyHandler(e) {
if (this.answered) return;
const key = e.key.toLowerCase();
if (['a', 'b', 'c', 'd'].includes(key)) {
e.preventDefault();
const optionButton = document.querySelector(`.option-enhanced[data-answer="${key}"]`);
if (optionButton) {
optionButton.click();
}
}
}
checkAnswer(selectedAnswer, question) {
if (this.answered) return;
this.answered = true;
const isCorrect = selectedAnswer === question.correct;
// Update session stats
this.sessionStats.total++;
if (isCorrect) {
this.sessionStats.correct++;
this.playSound('correct');
} else {
this.sessionStats.incorrect++;
this.playSound('incorrect');
}
this.updateSessionStats();
// Update global stats
if (window.statsManager) {
window.statsManager.updateStats(isCorrect);
}
// Show feedback
this.showFeedback(isCorrect, question);
BaseModule.hide('skip-question');
BaseModule.show('next-question');
}
showFeedback(isCorrect, question) {
const optionButtons = document.querySelectorAll('.option-enhanced');
optionButtons.forEach(button => {
const answer = button.getAttribute('data-answer');
if (answer === question.correct) {
button.classList.add('correct');
} else if (button.classList.contains('selected') && !isCorrect) {
button.classList.add('incorrect');
}
button.style.pointerEvents = 'none';
});
let feedbackHtml = '';
if (isCorrect) {
feedbackHtml = `
<div class="alert alert-success border-0 shadow-sm">
<div class="d-flex align-items-center">
<i class="fas fa-check-circle fa-2x text-success me-3"></i>
<div>
<h6 class="mb-1">¡Excelente!</h6>
<p class="mb-0">Has seleccionado la respuesta correcta.</p>
</div>
</div>
</div>
`;
} else {
feedbackHtml = `
<div class="alert alert-danger border-0 shadow-sm">
<div class="d-flex align-items-center">
<i class="fas fa-times-circle fa-2x text-danger me-3"></i>
<div>
<h6 class="mb-1">Respuesta incorrecta</h6>
<p class="mb-0">La respuesta correcta es: <strong>${question.correct.toUpperCase()})</strong></p>
</div>
</div>
</div>
`;
}
const feedbackElement = document.getElementById('feedback');
feedbackElement.innerHTML = feedbackHtml;
feedbackElement.style.display = 'none';
feedbackElement.style.opacity = '0';
feedbackElement.style.display = 'block';
// Fade in effect
setTimeout(() => {
feedbackElement.style.transition = 'opacity 0.3s ease';
feedbackElement.style.opacity = '1';
}, 10);
}
nextQuestion() {
document.removeEventListener('keydown', this.practiceKeyHandler);
this.currentIndex++;
this.displayQuestion();
}
updateSessionStats() {
document.getElementById('session-correct').textContent = this.sessionStats.correct;
document.getElementById('session-incorrect').textContent = this.sessionStats.incorrect;
const accuracy = this.sessionStats.total > 0 ?
Math.round((this.sessionStats.correct / this.sessionStats.total) * 100) : 0;
document.getElementById('session-accuracy').textContent = accuracy + '%';
// Update progress bar
const progressBar = document.getElementById('session-progress');
progressBar.style.width = accuracy + '%';
// Change bar color based on accuracy
progressBar.classList.remove('bg-danger', 'bg-warning', 'bg-success', 'bg-light');
if (accuracy >= 80) {
progressBar.classList.add('bg-success');
} else if (accuracy >= 60) {
progressBar.classList.add('bg-warning');
} else if (accuracy > 0) {
progressBar.classList.add('bg-danger');
} else {
progressBar.classList.add('bg-light');
}
}
resetSession() {
this.sessionStats = {
correct: 0,
incorrect: 0,
total: 0
};
this.updateSessionStats();
}
showResults() {
const accuracy = this.sessionStats.total > 0 ?
Math.round((this.sessionStats.correct / this.sessionStats.total) * 100) : 0;
document.getElementById('final-correct').textContent = this.sessionStats.correct;
document.getElementById('final-incorrect').textContent = this.sessionStats.incorrect;
document.getElementById('final-accuracy').textContent = accuracy + '%';
const finalProgress = document.getElementById('final-progress');
finalProgress.style.width = accuracy + '%';
// Change final progress bar color
finalProgress.classList.remove('bg-danger', 'bg-warning', 'bg-success');
if (accuracy >= 80) {
finalProgress.classList.add('bg-success');
if (window.Utils) {
Utils.createConfetti();
Utils.playSound('complete');
Utils.showNotification('¡Excelente trabajo! Has obtenido una puntuación sobresaliente.', 'success');
}
} else if (accuracy >= 60) {
finalProgress.classList.add('bg-warning');
if (window.Utils) {
Utils.playSound('complete');
Utils.showNotification('¡Buen trabajo! Sigue practicando para mejorar.', 'warning');
}
} else {
finalProgress.classList.add('bg-danger');
if (window.Utils) {
Utils.showNotification('Sigue estudiando. La práctica hace al maestro.', 'info');
}
}
BaseModule.hide('practice-content');
BaseModule.show('practice-complete');
}
}
// Auto-initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
if (document.getElementById('practice-content')) {
window.practiceMode = new PracticeMode();
}
});
// Export for global use
window.PracticeMode = PracticeMode;

View File

@@ -0,0 +1,223 @@
// Question Navigator Module
class QuestionNavigator {
constructor(containerId, options = {}) {
this.containerId = containerId;
this.questions = [];
this.currentIndex = 0;
this.visitedQuestions = new Set();
this.onQuestionSelect = options.onQuestionSelect || (() => {});
this.storagePrefix = options.storagePrefix || 'navigator';
this.init();
}
init() {
this.setupEventListeners();
}
setupEventListeners() {
// Jump button
const jumpBtn = document.getElementById('jump-btn');
if (jumpBtn) {
jumpBtn.addEventListener('click', () => this.jumpToQuestion());
}
// Jump input
const jumpInput = document.getElementById('jump-to-question');
if (jumpInput) {
jumpInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.jumpToQuestion();
}
});
}
// Reset button
const resetBtn = document.getElementById('reset-visited-btn');
if (resetBtn) {
resetBtn.addEventListener('click', () => this.resetVisitedQuestions());
}
}
setQuestions(questions) {
this.questions = questions;
this.createNavigator();
this.loadVisitedFromStorage();
this.updateNavigator();
}
createNavigator() {
const container = document.getElementById(this.containerId);
if (!container) return;
container.innerHTML = '';
this.questions.forEach((question, index) => {
const button = document.createElement('button');
button.className = 'question-nav-btn';
button.textContent = question.id;
button.setAttribute('data-index', index);
button.title = `Pregunta ${question.id}`;
button.addEventListener('click', () => {
this.selectQuestion(index);
});
container.appendChild(button);
});
}
selectQuestion(index) {
if (index >= 0 && index < this.questions.length) {
this.currentIndex = index;
this.visitedQuestions.add(index);
this.saveVisitedToStorage();
this.updateNavigator();
this.onQuestionSelect(index);
}
}
updateNavigator() {
const buttons = document.querySelectorAll(`#${this.containerId} .question-nav-btn`);
buttons.forEach((button, index) => {
button.classList.remove('current', 'visited');
if (index === this.currentIndex) {
button.classList.add('current');
this.visitedQuestions.add(index);
this.saveVisitedToStorage();
// Scroll to current button
button.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
} else if (this.visitedQuestions.has(index)) {
button.classList.add('visited');
}
});
this.updateProgress();
this.updateJumpInputMax();
}
updateProgress() {
const progressEl = document.getElementById('navigator-progress');
if (progressEl) {
progressEl.textContent = `${this.visitedQuestions.size} de ${this.questions.length} visitadas`;
}
}
updateJumpInputMax() {
const jumpInput = document.getElementById('jump-to-question');
if (jumpInput && this.questions.length > 0) {
jumpInput.max = this.questions[this.questions.length - 1].id;
}
}
jumpToQuestion() {
const jumpInput = document.getElementById('jump-to-question');
if (!jumpInput) return;
const questionId = parseInt(jumpInput.value);
if (!questionId || questionId < 1) {
this.showNotification('Por favor ingresa un número de pregunta válido', 'warning');
return;
}
const questionIndex = this.questions.findIndex(q => q.id === questionId);
if (questionIndex === -1) {
this.showNotification(`La pregunta ${questionId} no está en el rango actual`, 'warning');
return;
}
this.selectQuestion(questionIndex);
jumpInput.value = '';
this.playSound('click');
this.showNotification(`Navegando a pregunta ${questionId}`, 'success', 2000);
}
resetVisitedQuestions() {
if (this.visitedQuestions.size === 0) {
this.showNotification('No hay preguntas visitadas para limpiar', 'info', 3000);
return;
}
const confirmReset = confirm(
`¿Estás seguro de que quieres limpiar el progreso de ${this.visitedQuestions.size} preguntas visitadas?`
);
if (confirmReset) {
// Clear visited questions but keep current question
this.visitedQuestions.clear();
this.visitedQuestions.add(this.currentIndex);
this.saveVisitedToStorage();
this.updateNavigator();
this.playSound('complete');
this.showNotification('Progreso de preguntas visitadas limpiado', 'success', 3000);
// Animation for reset button
const resetBtn = document.getElementById('reset-visited-btn');
if (resetBtn) {
resetBtn.style.transform = 'scale(0.9)';
setTimeout(() => {
resetBtn.style.transform = 'scale(1)';
}, 150);
}
}
}
saveVisitedToStorage() {
try {
const storageKey = `${this.storagePrefix}_visited`;
localStorage.setItem(storageKey, JSON.stringify(Array.from(this.visitedQuestions)));
} catch (error) {
console.warn('Could not save visited questions to localStorage:', error);
}
}
loadVisitedFromStorage() {
try {
const storageKey = `${this.storagePrefix}_visited`;
const saved = localStorage.getItem(storageKey);
if (saved) {
const visitedArray = JSON.parse(saved);
this.visitedQuestions = new Set(visitedArray);
} else {
this.visitedQuestions = new Set();
}
} catch (error) {
console.warn('Could not load visited questions from localStorage:', error);
this.visitedQuestions = new Set();
}
}
setCurrentIndex(index) {
this.currentIndex = index;
this.updateNavigator();
}
// Utility methods
showNotification(message, type = 'info', duration = 3000) {
if (window.Utils) {
Utils.showNotification(message, type, duration);
} else {
alert(message);
}
}
playSound(type) {
if (window.Utils) {
Utils.playSound(type);
}
}
}
// Export for global use
window.QuestionNavigator = QuestionNavigator;

View File

@@ -0,0 +1,222 @@
// Stats Display Module for Index Page
class StatsDisplay {
constructor() {
this.init();
}
init() {
this.setupEventListeners();
this.loadStats();
}
setupEventListeners() {
// Reset stats modal listeners
document.getElementById('reset-stats-btn')?.addEventListener('click', () => {
this.showResetStatsModal();
});
document.getElementById('resetStatsModalClose')?.addEventListener('click', () => {
this.hideResetStatsModal();
});
document.getElementById('resetStatsModalCancel')?.addEventListener('click', () => {
this.hideResetStatsModal();
});
document.getElementById('resetStatsBackdrop')?.addEventListener('click', () => {
this.hideResetStatsModal();
});
document.getElementById('confirmResetStats')?.addEventListener('click', () => {
this.resetStats();
});
// Keyboard support for modal
document.addEventListener('keydown', (e) => {
const modal = document.getElementById('resetStatsModal');
if (modal && modal.style.display === 'block') {
if (e.key === 'Escape') {
this.hideResetStatsModal();
} else if (e.key === 'Enter') {
const focusedElement = document.activeElement;
if (focusedElement.id === 'confirmResetStats') {
this.resetStats();
} else {
this.hideResetStatsModal();
}
}
}
});
}
loadStats() {
// Load statistics from localStorage
let stats = {
total_answered: 0,
correct_answers: 0,
incorrect_answers: 0,
accuracy: 0
};
// Try to load from localStorage
try {
const savedStats = localStorage.getItem('balotario_stats');
if (savedStats) {
const parsedStats = JSON.parse(savedStats);
stats = {
total_answered: parsedStats.totalAnswered || 0,
correct_answers: parsedStats.correctAnswers || 0,
incorrect_answers: parsedStats.incorrectAnswers || 0,
accuracy: parsedStats.accuracy || 0
};
}
} catch (error) {
console.warn('Error loading stats from localStorage:', error);
}
// Update UI
this.updateStatsDisplay(stats);
}
updateStatsDisplay(stats) {
const totalAnsweredEl = document.getElementById('total-answered');
const accuracyEl = document.getElementById('accuracy');
const accuracyBarEl = document.getElementById('accuracy-bar');
const resetBtnEl = document.getElementById('reset-stats-btn');
if (totalAnsweredEl) totalAnsweredEl.textContent = stats.total_answered;
if (accuracyEl) accuracyEl.textContent = stats.accuracy + '%';
if (accuracyBarEl) accuracyBarEl.style.width = stats.accuracy + '%';
// Show/hide reset button based on stats
if (resetBtnEl) {
if (stats.total_answered > 0) {
resetBtnEl.style.display = 'inline-block';
} else {
resetBtnEl.style.display = 'none';
}
}
}
showResetStatsModal() {
if (window.Utils) {
Utils.playSound('click');
}
const modal = document.getElementById('resetStatsModal');
if (modal) {
modal.style.display = 'block';
document.body.classList.add('modal-open');
// Focus cancel button for accessibility
setTimeout(() => {
const cancelBtn = document.getElementById('resetStatsModalCancel');
if (cancelBtn) cancelBtn.focus();
}, 100);
}
}
hideResetStatsModal() {
const dialog = document.getElementById('resetStatsDialog');
if (dialog) {
dialog.classList.add('modal-closing');
setTimeout(() => {
const modal = document.getElementById('resetStatsModal');
if (modal) {
modal.style.display = 'none';
document.body.classList.remove('modal-open');
dialog.classList.remove('modal-closing');
}
}, 200);
}
}
resetStats() {
// Show loading in button
const confirmBtn = document.getElementById('confirmResetStats');
if (!confirmBtn) return;
const originalText = confirmBtn.innerHTML;
confirmBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Restableciendo...';
confirmBtn.disabled = true;
// Reset statistics directly in localStorage
try {
// Reset using statsManager if available
if (window.statsManager) {
window.statsManager.resetStats();
} else {
// Reset directly in localStorage
const resetStats = {
totalAnswered: 0,
correctAnswers: 0,
incorrectAnswers: 0,
accuracy: 0,
studyTime: 0,
examsTaken: 0,
bestScore: 0,
streak: 0,
lastActivity: null
};
localStorage.setItem('balotario_stats', JSON.stringify(resetStats));
}
// Simulate a small pause for better UX
setTimeout(() => {
// Reload statistics
this.loadStats();
// Hide modal
this.hideResetStatsModal();
// Restore button
confirmBtn.innerHTML = originalText;
confirmBtn.disabled = false;
// Show success notification
if (window.Utils) {
Utils.showNotification('Estadísticas restablecidas correctamente', 'success', 4000);
Utils.playSound('complete');
} else {
alert('Estadísticas restablecidas correctamente');
}
// Small animation on stats card
const statsCard = document.querySelector('.stats-card');
if (statsCard) {
statsCard.style.transform = 'scale(1.05)';
statsCard.style.transition = 'transform 0.3s ease';
setTimeout(() => {
statsCard.style.transform = 'scale(1)';
}, 300);
}
}, 800);
} catch (error) {
console.error('Error resetting stats:', error);
// Restore button on error
confirmBtn.innerHTML = originalText;
confirmBtn.disabled = false;
if (window.Utils) {
Utils.showNotification('Error al restablecer estadísticas', 'danger');
} else {
alert('Error al restablecer estadísticas');
}
}
}
}
// Auto-initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
if (document.getElementById('stats-content')) {
window.statsDisplay = new StatsDisplay();
}
});
// Export for global use
window.StatsDisplay = StatsDisplay;

View File

@@ -0,0 +1,229 @@
// Study Mode Module
class StudyMode extends BaseModule {
constructor() {
super();
this.showingAnswer = false;
this.navigator = null;
this.init();
}
init() {
this.setupEventListeners();
this.loadQuestions();
}
setupEventListeners() {
// Filter controls
document.getElementById('apply-filters')?.addEventListener('click', () => {
this.loadQuestions();
});
document.getElementById('reset-filters')?.addEventListener('click', () => {
document.getElementById('start-question').value = 1;
document.getElementById('end-question').value = 200;
this.loadQuestions();
});
// Navigation controls
document.getElementById('prev-btn')?.addEventListener('click', () => {
this.previousQuestion();
});
document.getElementById('next-btn')?.addEventListener('click', () => {
this.nextQuestion();
});
// Answer controls
document.getElementById('show-answer')?.addEventListener('click', () => {
this.showAnswer();
});
document.getElementById('hide-answer')?.addEventListener('click', () => {
this.hideAnswer();
});
// Keyboard navigation
document.addEventListener('keydown', (e) => {
this.handleKeyboard(e);
});
}
async loadQuestions() {
this.showLoading();
BaseModule.hide('study-content');
BaseModule.hide('no-questions');
const start = document.getElementById('start-question').value;
const end = document.getElementById('end-question').value;
try {
const data = await this.fetchQuestions({
mode: 'range',
start: start,
end: end
});
this.questions = data;
this.currentIndex = 0;
if (this.questions.length === 0) {
this.hideLoading();
BaseModule.show('no-questions');
return;
}
document.getElementById('total-questions').textContent = this.questions.length;
// Initialize navigator
this.initializeNavigator(start, end);
this.displayQuestion();
this.updateProgress('study-progress');
this.hideLoading();
BaseModule.show('study-content');
} catch (error) {
this.hideLoading();
console.error('Error in loadQuestions:', error);
}
}
initializeNavigator(start, end) {
if (!this.navigator) {
this.navigator = new QuestionNavigator('question-navigator', {
onQuestionSelect: (index) => {
this.currentIndex = index;
this.displayQuestion();
this.hideAnswer();
},
storagePrefix: `study_${start}_${end}`
});
}
this.navigator.setQuestions(this.questions);
}
displayQuestion() {
if (this.questions.length === 0) return;
const question = this.questions[this.currentIndex];
let html = this.createQuestionHTML(question, this.currentIndex, this.questions.length);
html += this.createOptionsHTML(question.options, question.correct);
document.getElementById('question-container').innerHTML = html;
// Animate options
this.animateOptions();
this.updateNavigation('prev-btn', 'next-btn', 'current-question');
this.updateProgress('study-progress');
if (this.navigator) {
this.navigator.setCurrentIndex(this.currentIndex);
}
}
animateOptions() {
const options = document.querySelectorAll('.option-btn');
options.forEach((option, index) => {
option.style.opacity = '0';
option.style.transform = 'translateY(20px)';
setTimeout(() => {
option.style.transition = 'all 0.3s ease';
option.style.opacity = '1';
option.style.transform = 'translateY(0)';
}, index * 100);
});
}
showAnswer() {
const correctOption = document.querySelector('.option-btn[data-correct="true"]');
if (correctOption) {
correctOption.classList.add('correct', 'correct-option');
correctOption.style.background = 'rgba(39, 174, 96, 0.3)';
correctOption.style.borderColor = '#27ae60';
correctOption.style.color = '#ffffff';
correctOption.style.fontWeight = 'bold';
const indicator = correctOption.querySelector('.answer-indicator');
if (indicator) {
indicator.style.display = 'inline';
}
}
// Dim incorrect options
const incorrectOptions = document.querySelectorAll('.option-btn[data-correct="false"]');
incorrectOptions.forEach(option => {
option.classList.add('text-muted');
option.style.opacity = '0.6';
});
BaseModule.hide('show-answer');
BaseModule.show('hide-answer');
this.showingAnswer = true;
}
hideAnswer() {
const allOptions = document.querySelectorAll('.option-btn');
allOptions.forEach(option => {
option.classList.remove('correct', 'correct-option', 'text-muted');
option.style.background = '';
option.style.borderColor = '';
option.style.color = '';
option.style.fontWeight = '';
option.style.opacity = '';
});
const indicators = document.querySelectorAll('.answer-indicator');
indicators.forEach(indicator => {
indicator.style.display = 'none';
});
BaseModule.show('show-answer');
BaseModule.hide('hide-answer');
this.showingAnswer = false;
}
previousQuestion() {
if (this.currentIndex > 0) {
this.currentIndex--;
this.displayQuestion();
this.hideAnswer();
}
}
nextQuestion() {
if (this.currentIndex < this.questions.length - 1) {
this.currentIndex++;
this.displayQuestion();
this.hideAnswer();
}
}
handleKeyboard(e) {
if (e.key === 'ArrowLeft' && this.currentIndex > 0) {
this.previousQuestion();
} else if (e.key === 'ArrowRight' && this.currentIndex < this.questions.length - 1) {
this.nextQuestion();
} else if (e.key === ' ') {
e.preventDefault();
if (this.showingAnswer) {
this.hideAnswer();
} else {
this.showAnswer();
}
}
}
}
// Auto-initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
if (document.getElementById('study-content')) {
window.studyMode = new StudyMode();
}
});
// Export for global use
window.StudyMode = StudyMode;