// 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} `; 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 = `
`; 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;