848 lines
43 KiB
JavaScript
848 lines
43 KiB
JavaScript
// ===== 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 = `
|
|
<div class="welcome-text">
|
|
<h2>Ola ${user.display_name}! 👋</h2>
|
|
<p>Pretes aprender portugues hoje?</p>
|
|
</div>
|
|
`;
|
|
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 = '<p class="empty-state">Aucun classement pour le moment</p>';
|
|
} else {
|
|
const medals = ['🥇', '🥈', '🥉'];
|
|
lb.innerHTML = leaderboard.leaderboard.map((u, i) => `
|
|
<div class="lb-row ${u.id === user.id ? 'lb-me' : ''}">
|
|
<span class="lb-rank">${medals[i] || (i + 1)}</span>
|
|
<span class="lb-avatar">${u.avatar}</span>
|
|
<span class="lb-name">${u.display_name}</span>
|
|
<span class="lb-points">⭐ ${u.total_points}</span>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// History chart
|
|
const hist = document.getElementById('dash-history');
|
|
const days = history.history.slice(0, 7).reverse();
|
|
if (days.length === 0) {
|
|
hist.innerHTML = '<p class="empty-state">Commence à jouer pour voir ton activité !</p>';
|
|
} else {
|
|
const maxPts = Math.max(...days.map(d => d.points_earned), 1);
|
|
hist.innerHTML = `<div class="chart-bars">
|
|
${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 `<div class="chart-col"><div class="chart-bar" style="height:${h}%"></div><div class="chart-label">${day}</div><div class="chart-val">${d.points_earned}</div></div>`;
|
|
}).join('')}
|
|
</div>`;
|
|
}
|
|
}
|
|
|
|
// ===== 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 =>
|
|
`<button class="cat-tab${k === currentPhraseCategory ? ' active' : ''}" data-cat="${k}">${k}</button>`
|
|
).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 `<div class="phrase-card">
|
|
<div class="phrase-pt">${p.pt}</div>
|
|
<div class="phonetic">${p.phonetic}</div>
|
|
<div class="phrase-fr">${p.fr}</div>
|
|
<div class="phrase-actions">
|
|
<button class="btn-listen" data-text="${p.pt}">🔊 Écouter</button>
|
|
<button class="btn-slow" data-text="${p.pt}">🐢 Lent</button>
|
|
<button class="btn-favorite${isFav ? ' active' : ''}" data-text="${p.pt}">${isFav ? '❤️' : '🤍'}</button>
|
|
</div>
|
|
</div>`;
|
|
}).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 = `<div class="game-header">${questionHTML}</div><div class="color-options">${options.map(o => `<button class="option-btn" data-val="${o.val}">${o.label}</button>`).join('')}</div>`;
|
|
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 = `<h3>Quelle est cette couleur ?</h3><div class="game-score"><div class="score-item"><div class="score-value">${gameState.score}</div><div class="score-label">Points</div></div></div><div style="width:100px;height:100px;border-radius:50%;margin:16px auto;border:4px solid #ddd;background:${correct.color};${correct.border ? 'border-color:#ccc' : ''}"></div>`;
|
|
area.innerHTML = `<div class="game-header">${scoreHTML}</div><div class="color-options">${opts.map(o => `<button class="option-btn" data-pt="${o.pt}">${o.pt}</button>`).join('')}</div>`;
|
|
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 = `<div class="game-header"><h3>Cet animal se dit ?</h3><div class="game-score"><div class="score-item"><div class="score-value">${gameState.score}</div><div class="score-label">Points</div></div></div><div style="font-size:72px;margin:16px auto;text-align:center">${correct.emoji}</div></div><div class="animal-options">${opts.map(o => `<button class="option-btn" data-pt="${o.pt}">${o.pt}</button>`).join('')}</div>`;
|
|
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 = `<div class="game-header"><h3>Le nombre ${correct.num} ?</h3><div class="game-score"><div class="score-item"><div class="score-value">${gameState.score}</div><div class="score-label">Points</div></div></div><div style="font-size:56px;font-weight:900;color:var(--green);margin:20px auto;text-align:center">${correct.num}</div></div><div class="number-options">${opts.map(o => `<button class="option-btn" data-pt="${o.pt}">${o.pt}</button>`).join('')}</div>`;
|
|
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 = `<div class="game-header"><h3>Memory</h3><div class="game-score"><div class="score-item"><div class="score-value" id="mm">0</div><div class="score-label">Coups</div></div><div class="score-item"><div class="score-value" id="mp">0/6</div><div class="score-label">Paires</div></div></div></div><div class="memory-grid">${pairs.map((p, i) => `<div class="memory-card" data-idx="${i}" data-id="${p.id}">?</div>`).join('')}</div>`;
|
|
|
|
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 = `<div class="game-header"><h3>Que signifie :</h3><div class="game-score"><div class="score-item"><div class="score-value">${gameState.score}</div><div class="score-label">Points</div></div></div><div class="quiz-sentence" style="cursor:pointer" id="ql">🔊 ${correct.pt}</div></div><div class="animal-options">${opts.map(o => `<button class="option-btn" data-pt="${o.pt}">${o.fr}</button>`).join('')}</div>`;
|
|
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 = `<div class="game-header"><h3>Relie les mots</h3></div><div class="drag-items">${shuffled.map(p => `<div class="drag-item" data-pt="${p.pt}">${p.pt}</div>`).join('')}</div>${pairs.map(p => `<div class="drop-zone-label">🇫🇷 ${p.fr}</div><div class="drop-zone" data-expected="${p.pt}"><span class="drop-zone-word">Appuie ici</span></div>`).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 = `<span class="dropped-word">✅ ${pt}</span>`;
|
|
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 = `<div class="game-header"><h3>Terminé ! 🎉 Score : ${gameState.score}/${words.length * 10}</h3></div>`;
|
|
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 = `
|
|
<div class="game-header">
|
|
<h3>🔤 Déchiffre le mot !</h3>
|
|
<div class="game-score"><div class="score-item"><div class="score-value">${gameState.score}</div><div class="score-label">Points</div></div></div>
|
|
<div style="font-size:64px;margin:12px auto">${w.emoji}</div>
|
|
<p style="color:var(--text-light);font-size:14px">${w.fr}</p>
|
|
</div>
|
|
<div class="scramble-display">${display.toUpperCase().split('').map((l,i) => `<span class="scramble-letter">${l}</span>`).join('')}</div>
|
|
<input class="exercise-input scramble-input" id="scramble-input" placeholder="Écris le mot..." autocomplete="off" autocapitalize="none">
|
|
<button class="primary-btn" id="scramble-check" style="max-width:200px;margin:12px auto">Vérifier</button>
|
|
<p id="scramble-feedback"></p>
|
|
`;
|
|
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 = `<span style="color:var(--green)">✅ ${w.word} = ${w.fr}</span>`;
|
|
SpeechEngine.speakPT(w.word);
|
|
celebrate();
|
|
} else {
|
|
fb.innerHTML = `<span style="color:var(--red)">❌ C'était "${w.word}"</span>`;
|
|
}
|
|
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 = `<div class="game-header"><h3>Bingo terminé ! 🎤🎉 Score : ${bingoScore}/${totalRounds}</h3></div>`;
|
|
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 = `
|
|
<div class="game-header">
|
|
<h3>🎤 Bingo - ${bingoRound}/${totalRounds}</h3>
|
|
<div class="game-score"><div class="score-item"><div class="score-value">${bingoScore}</div><div class="score-label">Trouvés</div></div></div>
|
|
<button class="primary-btn bingo-listen-btn" id="bingo-listen" style="margin:16px auto;font-size:48px;width:80px;height:80px;border-radius:50%">🔊</button>
|
|
<p style="color:var(--text-light)">Appuie pour écouter !</p>
|
|
</div>
|
|
<div class="bingo-grid">${opts.map(o => `
|
|
<button class="bingo-card" data-pt="${o.pt}">
|
|
<span class="bingo-emoji">${o.emoji}</span>
|
|
<span class="bingo-word">${o.pt}</span>
|
|
</button>
|
|
`).join('')}</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="game-header">
|
|
<h3>🖐️ Apanha as palavras !</h3>
|
|
<div class="game-score">
|
|
<div class="score-item"><div class="score-value" id="catch-caught">0</div><div class="score-label">Apanhados</div></div>
|
|
<div class="score-item"><div class="score-value" id="catch-timer">${catchDuration}s</div><div class="score-label">Tempo</div></div>
|
|
<div class="score-item"><div class="score-value" id="catch-missed">0</div><div class="score-label">Perdidos</div></div>
|
|
</div>
|
|
</div>
|
|
<div class="catch-zone" id="catch-zone"></div>
|
|
`;
|
|
|
|
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 = `<div class="game-header"><h3>Tempo esgotado! ⏰</h3><p>Apanhaste ${caught} palavras!</p></div>`;
|
|
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 = `<span class="catch-emoji">${w.emoji}</span><span class="catch-text">${w.pt}</span>`;
|
|
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 = `<div class="game-header"><h3>Terminé ! 🤔🎉 Score : ${gameState.score}/${riddles.length * 10}</h3></div>`;
|
|
addPoints(gameState.score, 'Riddle', 'game');
|
|
return;
|
|
}
|
|
const r = riddles[idx];
|
|
const opts = [...r.options].sort(() => Math.random() - 0.5);
|
|
area.innerHTML = `
|
|
<div class="game-header">
|
|
<h3>🤔 O que é?</h3>
|
|
<div class="game-score"><div class="score-item"><div class="score-value">${gameState.score}</div><div class="score-label">Points</div></div></div>
|
|
<div style="font-size:72px;margin:16px auto">${r.emoji}</div>
|
|
</div>
|
|
<div class="riddle-card">
|
|
<p class="riddle-text">"${r.riddle}"</p>
|
|
<p class="riddle-fr" style="font-size:13px;color:var(--text-light);font-style:italic">${r.fr}</p>
|
|
</div>
|
|
<div class="riddle-options">${opts.map(o => `<button class="option-btn riddle-opt" data-val="${o}">${o}</button>`).join('')}</div>
|
|
`;
|
|
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 = `<div class="game-header"><h3>Terminé ! Score : ${currentExercise.score}/${EXERCISES_FILL.length}</h3></div>`; exerciseDone(); return; }
|
|
const ex = EXERCISES_FILL[idx];
|
|
const parts = ex.template.split('___');
|
|
area.innerHTML = `<div class="game-header"><div class="progress-bar"><div class="progress-fill" style="width:${(idx / EXERCISES_FILL.length) * 100}%"></div></div><h3>Complète la phrase</h3><p class="exercise-hint">${ex.hint}</p><p class="exercise-question">${parts[0]}<span class="fill-blank" id="blank">____</span>${parts[1] || ''}</p></div><div class="fill-options">${[...ex.options].sort(() => Math.random() - 0.5).map(o => `<button class="fill-word" data-word="${o}">${o}</button>`).join('')}</div>`;
|
|
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 = `<div class="game-header"><h3>Terminé ! Score : ${currentExercise.score}/${EXERCISES_LISTEN.length}</h3></div>`; exerciseDone(); return; }
|
|
const ex = EXERCISES_LISTEN[idx];
|
|
area.innerHTML = `<div class="game-header"><div class="progress-bar"><div class="progress-fill" style="width:${(idx / EXERCISES_LISTEN.length) * 100}%"></div></div><h3>Écoute et écris</h3><p class="exercise-hint">${ex.fr}</p><button class="primary-btn" id="lw-listen" style="max-width:200px;margin:12px auto">🔊 Écouter</button><input class="exercise-input" id="lw-input" placeholder="Écris ici..." autocomplete="off" autocapitalize="none"><button class="primary-btn" id="lw-check" style="max-width:200px;margin:12px auto">Vérifier</button><p id="lw-feedback"></p></div>`;
|
|
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 = `<span style="color:var(--green)">✅ ${ex.pt}</span>`; SpeechEngine.speakPT(ex.pt); }
|
|
else { fb.innerHTML = `<span style="color:var(--red)">❌ ${ex.pt}</span>`; }
|
|
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 = `<div class="game-header"><h3>Terminé ! Score : ${currentExercise.score}/${EXERCISES_TRANSLATE.length}</h3></div>`; exerciseDone(); return; }
|
|
const ex = EXERCISES_TRANSLATE[idx];
|
|
area.innerHTML = `<div class="game-header"><div class="progress-bar"><div class="progress-fill" style="width:${(idx / EXERCISES_TRANSLATE.length) * 100}%"></div></div><h3>Traduis en portugais</h3><p class="exercise-question">${ex.fr}</p><input class="exercise-input" id="tr-input" placeholder="Ta traduction..." autocomplete="off" autocapitalize="none"><button class="primary-btn" id="tr-check" style="max-width:200px;margin:12px auto">Vérifier</button><p id="tr-feedback"></p></div>`;
|
|
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 = `<span style="color:var(--green)">✅ ${ex.pt}</span>`; SpeechEngine.speakPT(ex.pt); }
|
|
else { fb.innerHTML = `<span style="color:var(--red)">❌ ${ex.pt}</span>`; }
|
|
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 = `<div class="game-header"><h3>Terminé ! Score : ${currentExercise.score}/${CONJUGATIONS.length}</h3></div>`; exerciseDone(); return; }
|
|
const conj = CONJUGATIONS[idx];
|
|
const rp = pronouns[Math.floor(Math.random() * pronouns.length)];
|
|
const correct = conj.forms[rp];
|
|
area.innerHTML = `<div class="game-header"><div class="progress-bar"><div class="progress-fill" style="width:${(idx / CONJUGATIONS.length) * 100}%"></div></div><h3>Conjugue "${conj.verb}"</h3><p class="exercise-hint">${conj.meaning}</p><p class="exercise-question">${rp} ...?</p><input class="exercise-input" id="conj-input" placeholder="Ta réponse..." autocomplete="off" autocapitalize="none"><button class="primary-btn" id="conj-check" style="max-width:200px;margin:12px auto">Vérifier</button><p id="conj-feedback"></p></div>`;
|
|
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 = `<span style="color:var(--green)">✅ ${rp} ${correct}</span>`; SpeechEngine.speakPT(`${rp} ${correct}`); }
|
|
else { fb.innerHTML = `<span style="color:var(--red)">❌ ${rp} ${correct}</span><br><span style="font-size:13px;color:var(--text-light)">${pronouns.map(p => `${p} ${conj.forms[p]}`).join(', ')}</span>`; }
|
|
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 =>
|
|
`<button class="cat-tab${k === currentVocabCategory ? ' active' : ''}" data-cat="${k}">${k}</button>`
|
|
).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 => `<div class="vocab-card" data-word="${v.pt}"><div class="vocab-emoji">${v.emoji}</div><div class="vocab-word">${v.pt}</div><div class="vocab-translation">${v.fr}</div></div>`).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 => `
|
|
<div class="admin-user-card">
|
|
<div class="admin-user-info">
|
|
<span class="admin-avatar">${u.avatar}</span>
|
|
<div>
|
|
<strong>${u.display_name}</strong>
|
|
<small>@${u.username} ${u.is_admin ? '👑' : ''}</small>
|
|
</div>
|
|
</div>
|
|
<div class="admin-user-actions">
|
|
<span class="admin-points">⭐ ${u.total_points}</span>
|
|
<button class="admin-btn-toggle" data-id="${u.id}">${u.is_admin ? 'Retirer admin' : 'Rendre admin'}</button>
|
|
${u.id !== user.id ? `<button class="admin-btn-delete" data-id="${u.id}">🗑️</button>` : ''}
|
|
</div>
|
|
</div>
|
|
`).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(); }
|