// ===== AUTH ===== const token = localStorage.getItem('pt_token'); const user = JSON.parse(localStorage.getItem('pt_user') || 'null'); if (!token || !user) { window.location.href = '/login.html'; } const API = { headers() { return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }; }, async get(url) { const r = await fetch(url, { headers: this.headers() }); if (r.status === 401) { localStorage.clear(); window.location.href = '/login.html'; } return r.json(); }, async post(url, body) { const r = await fetch(url, { method: 'POST', headers: this.headers(), body: JSON.stringify(body) }); if (r.status === 401) { localStorage.clear(); window.location.href = '/login.html'; } return r.json(); }, async del(url) { const r = await fetch(url, { method: 'DELETE', headers: this.headers() }); return r.json(); }, async put(url) { const r = await fetch(url, { method: 'PUT', headers: this.headers() }); return r.json(); }, }; document.getElementById('app').classList.remove('hidden'); document.getElementById('header-avatar').textContent = user.avatar; if (user.is_admin) document.getElementById('nav-admin').style.display = ''; document.getElementById('btn-logout').addEventListener('click', () => { localStorage.clear(); window.location.href = '/login.html'; }); // ===== TOAST ===== function showToast(msg) { const t = document.getElementById('toast'); t.textContent = msg; t.classList.remove('hidden'); clearTimeout(t._timeout); t._timeout = setTimeout(() => t.classList.add('hidden'), 2000); } // ===== CONFETTI ===== function celebrate() { const c = document.createElement('div'); c.className = 'celebration'; document.body.appendChild(c); const colors = ['#006600', '#FF0000', '#FFD700', '#FFFFFF', '#FF69B4']; for (let i = 0; i < 50; i++) { const d = document.createElement('div'); d.className = 'confetti'; d.style.left = Math.random() * 100 + '%'; d.style.background = colors[Math.floor(Math.random() * colors.length)]; d.style.animationDelay = Math.random() * 0.5 + 's'; d.style.animationDuration = (1 + Math.random()) + 's'; c.appendChild(d); } setTimeout(() => c.remove(), 3000); } // ===== POINTS ===== async function addPoints(points, activity, type) { const data = await API.post('/api/points/add', { points, activity, activity_type: type }); document.getElementById('header-points-val').textContent = data.total_points; } async function loadMyPoints() { const data = await API.get('/api/points/me'); document.getElementById('header-points-val').textContent = data.total_points; return data; } // ===== AUDIO ===== const SpeechEngine = { speak(text, lang = 'pt-PT', rate = 0.9) { if (!('speechSynthesis' in window)) return; window.speechSynthesis.cancel(); const u = new SpeechSynthesisUtterance(text); u.lang = lang; u.rate = rate; u.pitch = 1; const voices = window.speechSynthesis.getVoices(); const ptMatch = voices.find(v => v.lang === 'pt-PT'); const ptFallback = voices.find(v => v.lang.startsWith('pt-PT')); const ptAny = voices.find(v => v.lang.startsWith('pt')); const match = ptMatch || ptFallback || ptAny; if (match) u.voice = match; window.speechSynthesis.speak(u); }, speakPT(t) { this.speak(t, 'pt-PT', 0.85); }, speakPTSlow(t) { this.speak(t, 'pt-PT', 0.55); }, speakFR(t) { this.speak(t, 'fr-FR', 0.9); } }; // ===== NAV ===== let currentTab = 'dashboard'; const tabs = document.querySelectorAll('.tab'); const navBtns = document.querySelectorAll('.nav-btn[data-tab]'); function switchTab(id) { currentTab = id; tabs.forEach(t => t.classList.toggle('active', t.id === `tab-${id}`)); navBtns.forEach(b => b.classList.toggle('active', b.dataset.tab === id)); window.scrollTo(0, 0); } navBtns.forEach(b => b.addEventListener('click', () => switchTab(b.dataset.tab))); // ===== DASHBOARD ===== async function loadDashboard() { const [points, leaderboard, history] = await Promise.all([ API.get('/api/points/me'), API.get('/api/points/leaderboard'), API.get('/api/points/history') ]); document.getElementById('dashboard-welcome').innerHTML = `

Ola ${user.display_name}! 👋

Pretes aprender portugues hoje?

