339 lines
10 KiB
JavaScript
339 lines
10 KiB
JavaScript
// 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;
|