Initial commit: Portugais App
This commit is contained in:
+847
@@ -0,0 +1,847 @@
|
||||
// ===== 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(); }
|
||||
+851
@@ -0,0 +1,851 @@
|
||||
// ===== LEVELS =====
|
||||
const LEVELS = {
|
||||
1: { name: "Débutant", minPoints: 0, color: "#4CAF50", icon: "🌱" },
|
||||
2: { name: "Élémentaire", minPoints: 200, color: "#2196F3", icon: "📘" },
|
||||
3: { name: "Intermédiaire", minPoints: 600, color: "#FF9800", icon: "📙" },
|
||||
4: { name: "Avancé", minPoints: 1500, color: "#E91E63", icon: "📕" },
|
||||
};
|
||||
|
||||
function getUserLevel(points) {
|
||||
let lvl = 1;
|
||||
for (const [k, v] of Object.entries(LEVELS)) { if (points >= v.minPoints) lvl = parseInt(k); }
|
||||
return lvl;
|
||||
}
|
||||
|
||||
function getLevelInfo(lvl) { return LEVELS[lvl] || LEVELS[1]; }
|
||||
|
||||
// ===== PHRASES PAR NIVEAU =====
|
||||
const PHRASES = {
|
||||
"Salutations": [
|
||||
{ pt: "Olá", fr: "Bonjour", phonetic: "oh-LAH", level: 1 },
|
||||
{ pt: "Bom dia", fr: "Bonjour (matin)", phonetic: "bom DEE-ah", level: 1 },
|
||||
{ pt: "Boa tarde", fr: "Bonjour (après-midi)", phonetic: "BOH-ah TAR-deh", level: 1 },
|
||||
{ pt: "Boa noite", fr: "Bonsoir / Bonne nuit", phonetic: "BOH-ah NOY-teh", level: 1 },
|
||||
{ pt: "Adeus", fr: "Au revoir", phonetic: "ah-DEUSH", level: 1 },
|
||||
{ pt: "Até logo", fr: "À bientôt", phonetic: "ah-TEH LOH-goo", level: 1 },
|
||||
{ pt: "Até amanhã", fr: "À demain", phonetic: "ah-TEH ah-mahn-YAHN", level: 1 },
|
||||
{ pt: "Como vai?", fr: "Comment vas-tu ?", phonetic: "KOH-moh VAI", level: 1 },
|
||||
{ pt: "Tudo bem?", fr: "Comment vas-tu ?", phonetic: "TOO-doh beng", level: 1 },
|
||||
{ pt: "Tudo bem!", fr: "Tout va bien !", phonetic: "TOO-doh beng", level: 1 },
|
||||
{ pt: "Tudo bom?", fr: "Tout va bien ?", phonetic: "TOO-doh bom", level: 1 },
|
||||
{ pt: "Prazer", fr: "Enchanté", phonetic: "prah-ZEHR", level: 1 },
|
||||
{ pt: "Muito prazer", fr: "Très enchanté", phonetic: "MWEEN-too prah-ZEHR", level: 1 },
|
||||
{ pt: "Bom dia, como está?", fr: "Bonjour, comment allez-vous ?", phonetic: "bom DEE-ah koh-moo eshtah", level: 2 },
|
||||
{ pt: "Olá, tudo bem consigo?", fr: "Bonjour, allez-vous bien ?", phonetic: "oh-LAH too-doh beng kohn-SEE-goo", level: 2 },
|
||||
{ pt: "Como passaste?", fr: "Comment as-tu passé ?", phonetic: "koh-moh pah-SAHS-teh", level: 2 },
|
||||
{ pt: "Foi um prazer conhecê-lo", fr: "Ce fut un plaisir de vous rencontrer", phonetic: "foy oom prah-ZEHR koh-neh-SHEH-loo", level: 3 },
|
||||
{ pt: "Espero que esteja bem", fr: "J'espère que vous allez bien", phonetic: "eh-SPEH-roo keh esh-TEH-zhah beng", level: 3 },
|
||||
{ pt: "Cumprimentos da minha parte", fr: "Mes salutations", phonetic: "koom-pree-MEN-too dash MEH-nyah PAR-teh", level: 4 },
|
||||
],
|
||||
"Famille": [
|
||||
{ pt: "Família", fr: "Famille", phonetic: "fah-MEE-lee-ah", level: 1 },
|
||||
{ pt: "Mãe", fr: "Maman", phonetic: "meng", level: 1 },
|
||||
{ pt: "Pai", fr: "Papa", phonetic: "pai", level: 1 },
|
||||
{ pt: "Filho", fr: "Fils", phonetic: "FEE-lyoo", level: 1 },
|
||||
{ pt: "Filha", fr: "Fille", phonetic: "FEE-lyah", level: 1 },
|
||||
{ pt: "Irmão", fr: "Frère", phonetic: "eer-MAHNG", level: 1 },
|
||||
{ pt: "Irmã", fr: "Sœur", phonetic: "eer-MAHN", level: 1 },
|
||||
{ pt: "Avô", fr: "Grand-père", phonetic: "ah-VOH", level: 1 },
|
||||
{ pt: "Avó", fr: "Grand-mère", phonetic: "ah-VOH", level: 1 },
|
||||
{ pt: "Bebé", fr: "Bébé", phonetic: "beh-BEH", level: 1 },
|
||||
{ pt: "Amor", fr: "Amour", phonetic: "ah-MOHR", level: 1 },
|
||||
{ pt: "Tio", fr: "Oncle", phonetic: "TOO", level: 2 },
|
||||
{ pt: "Tia", fr: "Tante", phonetic: "TEE-ah", level: 2 },
|
||||
{ pt: "Primo", fr: "Cousin", phonetic: "PREE-moo", level: 2 },
|
||||
{ pt: "Prima", fr: "Cousine", phonetic: "PREE-mah", level: 2 },
|
||||
{ pt: "Sobrinho", fr: "Neveu", phonetic: "soh-BREE-nyoo", level: 3 },
|
||||
{ pt: "Sobrinha", fr: "Nièce", phonetic: "soh-BREE-nyah", level: 3 },
|
||||
{ pt: "Genro", fr: "Beau-fils", phonetic: "ZHEN-roo", level: 3 },
|
||||
{ pt: "Nora", fr: "Belle-fille", phonetic: "NOH-rah", level: 3 },
|
||||
{ pt: "Padrasto", fr: "Beau-père (beau-père)", phonetic: "pah-DRAHS-too", level: 4 },
|
||||
{ pt: "Madrasta", fr: "Belle-mère (belle-mère)", phonetic: "mah-DRAHS-tah", level: 4 },
|
||||
{ pt: "Enteado", fr: "Beau-fils (fils du conjoint)", phonetic: "en-teh-AH-doo", level: 4 },
|
||||
{ pt: "Padrinho", fr: "Parrain", phonetic: "pah-DREE-nyoo", level: 3 },
|
||||
{ pt: "Madrinha", fr: "Marraine", phonetic: "mah-DREE-nyah", level: 3 },
|
||||
],
|
||||
"Nourriture": [
|
||||
{ pt: "Água", fr: "Eau", phonetic: "AH-goo-ah", level: 1 },
|
||||
{ pt: "Pão", fr: "Pain", phonetic: "pahng", level: 1 },
|
||||
{ pt: "Leite", fr: "Lait", phonetic: "LAY-teh", level: 1 },
|
||||
{ pt: "Café", fr: "Café", phonetic: "kah-FEH", level: 1 },
|
||||
{ pt: "Arroz", fr: "Riz", phonetic: "ah-HOHSH", level: 1 },
|
||||
{ pt: "Fruta", fr: "Fruit", phonetic: "FROO-tah", level: 1 },
|
||||
{ pt: "Carne", fr: "Viande", phonetic: "KAR-neh", level: 1 },
|
||||
{ pt: "Peixe", fr: "Poisson", phonetic: "PAY-sheh", level: 1 },
|
||||
{ pt: "Queijo", fr: "Fromage", phonetic: "KAY-zhoo", level: 1 },
|
||||
{ pt: "Sopa", fr: "Soupe", phonetic: "SOH-pah", level: 1 },
|
||||
{ pt: "Bom apetite!", fr: "Bon appétit !", phonetic: "bom ah-peh-TEE-teh", level: 1 },
|
||||
{ pt: "Manteiga", fr: "Beurre", phonetic: "man-TAY-gah", level: 2 },
|
||||
{ pt: "Ovos", fr: "Œufs", phonetic: "OH-vohsh", level: 2 },
|
||||
{ pt: "Batatas", fr: "Pommes de terre", phonetic: "bah-TAH-tahsh", level: 2 },
|
||||
{ pt: "Cenouras", fr: "Carottes", phonetic: "seh-NOH-rahsh", level: 2 },
|
||||
{ pt: "Tomates", fr: "Tomates", phonetic: "toh-MAH-tehsh", level: 2 },
|
||||
{ pt: "Cebolas", fr: "Oignons", phonetic: "seh-BOH-lahsh", level: 2 },
|
||||
{ pt: "Alho", fr: "Ail", phonetic: "AH-lyoo", level: 2 },
|
||||
{ pt: "Sal", fr: "Sel", phonetic: "sahl", level: 2 },
|
||||
{ pt: "Açúcar", fr: "Sucre", phonetic: "ah-SOO-kahr", level: 2 },
|
||||
{ pt: "Vinho", fr: "Vin", phonetic: "VEE-nyoo", level: 3 },
|
||||
{ pt: "Cerveja", fr: "Bière", phonetic: "sehr-VEH-zhah", level: 3 },
|
||||
{ pt: "Sobremesa", fr: "Dessert", phonetic: "soh-breh-MEH-zah", level: 3 },
|
||||
{ pt: "Prato do dia", fr: "Plat du jour", phonetic: "PRAH-too doo DEE-ah", level: 3 },
|
||||
{ pt: "Conta, por favor", fr: "L'addition, s'il vous plaît", phonetic: "KOHN-tah pohr fah-VOHR", level: 2 },
|
||||
{ pt: "Em mental", fr: "À emporter", phonetic: "eng mehn-TAHL", level: 4 },
|
||||
{ pt: "Ao molho", fr: "Sauce", phonetic: "ah-oo MOH-lyoo", level: 4 },
|
||||
{ pt: "Grelhado", fr: "Grillé", phonetic: "greh-LYAH-doo", level: 3 },
|
||||
{ pt: "Cozido", fr: "Bouilli", phonetic: "koh-ZEE-doo", level: 3 },
|
||||
{ pt: "Frito", fr: "Frit", phonetic: "FREE-too", level: 3 },
|
||||
],
|
||||
"Couleurs": [
|
||||
{ pt: "Vermelho", fr: "Rouge", phonetic: "vehr-MEH-lyoo", level: 1 },
|
||||
{ pt: "Azul", fr: "Bleu", phonetic: "ah-ZOOL", level: 1 },
|
||||
{ pt: "Verde", fr: "Vert", phonetic: "VEHR-deh", level: 1 },
|
||||
{ pt: "Amarelo", fr: "Jaune", phonetic: "ah-mah-REH-loh", level: 1 },
|
||||
{ pt: "Branco", fr: "Blanc", phonetic: "BRAHN-koo", level: 1 },
|
||||
{ pt: "Preto", fr: "Noir", phonetic: "PREH-too", level: 1 },
|
||||
{ pt: "Rosa", fr: "Rose", phonetic: "HOH-zah", level: 1 },
|
||||
{ pt: "Laranja", fr: "Orange", phonetic: "lah-RAHN-zhah", level: 1 },
|
||||
{ pt: "Castanho", fr: "Marron", phonetic: "kash-TAH-nyoo", level: 1 },
|
||||
{ pt: "Roxo", fr: "Violet", phonetic: "HOH-shoo", level: 1 },
|
||||
{ pt: "Cinza", fr: "Gris", phonetic: "SEEN-zah", level: 2 },
|
||||
{ pt: "Dourado", fr: "Doré", phonetic: "doh-RAH-doo", level: 2 },
|
||||
{ pt: "Prateado", fr: "Argenté", phonetic: "prah-teh-AH-doo", level: 3 },
|
||||
],
|
||||
"Animaux": [
|
||||
{ pt: "Cão", fr: "Chien", phonetic: "kahng", level: 1 },
|
||||
{ pt: "Gato", fr: "Chat", phonetic: "GAH-too", level: 1 },
|
||||
{ pt: "Pássaro", fr: "Oiseau", phonetic: "PAH-sah-hoo", level: 1 },
|
||||
{ pt: "Peixe", fr: "Poisson", phonetic: "PAY-sheh", level: 1 },
|
||||
{ pt: "Coelho", fr: "Lapin", phonetic: "koh-EH-lyoo", level: 1 },
|
||||
{ pt: "Cavalo", fr: "Cheval", phonetic: "kah-VAH-loh", level: 1 },
|
||||
{ pt: "Vaca", fr: "Vache", phonetic: "VAH-kah", level: 1 },
|
||||
{ pt: "Galinha", fr: "Poule", phonetic: "gah-LEE-nyah", level: 1 },
|
||||
{ pt: "Borboleta", fr: "Papillon", phonetic: "bohr-boh-LEH-tah", level: 1 },
|
||||
{ pt: "Formiga", fr: "Fourmi", phonetic: "fohr-MEE-gah", level: 1 },
|
||||
{ pt: "Tartaruga", fr: "Tortue", phonetic: "tar-tah-ROO-gah", level: 1 },
|
||||
{ pt: "Macaco", fr: "Singe", phonetic: "mah-KAH-koo", level: 1 },
|
||||
{ pt: "Elefante", fr: "Éléphant", phonetic: "eh-leh-FAHN-teh", level: 1 },
|
||||
{ pt: "Leão", fr: "Lion", phonetic: "leh-AHNG", level: 1 },
|
||||
{ pt: "Urso", fr: "Ours", phonetic: "OOR-soo", level: 2 },
|
||||
{ pt: "Rã", fr: "Grenouille", phonetic: "rahn", level: 2 },
|
||||
{ pt: "Ovelha", fr: "Mouton", phonetic: "oh-VEH-lyah", level: 2 },
|
||||
{ pt: "Porco", fr: "Cochon", phonetic: "POHR-koo", level: 2 },
|
||||
{ pt: "Pato", fr: "Canard", phonetic: "PAH-too", level: 2 },
|
||||
{ pt: "Golfinho", fr: "Dauphin", phonetic: "gohl-FEE-nyoo", level: 3 },
|
||||
{ pt: "Girafa", fr: "Girafe", phonetic: "zhee-RAH-fah", level: 3 },
|
||||
{ pt: "Zebra", fr: "Zèbre", phonetic: "ZEH-brah", level: 3 },
|
||||
{ pt: "Crocodilo", fr: "Crocodile", phonetic: "koh-koh-DEE-loo", level: 3 },
|
||||
{ pt: "Raposa", fr: "Renard", phonetic: "rah-POH-zah", level: 3 },
|
||||
{ pt: "Lobo", fr: "Loup", phonetic: "LOH-boo", level: 3 },
|
||||
{ pt: "Águia", fr: "Aigle", phonetic: "AH-gee-ah", level: 4 },
|
||||
{ pt: "Coruja", fr: "Hibou", phonetic: "koh-ROO-zhah", level: 4 },
|
||||
{ pt: "Lagarta", fr: "Chenille", phonetic: "lah-GAR-tah", level: 4 },
|
||||
],
|
||||
"Nombres": [
|
||||
{ pt: "Um", fr: "Un", phonetic: "oom", level: 1 },
|
||||
{ pt: "Dois", fr: "Deux", phonetic: "doysh", level: 1 },
|
||||
{ pt: "Três", fr: "Trois", phonetic: "trehsh", level: 1 },
|
||||
{ pt: "Quatro", fr: "Quatre", phonetic: "KWAH-troo", level: 1 },
|
||||
{ pt: "Cinco", fr: "Cinq", phonetic: "SEEN-koo", level: 1 },
|
||||
{ pt: "Seis", fr: "Six", phonetic: "saysh", level: 1 },
|
||||
{ pt: "Sete", fr: "Sept", phonetic: "SEH-teh", level: 1 },
|
||||
{ pt: "Oito", fr: "Huit", phonetic: "OY-too", level: 1 },
|
||||
{ pt: "Nove", fr: "Neuf", phonetic: "NOH-veh", level: 1 },
|
||||
{ pt: "Dez", fr: "Dix", phonetic: "dehsh", level: 1 },
|
||||
{ pt: "Onze", fr: "Onze", phonetic: "OHN-zeh", level: 2 },
|
||||
{ pt: "Doze", fr: "Douze", phonetic: "DOH-zeh", level: 2 },
|
||||
{ pt: "Treze", fr: "Treize", phonetic: "TREH-zeh", level: 2 },
|
||||
{ pt: "Catorze", fr: "Quatorze", phonetic: "kah-TOR-zeh", level: 2 },
|
||||
{ pt: "Quinze", fr: "Quinze", phonetic: "KEEN-zeh", level: 2 },
|
||||
{ pt: "Dezanove", fr: "Dix-neuf", phonetic: "deh-zah-NOH-veh", level: 2 },
|
||||
{ pt: "Vinte", fr: "Vingt", phonetic: "VEEN-teh", level: 1 },
|
||||
{ pt: "Trinta", fr: "Trente", phonetic: "TREEN-tah", level: 2 },
|
||||
{ pt: "Quarenta", fr: "Quarante", phonetic: "kwah-REHN-tah", level: 2 },
|
||||
{ pt: "Cinquenta", fr: "Cinquante", phonetic: "seen-KWEN-tah", level: 2 },
|
||||
{ pt: "Sessenta", fr: "Soixante", phonetic: "seh-SEN-tah", level: 2 },
|
||||
{ pt: "Setenta", fr: "Soixante-dix", phonetic: "seh-TEN-tah", level: 2 },
|
||||
{ pt: "Oitenta", fr: "Quatre-vingts", phonetic: "oy-TEN-tah", level: 2 },
|
||||
{ pt: "Noventa", fr: "Quatre-vingt-dix", phonetic: "noh-VEN-tah", level: 2 },
|
||||
{ pt: "Cem", fr: "Cent", phonetic: "seng", level: 1 },
|
||||
{ pt: "Duzentos", fr: "Deux cents", phonetic: "doo-ZEN-toosh", level: 3 },
|
||||
{ pt: "Trezentos", fr: "Trois cents", phonetic: "treh-ZEN-toosh", level: 3 },
|
||||
{ pt: "Mil", fr: "Mille", phonetic: "meel", level: 1 },
|
||||
{ pt: "Milhão", fr: "Un million", phonetic: "mee-LYAHNG", level: 3 },
|
||||
],
|
||||
"Politesse": [
|
||||
{ pt: "Obrigado", fr: "Merci (homme)", phonetic: "oh-bree-GAH-doo", level: 1 },
|
||||
{ pt: "Obrigada", fr: "Merci (femme)", phonetic: "oh-bree-GAH-dah", level: 1 },
|
||||
{ pt: "De nada", fr: "De rien", phonetic: "deh NAH-dah", level: 1 },
|
||||
{ pt: "Por favor", fr: "S'il vous plaît", phonetic: "pohr fah-VOHR", level: 1 },
|
||||
{ pt: "Desculpe", fr: "Excusez-moi", phonetic: "desh-KOOL-peh", level: 1 },
|
||||
{ pt: "Com licença", fr: "Avec votre permission", phonetic: "kong lee-SEN-sah", level: 1 },
|
||||
{ pt: "Sim", fr: "Oui", phonetic: "seeng", level: 1 },
|
||||
{ pt: "Não", fr: "Non", phonetic: "nahng", level: 1 },
|
||||
{ pt: "Com certeza", fr: "Certainement", phonetic: "kong sehr-TEH-zah", level: 1 },
|
||||
{ pt: "Claro", fr: "Bien sûr", phonetic: "KLAH-roo", level: 1 },
|
||||
{ pt: "Comprehendo", fr: "Je comprends", phonetic: "kohm-preh-EN-doo", level: 2 },
|
||||
{ pt: "Não compreendo", fr: "Je ne comprends pas", phonetic: "nahng kohm-preh-EN-doo", level: 2 },
|
||||
{ pt: "Pode falar devagar?", fr: "Pouvez-vous parler plus lentement ?", phonetic: "POH-deh fah-LAR deh-vah-GAR", level: 2 },
|
||||
{ pt: "Não falo português", fr: "Je ne parle pas portugais", phonetic: "nahng FAH-loo pohr-too-GAYSH", level: 1 },
|
||||
{ pt: "Fala inglês?", fr: "Parles-tu anglais ?", phonetic: "FAH-lah een-GLAYSH", level: 1 },
|
||||
{ pt: "Tem água?", fr: "Avez-vous de l'eau ?", phonetic: "teng AH-goo-ah", level: 2 },
|
||||
{ pt: "Pode ajudar-me?", fr: "Pouvez-vous m'aider ?", phonetic: "POH-deh ah-zhoo-DAR-meh", level: 2 },
|
||||
{ pt: "Com muito gusto", fr: "Avec beaucoup de plaisir", phonetic: "kong MWEEN-too GOOSH-too", level: 3 },
|
||||
{ pt: "Agradeço imenso", fr: "Je vous remercie énormément", phonetic: "ah-grah-DEH-soo een-MEN-soo", level: 3 },
|
||||
{ pt: "Não se incomode", fr: "Ne vous dérangez pas", phonetic: "nahng seh een-KOH-meh-deh", level: 4 },
|
||||
],
|
||||
"Phrases utiles": [
|
||||
{ pt: "Como te chamas?", fr: "Comment tu t'appelles ?", phonetic: "KOH-moo teh SHAH-mahsh", level: 1 },
|
||||
{ pt: "Chamo-me...", fr: "Je m'appelle...", phonetic: "SHAH-moo meh", level: 1 },
|
||||
{ pt: "De onde és?", fr: "D'où es-tu ?", phonetic: "deh OHN-deh ehsh", level: 1 },
|
||||
{ pt: "Sou francês", fr: "Je suis français", phonetic: "soh frahn-SESH", level: 1 },
|
||||
{ pt: "Sou francesa", fr: "Je suis française", phonetic: "soh frahn-SEH-zah", level: 1 },
|
||||
{ pt: "Onde fica...?", fr: "Où est... ?", phonetic: "OHN-deh FEE-kah", level: 1 },
|
||||
{ pt: "Quanto custa?", fr: "Combien ça coûte ?", phonetic: "KWAHN-too KOOSH-tah", level: 1 },
|
||||
{ pt: "A conta, por favor", fr: "L'addition, s'il vous plaît", phonetic: "ah KOHN-tah pohr fah-VOHR", level: 1 },
|
||||
{ pt: "Não percebo", fr: "Je ne comprends pas", phonetic: "nahng pehr-SEH-boo", level: 1 },
|
||||
{ pt: "Pode repetir?", fr: "Pouvez-vous répéter ?", phonetic: "POH-deh heh-peh-TEER", level: 1 },
|
||||
{ pt: "Está delicioso!", fr: "C'est délicieux !", phonetic: "eshtah deh-lee-see-OH-zoo", level: 1 },
|
||||
{ pt: "Tenho fome", fr: "J'ai faim", phonetic: "TEH-nyoo FOH-meh", level: 1 },
|
||||
{ pt: "Tenho sede", fr: "J'ai soif", phonetic: "TEH-nyoo SEH-deh", level: 1 },
|
||||
{ pt: "Ajuda!", fr: "Au secours !", phonetic: "ah-ZHOO-dah", level: 1 },
|
||||
{ pt: "Preciso de ajuda", fr: "J'ai besoin d'aide", phonetic: "preh-SEE-zoo deh ah-ZHOO-dah", level: 1 },
|
||||
{ pt: "Onde é a casa de banho?", fr: "Où sont les toilettes ?", phonetic: "OHN-deh eh ah KAH-zah deh bahn-YOO", level: 2 },
|
||||
{ pt: "Qual é o seu nome?", fr: "Quel est votre nom ?", phonetic: "kwahl eh oh seh NOH-meh", level: 2 },
|
||||
{ pt: "Gosto muito de Portugal", fr: "J'aime beaucoup le Portugal", phonetic: "GOSH-too MWEEN-too deh pohr-too-GAHL", level: 2 },
|
||||
{ pt: "Estou a aprender português", fr: "J'apprends le portugais", phonetic: "eshtoh ah ah-prehn-DEHR pohr-too-GAYSH", level: 2 },
|
||||
{ pt: "Faz calor?", fr: "Il fait chaud ?", phonetic: "fahsh kah-LOHR", level: 2 },
|
||||
{ pt: "Está a chover", fr: "Il pleut", phonetic: "eshtah ah shoh-VEHR", level: 2 },
|
||||
{ pt: "Que horas são?", fr: "Quelle heure est-il ?", phonetic: "keh OH-rahsh sang", level: 2 },
|
||||
{ pt: "Gostaria de reservar uma mesa", fr: "Je voudrais réserver une table", phonetic: "gosh-tah-REE-ah deh heh-zehr-VAHR OO-mah MEH-zah", level: 3 },
|
||||
{ pt: "Tem menus em inglês?", fr: "Avez-vous un menu en anglais ?", phonetic: "teng meh-NOOSH een-GLAYSH", level: 3 },
|
||||
{ pt: "Não tenho urgentemente isso", fr: "Je n'ai pas besoin de ça tout de suite", phonetic: "nahng TEH-nyoo oo-ZHEN-teh-MEN-teh EE-soo", level: 3 },
|
||||
{ pt: "Poderia mostrar-me o caminho?", fr: "Pourriez-vous me montrer le chemin ?", phonetic: "poh-deh-REE-ah moh-SHRAR-meh oh kah-MEE-nyoo", level: 3 },
|
||||
{ pt: "Estou perdido", fr: "Je suis perdu", phonetic: "eshtoh pehr-DEE-doo", level: 2 },
|
||||
{ pt: "Quanto tempo faz que mora aqui?", fr: "Depuis combien de temps habitez-vous ici ?", phonetic: "KWAHN-too TEN-poo fahsh keh MOH-rah ah-KEE", level: 4 },
|
||||
{ pt: "Adoraria visitar o Algarve", fr: "J'adorerais visiter l'Algarve", phonetic: "ah-doh-rah-REE-ah vee-zee-TAR oh AHL-gar-veh", level: 4 },
|
||||
{ pt: "Portugal é um país maravilhoso", fr: "Le Portugal est un pays merveilleux", phonetic: "pohr-too-GAHL eh oom pah-EESH mah-rah-VEE-lyoo-zoo", level: 3 },
|
||||
],
|
||||
"Transport & Direction": [
|
||||
{ pt: "Onde fica a estação de comboio?", fr: "Où est la gare ?", phonetic: "OHN-deh FEE-kah ah eshtah-SAHNG deh kohm-BOY-oo", level: 2 },
|
||||
{ pt: "Quero ir para Lisboa", fr: "Je veux aller à Lisbonne", phonetic: "KEH-roo eer PAH-rah lee-SHBO-ah", level: 2 },
|
||||
{ pt: "Quanto custa um bilhete?", fr: "Combien coûte un billet ?", phonetic: "KWAHN-too KOOSH-tah oom bee-LYEHT", level: 2 },
|
||||
{ pt: "Gire à direita", fr: "Tournez à droite", phonetic: "ZHEE-reh ah dee-RAY-tah", level: 2 },
|
||||
{ pt: "Gire à esquerda", fr: "Tournez à gauche", phonetic: "ZHEE-reh ah esh-KEHR-dah", level: 2 },
|
||||
{ pt: "Em frente", fr: "Tout droit", phonetic: "eng FREHN-teh", level: 2 },
|
||||
{ pt: "Virar à esquerda", fr: "Tourner à gauche", phonetic: "vee-RAR ah esh-KEHR-dah", level: 2 },
|
||||
{ pt: "Virar à direita", fr: "Tourner à droite", phonetic: "vee-RAR ah dee-RAY-tah", level: 2 },
|
||||
{ pt: "É muito longe?", fr: "C'est loin ?", phonetic: "eh MWEEN-too LOHN-zheh", level: 2 },
|
||||
{ pt: "É perto daqui", fr: "C'est près d'ici", phonetic: "eh PEHR-too dah-KEE", level: 2 },
|
||||
{ pt: "Preciso de um táxi", fr: "J'ai besoin d'un taxi", phonetic: "preh-SEE-zoo deh oom TAK-see", level: 2 },
|
||||
{ pt: "Onde posso estacionar?", fr: "Où puis-je me garer ?", phonetic: "OHN-deh POH-soo eshtah-see-oh-NAR", level: 3 },
|
||||
{ pt: "Qual é a melhor rota?", fr: "Quelle est la meilleure route ?", phonetic: "kwahl eh ah meh-LYOHR HOH-tah", level: 3 },
|
||||
{ pt: "Estou com o carro avariado", fr: "Ma voiture est en panne", phonetic: "eshtoh koh-oo KAH-hoo ah-vah-ree-AH-doo", level: 3 },
|
||||
{ pt: "Preciso de ajuda mecânica", fr: "J'ai besoin d'aide mécanique", phonetic: "preh-SEE-zoo deh ah-ZHOO-dah meh-KAH-nee-kah", level: 3 },
|
||||
],
|
||||
"Maison & Vocabulaire": [
|
||||
{ pt: "Casa", fr: "Maison", phonetic: "KAH-zah", level: 1 },
|
||||
{ pt: "Cozinha", fr: "Cuisine", phonetic: "koh-ZEE-nyah", level: 1 },
|
||||
{ pt: "Quarto", fr: "Chambre", phonetic: "KWAHR-too", level: 1 },
|
||||
{ pt: "Casa de banho", fr: "Salle de bain", phonetic: "KAH-zah deh bahn-YOO", level: 1 },
|
||||
{ pt: "Sala", fr: "Salon", phonetic: "SAH-lah", level: 1 },
|
||||
{ pt: "Jardim", fr: "Jardin", phonetic: "zhar-JEEN", level: 1 },
|
||||
{ pt: "Porta", fr: "Porte", phonetic: "POHR-tah", level: 1 },
|
||||
{ pt: "Janela", fr: "Fenêtre", phonetic: "zhah-NEH-lah", level: 1 },
|
||||
{ pt: "Chave", fr: "Clé", phonetic: "SHAH-veh", level: 2 },
|
||||
{ pt: "Mesa", fr: "Table", phonetic: "MEH-zah", level: 1 },
|
||||
{ pt: "Cadeira", fr: "Chaise", phonetic: "kah-DAY-rah", level: 1 },
|
||||
{ pt: "Cama", fr: "Lit", phonetic: "KAH-mah", level: 1 },
|
||||
{ pt: "Sofá", fr: "Canapé", phonetic: "soh-FAH", level: 2 },
|
||||
{ pt: "Televisão", fr: "Télévision", phonetic: "teh-leh-vee-ZAHNG", level: 2 },
|
||||
{ pt: "Frigorífico", fr: "Réfrigérateur", phonetic: "free-goh-REE-fee-koo", level: 3 },
|
||||
{ pt: "Máquina de lavar", fr: "Machine à laver", phonetic: "MAH-kee-nah deh lah-VAHR", level: 3 },
|
||||
{ pt: "Aspirador", fr: "Aspirateur", phonetic: "ah-spee-rah-DOHR", level: 3 },
|
||||
{ pt: "Tomada", fr: "Prise électrique", phonetic: "toh-MAH-dah", level: 2 },
|
||||
{ pt: "Interruptor", fr: "Interrupteur", phonetic: "een-teh-rrup-TOHR", level: 3 },
|
||||
{ pt: "Escadas", fr: "Escaliers", phonetic: "ehsh-KAH-dahsh", level: 2 },
|
||||
{ pt: "Varanda", fr: "Balcon", phonetic: "vah-RAHN-dah", level: 3 },
|
||||
{ pt: "Garagem", fr: "Garage", phonetic: "gah-RAH-zheng", level: 3 },
|
||||
{ pt: "Arroz", fr: "Riz", phonetic: "ah-HOHSH", level: 1 },
|
||||
{ pt: "Sala de estar", fr: "Pièce de vie", phonetic: "SAH-lah deh esh-TAR", level: 2 },
|
||||
],
|
||||
"Vêtements": [
|
||||
{ pt: "Camisa", fr: "Chemise", phonetic: "kah-MEE-zah", level: 1 },
|
||||
{ pt: "Calças", fr: "Pantalon", phonetic: "KAHL-sahsh", level: 1 },
|
||||
{ pt: "Vestido", fr: "Robe", phonetic: "vehs-TEE-doo", level: 1 },
|
||||
{ pt: "Sapatos", fr: "Chaussures", phonetic: "sah-PAH-toosh", level: 1 },
|
||||
{ pt: "Casaco", fr: "Manteau", phonetic: "kah-ZAH-koo", level: 1 },
|
||||
{ pt: "Chapéu", fr: "Chapeau", phonetic: "shah-PEH-oo", level: 1 },
|
||||
{ pt: "Meias", fr: "Chaussettes", phonetic: "MAY-ahsh", level: 1 },
|
||||
{ pt: "Cintura", fr: "Ceinture", phonetic: "seen-TOO-rah", level: 1 },
|
||||
{ pt: "Bota", fr: "Bottes", phonetic: "BOH-tah", level: 2 },
|
||||
{ pt: "Fato de banho", fr: "Maillot de bain", phonetic: "FAH-too deh bahn-YOO", level: 2 },
|
||||
{ pt: "Pijama", fr: "Pyjama", phonetic: "pee-ZHAH-mah", level: 2 },
|
||||
{ pt: "Blusa", fr: "Pull-over", phonetic: "BLOO-zah", level: 2 },
|
||||
{ pt: "Saia", fr: "Jupe", phonetic: "SAH-ee-ah", level: 2 },
|
||||
{ pt: "Ténis", fr: "Baskets", phonetic: "TEH-nish", level: 2 },
|
||||
{ pt: "Bermudas", fr: "Shorts", phonetic: "behr-MOO-dahsh", level: 2 },
|
||||
{ pt: "Gorro", fr: "Bonnet", phonetic: "GOH-hoo", level: 3 },
|
||||
{ pt: "Cachecol", fr: "Écharpe", phonetic: "kah-sheh-KOHL", level: 3 },
|
||||
{ pt: "Luvas", fr: "Gants", phonetic: "LOO-vahsh", level: 3 },
|
||||
{ pt: "Sunga", fr: "Maillot de bain (homme)", phonetic: "SOONG-gah", level: 3 },
|
||||
{ pt: "Brinco", fr: "Boucle d'oreille", phonetic: "BREEN-koo", level: 4 },
|
||||
],
|
||||
"Temps & Météo": [
|
||||
{ pt: "Está sol", fr: "Il fait soleil", phonetic: "eshtah sohl", level: 1 },
|
||||
{ pt: "Está a chover", fr: "Il pleut", phonetic: "eshtah ah shoh-VEHR", level: 1 },
|
||||
{ pt: "Está frio", fr: "Il fait froid", phonetic: "eshtah FREE-oh", level: 1 },
|
||||
{ pt: "Está quente", fr: "Il fait chaud", phonetic: "eshtah KWEN-teh", level: 1 },
|
||||
{ pt: "Está ventoso", fr: "Il fait venteux", phonetic: "eshtah ven-TOH-zoo", level: 2 },
|
||||
{ pt: "Está nublado", fr: "Il est nuageux", phonetic: "eshtah noo-BLAH-doo", level: 2 },
|
||||
{ pt: "Está nevoeiro", fr: "Il y a du brouillard", phonetic: "eshtah neh-voh-AY-roh", level: 3 },
|
||||
{ pt: "Granizo", fr: "Grêle", phonetic: "grah-NEE-zoo", level: 3 },
|
||||
{ pt: "Trovoada", fr: "Orage", phonetic: "troh-voh-AH-dah", level: 3 },
|
||||
{ pt: "Arco-íris", fr: "Arc-en-ciel", phonetic: "AR-koh-EE-rish", level: 3 },
|
||||
{ pt: "Temperatura", fr: "Température", phonetic: "ten-peh-rah-TOO-rah", level: 2 },
|
||||
{ pt: "Quente", fr: "Chaud", phonetic: "KWEN-teh", level: 1 },
|
||||
{ pt: "Frio", fr: "Froid", phonetic: "FREE-oh", level: 1 },
|
||||
{ pt: "Chuvoso", fr: "Pluvieux", phonetic: "shoo-VOH-zoo", level: 2 },
|
||||
{ pt: "Ensolarado", fr: "Ensoleillé", phonetic: "en-soh-leh-AH-doo", level: 2 },
|
||||
],
|
||||
"Corps": [
|
||||
{ pt: "Cabeça", fr: "Tête", phonetic: "kah-BEH-sah", level: 1 },
|
||||
{ pt: "Olhos", fr: "Yeux", phonetic: "OH-lyohsh", level: 1 },
|
||||
{ pt: "Boca", fr: "Bouche", phonetic: "BOH-kah", level: 1 },
|
||||
{ pt: "Mão", fr: "Main", phonetic: "mahng", level: 1 },
|
||||
{ pt: "Pé", fr: "Pied", phonetic: "peh", level: 1 },
|
||||
{ pt: "Orelha", fr: "Oreille", phonetic: "oh-REH-lyah", level: 1 },
|
||||
{ pt: "Nariz", fr: "Nez", phonetic: "nah-REESH", level: 1 },
|
||||
{ pt: "Barriga", fr: "Ventre", phonetic: "bah-REE-gah", level: 1 },
|
||||
{ pt: "Braço", fr: "Bras", phonetic: "BRAH-soo", level: 2 },
|
||||
{ pt: "Perna", fr: "Jambe", phonetic: "PEHR-nah", level: 2 },
|
||||
{ pt: "Costas", fr: "Dos", phonetic: "KOHSH-tahsh", level: 2 },
|
||||
{ pt: "Ombro", fr: "Épaule", phonetic: "OHM-broo", level: 2 },
|
||||
{ pt: "Joelho", fr: "Genou", phonetic: "zhoh-EH-lyoo", level: 2 },
|
||||
{ pt: "Dedo", fr: "Doigt", phonetic: "DEH-doo", level: 1 },
|
||||
{ pt: "Dentes", fr: "Dents", phonetic: "DEN-tehsh", level: 2 },
|
||||
{ pt: "Língua", fr: "Langue", phonetic: "LEENG-gwah", level: 2 },
|
||||
{ pt: "Pescoço", fr: "Cou", phonetic: "pesh-KOH-soo", level: 3 },
|
||||
{ pt: "Quadril", fr: "Hanche", phonetic: "kwah-DREEL", level: 4 },
|
||||
{ pt: "Tornozelo", fr: "Cheville", phonetic: "tohr-noh-ZEH-loo", level: 4 },
|
||||
{ pt: "Pulso", fr: "Poignet", phonetic: "POOL-soo", level: 3 },
|
||||
],
|
||||
"Professions": [
|
||||
{ pt: "Médico", fr: "Médecin", phonetic: "MEH-dee-koo", level: 2 },
|
||||
{ pt: "Enfermeiro", fr: "Infirmier", phonetic: "en-fehr-MAY-roh", level: 2 },
|
||||
{ pt: "Professor", fr: "Professeur", phonetic: "proh-feh-SOHR", level: 2 },
|
||||
{ pt: "Engenheiro", fr: "Ingénieur", phonetic: "en-zheh-NYAY-roh", level: 3 },
|
||||
{ pt: "Advogado", fr: "Avocat", phonetic: "ahd-voh-GAH-doo", level: 3 },
|
||||
{ pt: "Policia", fr: "Policier", phonetic: "poh-LEE-see-ah", level: 2 },
|
||||
{ pt: "Bombeiro", fr: "Pompier", phonetic: "bohm-BAY-roh", level: 2 },
|
||||
{ pt: "Cozinheiro", fr: "Cuisinier", phonetic: "koh-zee-NYAY-roh", level: 2 },
|
||||
{ pt: "Motorista", fr: "Chauffeur", phonetic: "moh-toh-REES-tah", level: 2 },
|
||||
{ pt: "Vendedor", fr: "Vendeur", phonetic: "ven-deh-DOHR", level: 3 },
|
||||
{ pt: "Jornalista", fr: "Journaliste", phonetic: "zhohr-nah-LEES-tah", level: 3 },
|
||||
{ pt: "Arquitecto", fr: "Architecte", phonetic: "ar-kee-TEHK-too", level: 4 },
|
||||
{ pt: "Dentista", fr: "Dentiste", phonetic: "den-TEES-tah", level: 2 },
|
||||
{ pt: "Farmacêutico", fr: "Pharmacien", phonetic: "far-mah-SEW-tee-koo", level: 4 },
|
||||
{ pt: "Padeiro", fr: "Boulanger", phonetic: "pah-DAY-roh", level: 3 },
|
||||
],
|
||||
"Vocabulaire Bébé (3 ans)": [
|
||||
{ pt: "Mãe", fr: "Maman", emoji: "👩", level: 1 },
|
||||
{ pt: "Pai", fr: "Papa", emoji: "👨", level: 1 },
|
||||
{ pt: "Cãozinho", fr: "Petit chien", emoji: "🐶", level: 1 },
|
||||
{ pt: "Gatinho", fr: "Petit chat", emoji: "🐱", level: 1 },
|
||||
{ pt: "Bola", fr: "Ballon", emoji: "⚽", level: 1 },
|
||||
{ pt: "Brinquedo", fr: "Jouet", emoji: "🧸", level: 1 },
|
||||
{ pt: "Boneca", fr: "Poupée", emoji: "🪆", level: 1 },
|
||||
{ pt: "Carro", fr: "Voiture", emoji: "🚗", level: 1 },
|
||||
{ pt: "Beijinho", fr: "Petit bisou", emoji: "💋", level: 1 },
|
||||
{ pt: "Eu amo-te", fr: "Je t'aime", emoji: "❤️", level: 1 },
|
||||
{ pt: "Obrigadinho", fr: "Petit merci", emoji: "🙏", level: 1 },
|
||||
{ pt: "Lindo", fr: "Joli", emoji: "✨", level: 1 },
|
||||
{ pt: "Grande", fr: "Grand", emoji: "📏", level: 1 },
|
||||
{ pt: "Pequenino", fr: "Petit", emoji: "🤏", level: 1 },
|
||||
{ pt: "Quente", fr: "Chaud", emoji: "🔥", level: 1 },
|
||||
{ pt: "Frio", fr: "Froid", emoji: "❄️", level: 1 },
|
||||
],
|
||||
"Expressions courantes": [
|
||||
{ pt: "Claro que sim", fr: "Bien sûr que oui", phonetic: "KLAH-roo keh seeng", level: 2 },
|
||||
{ pt: "Se calhar", fr: "Peut-être", phonetic: "seh kah-LYAR", level: 2 },
|
||||
{ pt: "Ena!", fr: "Waouh !", phonetic: "EH-nah", level: 2 },
|
||||
{ pt: "Fixe!", fr: "Génial !", phonetic: "FEESH", level: 2 },
|
||||
{ pt: "Bué!", fr: "Beaucoup !", phonetic: "BEH", level: 2 },
|
||||
{ pt: "Ya!", fr: "Oui (familier)", phonetic: "YAH", level: 2 },
|
||||
{ pt: "Pois é", fr: "C'est vrai", phonetic: "poysh eh", level: 3 },
|
||||
{ pt: "Olha só", fr: "Regarde", phonetic: "OH-lyah SOH", level: 2 },
|
||||
{ pt: "Queres saber?", fr: "Tu veux savoir ?", phonetic: "KEH-rehsh sah-BEHR", level: 3 },
|
||||
{ pt: "Eu sei lá", fr: "Je ne sais pas", phonetic: "eh seh LAH", level: 3 },
|
||||
{ pt: "Tem piada", fr: "C'est drôle", phonetic: "teng pee-AH-dah", level: 3 },
|
||||
{ pt: "Estou a gozar", fr: "Je plaisante", phonetic: "eshtoh ah goh-ZAR", level: 3 },
|
||||
{ pt: "Ficar a ver navios", fr: "Rester sur sa faim", phonetic: "fee-KAR ah vehr nah-VEE-ohsh", level: 4 },
|
||||
{ pt: "Meter a conversa em marcha", fr: "Lancer la conversation", phonetic: "MEH-tehr ah kohn-VEHR-sah eng MAR-shah", level: 4 },
|
||||
],
|
||||
};
|
||||
|
||||
// ===== ALL GAME DATA =====
|
||||
const COLORS_GAME = [
|
||||
{ color: "#CC0000", pt: "Vermelho", fr: "Rouge" },
|
||||
{ color: "#0066CC", pt: "Azul", fr: "Bleu" },
|
||||
{ color: "#009900", pt: "Verde", fr: "Vert" },
|
||||
{ color: "#FFCC00", pt: "Amarelo", fr: "Jaune" },
|
||||
{ color: "#FFFFFF", pt: "Branco", fr: "Blanc", border: true },
|
||||
{ color: "#1a1a2e", pt: "Preto", fr: "Noir" },
|
||||
{ color: "#FF69B4", pt: "Rosa", fr: "Rose" },
|
||||
{ color: "#FF8C00", pt: "Laranja", fr: "Orange" },
|
||||
{ color: "#8B4513", pt: "Castanho", fr: "Marron" },
|
||||
{ color: "#800080", pt: "Roxo", fr: "Violet" },
|
||||
{ color: "#808080", pt: "Cinza", fr: "Gris" },
|
||||
{ color: "#FFD700", pt: "Dourado", fr: "Doré" },
|
||||
];
|
||||
|
||||
const ANIMALS_GAME = [
|
||||
{ emoji: "🐕", pt: "Cão", fr: "Chien" },
|
||||
{ emoji: "🐈", pt: "Gato", fr: "Chat" },
|
||||
{ emoji: "🐦", pt: "Pássaro", fr: "Oiseau" },
|
||||
{ emoji: "🐟", pt: "Peixe", fr: "Poisson" },
|
||||
{ emoji: "🐇", pt: "Coelho", fr: "Lapin" },
|
||||
{ emoji: "🐴", pt: "Cavalo", fr: "Cheval" },
|
||||
{ emoji: "🐄", pt: "Vaca", fr: "Vache" },
|
||||
{ emoji: "🐔", pt: "Galinha", fr: "Poule" },
|
||||
{ emoji: "🦋", pt: "Borboleta", fr: "Papillon" },
|
||||
{ emoji: "🐜", pt: "Formiga", fr: "Fourmi" },
|
||||
{ emoji: "🐢", pt: "Tartaruga", fr: "Tortue" },
|
||||
{ emoji: "🐒", pt: "Macaco", fr: "Singe" },
|
||||
{ emoji: "🐘", pt: "Elefante", fr: "Éléphant" },
|
||||
{ emoji: "🦁", pt: "Leão", fr: "Lion" },
|
||||
{ emoji: "🐻", pt: "Urso", fr: "Ours" },
|
||||
{ emoji: "🐸", pt: "Rã", fr: "Grenouille" },
|
||||
{ emoji: "🐑", pt: "Ovelha", fr: "Mouton" },
|
||||
{ emoji: "🐷", pt: "Porco", fr: "Cochon" },
|
||||
{ emoji: "🦆", pt: "Pato", fr: "Canard" },
|
||||
{ emoji: "🐋", pt: "Baleia", fr: "Baleine" },
|
||||
{ emoji: "🦒", pt: "Girafa", fr: "Girafe" },
|
||||
{ emoji: "🐊", pt: "Crocodilo", fr: "Crocodile" },
|
||||
{ emoji: "🦊", pt: "Raposa", fr: "Renard" },
|
||||
{ emoji: "🐺", pt: "Lobo", fr: "Loup" },
|
||||
{ emoji: "🦅", pt: "Águia", fr: "Aigle" },
|
||||
{ emoji: "🦉", pt: "Coruja", fr: "Hibou" },
|
||||
];
|
||||
|
||||
const NUMBERS_GAME = [
|
||||
{ num: 1, pt: "Um", fr: "Un" }, { num: 2, pt: "Dois", fr: "Deux" },
|
||||
{ num: 3, pt: "Três", fr: "Trois" }, { num: 4, pt: "Quatro", fr: "Quatre" },
|
||||
{ num: 5, pt: "Cinco", fr: "Cinq" }, { num: 6, pt: "Seis", fr: "Six" },
|
||||
{ num: 7, pt: "Sete", fr: "Sept" }, { num: 8, pt: "Oito", fr: "Huit" },
|
||||
{ num: 9, pt: "Nove", fr: "Neuf" }, { num: 10, pt: "Dez", fr: "Dix" },
|
||||
{ num: 11, pt: "Onze", fr: "Onze" }, { num: 12, pt: "Doze", fr: "Douze" },
|
||||
{ num: 13, pt: "Treze", fr: "Treize" }, { num: 14, pt: "Catorze", fr: "Quatorze" },
|
||||
{ num: 15, pt: "Quinze", fr: "Quinze" }, { num: 20, pt: "Vinte", fr: "Vingt" },
|
||||
{ num: 30, pt: "Trinta", fr: "Trente" }, { num: 50, pt: "Cinquenta", fr: "Cinquante" },
|
||||
{ num: 100, pt: "Cem", fr: "Cent" }, { num: 1000, pt: "Mil", fr: "Mille" },
|
||||
];
|
||||
|
||||
const VOCAB = {
|
||||
"Vêtements": [
|
||||
{ pt: "Camisa", fr: "Chemise", emoji: "👔", level: 1 },
|
||||
{ pt: "Calças", fr: "Pantalon", emoji: "👖", level: 1 },
|
||||
{ pt: "Vestido", fr: "Robe", emoji: "👗", level: 1 },
|
||||
{ pt: "Sapatos", fr: "Chaussures", emoji: "👟", level: 1 },
|
||||
{ pt: "Casaco", fr: "Manteau", emoji: "🧥", level: 1 },
|
||||
{ pt: "Chapéu", fr: "Chapeau", emoji: "🎩", level: 1 },
|
||||
{ pt: "Bota", fr: "Bottes", emoji: "👢", level: 2 },
|
||||
{ pt: "Ténis", fr: "Baskets", emoji: "🏃", level: 2 },
|
||||
{ pt: "Blusa", fr: "Pull", emoji: "🧶", level: 2 },
|
||||
{ pt: "Fato de banho", fr: "Maillot de bain", emoji: "🩱", level: 2 },
|
||||
{ pt: "Pijama", fr: "Pyjama", emoji: "😴", level: 2 },
|
||||
],
|
||||
"Maison": [
|
||||
{ pt: "Casa", fr: "Maison", emoji: "🏠", level: 1 },
|
||||
{ pt: "Cozinha", fr: "Cuisine", emoji: "🍳", level: 1 },
|
||||
{ pt: "Quarto", fr: "Chambre", emoji: "🛏️", level: 1 },
|
||||
{ pt: "Casa de banho", fr: "Salle de bain", emoji: "🚿", level: 1 },
|
||||
{ pt: "Sala", fr: "Salon", emoji: "🛋️", level: 1 },
|
||||
{ pt: "Jardim", fr: "Jardin", emoji: "🌻", level: 1 },
|
||||
{ pt: "Porta", fr: "Porte", emoji: "🚪", level: 1 },
|
||||
{ pt: "Janela", fr: "Fenêtre", emoji: "🪟", level: 1 },
|
||||
{ pt: "Chave", fr: "Clé", emoji: "🔑", level: 2 },
|
||||
{ pt: "Mesa", fr: "Table", emoji: "🪑", level: 1 },
|
||||
{ pt: "Cama", fr: "Lit", emoji: "🛏️", level: 1 },
|
||||
{ pt: "Frigorífico", fr: "Réfrigérateur", emoji: "🧊", level: 3 },
|
||||
{ pt: "Varanda", fr: "Balcon", emoji: "🌿", level: 3 },
|
||||
{ pt: "Garagem", fr: "Garage", emoji: "🚗", level: 3 },
|
||||
],
|
||||
"Corps": [
|
||||
{ pt: "Cabeça", fr: "Tête", emoji: "🗣️", level: 1 },
|
||||
{ pt: "Olhos", fr: "Yeux", emoji: "👀", level: 1 },
|
||||
{ pt: "Boca", fr: "Bouche", emoji: "👄", level: 1 },
|
||||
{ pt: "Mão", fr: "Main", emoji: "✋", level: 1 },
|
||||
{ pt: "Pé", fr: "Pied", emoji: "🦶", level: 1 },
|
||||
{ pt: "Braço", fr: "Bras", emoji: "💪", level: 2 },
|
||||
{ pt: "Perna", fr: "Jambe", emoji: "🦵", level: 2 },
|
||||
{ pt: "Dedo", fr: "Doigt", emoji: "☝️", level: 1 },
|
||||
{ pt: "Dentes", fr: "Dents", emoji: "🦷", level: 2 },
|
||||
{ pt: "Joelho", fr: "Genou", emoji: "🦿", level: 2 },
|
||||
{ pt: "Ombro", fr: "Épaule", emoji: "🏋️", level: 2 },
|
||||
],
|
||||
"Nature": [
|
||||
{ pt: "Sol", fr: "Soleil", emoji: "☀️", level: 1 },
|
||||
{ pt: "Lua", fr: "Lune", emoji: "🌙", level: 1 },
|
||||
{ pt: "Estrela", fr: "Étoile", emoji: "⭐", level: 1 },
|
||||
{ pt: "Árvore", fr: "Arbre", emoji: "🌳", level: 1 },
|
||||
{ pt: "Flor", fr: "Fleur", emoji: "🌺", level: 1 },
|
||||
{ pt: "Mar", fr: "Mer", emoji: "🌊", level: 1 },
|
||||
{ pt: "Montanha", fr: "Montagne", emoji: "⛰️", level: 1 },
|
||||
{ pt: "Rio", fr: "Rivière", emoji: "🏞️", level: 1 },
|
||||
{ pt: "Nuvem", fr: "Nuage", emoji: "☁️", level: 2 },
|
||||
{ pt: "Chuva", fr: "Pluie", emoji: "🌧️", level: 2 },
|
||||
{ pt: "Neve", fr: "Neige", emoji: "❄️", level: 2 },
|
||||
{ pt: "Ventania", fr: "Vent", emoji: "💨", level: 2 },
|
||||
{ pt: "Relâmpago", fr: "Éclair", emoji: "⚡", level: 3 },
|
||||
{ pt: "Arco-íris", fr: "Arc-en-ciel", emoji: "🌈", level: 3 },
|
||||
{ pt: "Areia", fr: "Sable", emoji: "🏖️", level: 2 },
|
||||
{ pt: "Pedra", fr: "Pierre", emoji: "🪨", level: 2 },
|
||||
],
|
||||
"Nourriture": [
|
||||
{ pt: "Maçã", fr: "Pomme", emoji: "🍎", level: 1 },
|
||||
{ pt: "Banana", fr: "Banane", emoji: "🍌", level: 1 },
|
||||
{ pt: "Laranja", fr: "Orange", emoji: "🍊", level: 1 },
|
||||
{ pt: "Leite", fr: "Lait", emoji: "🥛", level: 1 },
|
||||
{ pt: "Queijo", fr: "Fromage", emoji: "🧀", level: 1 },
|
||||
{ pt: "Pão", fr: "Pain", emoji: "🍞", level: 1 },
|
||||
{ pt: "Bolo", fr: "Gâteau", emoji: "🎂", level: 1 },
|
||||
{ pt: "Gelado", fr: "Glace", emoji: "🍦", level: 1 },
|
||||
{ pt: "Manteiga", fr: "Beurre", emoji: "🧈", level: 2 },
|
||||
{ pt: "Ovos", fr: "Œufs", emoji: "🥚", level: 2 },
|
||||
{ pt: "Batatas", fr: "Pommes de terre", emoji: "🥔", level: 2 },
|
||||
{ pt: "Cenouras", fr: "Carottes", emoji: "🥕", level: 2 },
|
||||
{ pt: "Alho", fr: "Ail", emoji: "🧄", level: 2 },
|
||||
{ pt: "Cebola", fr: "Oignon", emoji: "🧅", level: 2 },
|
||||
{ pt: "Vinho", fr: "Vin", emoji: "🍷", level: 3 },
|
||||
{ pt: "Cerveja", fr: "Bière", emoji: "🍺", level: 3 },
|
||||
{ pt: "Melancia", fr: "Pastèque", emoji: "🍉", level: 2 },
|
||||
{ pt: "Morangos", fr: "Fraises", emoji: "🍓", level: 2 },
|
||||
{ pt: "Uvas", fr: "Raisins", emoji: "🍇", level: 2 },
|
||||
{ pt: "Abacaxi", fr: "Ananas", emoji: "🍍", level: 2 },
|
||||
],
|
||||
"Bébé (3 ans)": [
|
||||
{ pt: "Mãe", fr: "Maman", emoji: "👩", level: 1 },
|
||||
{ pt: "Pai", fr: "Papa", emoji: "👨", level: 1 },
|
||||
{ pt: "Cãozinho", fr: "Petit chien", emoji: "🐶", level: 1 },
|
||||
{ pt: "Gatinho", fr: "Petit chat", emoji: "🐱", level: 1 },
|
||||
{ pt: "Bola", fr: "Ballon", emoji: "⚽", level: 1 },
|
||||
{ pt: "Brinquedo", fr: "Jouet", emoji: "🧸", level: 1 },
|
||||
{ pt: "Boneca", fr: "Poupée", emoji: "🪆", level: 1 },
|
||||
{ pt: "Carro", fr: "Voiture", emoji: "🚗", level: 1 },
|
||||
{ pt: "Beijinho", fr: "Petit bisou", emoji: "💋", level: 1 },
|
||||
{ pt: "Eu amo-te", fr: "Je t'aime", emoji: "❤️", level: 1 },
|
||||
{ pt: "Lindo", fr: "Joli", emoji: "✨", level: 1 },
|
||||
{ pt: "Grande", fr: "Grand", emoji: "📏", level: 1 },
|
||||
],
|
||||
"Transports": [
|
||||
{ pt: "Carro", fr: "Voiture", emoji: "🚗", level: 1 },
|
||||
{ pt: "Autocarro", fr: "Bus", emoji: "🚌", level: 1 },
|
||||
{ pt: "Comboio", fr: "Train", emoji: "🚂", level: 1 },
|
||||
{ pt: "Avião", fr: "Avion", emoji: "✈️", level: 1 },
|
||||
{ pt: "Barco", fr: "Bateau", emoji: "🚢", level: 1 },
|
||||
{ pt: "Bicicleta", fr: "Vélo", emoji: "🚲", level: 1 },
|
||||
{ pt: "Táxi", fr: "Taxi", emoji: "🚕", level: 1 },
|
||||
{ pt: "Motocicleta", fr: "Moto", emoji: "🏍️", level: 2 },
|
||||
{ pt: "Embarcação", fr: "Embarcation", emoji: "⛵", level: 3 },
|
||||
{ pt: "Helicóptero", fr: "Hélicoptère", emoji: "🚁", level: 3 },
|
||||
],
|
||||
};
|
||||
|
||||
// ===== EXERCISES PAR NIVEAU =====
|
||||
const EXERCISES_FILL = [
|
||||
// Niveau 1
|
||||
{ template: "Bom ___!", answer: "dia", options: ["dia", "noite", "tarde", "bom"], hint: "Bonjour (matin)", level: 1 },
|
||||
{ template: "Tudo ___?", answer: "bem", options: ["bem", "mal", "bom", "mau"], hint: "Comment vas-tu ?", level: 1 },
|
||||
{ template: "___-me Chico.", answer: "Chamo", options: ["Chamo", "Falo", "Digo", "Ponho"], hint: "Je m'appelle...", level: 1 },
|
||||
{ template: "Eu ___ português.", answer: "falo", options: ["falo", "digo", "como", "ponho"], hint: "Je parle portugais", level: 1 },
|
||||
{ template: "___ favor.", answer: "Por", options: ["Por", "De", "Em", "Com"], hint: "S'il vous plaît", level: 1 },
|
||||
{ template: "Muito ___.", answer: "obrigado", options: ["obrigado", "desculpe", "por favor", "por nada"], hint: "Merci beaucoup", level: 1 },
|
||||
{ template: "Onde ___ a casa de banho?", answer: "fica", options: ["fica", "é", "está", "tem"], hint: "Où sont les toilettes ?", level: 1 },
|
||||
{ template: "Eu tenho ___.", answer: "fome", options: ["fome", "sede", "sono", "medo"], hint: "J'ai faim", level: 1 },
|
||||
{ template: "___ te chamas?", answer: "Como", options: ["Como", "Onde", "Quando", "Porque"], hint: "Comment tu t'appelles ?", level: 1 },
|
||||
{ template: "___ é a tua casa?", answer: "Onde", options: ["Onde", "Como", "Quando", "Porque"], hint: "Où est ta maison ?", level: 1 },
|
||||
{ template: "Eu ___ um café.", answer: "quero", options: ["quero", "tenho", "posso", "vou"], hint: "Je veux un café", level: 1 },
|
||||
{ template: "___ custa?", answer: "Quanto", options: ["Quanto", "Como", "Onde", "Quando"], hint: "Combien ?", level: 1 },
|
||||
{ template: "Sim, ___ tudo bem.", answer: "tudo", options: ["tudo", "nada", "bom", "mau"], hint: "Oui, tout va bien", level: 1 },
|
||||
{ template: "Eu ___ de Lisboa.", answer: "sou", options: ["sou", "estou", "tenho", "vou"], hint: "Je suis de Lisbonne", level: 1 },
|
||||
{ template: "___ é muito bom.", answer: "Isto", options: ["Isto", "Isso", "Aquilo", "Ele"], hint: "C'est très bon", level: 1 },
|
||||
// Niveau 2
|
||||
{ template: "___ português há dois anos.", answer: "Falo", options: ["Falo", "Digo", "Sei", "Conheço"], hint: "Je parle portugais depuis 2 ans", level: 2 },
|
||||
{ template: "Eu ___ ao trabalho de autocarro.", answer: "vou", options: ["vou", "venho", "estou", "tenho"], hint: "Je vais au travail en bus", level: 2 },
|
||||
{ template: "___ uma mesa para dois, por favor.", answer: "Quero", options: ["Quero", "Tenho", "Preciso", "Vou"], hint: "Une table pour deux", level: 2 },
|
||||
{ template: "___ fala mais devagar, por favor.", answer: "Pode", options: ["Pode", "Deve", "Quer", "Vai"], hint: "Parlez plus lentement", level: 2 },
|
||||
{ template: "Estou a ___ português.", answer: "aprender", options: ["aprender", "estudar", "falar", "dizer"], hint: "J'apprends le portugais", level: 2 },
|
||||
{ template: "___ horas são agora?", answer: "Que", options: ["Que", "Como", "Onde", "Quanto"], hint: "Quelle heure est-il ?", level: 2 },
|
||||
{ template: "A chuva ___ a cair.", answer: "está", options: ["está", "vai", "tem", "faz"], hint: "Il pleut", level: 2 },
|
||||
{ template: "Eu ___ Lisboa muito.", answer: "gosto", options: ["gosto", "quero", "tenho", "faço"], hint: "J'aime beaucoup Lisbonne", level: 2 },
|
||||
{ template: "___ tempo faz hoje?", answer: "Que", options: ["Que", "Como", "Quanto", "Quando"], hint: "Quel temps fait-il ?", level: 2 },
|
||||
{ template: "Preciso de ___ o médico.", answer: "ver", options: ["ver", "vir", "virar", "viver"], hint: "J'ai besoin de voir le médecin", level: 2 },
|
||||
// Niveau 3
|
||||
{ template: "Gostaria de ___ uma reserva.", answer: "fazer", options: ["fazer", "dar", "ter", "pôr"], hint: "Je voudrais faire une réservation", level: 3 },
|
||||
{ template: "___ que me pode ajudar.", answer: "Penso", options: ["Penso", "Creio", "Digo", "Sei"], hint: "Je pense que vous pouvez m'aider", level: 3 },
|
||||
{ template: "Eu ___ a Lisboa no próximo mês.", answer: "vou", options: ["vou", "estou", "tenho", "faço"], hint: "Je vais à Lisbonne le mois prochain", level: 3 },
|
||||
{ template: "___ um bocadinho mais devagar.", answer: "Fale", options: ["Fale", "Diga", "Diga", "Ponha"], hint: "Parlez un peu plus lentement", level: 3 },
|
||||
{ template: "Já ___ ao Brasil.", answer: "fui", options: ["fui", "vou", "estive", "andei"], hint: "J'ai déjà été au Brésil", level: 3 },
|
||||
// Niveau 4
|
||||
{ template: "___ dispenso a sua ajuda.", answer: "Agradeço", options: ["Agradeço", "Preciso", "Quero", "Aceito"], hint: "Je vous remercie mais je n'ai pas besoin d'aide", level: 4 },
|
||||
{ template: "Não me ___ com isso.", answer: "incomode", options: ["incomode", "preocupe", "chame", "ligue"], hint: "Ne vous dérangez pas", level: 4 },
|
||||
{ template: "Tem ___ que me emprestar o carro?", level: 4, answer: "bondade", options: ["bondade", "gusto", "razão", "certeza"], hint: "Avez-vous la gentillesse de me prêter la voiture ?", level: 4 },
|
||||
];
|
||||
|
||||
const EXERCISES_LISTEN = [
|
||||
{ pt: "Olá", fr: "Bonjour", level: 1 },
|
||||
{ pt: "Bom dia", fr: "Bonjour (matin)", level: 1 },
|
||||
{ pt: "Obrigado", fr: "Merci (homme)", level: 1 },
|
||||
{ pt: "Por favor", fr: "S'il vous plaît", level: 1 },
|
||||
{ pt: "Como vai?", fr: "Comment vas-tu?", level: 1 },
|
||||
{ pt: "Até logo", fr: "À bientôt", level: 1 },
|
||||
{ pt: "Boa noite", fr: "Bonsoir", level: 1 },
|
||||
{ pt: "Desculpe", fr: "Excusez-moi", level: 1 },
|
||||
{ pt: "Tudo bem", fr: "Tout va bien", level: 1 },
|
||||
{ pt: "Adeus", fr: "Au revoir", level: 1 },
|
||||
{ pt: "Sim", fr: "Oui", level: 1 },
|
||||
{ pt: "Não", fr: "Non", level: 1 },
|
||||
{ pt: "Água", fr: "Eau", level: 1 },
|
||||
{ pt: "Pão", fr: "Pain", level: 1 },
|
||||
{ pt: "Casa", fr: "Maison", level: 1 },
|
||||
{ pt: "Onde fica?", fr: "Où est-ce?", level: 2 },
|
||||
{ pt: "Quanto custa?", fr: "Combien ça coûte?", level: 2 },
|
||||
{ pt: "Tenho fome", fr: "J'ai faim", level: 2 },
|
||||
{ pt: "Estou perdido", fr: "Je suis perdu", level: 2 },
|
||||
{ pt: "Pode repetir?", fr: "Pouvez-vous répéter?", level: 2 },
|
||||
{ pt: "Bom apetite!", fr: "Bon appétit!", level: 1 },
|
||||
{ pt: "Com licença", fr: "Avec votre permission", level: 2 },
|
||||
{ pt: "É muito bom", fr: "C'est très bon", level: 2 },
|
||||
{ pt: "Que horas são?", fr: "Quelle heure est-il?", level: 2 },
|
||||
{ pt: "Está a chover", fr: "Il pleut", level: 2 },
|
||||
{ pt: "Gosto muito de Portugal", fr: "J'aime beaucoup le Portugal", level: 3 },
|
||||
{ pt: "Faz calor hoje", fr: "Il fait chaud aujourd'hui", level: 2 },
|
||||
{ pt: "Onde é a estação?", fr: "Où est la gare?", level: 2 },
|
||||
{ pt: "Preciso de ajuda", fr: "J'ai besoin d'aide", level: 2 },
|
||||
{ pt: "Tem menus em inglês?", fr: "Avez-vous un menu en anglais?", level: 3 },
|
||||
{ pt: "Gostaria de reservar", fr: "Je voudrais réserver", level: 3 },
|
||||
{ pt: "Estou a aprender português", fr: "J'apprends le portugais", level: 2 },
|
||||
];
|
||||
|
||||
const EXERCISES_TRANSLATE = [
|
||||
{ fr: "Bonjour", pt: "Olá", level: 1 },
|
||||
{ fr: "Merci", pt: "Obrigado", level: 1 },
|
||||
{ fr: "S'il vous plaît", pt: "Por favor", level: 1 },
|
||||
{ fr: "Comment vas-tu?", pt: "Como vais?", level: 1 },
|
||||
{ fr: "Au revoir", pt: "Adeus", level: 1 },
|
||||
{ fr: "Oui", pt: "Sim", level: 1 },
|
||||
{ fr: "Non", pt: "Não", level: 1 },
|
||||
{ fr: "Bon appétit!", pt: "Bom apetite!", level: 1 },
|
||||
{ fr: "Je m'appelle...", pt: "Chamo-me...", level: 1 },
|
||||
{ fr: "D'où es-tu?", pt: "De onde és?", level: 1 },
|
||||
{ fr: "Où est...?", pt: "Onde fica...?", level: 1 },
|
||||
{ fr: "Combien ça coûte?", pt: "Quanto custa?", level: 1 },
|
||||
{ fr: "J'ai faim", pt: "Tenho fome", level: 2 },
|
||||
{ fr: "J'ai soif", pt: "Tenho sede", level: 2 },
|
||||
{ fr: "Je ne comprends pas", pt: "Não compreendo", level: 2 },
|
||||
{ fr: "Il fait chaud", pt: "Está quente", level: 2 },
|
||||
{ fr: "Il pleut", pt: "Está a chover", level: 2 },
|
||||
{ fr: "Quelle heure est-il?", pt: "Que horas são?", level: 2 },
|
||||
{ fr: "Je suis perdu", pt: "Estou perdido", level: 2 },
|
||||
{ fr: "Pouvez-vous m'aider?", pt: "Pode ajudar-me?", level: 2 },
|
||||
{ fr: "Parlez plus lentement", pt: "Fale mais devagar", level: 3 },
|
||||
{ fr: "J'apprends le portugais", pt: "Estou a aprender português", level: 2 },
|
||||
{ fr: "Je voudrais réserver", pt: "Gostaria de reservar", level: 3 },
|
||||
{ fr: "C'est délicieux", pt: "Está delicioso", level: 2 },
|
||||
{ fr: "Je veux un café", pt: "Eu quero um café", level: 2 },
|
||||
{ fr: "Excusez-moi", pt: "Com licença", level: 2 },
|
||||
{ fr: "La l'addition", pt: "A conta", level: 2 },
|
||||
{ fr: "C'est loin?", pt: "É muito longe?", level: 2 },
|
||||
{ fr: "C'est près", pt: "É perto", level: 2 },
|
||||
{ fr: "Où sont les toilettes?", pt: "Onde fica a casa de banho?", level: 2 },
|
||||
{ fr: "Avez-vous un menu?", pt: "Tem uma ementa?", level: 3 },
|
||||
{ fr: "Je voudrais une table", pt: "Gostaria de uma mesa", level: 3 },
|
||||
{ fr: "Il fait froid", pt: "Está frio", level: 1 },
|
||||
{ fr: "Il fait soleil", pt: "Está sol", level: 1 },
|
||||
{ fr: "Il fait vent", pt: "Está ventoso", level: 2 },
|
||||
{ fr: "Merci beaucoup", pt: "Muito obrigado", level: 1 },
|
||||
{ fr: "De rien", pt: "De nada", level: 1 },
|
||||
{ fr: "Enchanté", pt: "Prazer", level: 1 },
|
||||
{ fr: "Bonne nuit", pt: "Boa noite", level: 1 },
|
||||
{ fr: "À demain", pt: "Até amanhã", level: 1 },
|
||||
];
|
||||
|
||||
const CONJUGATIONS = [
|
||||
{ verb: "Ser (être - permanent)", forms: { eu: "sou", tu: "és", ele: "é", nós: "somos", eles: "são" }, meaning: "Être (nature, identité)", level: 1 },
|
||||
{ verb: "Estar (être - état)", forms: { eu: "estou", tu: "estás", ele: "está", nós: "estamos", eles: "estão" }, meaning: "Être (état, lieu)", level: 1 },
|
||||
{ verb: "Ter (avoir)", forms: { eu: "tenho", tu: "tens", ele: "tem", nós: "temos", eles: "têm" }, meaning: "Avoir", level: 1 },
|
||||
{ verb: "Falar (parler)", forms: { eu: "falo", tu: "falas", ele: "fala", nós: "falamos", eles: "falam" }, meaning: "Parler", level: 1 },
|
||||
{ verb: "Gostar (aimer)", forms: { eu: "gosto", tu: "gostas", ele: "gosta", nós: "gostamos", eles: "gostam" }, meaning: "Aimer", level: 1 },
|
||||
{ verb: "Ir (aller)", forms: { eu: "vou", tu: "vais", ele: "vai", nós: "vamos", eles: "vão" }, meaning: "Aller", level: 1 },
|
||||
{ verb: "Poder (pouvoir)", forms: { eu: "posso", tu: "podes", ele: "pode", nós: "podemos", eles: "podem" }, meaning: "Pouvoir", level: 2 },
|
||||
{ verb: "Querer (vouloir)", forms: { eu: "quero", tu: "queres", ele: "quer", nós: "queremos", eles: "querem" }, meaning: "Vouloir", level: 1 },
|
||||
{ verb: "Saber (savoir)", forms: { eu: "sei", tu: "sabes", ele: "sabe", nós: "sabemos", eles: "sabem" }, meaning: "Savoir (faits)", level: 2 },
|
||||
{ verb: "Conhecer (connaître)", forms: { eu: "conheço", tu: "conheces", ele: "conhece", nós: "conhecemos", eles: "conhecem" }, meaning: "Connaître (personnes, lieux)", level: 2 },
|
||||
{ verb: "Fazer (faire)", forms: { eu: "faço", tu: "fazes", ele: "faz", nós: "fazemos", eles: "fazem" }, meaning: "Faire", level: 2 },
|
||||
{ verb: "Dizer (dire)", forms: { eu: "digo", tu: "dizes", ele: "diz", nós: "dizemos", eles: "dizem" }, meaning: "Dire", level: 2 },
|
||||
{ verb: "Vir (venir)", forms: { eu: "venho", tu: "vens", ele: "vem", nós: "vimos", eles: "vêm" }, meaning: "Venir", level: 2 },
|
||||
{ verb: "Dar (donner)", forms: { eu: "dou", tu: "dás", ele: "dá", nós: "damos", eles: "dão" }, meaning: "Donner", level: 2 },
|
||||
{ verb: "Ver (voir)", forms: { eu: "vejo", tu: "ves", ele: "vê", nós: "vemos", eles: "veem" }, meaning: "Voir", level: 2 },
|
||||
{ verb: "Trabalhar (travailler)", forms: { eu: "trabalho", tu: "trabalhas", ele: "trabalha", nós: "trabalhamos", eles: "trabalham" }, meaning: "Travailler", level: 2 },
|
||||
{ verb: "Estudar (étudier)", forms: { eu: "estudo", tu: "estudas", ele: "estuda", nós: "estudamos", eles: "estudam" }, meaning: "Étudier", level: 1 },
|
||||
{ verb: "Comprar (acheter)", forms: { eu: "compro", tu: "compras", ele: "compra", nós: "compramos", eles: "compram" }, meaning: "Acheter", level: 2 },
|
||||
{ verb: "Viajar (voyager)", forms: { eu: "viajo", tu: "viajas", ele: "viaja", nós: "viajamos", eles: "viajam" }, meaning: "Voyager", level: 2 },
|
||||
{ verb: "Precisar (avoir besoin)", forms: { eu: "preciso", tu: "precisas", ele: "precisa", nós: "precisamos", eles: "precisam" }, meaning: "Avoir besoin de", level: 2 },
|
||||
{ verb: "Ajudar (aider)", forms: { eu: "ajudo", tu: "ajudas", ele: "ajuda", nós: "ajudamos", eles: "ajudam" }, meaning: "Aider", level: 2 },
|
||||
{ verb: "Chegar (arriver)", forms: { eu: "chego", tu: "chegas", ele: "chega", nós: "chegamos", eles: "chegam" }, meaning: "Arriver", level: 3 },
|
||||
{ verb: "Passar (passer)", forms: { eu: "passo", tu: "passas", ele: "passa", nós: "passamos", eles: "passam" }, meaning: "Passer", level: 3 },
|
||||
{ verb: "Sentir (sentir)", forms: { eu: "sinto", tu: "sentes", ele: "sente", nós: "sentimos", eles: "sentem" }, meaning: "Sentir", level: 3 },
|
||||
{ verb: "Pedir (demander)", forms: { eu: "peço", tu: "pedes", ele: "pede", nós: "pedimos", eles: "pedem" }, meaning: "Demander", level: 3 },
|
||||
{ verb: "Abrir (ouvrir)", forms: { eu: "abro", tu: "abres", ele: "abre", nós: "abrimos", eles: "abrem" }, meaning: "Ouvrir", level: 3 },
|
||||
{ verb: "Meter (mettre)", forms: { eu: "meto", tu: "metes", ele: "mete", nós: "metemos", eles: "metem" }, meaning: "Mettre", level: 3 },
|
||||
{ verb: "Trazer (apporter)", forms: { eu: "trago", tu: "trazes", ele: "traz", nós: "trazemos", eles: "trazem" }, meaning: "Apporter", level: 4 },
|
||||
{ verb: "Conseguir (réussir)", forms: { eu: "consigo", tu: "consegues", ele: "consegue", nós: "conseguimos", eles: "conseguem" }, meaning: "Réussir, parvenir", level: 4 },
|
||||
{ verb: "Descobrir (découvrir)", forms: { eu: "descubro", tu: "descobres", ele: "descobre", nós: "descobrimos", eles: "descobrem" }, meaning: "Découvrir", level: 4 },
|
||||
];
|
||||
|
||||
// ===== VOCAB EXTRA PAR NIVEAU =====
|
||||
const VOCAB_EXTRA = {
|
||||
2: [
|
||||
{ pt: "Relógio", fr: "Montre", emoji: "⌚" },
|
||||
{ pt: "Telemóvel", fr: "Téléphone", emoji: "📱" },
|
||||
{ pt: "Computador", fr: "Ordinateur", emoji: "💻" },
|
||||
{ pt: "Livro", fr: "Livre", emoji: "📚" },
|
||||
{ pt: "Lápis", fr: "Crayon", emoji: "✏️" },
|
||||
{ pt: "Escola", fr: "École", emoji: "🏫" },
|
||||
{ pt: "Dinheiro", fr: "Argent", emoji: "💰" },
|
||||
{ pt: "Relógio", fr: "Montre", emoji: "⏰" },
|
||||
{ pt: "Chave", fr: "Clé", emoji: "🔑" },
|
||||
{ pt: "Guarda-chuva", fr: "Parapluie", emoji: "☂️" },
|
||||
{ pt: "Mochila", fr: "Sac à dos", emoji: "🎒" },
|
||||
{ pt: "Caneta", fr: "Stylo", emoji: "🖊️" },
|
||||
{ pt: "Carteira", fr: "Portefeuille", emoji: "👛" },
|
||||
{ pt: "Óculos", fr: "Lunettes", emoji: "👓" },
|
||||
{ pt: "Fato", fr: "Costume", emoji: "🤵" },
|
||||
],
|
||||
3: [
|
||||
{ pt: "Desenvolvimento", fr: "Développement", emoji: "📈" },
|
||||
{ pt: "Experiência", fr: "Expérience", emoji: "🎓" },
|
||||
{ pt: "Oportunidade", fr: "Opportunité", emoji: "🚪" },
|
||||
{ pt: "Responsabilidade", fr: "Responsabilité", emoji: "⚖️" },
|
||||
{ pt: "Conhecimento", fr: "Connaissance", emoji: "🧠" },
|
||||
{ pt: "Agricultura", fr: "Agriculture", emoji: "🌾" },
|
||||
{ pt: "Indústria", fr: "Industrie", emoji: "🏭" },
|
||||
{ pt: "Comércio", fr: "Commerce", emoji: "🏪" },
|
||||
{ pt: "Turismo", fr: "Tourisme", emoji: "🗺️" },
|
||||
{ pt: "Tecnologia", fr: "Technologie", emoji: "🤖" },
|
||||
],
|
||||
4: [
|
||||
{ pt: "Saudade", fr: "Nostalgie (mot PT)", emoji: "💙" },
|
||||
{ pt: "Fado", fr: "Fado (musique PT)", emoji: "🎵" },
|
||||
{ pt: "Azulejo", fr: "Azulejo (carreau)", emoji: "🏛️" },
|
||||
{ pt: "Pastéis de nata", fr: "Pastéis de nata", emoji: "🥧" },
|
||||
{ pt: "Festas populares", fr: "Fêtes populaires", emoji: "🎉" },
|
||||
{ pt: "Cortiça", fr: "Liège", emoji: "🍾" },
|
||||
{ pt: "Serra da Estrela", fr: "Serra da Estrela", emoji: "🏔️" },
|
||||
{ pt: "Algarve", fr: "Algarve", emoji: "🏖️" },
|
||||
{ pt: "Madeira", fr: "Madère", emoji: "🌺" },
|
||||
{ pt: "Açores", fr: "Açores", emoji: "🌋" },
|
||||
],
|
||||
};
|
||||
|
||||
// ===== SCRAMBLE GAME DATA =====
|
||||
const SCRAMBLE_WORDS = [
|
||||
{ word: "cão", fr: "Chien", emoji: "🐕", level: 1 },
|
||||
{ word: "gato", fr: "Chat", emoji: "🐈", level: 1 },
|
||||
{ word: "sol", fr: "Soleil", emoji: "☀️", level: 1 },
|
||||
{ word: "lua", fr: "Lune", emoji: "🌙", level: 1 },
|
||||
{ word: "água", fr: "Eau", emoji: "💧", level: 1 },
|
||||
{ word: "pão", fr: "Pain", emoji: "🍞", level: 1 },
|
||||
{ word: "mão", fr: "Main", emoji: "✋", level: 1 },
|
||||
{ word: "casa", fr: "Maison", emoji: "🏠", level: 1 },
|
||||
{ word: "flor", fr: "Fleur", emoji: "🌸", level: 1 },
|
||||
{ word: "bola", fr: "Ballon", emoji: "⚽", level: 1 },
|
||||
{ word: "peixe", fr: "Poisson", emoji: "🐟", level: 1 },
|
||||
{ word: "mãe", fr: "Maman", emoji: "👩", level: 1 },
|
||||
{ word: "pai", fr: "Papa", emoji: "👨", level: 1 },
|
||||
{ word: "olho", fr: "Œil", emoji: "👁️", level: 1 },
|
||||
{ word: "boca", fr: "Bouche", emoji: "👄", level: 1 },
|
||||
{ word: "dedo", fr: "Doigt", emoji: "☝️", level: 1 },
|
||||
{ word: "pé", fr: "Pied", emoji: "🦶", level: 1 },
|
||||
{ word: "arroz", fr: "Riz", emoji: "🍚", level: 1 },
|
||||
{ word: "leite", fr: "Lait", emoji: "🥛", level: 1 },
|
||||
{ word: "café", fr: "Café", emoji: "☕", level: 1 },
|
||||
{ word: "carro", fr: "Voiture", emoji: "🚗", level: 1 },
|
||||
{ word: "escola", fr: "École", emoji: "🏫", level: 2 },
|
||||
{ word: "livro", fr: "Livre", emoji: "📚", level: 2 },
|
||||
{ word: "amigo", fr: "Ami", emoji: "🤝", level: 2 },
|
||||
{ word: "feliz", fr: "Heureux", emoji: "😊", level: 2 },
|
||||
{ word: "grande", fr: "Grand", emoji: "📏", level: 2 },
|
||||
{ word: "bonito", fr: "Joli", emoji: "✨", level: 2 },
|
||||
{ word: "sapato", fr: "Chaussure", emoji: "👟", level: 2 },
|
||||
{ word: "janela", fr: "Fenêtre", emoji: "🪟", level: 2 },
|
||||
{ word: "porta", fr: "Porte", emoji: "🚪", level: 2 },
|
||||
{ word: "banana", fr: "Banane", emoji: "🍌", level: 2 },
|
||||
{ word: "melancia", fr: "Pastèque", emoji: "🍉", level: 2 },
|
||||
{ word: "morango", fr: "Fraise", emoji: "🍓", level: 2 },
|
||||
{ word: "cavalo", fr: "Cheval", emoji: "🐴", level: 2 },
|
||||
{ word: "borboleta", fr: "Papillon", emoji: "🦋", level: 3 },
|
||||
{ word: "elefante", fr: "Éléphant", emoji: "🐘", level: 3 },
|
||||
{ word: "girafa", fr: "Girafe", emoji: "🦒", level: 3 },
|
||||
{ word: "crocodilo", fr: "Crocodile", emoji: "🐊", level: 3 },
|
||||
{ word: "raposa", fr: "Renard", emoji: "🦊", level: 3 },
|
||||
{ word: "montanha", fr: "Montagne", emoji: "⛰️", level: 3 },
|
||||
];
|
||||
|
||||
// ===== BINGO GAME DATA =====
|
||||
const BINGO_WORDS = [
|
||||
{ pt: "Olá", fr: "Bonjour", emoji: "👋" },
|
||||
{ pt: "Obrigado", fr: "Merci", emoji: "🙏" },
|
||||
{ pt: "Sim", fr: "Oui", emoji: "✅" },
|
||||
{ pt: "Não", fr: "Non", emoji: "❌" },
|
||||
{ pt: "Cão", fr: "Chien", emoji: "🐕" },
|
||||
{ pt: "Gato", fr: "Chat", emoji: "🐈" },
|
||||
{ pt: "Pão", fr: "Pain", emoji: "🍞" },
|
||||
{ pt: "Água", fr: "Eau", emoji: "💧" },
|
||||
{ pt: "Sol", fr: "Soleil", emoji: "☀️" },
|
||||
{ pt: "Lua", fr: "Lune", emoji: "🌙" },
|
||||
{ pt: "Mãe", fr: "Maman", emoji: "👩" },
|
||||
{ pt: "Pai", fr: "Papa", emoji: "👨" },
|
||||
{ pt: "Bola", fr: "Ballon", emoji: "⚽" },
|
||||
{ pt: "Flor", fr: "Fleur", emoji: "🌸" },
|
||||
{ pt: "Estrela", fr: "Étoile", emoji: "⭐" },
|
||||
{ pt: "Casa", fr: "Maison", emoji: "🏠" },
|
||||
{ pt: "Leite", fr: "Lait", emoji: "🥛" },
|
||||
{ pt: "Café", fr: "Café", emoji: "☕" },
|
||||
{ pt: "Carro", fr: "Voiture", emoji: "🚗" },
|
||||
{ pt: "Escola", fr: "École", emoji: "🏫" },
|
||||
{ pt: "Amigo", fr: "Ami", emoji: "🤝" },
|
||||
{ pt: "Feliz", fr: "Heureux", emoji: "😊" },
|
||||
{ pt: "Bonito", fr: "Joli", emoji: "✨" },
|
||||
{ pt: "Grande", fr: "Grand", emoji: "📏" },
|
||||
{ pt: "Peixe", fr: "Poisson", emoji: "🐟" },
|
||||
{ pt: "Árvore", fr: "Arbre", emoji: "🌳" },
|
||||
{ pt: "Montanha", fr: "Montagne", emoji: "⛰️" },
|
||||
{ pt: "Mar", fr: "Mer", emoji: "🌊" },
|
||||
{ pt: "Rio", fr: "Rivière", emoji: "🏞️" },
|
||||
{ pt: "Nuvem", fr: "Nuage", emoji: "☁️" },
|
||||
];
|
||||
|
||||
// ===== RIDDLE GAME DATA =====
|
||||
const RIDDLES = [
|
||||
{ emoji: "☀️", riddle: "Sou quente e brilho no céu!", answer: "Sol", fr: "Je suis chaud et je brille dans le ciel !", options: ["Sol", "Lua", "Estrela", "Nuvem"], level: 1 },
|
||||
{ emoji: "🌙", riddle: "Apareço à noite e mudo de forma!", answer: "Lua", fr: "J'apparais la nuit et je change de forme !", options: ["Lua", "Sol", "Nuvem", "Estrela"], level: 1 },
|
||||
{ emoji: "🐕", riddle: "Sou o melhor amigo do homem e faz 'au au'!", answer: "Cão", fr: "Je suis le meilleur ami de l'homme et fais 'ouaf ouaf' !", options: ["Cão", "Gato", "Pato", "Peixe"], level: 1 },
|
||||
{ emoji: "🐈", riddle: "Adoro dormir e fazer 'miau'!", answer: "Gato", fr: "J'adore dormir et faire 'miaou' !", options: ["Gato", "Cão", "Rato", "Coelho"], level: 1 },
|
||||
{ emoji: "💧", riddle: "Bebes-me todos os dias e sou transparente!", answer: "Água", fr: "Tu me bois tous les jours et je suis transparent !", options: ["Água", "Leite", "Sumo", "Café"], level: 1 },
|
||||
{ emoji: "🍞", riddle: "Comes-me no pequeno-almoço com manteiga!", answer: "Pão", fr: "Tu me manges au petit-déjeuner avec du beurre !", options: ["Pão", "Queijo", "Arroz", "Bolo"], level: 1 },
|
||||
{ emoji: "🏠", riddle: "Moras dentro de mim e tenho porta e janelas!", answer: "Casa", fr: "Tu vis dans ma maison et j'ai une porte et des fenêtres !", options: ["Casa", "Escola", "Carro", "Jardim"], level: 1 },
|
||||
{ emoji: "🚗", riddle: "Tenho quatro rodas e levo-te para todo o lado!", answer: "Carro", fr: "J'ai quatre roues et je te mène partout !", options: ["Carro", "Bicicleta", "Autocarro", "Barco"], level: 1 },
|
||||
{ emoji: "🏫", riddle: "Vens aqui para aprender e estudar!", answer: "Escola", fr: "Tu viens ici pour apprendre et étudier !", options: ["Escola", "Casa", "Parque", "Loja"], level: 1 },
|
||||
{ emoji: "⚽", riddle: "Golpeas-me com os pés no campo!", answer: "Bola", fr: "Tu me frappes avec tes pieds sur le terrain !", options: ["Bola", "Patins", "Bicicleta", "Skate"], level: 1 },
|
||||
{ emoji: "🌸", riddle: "Sou bonita, tenho cheiro e cresço no jardim!", answer: "Flor", fr: "Je suis jolie, j'ai une odeur et je pousse dans le jardin !", options: ["Flor", "Árvore", "Relva", "Folha"], level: 1 },
|
||||
{ emoji: "🐟", riddle: "Vivo na água e faço 'glu glu'!", answer: "Peixe", fr: "Je vis dans l'eau et fais 'glou glou' !", options: ["Peixe", "Golfinho", "Tartaruga", "Rã"], level: 1 },
|
||||
{ emoji: "⭐", riddle: "Brilho no céu à noite, sou pequena e fixa!", answer: "Estrela", fr: "Je brille dans le ciel la nuit, je suis petite et fixe !", options: ["Estrela", "Lua", "Sol", "Planeta"], level: 1 },
|
||||
{ emoji: "🌳", riddle: "Tenho folhas, ramos e dou sombra!", answer: "Árvore", fr: "J'ai des feuilles, des branches et je fais de l'ombre !", options: ["Árvore", "Flor", "Relva", "Montanha"], level: 1 },
|
||||
{ emoji: "🌧️", riddle: "Cai do céu e molha tudo!", answer: "Chuva", fr: "Je tombe du ciel et je mouille tout !", options: ["Chuva", "Neve", "Granizo", "Orvalho"], level: 2 },
|
||||
{ emoji: "🐄", riddle: "Dai-me leite e faço 'muuu'!", answer: "Vaca", fr: "Je donne du lait et fais 'meuh' !", options: ["Vaca", "Cavalo", "Ovelha", "Porco"], level: 1 },
|
||||
{ emoji: "🐴", riddle: "Tenho quatro patas e corro muito depressa!", answer: "Cavalo", fr: "J'ai quatre pattes et je cours très vite !", options: ["Cavalo", "Cão", "Gato", "Urso"], level: 1 },
|
||||
{ emoji: "✋", riddle: "Tenho cinco dedos e sirvo para apertar a mão!", answer: "Mão", fr: "J'ai cinq doigts et je sers à serrer la main !", options: ["Mão", "Pé", "Braço", "Dedo"], level: 1 },
|
||||
{ emoji: "🦶", riddle: "Ando comigo e estou debaixo de ti!", answer: "Pé", fr: "Je marche avec toi et je suis sous toi !", options: ["Pé", "Mão", "Braço", "Perna"], level: 1 },
|
||||
{ emoji: "☁️", riddle: "Sou branca, voo no céu e faço sombra!", answer: "Nuvem", fr: "Je suis blanche, je vole dans le ciel et je fais de l'ombre !", options: ["Nuvem", "Sol", "Lua", "Fumaça"], level: 1 },
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192"><rect width="192" height="192" rx="32" fill="#00873E"/><circle cx="96" cy="80" r="44" fill="#E53935"/><circle cx="96" cy="80" r="34" fill="#FFB300"/><circle cx="96" cy="80" r="18" fill="#00873E"/><text x="96" y="158" text-anchor="middle" fill="white" font-family="Arial,sans-serif" font-size="28" font-weight="bold">PT</text></svg>
|
||||
|
After Width: | Height: | Size: 419 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><rect width="512" height="512" rx="64" fill="#00873E"/><circle cx="256" cy="210" r="120" fill="#E53935"/><circle cx="256" cy="210" r="92" fill="#FFB300"/><circle cx="256" cy="210" r="50" fill="#00873E"/><text x="256" y="400" text-anchor="middle" fill="white" font-family="Arial,sans-serif" font-size="72" font-weight="bold">PT</text></svg>
|
||||
|
After Width: | Height: | Size: 427 B |
@@ -0,0 +1,273 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<meta name="theme-color" content="#006600">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<title>Portugais pour Tous</title>
|
||||
<link rel="stylesheet" href="style.css?v=4">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="hidden">
|
||||
<header>
|
||||
<div class="header-left">
|
||||
<span class="header-flag">🇵🇹</span>
|
||||
<h1>Portugais</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="user-points-badge" id="header-points">
|
||||
<span class="points-icon">⭐</span>
|
||||
<span class="points-value" id="header-points-val">0</span>
|
||||
</div>
|
||||
<div class="user-avatar" id="header-avatar">🇵🇹</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav id="bottom-nav">
|
||||
<button class="nav-btn active" data-tab="dashboard">
|
||||
<span class="nav-icon">🏠</span>
|
||||
<span class="nav-label">Accueil</span>
|
||||
</button>
|
||||
<button class="nav-btn" data-tab="translator">
|
||||
<span class="nav-icon">🔄</span>
|
||||
<span class="nav-label">Traduire</span>
|
||||
</button>
|
||||
<button class="nav-btn" data-tab="phrases">
|
||||
<span class="nav-icon">💬</span>
|
||||
<span class="nav-label">Phrases</span>
|
||||
</button>
|
||||
<button class="nav-btn" data-tab="games">
|
||||
<span class="nav-icon">🎮</span>
|
||||
<span class="nav-label">Jeux</span>
|
||||
</button>
|
||||
<button class="nav-btn" data-tab="exercises">
|
||||
<span class="nav-icon">📝</span>
|
||||
<span class="nav-label">Exercices</span>
|
||||
</button>
|
||||
<button class="nav-btn" data-tab="vocab">
|
||||
<span class="nav-icon">📖</span>
|
||||
<span class="nav-label">Vocab.</span>
|
||||
</button>
|
||||
<button class="nav-btn" data-tab="admin" id="nav-admin" style="display:none">
|
||||
<span class="nav-icon">⚙️</span>
|
||||
<span class="nav-label">Admin</span>
|
||||
</button>
|
||||
<button class="nav-btn" id="btn-logout">
|
||||
<span class="nav-icon">🚪</span>
|
||||
<span class="nav-label">Quitter</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<main id="content">
|
||||
<!-- DASHBOARD -->
|
||||
<section id="tab-dashboard" class="tab active">
|
||||
<div class="dashboard-welcome" id="dashboard-welcome"></div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card stat-points">
|
||||
<div class="stat-icon">⭐</div>
|
||||
<div class="stat-value" id="dash-total-points">0</div>
|
||||
<div class="stat-label">Points totaux</div>
|
||||
</div>
|
||||
<div class="stat-card stat-streak">
|
||||
<div class="stat-icon">🔥</div>
|
||||
<div class="stat-value" id="dash-streak">0</div>
|
||||
<div class="stat-label">Jours consécutifs</div>
|
||||
</div>
|
||||
<div class="stat-card stat-today">
|
||||
<div class="stat-icon">📈</div>
|
||||
<div class="stat-value" id="dash-today-points">0</div>
|
||||
<div class="stat-label">Points aujourd'hui</div>
|
||||
</div>
|
||||
<div class="stat-card stat-exercises">
|
||||
<div class="stat-icon">✅</div>
|
||||
<div class="stat-value" id="dash-today-exercises">0</div>
|
||||
<div class="stat-label">Exercices aujourd'hui</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dash-section">
|
||||
<h3>🎯 Exercice du jour</h3>
|
||||
<div class="daily-exercise-card" id="daily-exercise">
|
||||
<p>Commence un jeu ou un exercice pour gagner des points !</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dash-section">
|
||||
<h3>🏆 Classement</h3>
|
||||
<div id="dash-leaderboard" class="leaderboard"></div>
|
||||
</div>
|
||||
|
||||
<div class="dash-section">
|
||||
<h3>📊 Ton activité (7 derniers jours)</h3>
|
||||
<div id="dash-history" class="history-chart"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TRANSLATOR -->
|
||||
<section id="tab-translator" class="tab">
|
||||
<div class="translator-card">
|
||||
<div class="lang-selector">
|
||||
<button id="lang-from" class="lang-btn" data-lang="pt">🇵🇹 Portugais</button>
|
||||
<button id="swap-lang" class="swap-btn">⇄</button>
|
||||
<button id="lang-to" class="lang-btn" data-lang="fr">🇫🇷 Français</button>
|
||||
</div>
|
||||
<div class="translator-body">
|
||||
<div class="input-group">
|
||||
<textarea id="input-text" placeholder="Écrivez ou collez le texte..." rows="4"></textarea>
|
||||
<div class="input-actions">
|
||||
<button id="listen-input" class="icon-btn" title="Écouter">🔊</button>
|
||||
<button id="clear-input" class="icon-btn" title="Effacer">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="translate-btn" class="primary-btn">Traduire</button>
|
||||
<div class="input-group output-group">
|
||||
<div id="output-text" class="output-area" contenteditable="false"></div>
|
||||
<div class="input-actions">
|
||||
<button id="listen-output" class="icon-btn" title="Écouter">🔊</button>
|
||||
<button id="copy-output" class="icon-btn" title="Copier">📋</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PHRASES -->
|
||||
<section id="tab-phrases" class="tab">
|
||||
<div class="category-tabs" id="phrase-categories"></div>
|
||||
<div id="phrases-list" class="card-list"></div>
|
||||
</section>
|
||||
|
||||
<!-- GAMES -->
|
||||
<section id="tab-games" class="tab">
|
||||
<h2>Choisis un jeu !</h2>
|
||||
<div class="game-menu">
|
||||
<div class="game-card" data-game="colors">
|
||||
<div class="game-emoji">🎨</div>
|
||||
<h3>Les Couleurs</h3>
|
||||
<p>Apprends les couleurs</p>
|
||||
</div>
|
||||
<div class="game-card" data-game="animals">
|
||||
<div class="game-emoji">🐾</div>
|
||||
<h3>Les Animaux</h3>
|
||||
<p>Devine les animaux</p>
|
||||
</div>
|
||||
<div class="game-card" data-game="numbers">
|
||||
<div class="game-emoji">🔢</div>
|
||||
<h3>Les Nombres</h3>
|
||||
<p>Compte en portugais</p>
|
||||
</div>
|
||||
<div class="game-card" data-game="memory">
|
||||
<div class="game-emoji">🧠</div>
|
||||
<h3>Memory</h3>
|
||||
<p>Retrouve les paires</p>
|
||||
</div>
|
||||
<div class="game-card" data-game="quiz">
|
||||
<div class="game-emoji">❓</div>
|
||||
<h3>Quiz</h3>
|
||||
<p>Teste-toi !</p>
|
||||
</div>
|
||||
<div class="game-card" data-game="dragdrop">
|
||||
<div class="game-emoji">🎯</div>
|
||||
<h3>Associe</h3>
|
||||
<p>Relie les mots</p>
|
||||
</div>
|
||||
<div class="game-card" data-game="scramble">
|
||||
<div class="game-emoji">🔤</div>
|
||||
<h3>Embaralha</h3>
|
||||
<p>Remets les lettres !</p>
|
||||
</div>
|
||||
<div class="game-card" data-game="bingo">
|
||||
<div class="game-emoji">🎤</div>
|
||||
<h3>Bingo</h3>
|
||||
<p>Écoute et trouve !</p>
|
||||
</div>
|
||||
<div class="game-card" data-game="catch">
|
||||
<div class="game-emoji">🖐️</div>
|
||||
<h3>Apanha</h3>
|
||||
<p>Tape sur les mots !</p>
|
||||
</div>
|
||||
<div class="game-card" data-game="riddle">
|
||||
<div class="game-emoji">🤔</div>
|
||||
<h3>O que é?</h3>
|
||||
<p>Devinettes amusantes</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="game-area" class="hidden"></div>
|
||||
<button id="back-to-games" class="secondary-btn hidden">← Retour aux jeux</button>
|
||||
</section>
|
||||
|
||||
<!-- EXERCISES -->
|
||||
<section id="tab-exercises" class="tab">
|
||||
<h2>Exercices</h2>
|
||||
<div class="game-menu">
|
||||
<div class="game-card" data-exercise="fill">
|
||||
<div class="game-emoji">✏️</div>
|
||||
<h3>Complète la phrase</h3>
|
||||
<p>Remplis les mots manquants</p>
|
||||
</div>
|
||||
<div class="game-card" data-exercise="listenwrite">
|
||||
<div class="game-emoji">👂</div>
|
||||
<h3>Écoute et écris</h3>
|
||||
<p>Tape ce que tu entends</p>
|
||||
</div>
|
||||
<div class="game-card" data-exercise="translate">
|
||||
<div class="game-emoji">🌐</div>
|
||||
<h3>Traduis la phrase</h3>
|
||||
<p>FR → PT</p>
|
||||
</div>
|
||||
<div class="game-card" data-exercise="conjugation">
|
||||
<div class="game-emoji">📚</div>
|
||||
<h3>Conjugaison</h3>
|
||||
<p>Conjugue les verbes</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="exercise-area" class="hidden"></div>
|
||||
<button id="back-to-exercises" class="secondary-btn hidden">← Retour aux exercices</button>
|
||||
</section>
|
||||
|
||||
<!-- VOCABULARY -->
|
||||
<section id="tab-vocab" class="tab">
|
||||
<div class="category-tabs" id="vocab-categories"></div>
|
||||
<div id="vocab-grid" class="vocab-grid"></div>
|
||||
</section>
|
||||
|
||||
<!-- ADMIN -->
|
||||
<section id="tab-admin" class="tab">
|
||||
<h2>Administration</h2>
|
||||
<div class="admin-section">
|
||||
<h3>Ajouter un utilisateur</h3>
|
||||
<form id="add-user-form" class="admin-form">
|
||||
<input type="text" id="new-username" placeholder="Nom d'utilisateur" required>
|
||||
<input type="text" id="new-displayname" placeholder="Nom affiché" required>
|
||||
<input type="password" id="new-password" placeholder="Mot de passe" required>
|
||||
<select id="new-avatar">
|
||||
<option value="🇵🇹">🇵🇹 Portugal</option>
|
||||
<option value="👦">👦 Garçon</option>
|
||||
<option value="👧">👧 Fille</option>
|
||||
<option value="👨">👨 Homme</option>
|
||||
<option value="👩">👩 Femme</option>
|
||||
<option value="🧒">🧒 Enfant</option>
|
||||
<option value="👦🏻">👦🏻 Petit garçon</option>
|
||||
<option value="👧🏻">👧🏻 Petite fille</option>
|
||||
</select>
|
||||
<button type="submit" class="primary-btn">Ajouter</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="admin-section">
|
||||
<h3>Utilisateurs</h3>
|
||||
<div id="admin-users-list" class="admin-users"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast hidden"></div>
|
||||
|
||||
<script src="data.js?v=4"></script>
|
||||
<script src="app.js?v=4"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<meta name="theme-color" content="#006600">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<title>Connexion - Portugais pour Tous</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body class="auth-page">
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-flag">🇵🇹</div>
|
||||
<h1>Portugais pour Tous</h1>
|
||||
<p class="auth-subtitle">Connecte-toi pour apprendre !</p>
|
||||
|
||||
<form id="login-form" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label for="login-username">Nom d'utilisateur</label>
|
||||
<input type="text" id="login-username" placeholder="Ton prénom..." autocomplete="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="login-password">Mot de passe</label>
|
||||
<input type="password" id="login-password" placeholder="Mot de passe..." autocomplete="current-password" required>
|
||||
</div>
|
||||
<div id="login-error" class="form-error hidden"></div>
|
||||
<button type="submit" class="auth-btn">
|
||||
<span>Se connecter</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p>Apprends le portugais en t'amusant !</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
if (localStorage.getItem('pt_token')) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
document.getElementById('login-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const errorEl = document.getElementById('login-error');
|
||||
errorEl.classList.add('hidden');
|
||||
|
||||
const username = document.getElementById('login-username').value.trim();
|
||||
const password = document.getElementById('login-password').value;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
|
||||
localStorage.setItem('pt_token', data.token);
|
||||
localStorage.setItem('pt_user', JSON.stringify(data.user));
|
||||
window.location.href = '/';
|
||||
} catch (err) {
|
||||
errorEl.textContent = err.message;
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "Portugais pour Tous",
|
||||
"short_name": "Portugais",
|
||||
"description": "Apprenez le portugais en t'amusant",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#00873E",
|
||||
"theme_color": "#00873E",
|
||||
"orientation": "portrait",
|
||||
"icons": [
|
||||
{ "src": "icons/icon-192.svg", "sizes": "192x192", "type": "image/svg+xml" },
|
||||
{ "src": "icons/icon-512.svg", "sizes": "512x512", "type": "image/svg+xml" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||||
:root{--green:#00873E;--green-light:#00A854;--green-dark:#005A2B;--green-bg:#E8F5E9;--red:#E53935;--gold:#FFB300;--bg:#F5F7FA;--card:#FFFFFF;--text:#1A2332;--text-light:#6B7A8D;--border:#E2E8F0;--shadow:0 2px 16px rgba(0,0,0,0.06);--shadow-lg:0 8px 32px rgba(0,0,0,0.1);--radius:16px;--radius-sm:10px;--safe-bottom:env(safe-area-inset-bottom,0px)}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);min-height:100dvh;overflow-x:hidden}
|
||||
#app{max-width:480px;margin:0 auto;min-height:100dvh;display:flex;flex-direction:column}
|
||||
|
||||
/* Auth */
|
||||
.auth-page{background:linear-gradient(160deg,#006600 0%,#004400 40%,#002200 100%);display:flex;align-items:center;justify-content:center;min-height:100dvh;padding:20px}
|
||||
.auth-container{width:100%;max-width:400px}
|
||||
.auth-card{background:var(--card);border-radius:24px;padding:40px 28px;box-shadow:var(--shadow-lg);text-align:center}
|
||||
.auth-flag{font-size:64px;margin-bottom:8px}
|
||||
.auth-card h1{font-size:1.4em;font-weight:800;color:var(--green-dark);margin-bottom:4px}
|
||||
.auth-subtitle{color:var(--text-light);font-size:14px;margin-bottom:28px}
|
||||
.auth-form{display:flex;flex-direction:column;gap:16px}
|
||||
.form-group{display:flex;flex-direction:column;gap:6px;text-align:left}
|
||||
.form-group label{font-size:13px;font-weight:600;color:var(--text-light)}
|
||||
.form-group input{padding:14px 16px;border:2px solid var(--border);border-radius:var(--radius-sm);font-size:16px;font-family:inherit;transition:border-color .2s}
|
||||
.form-group input:focus{outline:none;border-color:var(--green)}
|
||||
.form-error{background:#FEE;border:1px solid #FCC;color:var(--red);padding:10px;border-radius:var(--radius-sm);font-size:13px;text-align:center}
|
||||
.auth-btn{padding:16px;border:none;border-radius:var(--radius-sm);background:linear-gradient(135deg,var(--green),var(--green-light));color:#fff;font-size:16px;font-weight:700;cursor:pointer;transition:transform .2s;box-shadow:0 4px 16px rgba(0,135,62,.3)}
|
||||
.auth-btn:active{transform:scale(.97)}
|
||||
.auth-footer{text-align:center;margin-top:24px;color:rgba(255,255,255,.7);font-size:13px}
|
||||
|
||||
/* Header */
|
||||
header{background:linear-gradient(135deg,var(--green),var(--green-dark));color:#fff;padding:14px 16px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;box-shadow:0 2px 12px rgba(0,135,62,.25)}
|
||||
.header-left{display:flex;align-items:center;gap:8px}
|
||||
.header-flag{font-size:28px}
|
||||
.header-left h1{font-size:1.1em;font-weight:700}
|
||||
.header-right{display:flex;align-items:center;gap:10px}
|
||||
.user-points-badge{display:flex;align-items:center;gap:4px;background:rgba(255,255,255,.2);border-radius:20px;padding:4px 12px;font-size:14px;font-weight:700}
|
||||
.user-avatar{width:36px;height:36px;border-radius:50%;background:rgba(255,255,255,.2);display:flex;align-items:center;justify-content:center;font-size:20px;cursor:pointer}
|
||||
|
||||
/* Bottom Nav */
|
||||
#bottom-nav{position:fixed;bottom:0;left:50%;transform:translateX(-50%);width:100%;max-width:480px;display:flex;background:var(--card);border-top:1px solid var(--border);padding-bottom:var(--safe-bottom);z-index:100;box-shadow:0 -2px 12px rgba(0,0,0,.08)}
|
||||
.nav-btn{flex:1;display:flex;flex-direction:column;align-items:center;padding:6px 2px;border:none;background:none;cursor:pointer;color:var(--text-light);transition:all .2s;font-size:9px}
|
||||
.nav-btn.active{color:var(--green);font-weight:700}
|
||||
.nav-icon{font-size:18px;margin-bottom:1px}
|
||||
.nav-label{font-size:9px;line-height:1}
|
||||
|
||||
/* Main */
|
||||
main{flex:1;padding:16px;padding-bottom:80px}
|
||||
.tab{display:none;animation:fadeIn .2s ease}
|
||||
.tab.active{display:block}
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
|
||||
|
||||
/* Dashboard */
|
||||
.dashboard-welcome{margin-bottom:20px}
|
||||
.dashboard-welcome h2{font-size:1.3em;font-weight:800;color:var(--green-dark)}
|
||||
.dashboard-welcome p{color:var(--text-light);font-size:14px}
|
||||
.stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:20px}
|
||||
.stat-card{background:var(--card);border-radius:var(--radius);padding:16px;text-align:center;box-shadow:var(--shadow);position:relative;overflow:hidden}
|
||||
.stat-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px}
|
||||
.stat-points::before{background:var(--gold)}
|
||||
.stat-streak::before{background:var(--red)}
|
||||
.stat-today::before{background:var(--green)}
|
||||
.stat-exercises::before{background:#7C4DFF}
|
||||
.stat-icon{font-size:24px;margin-bottom:4px}
|
||||
.stat-value{font-size:28px;font-weight:900;color:var(--text)}
|
||||
.stat-label{font-size:11px;color:var(--text-light);font-weight:500}
|
||||
.dash-section{margin-bottom:20px}
|
||||
.dash-section h3{font-size:15px;font-weight:700;color:var(--text);margin-bottom:10px}
|
||||
.daily-exercise-card{background:var(--card);border-radius:var(--radius);padding:20px;box-shadow:var(--shadow);text-align:center}
|
||||
.daily-exercise-card p{color:var(--text-light);font-size:14px}
|
||||
|
||||
/* Leaderboard */
|
||||
.leaderboard{background:var(--card);border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden}
|
||||
.lb-row{display:flex;align-items:center;padding:12px 16px;border-bottom:1px solid var(--border);gap:10px}
|
||||
.lb-row:last-child{border-bottom:none}
|
||||
.lb-me{background:var(--green-bg)}
|
||||
.lb-rank{font-size:18px;width:32px;text-align:center}
|
||||
.lb-avatar{font-size:20px}
|
||||
.lb-name{flex:1;font-weight:600;font-size:14px}
|
||||
.lb-points{font-weight:700;color:var(--green);font-size:13px}
|
||||
.empty-state{text-align:center;color:var(--text-light);padding:20px;font-size:14px}
|
||||
|
||||
/* History Chart */
|
||||
.chart-bars{display:flex;gap:6px;align-items:flex-end;height:120px;padding:10px;background:var(--card);border-radius:var(--radius);box-shadow:var(--shadow)}
|
||||
.chart-col{flex:1;display:flex;flex-direction:column;align-items:center;height:100%}
|
||||
.chart-bar{width:100%;background:linear-gradient(180deg,var(--green),var(--green-light));border-radius:6px 6px 0 0;margin-top:auto;min-height:4px;transition:height .5s}
|
||||
.chart-label{font-size:10px;color:var(--text-light);margin-top:4px}
|
||||
.chart-val{font-size:10px;font-weight:700;color:var(--green)}
|
||||
|
||||
/* Translator */
|
||||
.translator-card{background:var(--card);border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden}
|
||||
.lang-selector{display:flex;align-items:center;padding:12px;background:linear-gradient(135deg,var(--green),var(--green-light));color:#fff}
|
||||
.lang-btn{flex:1;padding:10px;border:2px solid rgba(255,255,255,.3);border-radius:var(--radius-sm);background:rgba(255,255,255,.15);color:#fff;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s}
|
||||
.lang-btn:active{transform:scale(.95)}
|
||||
.swap-btn{width:44px;height:44px;border-radius:50%;border:none;background:var(--gold);color:var(--green-dark);font-size:20px;font-weight:900;cursor:pointer;margin:0 8px;transition:transform .3s;box-shadow:0 2px 8px rgba(0,0,0,.2)}
|
||||
.swap-btn:active{transform:rotate(180deg) scale(.9)}
|
||||
.translator-body{padding:16px}
|
||||
.input-group{position:relative;margin-bottom:12px}
|
||||
textarea,.output-area{width:100%;padding:14px;padding-right:50px;border:2px solid var(--border);border-radius:var(--radius-sm);font-size:16px;font-family:inherit;resize:none;transition:border-color .2s;background:#FAFBFC}
|
||||
textarea:focus,.output-area:focus{outline:none;border-color:var(--green)}
|
||||
.output-area{min-height:80px;background:#F0FFF4;cursor:pointer}
|
||||
.output-group{margin-bottom:0}
|
||||
.input-actions{position:absolute;right:8px;top:8px;display:flex;flex-direction:column;gap:4px}
|
||||
.icon-btn{width:36px;height:36px;border:none;border-radius:var(--radius-sm);background:rgba(0,135,62,.1);cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center;transition:all .2s}
|
||||
.icon-btn:active{background:var(--green);color:#fff;transform:scale(.9)}
|
||||
|
||||
/* Buttons */
|
||||
.primary-btn{width:100%;padding:14px;border:none;border-radius:var(--radius-sm);background:linear-gradient(135deg,var(--green),var(--green-light));color:#fff;font-size:16px;font-weight:700;cursor:pointer;transition:all .2s;margin-bottom:12px;box-shadow:0 4px 12px rgba(0,135,62,.2)}
|
||||
.primary-btn:active{transform:scale(.97)}
|
||||
.secondary-btn{padding:12px 24px;border:2px solid var(--green);border-radius:var(--radius-sm);background:#fff;color:var(--green);font-size:14px;font-weight:600;cursor:pointer;transition:all .2s;margin-top:16px;display:block;width:100%}
|
||||
.secondary-btn:active{background:var(--green);color:#fff}
|
||||
|
||||
/* Category Tabs */
|
||||
.category-tabs{display:flex;gap:8px;overflow-x:auto;padding-bottom:12px;margin-bottom:16px;scrollbar-width:none}
|
||||
.category-tabs::-webkit-scrollbar{display:none}
|
||||
.cat-tab{padding:8px 16px;border-radius:20px;border:2px solid var(--border);background:#fff;color:var(--text);font-size:13px;font-weight:500;white-space:nowrap;cursor:pointer;transition:all .2s}
|
||||
.cat-tab.active{background:var(--green);color:#fff;border-color:var(--green)}
|
||||
|
||||
/* Phrase Cards */
|
||||
.card-list{display:flex;flex-direction:column;gap:12px}
|
||||
.phrase-card{background:var(--card);border-radius:var(--radius);padding:16px;box-shadow:var(--shadow);transition:transform .2s}
|
||||
.phrase-card:active{transform:scale(.98)}
|
||||
.phrase-pt{font-size:18px;font-weight:700;color:var(--green-dark);margin-bottom:4px}
|
||||
.phrase-fr{font-size:15px;color:var(--text-light);margin-bottom:10px}
|
||||
.phonetic{font-size:12px;color:var(--text-light);font-style:italic;background:var(--green-bg);padding:3px 8px;border-radius:6px;display:inline-block;margin-bottom:8px}
|
||||
.phrase-actions{display:flex;gap:6px;flex-wrap:wrap}
|
||||
.phrase-actions button{padding:7px 12px;border:none;border-radius:8px;font-size:12px;font-weight:500;cursor:pointer;transition:all .2s}
|
||||
.btn-listen{background:var(--green);color:#fff}
|
||||
.btn-listen:active{transform:scale(.9)}
|
||||
.btn-slow{background:var(--gold);color:var(--green-dark)}
|
||||
.btn-favorite{background:#FEE;color:var(--red)}
|
||||
.btn-favorite.active{background:var(--red);color:#fff}
|
||||
|
||||
/* Games */
|
||||
h2{text-align:center;margin-bottom:20px;color:var(--green-dark);font-size:1.2em}
|
||||
.game-menu{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||
.game-card{background:var(--card);border-radius:var(--radius);padding:20px 12px;text-align:center;box-shadow:var(--shadow);cursor:pointer;transition:all .2s;border:2px solid transparent}
|
||||
.game-card:active{transform:scale(.95);border-color:var(--green)}
|
||||
.game-emoji{font-size:36px;margin-bottom:8px}
|
||||
.game-card h3{font-size:14px;margin-bottom:4px;color:var(--green-dark)}
|
||||
.game-card p{font-size:11px;color:var(--text-light)}
|
||||
|
||||
/* Game Area */
|
||||
#game-area,#exercise-area{background:var(--card);border-radius:var(--radius);padding:20px;box-shadow:var(--shadow);margin-top:16px;min-height:280px}
|
||||
.hidden{display:none !important}
|
||||
.game-header{text-align:center;margin-bottom:16px}
|
||||
.game-score{display:flex;justify-content:center;gap:20px;margin-bottom:12px}
|
||||
.score-item{text-align:center}
|
||||
.score-value{font-size:28px;font-weight:900;color:var(--green)}
|
||||
.score-label{font-size:11px;color:var(--text-light)}
|
||||
.quiz-sentence{font-size:20px;font-weight:700;color:var(--green);margin:16px auto;text-align:center;cursor:pointer;padding:12px;background:var(--green-bg);border-radius:var(--radius-sm)}
|
||||
|
||||
/* Option Buttons */
|
||||
.color-options,.animal-options,.number-options{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:16px}
|
||||
.option-btn{padding:16px;border:3px solid var(--border);border-radius:var(--radius);background:#fff;font-size:15px;font-weight:600;cursor:pointer;transition:all .2s;text-align:center}
|
||||
.option-btn:active{transform:scale(.95)}
|
||||
.option-btn.correct{border-color:var(--green);background:#E8F5E9;animation:correctPulse .5s}
|
||||
.option-btn.wrong{border-color:var(--red);background:#FFEBEE;animation:shake .4s}
|
||||
@keyframes correctPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.05)}}
|
||||
@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-8px)}75%{transform:translateX(8px)}}
|
||||
|
||||
/* Memory */
|
||||
.memory-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
|
||||
.memory-card{aspect-ratio:1;border:3px solid var(--border);border-radius:var(--radius);display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;cursor:pointer;transition:all .3s;background:linear-gradient(135deg,var(--green),var(--green-light));color:#fff}
|
||||
.memory-card.flipped{background:#fff;color:var(--text);border-color:var(--green)}
|
||||
.memory-card.matched{background:var(--gold);color:var(--green-dark);border-color:var(--gold);pointer-events:none}
|
||||
|
||||
/* Drag & Drop */
|
||||
.drag-items{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-bottom:20px}
|
||||
.drag-item{padding:10px 18px;background:var(--green);color:#fff;border-radius:var(--radius-sm);font-weight:600;cursor:pointer;transition:all .2s;user-select:none;-webkit-user-select:none}
|
||||
.drag-item:active{transform:scale(.95)}
|
||||
.drop-zone{min-height:50px;border:3px dashed var(--border);border-radius:var(--radius);display:flex;align-items:center;justify-content:center;margin-bottom:12px;padding:12px;transition:all .2s}
|
||||
.drop-zone-label{font-size:14px;font-weight:600;color:var(--text);margin-bottom:8px}
|
||||
.drop-zone-word{font-size:14px;font-weight:600;color:var(--text-light)}
|
||||
.dropped-word{padding:8px 16px;background:var(--green);color:#fff;border-radius:var(--radius-sm);font-weight:600}
|
||||
|
||||
/* Exercises */
|
||||
.exercise-hint{text-align:center;color:var(--text-light);margin-bottom:12px;font-size:14px}
|
||||
.exercise-question{font-size:22px;font-weight:700;text-align:center;margin-bottom:16px;color:var(--green-dark)}
|
||||
.fill-options{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:16px}
|
||||
.fill-word{padding:10px 16px;border:2px solid var(--border);border-radius:var(--radius-sm);background:#fff;font-size:15px;cursor:pointer;transition:all .2s}
|
||||
.fill-word:active{transform:scale(.95)}
|
||||
.fill-blank{display:inline-block;min-width:80px;padding:4px 10px;border-bottom:3px solid var(--green);font-weight:700;color:var(--green);margin:0 4px}
|
||||
.fill-blank.correct-blank{background:#E8F5E9;border-radius:6px}
|
||||
.fill-blank.wrong-blank{background:#FFEBEE;border-color:var(--red)}
|
||||
.exercise-input{width:100%;padding:14px;border:2px solid var(--border);border-radius:var(--radius-sm);font-size:18px;text-align:center;font-family:inherit;margin-top:12px;transition:border-color .2s}
|
||||
.exercise-input:focus{outline:none;border-color:var(--green)}
|
||||
|
||||
/* Vocab */
|
||||
.vocab-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||||
.vocab-card{background:var(--card);border-radius:var(--radius);padding:16px;text-align:center;box-shadow:var(--shadow);cursor:pointer;transition:all .2s}
|
||||
.vocab-card:active{transform:scale(.95)}
|
||||
.vocab-word{font-size:16px;font-weight:700;color:var(--green-dark);margin-bottom:4px}
|
||||
.vocab-translation{font-size:13px;color:var(--text-light);margin-bottom:8px}
|
||||
.vocab-emoji{font-size:30px}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-bar{width:100%;height:6px;background:var(--border);border-radius:3px;margin:10px 0;overflow:hidden}
|
||||
.progress-fill{height:100%;background:linear-gradient(90deg,var(--green),var(--gold));border-radius:3px;transition:width .5s ease}
|
||||
|
||||
/* Admin */
|
||||
.admin-section{background:var(--card);border-radius:var(--radius);padding:20px;box-shadow:var(--shadow);margin-bottom:16px}
|
||||
.admin-section h3{font-size:16px;font-weight:700;color:var(--green-dark);margin-bottom:12px}
|
||||
.admin-form{display:flex;flex-direction:column;gap:12px}
|
||||
.admin-form input,.admin-form select{padding:12px;border:2px solid var(--border);border-radius:var(--radius-sm);font-size:15px;font-family:inherit}
|
||||
.admin-form input:focus,.admin-form select:focus{outline:none;border-color:var(--green)}
|
||||
.admin-users{display:flex;flex-direction:column;gap:8px}
|
||||
.admin-user-card{display:flex;align-items:center;justify-content:space-between;padding:12px;background:var(--bg);border-radius:var(--radius-sm);gap:8px}
|
||||
.admin-user-info{display:flex;align-items:center;gap:10px}
|
||||
.admin-avatar{font-size:24px}
|
||||
.admin-user-info strong{font-size:14px;display:block}
|
||||
.admin-user-info small{color:var(--text-light);font-size:12px}
|
||||
.admin-user-actions{display:flex;align-items:center;gap:8px}
|
||||
.admin-points{font-size:13px;font-weight:700;color:var(--green)}
|
||||
.admin-btn-toggle{padding:6px 10px;border:none;border-radius:6px;background:var(--green-bg);color:var(--green);font-size:11px;font-weight:600;cursor:pointer}
|
||||
.admin-btn-delete{padding:6px 8px;border:none;border-radius:6px;background:#FFEBEE;color:var(--red);font-size:14px;cursor:pointer}
|
||||
|
||||
/* Toast */
|
||||
.toast{position:fixed;bottom:100px;left:50%;transform:translateX(-50%);padding:12px 24px;border-radius:12px;background:var(--green-dark);color:#fff;font-weight:600;font-size:14px;z-index:1000;box-shadow:0 4px 16px rgba(0,0,0,.3);animation:toastIn .3s ease;white-space:nowrap}
|
||||
@keyframes toastIn{from{opacity:0;transform:translateX(-50%) translateY(20px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
|
||||
|
||||
/* Celebration */
|
||||
.celebration{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:999}
|
||||
.confetti{position:absolute;width:10px;height:10px;border-radius:2px;animation:confettiFall 2s ease forwards}
|
||||
@keyframes confettiFall{0%{transform:translateY(-20px) rotate(0);opacity:1}100%{transform:translateY(100vh) rotate(720deg);opacity:0}}
|
||||
|
||||
@media(max-width:380px){.game-menu{grid-template-columns:1fr}.vocab-grid{grid-template-columns:1fr}}
|
||||
|
||||
/* Scramble Game */
|
||||
.scramble-display{display:flex;gap:8px;justify-content:center;margin:16px 0}
|
||||
.scramble-letter{width:48px;height:56px;border:3px solid var(--green);border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;font-size:28px;font-weight:900;color:var(--green-dark);background:var(--green-bg);animation:letterPop .3s ease}
|
||||
@keyframes letterPop{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}
|
||||
.scramble-input{text-align:center;font-size:22px;font-weight:700;letter-spacing:2px}
|
||||
|
||||
/* Bingo Game */
|
||||
.bingo-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:16px}
|
||||
.bingo-card{padding:16px 8px;border:3px solid var(--border);border-radius:var(--radius);background:#fff;cursor:pointer;transition:all .2s;text-align:center;display:flex;flex-direction:column;align-items:center;gap:6px}
|
||||
.bingo-card:active{transform:scale(.95);border-color:var(--green)}
|
||||
.bingo-card.correct{border-color:var(--green);background:#E8F5E9;animation:correctPulse .5s}
|
||||
.bingo-card.wrong{border-color:var(--red);background:#FFEBEE;animation:shake .4s}
|
||||
.bingo-emoji{font-size:32px}
|
||||
.bingo-word{font-size:13px;font-weight:700;color:var(--text)}
|
||||
.bingo-listen-btn{background:linear-gradient(135deg,var(--green),var(--green-light));color:#fff;border:none;cursor:pointer;box-shadow:0 4px 16px rgba(0,135,62,.3);transition:all .2s}
|
||||
.bingo-listen-btn:active{transform:scale(.9);box-shadow:0 2px 8px rgba(0,135,62,.2)}
|
||||
|
||||
/* Catch Words Game */
|
||||
.catch-zone{position:relative;width:100%;height:400px;border:3px dashed var(--border);border-radius:var(--radius);overflow:hidden;background:linear-gradient(180deg,#E8F5E9 0%,#fff 100%)}
|
||||
.catch-word{position:absolute;top:-60px;display:flex;align-items:center;gap:6px;padding:10px 16px;background:var(--green);color:#fff;border-radius:var(--radius-sm);font-weight:700;font-size:16px;cursor:pointer;transition:all .3s;animation:catchFall 3s linear forwards;z-index:10}
|
||||
.catch-word:active{transform:scale(1.1)}
|
||||
.catch-word.caught{background:var(--gold);color:var(--green-dark);animation:none;transform:scale(1.2);opacity:0;transition:all .3s}
|
||||
.catch-word.missed-word{background:var(--red);opacity:0.5;animation:none}
|
||||
.catch-emoji{font-size:24px}
|
||||
.catch-text{font-size:16px}
|
||||
@keyframes catchFall{0%{top:-60px}100%{top:420px}}
|
||||
|
||||
/* Riddle Game */
|
||||
.riddle-card{background:linear-gradient(135deg,var(--green-bg),#fff);border:2px solid var(--green);border-radius:var(--radius);padding:20px;margin:16px 0;text-align:center}
|
||||
.riddle-text{font-size:18px;font-weight:700;color:var(--green-dark);line-height:1.4}
|
||||
.riddle-fr{margin-top:8px}
|
||||
.riddle-options{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:16px}
|
||||
.riddle-opt{padding:14px;border:3px solid var(--border);border-radius:var(--radius);background:#fff;font-size:15px;font-weight:600;cursor:pointer;transition:all .2s;text-align:center}
|
||||
.riddle-opt:active{transform:scale(.95)}
|
||||
.riddle-opt.correct{border-color:var(--green);background:#E8F5E9;animation:correctPulse .5s}
|
||||
.riddle-opt.wrong{border-color:var(--red);background:#FFEBEE;animation:shake .4s}
|
||||
@@ -0,0 +1,5 @@
|
||||
const CACHE_NAME='pt-v5';
|
||||
const urls=['/','/login.html'];
|
||||
self.addEventListener('install',e=>{e.waitUntil(caches.open(CACHE_NAME).then(c=>c.addAll(urls)));self.skipWaiting()});
|
||||
self.addEventListener('activate',e=>{e.waitUntil(caches.keys().then(n=>Promise.all(n.filter(x=>x!==CACHE_NAME).map(x=>caches.delete(x)))).then(()=>self.clients.claim()))});
|
||||
self.addEventListener('fetch',e=>{if(e.request.method!=='GET')return;e.respondWith(fetch(e.request).then(r=>{const clone=r.clone();caches.open(CACHE_NAME).then(c=>c.put(e.request,clone));return r}).catch(()=>caches.match(e.request)))});
|
||||
Reference in New Issue
Block a user