`; document.getElementById('dash-total-points').textContent = points.total_points; document.getElementById('dash-streak').textContent = points.today?.streak || 0; document.getElementById('dash-today-points').textContent = points.today?.points_earned || 0; document.getElementById('dash-today-exercises').textContent = points.today?.exercises_done || 0; // Leaderboard const lb = document.getElementById('dash-leaderboard'); if (leaderboard.leaderboard.length === 0) { lb.innerHTML = '

Aucun classement pour le moment

'; } else { const medals = ['🥇', '🥈', '🥉']; lb.innerHTML = leaderboard.leaderboard.map((u, i) => `
${medals[i] || (i + 1)} ${u.avatar} ${u.display_name} ⭐ ${u.total_points}
`).join(''); } // History chart const hist = document.getElementById('dash-history'); const days = history.history.slice(0, 7).reverse(); if (days.length === 0) { hist.innerHTML = '

Commence à jouer pour voir ton activité !

'; } else { const maxPts = Math.max(...days.map(d => d.points_earned), 1); hist.innerHTML = `
${days.map(d => { const h = Math.max((d.points_earned / maxPts) * 100, 4); const day = new Date(d.date).toLocaleDateString('fr-FR', { weekday: 'short' }); return `
${day}
${d.points_earned}
`; }).join('')}
`; } } // ===== PHRASES ===== let currentPhraseCategory = Object.keys(PHRASES)[0]; let userFavorites = []; async function loadFavorites() { const data = await API.get('/api/favorites'); userFavorites = data.favorites; } function renderPhraseCategories() { const c = document.getElementById('phrase-categories'); c.innerHTML = Object.keys(PHRASES).map(k => `` ).join(''); c.querySelectorAll('.cat-tab').forEach(b => b.addEventListener('click', () => { currentPhraseCategory = b.dataset.cat; renderPhraseCategories(); renderPhrases(); })); } function renderPhrases() { const list = document.getElementById('phrases-list'); const phrases = PHRASES[currentPhraseCategory] || []; list.innerHTML = phrases.map(p => { const isFav = userFavorites.includes(p.pt); return `
${p.pt}
${p.phonetic}
${p.fr}
`; }).join(''); list.querySelectorAll('.btn-listen').forEach(b => b.addEventListener('click', () => SpeechEngine.speakPT(b.dataset.text))); list.querySelectorAll('.btn-slow').forEach(b => b.addEventListener('click', () => SpeechEngine.speakPTSlow(b.dataset.text))); list.querySelectorAll('.btn-favorite').forEach(b => b.addEventListener('click', async () => { const data = await API.post('/api/favorites/toggle', { phrase: b.dataset.text }); await loadFavorites(); renderPhrases(); showToast(data.favorited ? 'Ajouté aux favoris ❤️' : 'Retiré des favoris'); })); } // ===== TRANSLATOR ===== let fromLang = 'pt', toLang = 'fr'; document.getElementById('swap-lang').addEventListener('click', () => { [fromLang, toLang] = [toLang, fromLang]; document.getElementById('lang-from').textContent = fromLang === 'pt' ? '🇵🇹 Portugais' : '🇫🇷 Français'; document.getElementById('lang-to').textContent = toLang === 'pt' ? '🇵🇹 Portugais' : '🇫🇷 Français'; const i = document.getElementById('input-text').value; const o = document.getElementById('output-text').textContent; if (o) { document.getElementById('input-text').value = o; document.getElementById('output-text').textContent = i; } }); document.getElementById('translate-btn').addEventListener('click', async () => { const text = document.getElementById('input-text').value.trim(); if (!text) return; const out = document.getElementById('output-text'); out.textContent = 'Traduction...'; try { const r = await fetch(`https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${fromLang === 'pt' ? 'pt-PT' : 'fr'}|${toLang === 'pt' ? 'pt-PT' : 'fr'}`); const d = await r.json(); out.textContent = d.responseData.translatedText; } catch { out.textContent = 'Erreur de traduction'; } }); document.getElementById('listen-input').addEventListener('click', () => { const t = document.getElementById('input-text').value.trim(); if (t) fromLang === 'pt' ? SpeechEngine.speakPT(t) : SpeechEngine.speakFR(t); }); document.getElementById('listen-output').addEventListener('click', () => { const t = document.getElementById('output-text').textContent.trim(); if (t) toLang === 'pt' ? SpeechEngine.speakPT(t) : SpeechEngine.speakFR(t); }); document.getElementById('clear-input').addEventListener('click', () => { document.getElementById('input-text').value = ''; document.getElementById('output-text').textContent = ''; }); document.getElementById('copy-output').addEventListener('click', () => { const t = document.getElementById('output-text').textContent; if (t) { navigator.clipboard.writeText(t); showToast('Copié !'); } }); // ===== GAMES ===== let gameState = null; document.querySelectorAll('.game-card[data-game]').forEach(c => c.addEventListener('click', () => startGame(c.dataset.game))); document.getElementById('back-to-games').addEventListener('click', () => { document.getElementById('game-area').classList.add('hidden'); document.getElementById('back-to-games').classList.add('hidden'); document.querySelector('#tab-games .game-menu').classList.remove('hidden'); gameState = null; }); function startGame(type) { document.querySelector('#tab-games .game-menu').classList.add('hidden'); const area = document.getElementById('game-area'); document.getElementById('back-to-games').classList.remove('hidden'); area.classList.remove('hidden'); area.innerHTML = ''; gameState = { type, score: 0, total: 0 }; if (type === 'colors') startColorsGame(area); else if (type === 'animals') startAnimalsGame(area); else if (type === 'numbers') startNumbersGame(area); else if (type === 'memory') startMemoryGame(area); else if (type === 'quiz') startQuizGame(area); else if (type === 'dragdrop') startDragDropGame(area); else if (type === 'scramble') startScrambleGame(area); else if (type === 'bingo') startBingoGame(area); else if (type === 'catch') startCatchGame(area); else if (type === 'riddle') startRiddleGame(area); } function gameRound(area, questionHTML, options, getCorrectPt, onCorrect) { area.innerHTML = `
${questionHTML}
${options.map(o => ``).join('')}
`; area.querySelectorAll('.option-btn').forEach(btn => { btn.addEventListener('click', () => { gameState.total++; if (btn.dataset.val === getCorrectPt()) { gameState.score += 10; btn.classList.add('correct'); onCorrect(); celebrate(); } else { btn.classList.add('wrong'); } area.querySelectorAll('.option-btn').forEach(b => b.style.pointerEvents = 'none'); addPoints(btn.classList.contains('correct') ? 10 : 0, gameState.type, 'game'); setTimeout(() => gameRound(...arguments), 1200); }); }); } function startColorsGame(area) { (function round() { const correct = COLORS_GAME[Math.floor(Math.random() * COLORS_GAME.length)]; let opts = [correct]; while (opts.length < 4) { const c = COLORS_GAME[Math.floor(Math.random() * COLORS_GAME.length)]; if (!opts.find(o => o.pt === c.pt)) opts.push(c); } opts.sort(() => Math.random() - 0.5); const scoreHTML = `

