Files
driving-academy/static/js/app.js
2025-10-26 23:39:49 -05:00

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;