- Fix WebSocket server to properly broadcast logs to all connected clients - Integrate professional logging system with real-time WebSocket interface - Add network status indicator with DigitalOcean Spaces connectivity - Implement AWS Signature V4 authentication for private bucket access - Add JSON content loader with backward compatibility to JS modules - Restore navigation breadcrumb system with comprehensive logging - Add multiple content formats: JSON + JS with automatic discovery - Enhance top bar with logger toggle and network status indicator - Remove deprecated temp-games module and clean up unused files 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
511 lines
18 KiB
JavaScript
511 lines
18 KiB
JavaScript
// === SYSTÈME DE NAVIGATION ===
|
||
|
||
const AppNavigation = {
|
||
currentPage: 'home',
|
||
navigationHistory: ['home'],
|
||
gamesConfig: null,
|
||
contentScanner: new ContentScanner(),
|
||
scannedContent: null,
|
||
|
||
init() {
|
||
this.loadGamesConfig();
|
||
this.initContentScanner();
|
||
this.setupEventListeners();
|
||
this.handleInitialRoute();
|
||
},
|
||
|
||
async loadGamesConfig() {
|
||
// Utilisation directe de la config par défaut (pas de fetch)
|
||
logSh('📁 Utilisation de la configuration par défaut', 'INFO');
|
||
this.gamesConfig = this.getDefaultConfig();
|
||
},
|
||
|
||
async initContentScanner() {
|
||
try {
|
||
logSh('🔍 Initialisation du scanner de contenu...', 'INFO');
|
||
this.scannedContent = await this.contentScanner.scanAllContent();
|
||
logSh(`✅ ${this.scannedContent.found.length} modules de contenu détectés automatiquement`, 'INFO');
|
||
} catch (error) {
|
||
logSh('Erreur scan contenu:', error, 'ERROR');
|
||
}
|
||
},
|
||
|
||
getDefaultConfig() {
|
||
return {
|
||
games: {
|
||
'whack-a-mole': {
|
||
enabled: true,
|
||
name: 'Whack-a-Mole',
|
||
icon: '🔨',
|
||
description: 'Tape sur les bonnes réponses !'
|
||
},
|
||
'whack-a-mole-hard': {
|
||
enabled: true,
|
||
name: 'Whack-a-Mole Hard',
|
||
icon: '💥',
|
||
description: '3 moles at once, 5x3 grid, harder!'
|
||
},
|
||
'memory-match': {
|
||
enabled: true,
|
||
name: 'Memory Match',
|
||
icon: '🧠',
|
||
description: 'Find matching English-French pairs!'
|
||
},
|
||
'quiz-game': {
|
||
enabled: true,
|
||
name: 'Quiz Game',
|
||
icon: '❓',
|
||
description: 'Answer vocabulary questions!'
|
||
},
|
||
'fill-the-blank': {
|
||
enabled: true,
|
||
name: 'Fill the Blank',
|
||
icon: '📝',
|
||
description: 'Complète les phrases en remplissant les blancs !'
|
||
},
|
||
'text-reader': {
|
||
enabled: true,
|
||
name: 'Text Reader',
|
||
icon: '📖',
|
||
description: 'Read texts sentence by sentence'
|
||
},
|
||
'adventure-reader': {
|
||
enabled: true,
|
||
name: 'Adventure Reader',
|
||
icon: '⚔️',
|
||
description: 'Zelda-style adventure with vocabulary!'
|
||
}
|
||
},
|
||
content: {
|
||
'sbs-level-8': {
|
||
enabled: true,
|
||
name: 'SBS Level 8',
|
||
icon: '📚',
|
||
description: 'Vocabulaire manuel SBS'
|
||
},
|
||
'animals': {
|
||
enabled: false,
|
||
name: 'Animals',
|
||
icon: '🐱',
|
||
description: 'Vocabulaire des animaux'
|
||
},
|
||
'colors': {
|
||
enabled: false,
|
||
name: 'Colors & Numbers',
|
||
icon: '🌈',
|
||
description: 'Couleurs et nombres'
|
||
}
|
||
}
|
||
};
|
||
},
|
||
|
||
setupEventListeners() {
|
||
// Navigation par URL
|
||
window.addEventListener('popstate', () => {
|
||
this.handleInitialRoute();
|
||
});
|
||
|
||
// Raccourcis clavier
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') {
|
||
this.goBack();
|
||
}
|
||
});
|
||
|
||
// Scroll pour masquer/afficher la breadcrumb
|
||
this.setupScrollBehavior();
|
||
},
|
||
|
||
handleInitialRoute() {
|
||
const params = Utils.getUrlParams();
|
||
|
||
if (params.page === 'play' && params.game && params.content) {
|
||
this.showGamePage(params.game, params.content);
|
||
} else if (params.page === 'levels' && params.game) {
|
||
this.showLevelsPage(params.game);
|
||
} else if (params.page === 'games') {
|
||
this.showGamesPage();
|
||
} else {
|
||
this.showHomePage();
|
||
}
|
||
},
|
||
|
||
setupScrollBehavior() {
|
||
let lastScrollY = window.scrollY;
|
||
let breadcrumb = null;
|
||
|
||
const handleScroll = () => {
|
||
if (!breadcrumb) {
|
||
breadcrumb = document.querySelector('.breadcrumb');
|
||
if (!breadcrumb) return;
|
||
}
|
||
|
||
const currentScrollY = window.scrollY;
|
||
|
||
// Si on scroll vers le bas et qu'on a scrollé plus de 50px
|
||
if (currentScrollY > lastScrollY && currentScrollY > 50) {
|
||
breadcrumb.classList.add('hidden');
|
||
breadcrumb.classList.remove('visible');
|
||
}
|
||
// Si on scroll vers le haut ou qu'on est près du top
|
||
else if (currentScrollY < lastScrollY || currentScrollY <= 50) {
|
||
breadcrumb.classList.remove('hidden');
|
||
breadcrumb.classList.add('visible');
|
||
}
|
||
|
||
lastScrollY = currentScrollY;
|
||
};
|
||
|
||
// Throttle scroll event pour les performances
|
||
let ticking = false;
|
||
window.addEventListener('scroll', () => {
|
||
if (!ticking) {
|
||
requestAnimationFrame(() => {
|
||
handleScroll();
|
||
ticking = false;
|
||
});
|
||
ticking = true;
|
||
}
|
||
});
|
||
},
|
||
|
||
// Navigation vers une page
|
||
navigateTo(page, game = null, content = null) {
|
||
logSh(`🧭 Navigation vers: ${page} ${game ? `(jeu: ${game})` : ''} ${content ? `(contenu: ${content})` : ''}`, 'INFO');
|
||
const params = { page };
|
||
if (game) params.game = game;
|
||
if (content) params.content = content;
|
||
|
||
Utils.setUrlParams(params);
|
||
|
||
// Mise à jour historique
|
||
if (this.currentPage !== page) {
|
||
this.navigationHistory.push(page);
|
||
}
|
||
|
||
this.currentPage = page;
|
||
|
||
// Affichage de la page appropriée
|
||
switch(page) {
|
||
case 'games':
|
||
this.showGamesPage();
|
||
break;
|
||
case 'levels':
|
||
this.showLevelsPage(game);
|
||
break;
|
||
case 'play':
|
||
this.showGamePage(game, content);
|
||
break;
|
||
default:
|
||
this.showHomePage();
|
||
}
|
||
|
||
this.updateBreadcrumb();
|
||
},
|
||
|
||
// Retour en arrière
|
||
goBack() {
|
||
logSh(`⬅️ Retour en arrière depuis: ${this.currentPage}`, 'INFO');
|
||
if (this.navigationHistory.length > 1) {
|
||
this.navigationHistory.pop(); // Retirer la page actuelle
|
||
const previousPage = this.navigationHistory[this.navigationHistory.length - 1];
|
||
logSh(`📍 Retour vers: ${previousPage}`, 'DEBUG');
|
||
|
||
const params = Utils.getUrlParams();
|
||
|
||
if (previousPage === 'levels') {
|
||
this.navigateTo('levels', params.game);
|
||
} else if (previousPage === 'games') {
|
||
this.navigateTo('games');
|
||
} else {
|
||
this.navigateTo('home');
|
||
}
|
||
}
|
||
},
|
||
|
||
// Affichage page d'accueil
|
||
showHomePage() {
|
||
logSh('🏠 Affichage page d\'accueil', 'INFO');
|
||
this.hideAllPages();
|
||
document.getElementById('home-page').classList.add('active');
|
||
this.currentPage = 'home';
|
||
this.updateBreadcrumb();
|
||
},
|
||
|
||
// Affichage page sélection jeux
|
||
showGamesPage() {
|
||
logSh('🎮 Affichage page sélection des jeux', 'INFO');
|
||
this.hideAllPages();
|
||
document.getElementById('games-page').classList.add('active');
|
||
this.renderGamesGrid();
|
||
this.currentPage = 'games';
|
||
this.updateBreadcrumb();
|
||
},
|
||
|
||
// Affichage page sélection niveaux
|
||
showLevelsPage(gameType) {
|
||
logSh(`📚 Affichage page sélection des niveaux pour: ${gameType}`, 'INFO');
|
||
this.hideAllPages();
|
||
document.getElementById('levels-page').classList.add('active');
|
||
this.renderLevelsGrid(gameType);
|
||
this.currentPage = 'levels';
|
||
|
||
// Mise à jour de la description
|
||
const gameInfo = this.gamesConfig?.games[gameType];
|
||
if (gameInfo) {
|
||
logSh(`🎯 Description mise à jour: ${gameInfo.name}`, 'DEBUG');
|
||
document.getElementById('level-description').textContent =
|
||
`Sélectionne le contenu pour jouer à ${gameInfo.name}`;
|
||
}
|
||
|
||
this.updateBreadcrumb();
|
||
},
|
||
|
||
// Affichage page de jeu
|
||
async showGamePage(gameType, contentType) {
|
||
this.hideAllPages();
|
||
document.getElementById('game-page').classList.add('active');
|
||
this.currentPage = 'play';
|
||
|
||
Utils.showLoading();
|
||
|
||
try {
|
||
await GameLoader.loadGame(gameType, contentType);
|
||
this.updateBreadcrumb();
|
||
} catch (error) {
|
||
logSh('Erreur chargement jeu:', error, 'ERROR');
|
||
Utils.showToast('Erreur lors du chargement du jeu', 'error');
|
||
this.goBack();
|
||
} finally {
|
||
Utils.hideLoading();
|
||
}
|
||
},
|
||
|
||
// Masquer toutes les pages
|
||
hideAllPages() {
|
||
document.querySelectorAll('.page').forEach(page => {
|
||
page.classList.remove('active');
|
||
});
|
||
},
|
||
|
||
// Rendu grille des jeux
|
||
renderGamesGrid() {
|
||
logSh('🎲 Génération de la grille des jeux...', 'DEBUG');
|
||
const grid = document.getElementById('games-grid');
|
||
grid.innerHTML = '';
|
||
|
||
if (!this.gamesConfig) {
|
||
logSh('❌ Pas de configuration de jeux disponible', 'ERROR');
|
||
return;
|
||
}
|
||
|
||
const enabledGames = Object.entries(this.gamesConfig.games).filter(([key, game]) => game.enabled);
|
||
logSh(`🎯 ${enabledGames.length} jeux activés trouvés`, 'INFO');
|
||
|
||
enabledGames.forEach(([key, game]) => {
|
||
logSh(`➕ Ajout de la carte: ${game.name}`, 'DEBUG');
|
||
const card = this.createGameCard(key, game);
|
||
grid.appendChild(card);
|
||
});
|
||
},
|
||
|
||
// Création d'une carte de jeu
|
||
createGameCard(gameKey, gameInfo) {
|
||
const card = document.createElement('div');
|
||
card.className = 'game-card';
|
||
card.innerHTML = `
|
||
<div class="icon">${gameInfo.icon}</div>
|
||
<div class="title">${gameInfo.name}</div>
|
||
<div class="description">${gameInfo.description}</div>
|
||
`;
|
||
|
||
card.addEventListener('click', () => {
|
||
logSh(`🎮 Clic sur la carte du jeu: ${gameInfo.name} (${gameKey})`, 'INFO');
|
||
Utils.animateElement(card, 'pulse');
|
||
this.navigateTo('levels', gameKey);
|
||
});
|
||
|
||
return card;
|
||
},
|
||
|
||
// Rendu grille des niveaux
|
||
async renderLevelsGrid(gameType) {
|
||
const grid = document.getElementById('levels-grid');
|
||
grid.innerHTML = '<div class="loading-content">🔍 Recherche du contenu disponible...</div>';
|
||
|
||
try {
|
||
// Obtenir tout le contenu disponible automatiquement
|
||
const availableContent = await this.contentScanner.getAvailableContent();
|
||
|
||
if (availableContent.length === 0) {
|
||
grid.innerHTML = '<div class="no-content">Aucun contenu trouvé</div>';
|
||
return;
|
||
}
|
||
|
||
// Effacer le loading
|
||
grid.innerHTML = '';
|
||
|
||
// Filtrer par compatibilité avec le jeu si possible
|
||
const compatibleContent = await this.contentScanner.getContentByGame(gameType);
|
||
const contentToShow = compatibleContent.length > 0 ? compatibleContent : availableContent;
|
||
|
||
logSh(`📋 Affichage de ${contentToShow.length} modules pour ${gameType}`, 'INFO');
|
||
|
||
// Créer les cartes pour chaque contenu trouvé
|
||
contentToShow.forEach(content => {
|
||
const card = this.createLevelCard(content.id, content, gameType);
|
||
grid.appendChild(card);
|
||
});
|
||
|
||
// Ajouter info de compatibilité si filtré
|
||
if (compatibleContent.length > 0 && compatibleContent.length < availableContent.length) {
|
||
const infoDiv = document.createElement('div');
|
||
infoDiv.className = 'content-info';
|
||
infoDiv.innerHTML = `
|
||
<p><em>Affichage des contenus les plus compatibles avec ${gameType}</em></p>
|
||
<button onclick="AppNavigation.showAllContent('${gameType}')" class="show-all-btn">
|
||
Voir tous les contenus (${availableContent.length})
|
||
</button>
|
||
`;
|
||
grid.appendChild(infoDiv);
|
||
}
|
||
|
||
} catch (error) {
|
||
logSh('Erreur rendu levels:', error, 'ERROR');
|
||
grid.innerHTML = '<div class="error-content">❌ Erreur lors du chargement du contenu</div>';
|
||
}
|
||
},
|
||
|
||
// Méthode pour afficher tout le contenu
|
||
async showAllContent(gameType) {
|
||
const grid = document.getElementById('levels-grid');
|
||
grid.innerHTML = '';
|
||
|
||
const availableContent = await this.contentScanner.getAvailableContent();
|
||
|
||
availableContent.forEach(content => {
|
||
const card = this.createLevelCard(content.id, content, gameType);
|
||
grid.appendChild(card);
|
||
});
|
||
},
|
||
|
||
// Création d'une carte de niveau
|
||
createLevelCard(contentKey, contentInfo, gameType) {
|
||
const card = document.createElement('div');
|
||
card.className = 'level-card';
|
||
|
||
// Calculer les statistiques à afficher
|
||
const stats = [];
|
||
if (contentInfo.stats) {
|
||
if (contentInfo.stats.vocabularyCount > 0) {
|
||
stats.push(`📚 ${contentInfo.stats.vocabularyCount} mots`);
|
||
}
|
||
if (contentInfo.stats.sentenceCount > 0) {
|
||
stats.push(`💬 ${contentInfo.stats.sentenceCount} phrases`);
|
||
}
|
||
if (contentInfo.stats.dialogueCount > 0) {
|
||
stats.push(`🎭 ${contentInfo.stats.dialogueCount} dialogues`);
|
||
}
|
||
}
|
||
|
||
// Indicateur de compatibilité
|
||
const compatibility = contentInfo.gameCompatibility?.[gameType];
|
||
const compatScore = compatibility?.score || 0;
|
||
const compatClass = compatScore > 70 ? 'high-compat' : compatScore > 40 ? 'medium-compat' : 'low-compat';
|
||
|
||
card.innerHTML = `
|
||
<div class="card-header">
|
||
<div class="icon">${contentInfo.icon}</div>
|
||
${compatibility ? `<div class="compatibility ${compatClass}" title="Compatibilité: ${compatScore}%">
|
||
${compatScore > 70 ? '🟢' : compatScore > 40 ? '🟡' : '🟠'}
|
||
</div>` : ''}
|
||
</div>
|
||
<div class="title">${contentInfo.name}</div>
|
||
<div class="description">${contentInfo.description}</div>
|
||
<div class="content-stats">
|
||
<span class="difficulty-badge difficulty-${contentInfo.difficulty}">${contentInfo.difficulty}</span>
|
||
<span class="items-count">${contentInfo.metadata.totalItems} éléments</span>
|
||
<span class="time-estimate">~${contentInfo.metadata.estimatedTime}min</span>
|
||
</div>
|
||
${stats.length > 0 ? `<div class="detailed-stats">${stats.join(' • ')}</div>` : ''}
|
||
`;
|
||
|
||
card.addEventListener('click', () => {
|
||
logSh(`📚 Clic sur la carte du contenu: ${contentInfo.name} (${contentKey}) pour le jeu ${gameType}`, 'INFO');
|
||
Utils.animateElement(card, 'pulse');
|
||
this.navigateTo('play', gameType, contentKey);
|
||
});
|
||
|
||
return card;
|
||
},
|
||
|
||
// Mise à jour du breadcrumb
|
||
updateBreadcrumb() {
|
||
logSh(`🍞 Mise à jour du breadcrumb pour page: ${this.currentPage}`, 'DEBUG');
|
||
const breadcrumb = document.getElementById('breadcrumb');
|
||
breadcrumb.innerHTML = '';
|
||
|
||
const params = Utils.getUrlParams();
|
||
|
||
// Accueil
|
||
const homeItem = this.createBreadcrumbItem('🏠 Accueil', 'home',
|
||
this.currentPage === 'home');
|
||
breadcrumb.appendChild(homeItem);
|
||
|
||
// Jeux
|
||
if (['games', 'levels', 'play'].includes(this.currentPage)) {
|
||
const gamesItem = this.createBreadcrumbItem('🎮 Jeux', 'games',
|
||
this.currentPage === 'games');
|
||
breadcrumb.appendChild(gamesItem);
|
||
}
|
||
|
||
// Niveaux
|
||
if (['levels', 'play'].includes(this.currentPage) && params.game) {
|
||
const gameInfo = this.gamesConfig?.games[params.game];
|
||
const levelText = gameInfo ? `${gameInfo.icon} ${gameInfo.name}` : 'Niveaux';
|
||
const levelsItem = this.createBreadcrumbItem(levelText, 'levels',
|
||
this.currentPage === 'levels');
|
||
breadcrumb.appendChild(levelsItem);
|
||
}
|
||
|
||
// Jeu en cours
|
||
if (this.currentPage === 'play' && params.content) {
|
||
const contentInfo = this.gamesConfig?.content[params.content];
|
||
const playText = contentInfo ? `🎯 ${contentInfo.name}` : 'Jeu';
|
||
const playItem = this.createBreadcrumbItem(playText, 'play', true);
|
||
breadcrumb.appendChild(playItem);
|
||
}
|
||
},
|
||
|
||
// Création d'un élément breadcrumb
|
||
createBreadcrumbItem(text, page, isActive) {
|
||
const item = document.createElement('button');
|
||
item.className = `breadcrumb-item ${isActive ? 'active' : ''}`;
|
||
item.textContent = text;
|
||
item.dataset.page = page;
|
||
|
||
if (!isActive) {
|
||
item.addEventListener('click', () => {
|
||
logSh(`🍞 Clic sur breadcrumb: ${text} → ${page}`, 'INFO');
|
||
const params = Utils.getUrlParams();
|
||
|
||
if (page === 'home') {
|
||
this.navigateTo('home');
|
||
} else if (page === 'games') {
|
||
this.navigateTo('games');
|
||
} else if (page === 'levels') {
|
||
this.navigateTo('levels', params.game);
|
||
}
|
||
});
|
||
}
|
||
|
||
return item;
|
||
}
|
||
};
|
||
|
||
// Fonctions globales pour l'HTML
|
||
window.navigateTo = (page, game, content) => AppNavigation.navigateTo(page, game, content);
|
||
window.goBack = () => AppNavigation.goBack();
|
||
|
||
// Export
|
||
window.AppNavigation = AppNavigation; |