Quelle est cette couleur ?

${gameState.score}
Points
`; area.innerHTML = `
${scoreHTML}
${opts.map(o => ``).join('')}
`; area.querySelectorAll('.option-btn').forEach(btn => btn.addEventListener('click', () => { gameState.total++; if (btn.dataset.pt === correct.pt) { gameState.score += 10; btn.classList.add('correct'); SpeechEngine.speakPT(correct.pt); showToast(`✅ ${correct.pt} = ${correct.fr}`); celebrate(); } else { btn.classList.add('wrong'); showToast(`❌ ${correct.pt} = ${correct.fr}`); } area.querySelectorAll('.option-btn').forEach(b => b.style.pointerEvents = 'none'); addPoints(btn.classList.contains('correct') ? 10 : 0, 'Couleurs', 'game'); setTimeout(round, 1200); })); })(); } function startAnimalsGame(area) { (function round() { const correct = ANIMALS_GAME[Math.floor(Math.random() * ANIMALS_GAME.length)]; let opts = [correct]; while (opts.length < 4) { const a = ANIMALS_GAME[Math.floor(Math.random() * ANIMALS_GAME.length)]; if (!opts.find(o => o.pt === a.pt)) opts.push(a); } opts.sort(() => Math.random() - 0.5); area.innerHTML = `

Cet animal se dit ?

