// ===== 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 `
`;
}).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 = `${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 ?
`;
area.innerHTML = `${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 = `${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 = `${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 = `${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 = `${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 = `${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 = ``;
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 = `
${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 = ``;
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 = `
${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 = `
`;
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 = ``;
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 = ``;
addPoints(gameState.score, 'Riddle', 'game');
return;
}
const r = riddles[idx];
const opts = [...r.options].sort(() => Math.random() - 0.5);
area.innerHTML = `
${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 = ``; exerciseDone(); return; }
const ex = EXERCISES_FILL[idx];
const parts = ex.template.split('___');
area.innerHTML = `${[...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 = ``; exerciseDone(); return; }
const ex = EXERCISES_LISTEN[idx];
area.innerHTML = ``;
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 = ``; exerciseDone(); return; }
const ex = EXERCISES_TRANSLATE[idx];
area.innerHTML = ``;
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 = ``; exerciseDone(); return; }
const conj = CONJUGATIONS[idx];
const rp = pronouns[Math.floor(Math.random() * pronouns.length)];
const correct = conj.forms[rp];
area.innerHTML = ``;
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 => ``).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(); }