- Complete SPA architecture with dynamic module loading - 9 different educational games (whack-a-mole, memory, quiz, etc.) - Rich content system supporting multimedia (audio, images, video) - Chinese study mode with character recognition - Adaptive game system based on available content - Content types: vocabulary, grammar, poems, fill-blanks, corrections - AI-powered text evaluation for open-ended answers - Flexible content schema with backward compatibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
176 lines
5.0 KiB
JavaScript
176 lines
5.0 KiB
JavaScript
// === UTILITIES GÉNÉRALES ===
|
|
|
|
const Utils = {
|
|
// Gestion des paramètres URL
|
|
getUrlParams() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
return {
|
|
page: params.get('page') || 'home',
|
|
game: params.get('game') || null,
|
|
content: params.get('content') || null
|
|
};
|
|
},
|
|
|
|
setUrlParams(params) {
|
|
const url = new URL(window.location);
|
|
Object.keys(params).forEach(key => {
|
|
if (params[key]) {
|
|
url.searchParams.set(key, params[key]);
|
|
} else {
|
|
url.searchParams.delete(key);
|
|
}
|
|
});
|
|
window.history.pushState({}, '', url);
|
|
},
|
|
|
|
// Affichage/masquage du loading
|
|
showLoading() {
|
|
document.getElementById('loading').classList.add('show');
|
|
},
|
|
|
|
hideLoading() {
|
|
document.getElementById('loading').classList.remove('show');
|
|
},
|
|
|
|
// Notifications toast
|
|
showToast(message, type = 'info') {
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast toast-${type}`;
|
|
toast.textContent = message;
|
|
|
|
toast.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: ${type === 'success' ? 'var(--secondary-color)' :
|
|
type === 'error' ? 'var(--error-color)' : 'var(--primary-color)'};
|
|
color: white;
|
|
padding: 15px 20px;
|
|
border-radius: var(--border-radius);
|
|
box-shadow: var(--shadow);
|
|
z-index: 1001;
|
|
animation: slideIn 0.3s ease-out;
|
|
`;
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.style.animation = 'slideOut 0.3s ease-in';
|
|
setTimeout(() => document.body.removeChild(toast), 300);
|
|
}, 3000);
|
|
},
|
|
|
|
// Animation d'éléments
|
|
animateElement(element, animation, duration = 300) {
|
|
return new Promise(resolve => {
|
|
element.style.animation = `${animation} ${duration}ms ease-out`;
|
|
setTimeout(() => {
|
|
element.style.animation = '';
|
|
resolve();
|
|
}, duration);
|
|
});
|
|
},
|
|
|
|
// Génération d'ID unique
|
|
generateId() {
|
|
return Math.random().toString(36).substr(2, 9);
|
|
},
|
|
|
|
// Mélange d'array (Fisher-Yates)
|
|
shuffleArray(array) {
|
|
const shuffled = [...array];
|
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
}
|
|
return shuffled;
|
|
},
|
|
|
|
// Sélection aléatoire d'éléments
|
|
getRandomItems(array, count) {
|
|
const shuffled = this.shuffleArray(array);
|
|
return shuffled.slice(0, count);
|
|
},
|
|
|
|
// Formatage du temps
|
|
formatTime(seconds) {
|
|
const mins = Math.floor(seconds / 60);
|
|
const secs = seconds % 60;
|
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
},
|
|
|
|
// Debounce pour événements
|
|
debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
},
|
|
|
|
// Vérification de support audio
|
|
canPlayAudio() {
|
|
return 'Audio' in window;
|
|
},
|
|
|
|
// Chargement d'image avec promise
|
|
loadImage(src) {
|
|
return new Promise((resolve, reject) => {
|
|
const img = new Image();
|
|
img.onload = () => resolve(img);
|
|
img.onerror = reject;
|
|
img.src = src;
|
|
});
|
|
},
|
|
|
|
// Stockage local sécurisé
|
|
storage: {
|
|
set(key, value) {
|
|
try {
|
|
localStorage.setItem(key, JSON.stringify(value));
|
|
} catch (e) {
|
|
console.warn('LocalStorage not available:', e);
|
|
}
|
|
},
|
|
|
|
get(key, defaultValue = null) {
|
|
try {
|
|
const item = localStorage.getItem(key);
|
|
return item ? JSON.parse(item) : defaultValue;
|
|
} catch (e) {
|
|
console.warn('LocalStorage read error:', e);
|
|
return defaultValue;
|
|
}
|
|
},
|
|
|
|
remove(key) {
|
|
try {
|
|
localStorage.removeItem(key);
|
|
} catch (e) {
|
|
console.warn('LocalStorage remove error:', e);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Ajout de styles CSS dynamiques pour les animations
|
|
const styleSheet = document.createElement('style');
|
|
styleSheet.textContent = `
|
|
@keyframes slideIn {
|
|
from { transform: translateX(100%); opacity: 0; }
|
|
to { transform: translateX(0); opacity: 1; }
|
|
}
|
|
|
|
@keyframes slideOut {
|
|
from { transform: translateX(0); opacity: 1; }
|
|
to { transform: translateX(100%); opacity: 0; }
|
|
}
|
|
`;
|
|
document.head.appendChild(styleSheet);
|
|
|
|
// Export global
|
|
window.Utils = Utils; |