${gameState.score}
Points
${correct.emoji}
${opts.map(o => ``).join('')}
`; area.querySelectorAll('.option-btn').forEach(btn => btn.addEventListener('click', () => { gameState.total++; if (btn.dataset.pt === correct.pt) { gameState.score += 10; btn.classList.add('correct'); SpeechEngine.speakPT(correct.pt); showToast(`✅ ${correct.emoji} = ${correct.pt}`); celebrate(); } else { btn.classList.add('wrong'); showToast(`❌ ${correct.pt} = ${correct.fr}`); } area.querySelectorAll('.option-btn').forEach(b => b.style.pointerEvents = 'none'); addPoints(btn.classList.contains('correct') ? 10 : 0, 'Animaux', 'game'); setTimeout(round, 1200); })); })(); } function startNumbersGame(area) { (function round() { const correct = NUMBERS_GAME[Math.floor(Math.random() * NUMBERS_GAME.length)]; let opts = [correct]; while (opts.length < 4) { const n = NUMBERS_GAME[Math.floor(Math.random() * NUMBERS_GAME.length)]; if (!opts.find(o => o.num === n.num)) opts.push(n); } opts.sort(() => Math.random() - 0.5); area.innerHTML = `

Le nombre ${correct.num} ?

${gameState.score}
Points
${correct.num}
${opts.map(o => ``).join('')}
`; area.querySelectorAll('.option-btn').forEach(btn => btn.addEventListener('click', () => { gameState.total++; if (btn.dataset.pt === correct.pt) { gameState.score += 10; btn.classList.add('correct'); SpeechEngine.speakPT(correct.pt); showToast(`✅ ${correct.num} = ${correct.pt}`); celebrate(); } else { btn.classList.add('wrong'); showToast(`❌ ${correct.pt}`); } area.querySelectorAll('.option-btn').forEach(b => b.style.pointerEvents = 'none'); addPoints(btn.classList.contains('correct') ? 10 : 0, 'Nombres', 'game'); setTimeout(round, 1200); })); })(); } function startMemoryGame(area) { const animals = ANIMALS_GAME.slice(0, 6); const pairs = []; animals.forEach(a => { pairs.push({ id: a.pt, display: a.emoji }); pairs.push({ id: a.pt, display: a.pt }); }); pairs.sort(() => Math.random() - 0.5); let flipped = [], matched = 0, lockBoard = false, moves = 0; area.innerHTML = `

Memory

0
Coups
0/6
Paires
${pairs.map((p, i) => `
?
`).join('')}
`; area.querySelectorAll('.memory-card').forEach(card => card.addEventListener('click', () => { if (lockBoard || card.classList.contains('flipped') || card.classList.contains('matched')) return; const idx = parseInt(card.dataset.idx); card.classList.add('flipped'); card.textContent = pairs[idx].display; flipped.push({ el: card, id: pairs[idx].id }); if (flipped.length === 2) { lockBoard = true; moves++; document.getElementById('mm').textContent = moves; if (flipped[0].id === flipped[1].id) { flipped.forEach(f => f.el.classList.add('matched')); matched++; document.getElementById('mp').textContent = `${matched}/6`; SpeechEngine.speakPT(flipped[0].id); flipped = []; lockBoard = false; addPoints(5, 'Memory', 'game'); if (matched === 6) { celebrate(); showToast(`Bravo ! ${moves} coups !`); addPoints(20, 'Memory complet', 'game'); } } else { setTimeout(() => { flipped.forEach(f => { f.el.classList.remove('flipped'); f.el.textContent = '?'; }); flipped = []; lockBoard = false; }, 800); } } })); } function startQuizGame(area) { const all = Object.values(PHRASES).flat(); (function round() { const correct = all[Math.floor(Math.random() * all.length)]; let opts = [correct]; while (opts.length < 4) { const p = all[Math.floor(Math.random() * all.length)]; if (!opts.find(o => o.pt === p.pt)) opts.push(p); } opts.sort(() => Math.random() - 0.5); area.innerHTML = `

Que signifie :

${gameState.score}
Points
🔊 ${correct.pt}
${opts.map(o => ``).join('')}
`; document.getElementById('ql').addEventListener('click', () => SpeechEngine.speakPT(correct.pt)); area.querySelectorAll('.option-btn').forEach(btn => btn.addEventListener('click', () => { gameState.total++; if (btn.dataset.pt === correct.pt) { gameState.score += 10; btn.classList.add('correct'); showToast(`✅ ${correct.pt} = ${correct.fr}`); celebrate(); } else { btn.classList.add('wrong'); showToast(`❌ ${correct.pt} = ${correct.fr}`); } area.querySelectorAll('.option-btn').forEach(b => b.style.pointerEvents = 'none'); addPoints(btn.classList.contains('correct') ? 10 : 0, 'Quiz', 'game'); setTimeout(round, 1500); })); })(); } function startDragDropGame(area) { const pairs = [{ fr: "Chien", pt: "Cão" }, { fr: "Chat", pt: "Gato" }, { fr: "Maison", pt: "Casa" }, { fr: "Eau", pt: "Água" }, { fr: "Pain", pt: "Pão" }]; const shuffled = [...pairs].sort(() => Math.random() - 0.5); let matched = 0, dragged = null; function render() { area.innerHTML = `

Relie les mots

${shuffled.map(p => `
${p.pt}
`).join('')}
${pairs.map(p => `
🇫🇷 ${p.fr}
Appuie ici
`).join('')}`; area.querySelectorAll('.drag-item').forEach(item => item.addEventListener('click', () => { if (item.style.opacity === '0.4') return; area.querySelectorAll('.drag-item').forEach(i => i.style.outline = ''); item.style.outline = '3px solid var(--gold)'; dragged = item; })); area.querySelectorAll('.drop-zone').forEach(zone => zone.addEventListener('click', () => { if (!dragged) return; const pt = dragged.dataset.pt, expected = zone.dataset.expected; if (pt === expected) { zone.innerHTML = `✅ ${pt}`; zone.style.borderColor = 'var(--green)'; zone.style.background = '#e0ffe0'; dragged.style.opacity = '0.4'; dragged.style.pointerEvents = 'none'; SpeechEngine.speakPT(pt); matched++; addPoints(5, 'Associe', 'game'); if (matched === pairs.length) { celebrate(); showToast('Bravo !'); addPoints(15, 'Associe complet', 'game'); } } else { zone.style.borderColor = 'var(--red)'; zone.style.background = '#ffe0e0'; showToast('❌ Essaye encore !'); setTimeout(() => { zone.style.borderColor = ''; zone.style.background = ''; }, 600); } dragged = null; area.querySelectorAll('.drag-item').forEach(i => i.style.outline = ''); })); } render(); } // ===== SCRAMBLE GAME ===== function startScrambleGame(area) { const words = [...SCRAMBLE_WORDS].sort(() => Math.random() - 0.5); let idx = 0; (function round() { if (idx >= words.length) { area.innerHTML = `

Terminé ! 🎉 Score : ${gameState.score}/${words.length * 10}

`; addPoints(gameState.score, 'Scramble', 'game'); return; } const w = words[idx]; const scrambled = w.word.split('').sort(() => Math.random() - 0.5).join(''); const isSame = scrambled === w.word; const display = isSame ? w.word.split('').reverse().join('') : scrambled; area.innerHTML = `

🔤 Déchiffre le mot !

${gameState.score}
Points
${w.emoji}

${w.fr}

${display.toUpperCase().split('').map((l,i) => `${l}`).join('')}

`; const check = () => { const val = document.getElementById('scramble-input').value.trim().toLowerCase(); const fb = document.getElementById('scramble-feedback'); gameState.total++; if (val === w.word) { gameState.score += 10; fb.innerHTML = `✅ ${w.word} = ${w.fr}`; SpeechEngine.speakPT(w.word); celebrate(); } else { fb.innerHTML = `❌ C'était "${w.word}"`; } document.getElementById('scramble-input').disabled = true; document.getElementById('scramble-check').disabled = true; setTimeout(() => { idx++; round(); }, 1500); }; document.getElementById('scramble-check').addEventListener('click', check); document.getElementById('scramble-input').addEventListener('keydown', e => { if (e.key === 'Enter') check(); }); document.getElementById('scramble-input').focus(); })(); } // ===== BINGO GAME ===== function startBingoGame(area) { let bingoScore = 0; let bingoRound = 0; const totalRounds = 10; function newRound() { if (bingoRound >= totalRounds) { area.innerHTML = `

Bingo terminé ! 🎤🎉 Score : ${bingoScore}/${totalRounds}

`; addPoints(bingoScore * 15, 'Bingo', 'game'); return; } bingoRound++; const correct = BINGO_WORDS[Math.floor(Math.random() * BINGO_WORDS.length)]; let opts = [correct]; while (opts.length < 6) { const w = BINGO_WORDS[Math.floor(Math.random() * BINGO_WORDS.length)]; if (!opts.find(o => o.pt === w.pt)) opts.push(w); } opts.sort(() => Math.random() - 0.5); area.innerHTML = `

🎤 Bingo - ${bingoRound}/${totalRounds}

${bingoScore}
Trouvés

Appuie pour écouter !

${opts.map(o => ` `).join('')}
`; document.getElementById('bingo-listen').addEventListener('click', () => SpeechEngine.speakPT(correct.pt)); SpeechEngine.speakPT(correct.pt); area.querySelectorAll('.bingo-card').forEach(btn => { btn.addEventListener('click', () => { area.querySelectorAll('.bingo-card').forEach(b => b.style.pointerEvents = 'none'); gameState.total++; if (btn.dataset.pt === correct.pt) { bingoScore++; gameState.score += 15; btn.classList.add('correct'); showToast(`✅ ${correct.emoji} ${correct.pt} = ${correct.fr}`); celebrate(); } else { btn.classList.add('wrong'); area.querySelector(`.bingo-card[data-pt="${correct.pt}"]`)?.classList.add('correct'); showToast(`❌ C'était ${correct.emoji} ${correct.pt}`); } addPoints(btn.dataset.pt === correct.pt ? 15 : 0, 'Bingo', 'game'); setTimeout(newRound, 1500); }); }); } newRound(); } // ===== CATCH WORDS GAME ===== function startCatchGame(area) { const words = [...BINGO_WORDS].sort(() => Math.random() - 0.5).slice(0, 20); let caught = 0, missed = 0, gameActive = true; const catchDuration = 30; area.innerHTML = `

🖐️ Apanha as palavras !

0
Apanhados
${catchDuration}s
Tempo
0
Perdidos
`; const zone = document.getElementById('catch-zone'); let timer = catchDuration; const timerInterval = setInterval(() => { timer--; document.getElementById('catch-timer').textContent = timer + 's'; if (timer <= 0) { clearInterval(timerInterval); clearInterval(spawnInterval); gameActive = false; const pts = caught * 10; area.innerHTML = `

Tempo esgotado! ⏰

Apanhaste ${caught} palavras!

`; addPoints(pts, 'Apanha', 'game'); if (caught >= 5) celebrate(); } }, 1000); let wordIdx = 0; const spawnInterval = setInterval(() => { if (!gameActive || wordIdx >= words.length) return; const w = words[wordIdx++]; const el = document.createElement('div'); el.className = 'catch-word'; el.innerHTML = `${w.emoji}${w.pt}`; el.style.left = Math.random() * 80 + 10 + '%'; el.dataset.pt = w.pt; el.dataset.fr = w.fr; el.addEventListener('click', () => { if (el.classList.contains('caught') || el.classList.contains('missed-word')) return; caught++; gameState.score += 10; el.classList.add('caught'); document.getElementById('catch-caught').textContent = caught; SpeechEngine.speakPT(w.pt); showToast(`✅ ${w.emoji} ${w.pt}`); setTimeout(() => el.remove(), 500); }); zone.appendChild(el); setTimeout(() => { if (!el.classList.contains('caught')) { el.classList.add('missed-word'); missed++; const m = document.getElementById('catch-missed'); if (m) m.textContent = missed; setTimeout(() => el.remove(), 800); } }, 3000); }, 800); } // ===== RIDDLE GAME ===== function startRiddleGame(area) { const riddles = [...RIDDLES].sort(() => Math.random() - 0.5); let idx = 0; (function round() { if (idx >= riddles.length) { area.innerHTML = `

Terminé ! 🤔🎉 Score : ${gameState.score}/${riddles.length * 10}

`; addPoints(gameState.score, 'Riddle', 'game'); return; } const r = riddles[idx]; const opts = [...r.options].sort(() => Math.random() - 0.5); area.innerHTML = `

🤔 O que é?

${gameState.score}
Points
${r.emoji}

"${r.riddle}"

${r.fr}

${opts.map(o => ``).join('')}
`; area.querySelectorAll('.riddle-opt').forEach(btn => { btn.addEventListener('click', () => { area.querySelectorAll('.riddle-opt').forEach(b => b.style.pointerEvents = 'none'); gameState.total++; if (btn.dataset.val === r.answer) { gameState.score += 10; btn.classList.add('correct'); showToast(`✅ ${r.answer} !`); SpeechEngine.speakPT(r.answer); celebrate(); } else { btn.classList.add('wrong'); area.querySelector(`.riddle-opt[data-val="${r.answer}"]`)?.classList.add('correct'); showToast(`❌ C'était "${r.answer}"`); } addPoints(btn.dataset.val === r.answer ? 10 : 0, 'Riddle', 'game'); setTimeout(round, 1500); }); }); })(); } // ===== EXERCISES ===== let currentExercise = null; document.querySelectorAll('.game-card[data-exercise]').forEach(c => c.addEventListener('click', () => startExercise(c.dataset.exercise))); document.getElementById('back-to-exercises').addEventListener('click', () => { document.getElementById('exercise-area').classList.add('hidden'); document.getElementById('back-to-exercises').classList.add('hidden'); document.querySelector('#tab-exercises .game-menu').classList.remove('hidden'); currentExercise = null; }); function startExercise(type) { document.querySelector('#tab-exercises .game-menu').classList.add('hidden'); const area = document.getElementById('exercise-area'); document.getElementById('back-to-exercises').classList.remove('hidden'); area.classList.remove('hidden'); area.innerHTML = ''; currentExercise = { type, score: 0, total: 0 }; if (type === 'fill') startFillExercise(area); else if (type === 'listenwrite') startListenWriteExercise(area); else if (type === 'translate') startTranslateExercise(area); else if (type === 'conjugation') startConjugationExercise(area); } function exerciseDone() { const score = currentExercise.score; const total = currentExercise.total || currentExercise.type === 'fill' ? EXERCISES_FILL.length : EXERCISES_LISTEN.length; addPoints(score * 5, currentExercise.type, 'exercise'); if (score >= Math.floor(total * 0.7)) celebrate(); } function startFillExercise(area) { let idx = 0; (function question() { if (idx >= EXERCISES_FILL.length) { area.innerHTML = `

Terminé ! Score : ${currentExercise.score}/${EXERCISES_FILL.length}

`; exerciseDone(); return; } const ex = EXERCISES_FILL[idx]; const parts = ex.template.split('___'); area.innerHTML = `

Complète la phrase

${ex.hint}

${parts[0]}____${parts[1] || ''}

${[...ex.options].sort(() => Math.random() - 0.5).map(o => ``).join('')}
`; area.querySelectorAll('.fill-word').forEach(btn => btn.addEventListener('click', () => { currentExercise.total++; const blank = document.getElementById('blank'); if (btn.dataset.word === ex.answer) { currentExercise.score++; blank.textContent = btn.dataset.word; blank.classList.add('correct-blank'); SpeechEngine.speakPT(ex.template.replace('___', ex.answer)); showToast('✅ Bravo !'); } else { blank.textContent = btn.dataset.word; blank.classList.add('wrong-blank'); showToast(`❌ "${ex.answer}"`); } area.querySelectorAll('.fill-word').forEach(b => b.style.pointerEvents = 'none'); setTimeout(() => { idx++; question(); }, 1500); })); })(); } function startListenWriteExercise(area) { let idx = 0; (function question() { if (idx >= EXERCISES_LISTEN.length) { area.innerHTML = `

Terminé ! Score : ${currentExercise.score}/${EXERCISES_LISTEN.length}

`; exerciseDone(); return; } const ex = EXERCISES_LISTEN[idx]; area.innerHTML = `

Écoute et écris

${ex.fr}

`; document.getElementById('lw-listen').addEventListener('click', () => SpeechEngine.speakPT(ex.pt)); const check = () => { const val = document.getElementById('lw-input').value.trim().toLowerCase(); const correct = ex.pt.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); const userNorm = val.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); currentExercise.total++; const fb = document.getElementById('lw-feedback'); if (userNorm === correct || val === ex.pt.toLowerCase()) { currentExercise.score++; fb.innerHTML = `✅ ${ex.pt}`; SpeechEngine.speakPT(ex.pt); } else { fb.innerHTML = `❌ ${ex.pt}`; } setTimeout(() => { idx++; question(); }, 2000); }; document.getElementById('lw-check').addEventListener('click', check); document.getElementById('lw-input').addEventListener('keydown', e => { if (e.key === 'Enter') check(); }); SpeechEngine.speakPT(ex.pt); })(); } function startTranslateExercise(area) { let idx = 0; (function question() { if (idx >= EXERCISES_TRANSLATE.length) { area.innerHTML = `

Terminé ! Score : ${currentExercise.score}/${EXERCISES_TRANSLATE.length}

`; exerciseDone(); return; } const ex = EXERCISES_TRANSLATE[idx]; area.innerHTML = `

Traduis en portugais

${ex.fr}

`; const check = () => { const val = document.getElementById('tr-input').value.trim().toLowerCase(); const correct = ex.pt.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); const userNorm = val.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); currentExercise.total++; const fb = document.getElementById('tr-feedback'); if (userNorm === correct || val === ex.pt.toLowerCase()) { currentExercise.score++; fb.innerHTML = `✅ ${ex.pt}`; SpeechEngine.speakPT(ex.pt); } else { fb.innerHTML = `❌ ${ex.pt}`; } setTimeout(() => { idx++; question(); }, 2000); }; document.getElementById('tr-check').addEventListener('click', check); document.getElementById('tr-input').addEventListener('keydown', e => { if (e.key === 'Enter') check(); }); })(); } function startConjugationExercise(area) { let idx = 0; const pronouns = ['eu', 'tu', 'ele', 'nós', 'eles']; (function question() { if (idx >= CONJUGATIONS.length) { area.innerHTML = `

Terminé ! Score : ${currentExercise.score}/${CONJUGATIONS.length}

`; exerciseDone(); return; } const conj = CONJUGATIONS[idx]; const rp = pronouns[Math.floor(Math.random() * pronouns.length)]; const correct = conj.forms[rp]; area.innerHTML = `

Conjugue "${conj.verb}"

${conj.meaning}

${rp} ...?

`; const check = () => { const val = document.getElementById('conj-input').value.trim().toLowerCase(); const correctNorm = correct.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); const userNorm = val.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); currentExercise.total++; const fb = document.getElementById('conj-feedback'); if (userNorm === correctNorm || val === correct.toLowerCase()) { currentExercise.score++; fb.innerHTML = `✅ ${rp} ${correct}`; SpeechEngine.speakPT(`${rp} ${correct}`); } else { fb.innerHTML = `❌ ${rp} ${correct}
${pronouns.map(p => `${p} ${conj.forms[p]}`).join(', ')}`; } setTimeout(() => { idx++; question(); }, 2500); }; document.getElementById('conj-check').addEventListener('click', check); document.getElementById('conj-input').addEventListener('keydown', e => { if (e.key === 'Enter') check(); }); })(); } // ===== VOCAB ===== let currentVocabCategory = Object.keys(VOCAB)[0]; function renderVocabCategories() { const c = document.getElementById('vocab-categories'); c.innerHTML = Object.keys(VOCAB).map(k => `` ).join(''); c.querySelectorAll('.cat-tab').forEach(b => b.addEventListener('click', () => { currentVocabCategory = b.dataset.cat; renderVocabCategories(); renderVocab(); })); } function renderVocab() { const g = document.getElementById('vocab-grid'); const items = VOCAB[currentVocabCategory] || []; g.innerHTML = items.map(v => `
${v.emoji}
${v.pt}
${v.fr}
`).join(''); g.querySelectorAll('.vocab-card').forEach(c => c.addEventListener('click', () => SpeechEngine.speakPT(c.dataset.word))); } // ===== ADMIN ===== async function loadAdmin() { if (!user.is_admin) return; const data = await API.get('/api/admin/users'); const list = document.getElementById('admin-users-list'); list.innerHTML = data.users.map(u => `
${u.avatar}
${u.display_name} @${u.username} ${u.is_admin ? '👑' : ''}
⭐ ${u.total_points} ${u.id !== user.id ? `` : ''}
`).join(''); list.querySelectorAll('.admin-btn-delete').forEach(btn => btn.addEventListener('click', async () => { if (!confirm('Supprimer cet utilisateur ?')) return; await API.del(`/api/admin/users/${btn.dataset.id}`); loadAdmin(); })); list.querySelectorAll('.admin-btn-toggle').forEach(btn => btn.addEventListener('click', async () => { await API.put(`/api/admin/users/${btn.dataset.id}/toggle-admin`); loadAdmin(); })); } document.getElementById('add-user-form').addEventListener('submit', async (e) => { e.preventDefault(); const username = document.getElementById('new-username').value.trim(); const display_name = document.getElementById('new-displayname').value.trim(); const password = document.getElementById('new-password').value; const avatar = document.getElementById('new-avatar').value; const data = await API.post('/api/register', { username, password, display_name, avatar }); if (data.error) { showToast(data.error); return; } showToast(`✅ ${display_name} ajouté !`); e.target.reset(); loadAdmin(); }); // ===== INIT ===== async function init() { await Promise.all([loadMyPoints(), loadFavorites()]); renderPhraseCategories(); renderPhrases(); renderVocabCategories(); renderVocab(); loadDashboard(); if (user.is_admin) loadAdmin(); } init(); if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').catch(() => {}); } if ('speechSynthesis' in window) { window.speechSynthesis.getVoices(); window.speechSynthesis.onvoiceschanged = () => window.speechSynthesis.getVoices(); }