490 lines
15 KiB
JavaScript
490 lines
15 KiB
JavaScript
// Mobile menu toggle
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const toggle = document.querySelector('.menu-toggle');
|
|
const navList = document.querySelector('.nav-list');
|
|
|
|
if (toggle) {
|
|
toggle.addEventListener('click', function() {
|
|
navList.classList.toggle('open');
|
|
});
|
|
}
|
|
|
|
// Close menu on link click
|
|
const navLinks = document.querySelectorAll('.nav-list a');
|
|
navLinks.forEach(function(link) {
|
|
link.addEventListener('click', function() {
|
|
if (navList) navList.classList.remove('open');
|
|
});
|
|
});
|
|
|
|
// Highlight active page in nav
|
|
const currentPage = window.location.pathname.split('/').pop() || 'index.html';
|
|
navLinks.forEach(function(link) {
|
|
const href = link.getAttribute('href');
|
|
if (href === currentPage) {
|
|
link.classList.add('active');
|
|
}
|
|
});
|
|
});
|
|
|
|
// --- Exercises ---
|
|
function checkExercise(btn) {
|
|
const exercise = btn.closest('.exercise');
|
|
const selected = exercise.querySelector('input[type="radio"]:checked');
|
|
const feedback = exercise.querySelector('.feedback');
|
|
|
|
if (!selected) {
|
|
feedback.textContent = 'Please select an answer!';
|
|
feedback.className = 'feedback wrong';
|
|
feedback.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
const options = exercise.querySelectorAll('.option');
|
|
options.forEach(function(opt) {
|
|
opt.classList.remove('correct', 'wrong');
|
|
});
|
|
|
|
const parent = selected.closest('.option');
|
|
const isCorrect = selected.value === 'true';
|
|
|
|
if (isCorrect) {
|
|
parent.classList.add('correct');
|
|
feedback.textContent = 'Correct! +10 XP';
|
|
feedback.className = 'feedback correct';
|
|
var exId = exercise.getAttribute('data-ex-id') || exercise.querySelector('h3').textContent.trim();
|
|
markExerciseDone(exId);
|
|
} else {
|
|
parent.classList.add('wrong');
|
|
feedback.textContent = 'Try again!';
|
|
feedback.className = 'feedback wrong';
|
|
options.forEach(function(opt) {
|
|
const radio = opt.querySelector('input[type="radio"]');
|
|
if (radio && radio.value === 'true') {
|
|
opt.classList.add('correct');
|
|
}
|
|
});
|
|
}
|
|
feedback.style.display = 'block';
|
|
}
|
|
|
|
function checkFill(input) {
|
|
const exercise = input.closest('.exercise');
|
|
const answer = input.getAttribute('data-answer');
|
|
const feedback = exercise.querySelector('.feedback');
|
|
|
|
input.classList.remove('correct', 'wrong');
|
|
|
|
if (input.value.trim().toLowerCase() === answer.toLowerCase()) {
|
|
input.classList.add('correct');
|
|
feedback.textContent = 'Correct! +10 XP';
|
|
feedback.className = 'feedback correct';
|
|
var exId = input.closest('.exercise').getAttribute('data-ex-id') || input.closest('.exercise').querySelector('h3').textContent.trim();
|
|
markExerciseDone(exId);
|
|
} else {
|
|
input.classList.add('wrong');
|
|
feedback.textContent = 'Try again! Correct answer: ' + answer;
|
|
feedback.className = 'feedback wrong';
|
|
}
|
|
feedback.style.display = 'block';
|
|
}
|
|
|
|
function checkTranslate(btn) {
|
|
const exercise = btn.closest('.exercise');
|
|
const input = exercise.querySelector('input[type="text"]');
|
|
const feedback = exercise.querySelector('.feedback');
|
|
const answer = input.getAttribute('data-answer');
|
|
|
|
input.classList.remove('correct', 'wrong');
|
|
|
|
if (input.value.trim().toLowerCase() === answer.toLowerCase()) {
|
|
input.classList.add('correct');
|
|
feedback.textContent = 'Correct! +10 XP';
|
|
feedback.className = 'feedback correct';
|
|
var exId = exercise.getAttribute('data-ex-id') || exercise.querySelector('h3').textContent.trim();
|
|
markExerciseDone(exId);
|
|
} else {
|
|
input.classList.add('wrong');
|
|
feedback.textContent = 'Try again! Correct answer: ' + answer;
|
|
feedback.className = 'feedback wrong';
|
|
}
|
|
feedback.style.display = 'block';
|
|
}
|
|
|
|
function checkMatch(btn) {
|
|
const exercise = btn.closest('.exercise');
|
|
const inputs = exercise.querySelectorAll('input[type="text"]');
|
|
const feedback = exercise.querySelector('.feedback');
|
|
let allCorrect = true;
|
|
|
|
inputs.forEach(function(input) {
|
|
const answer = input.getAttribute('data-answer');
|
|
input.classList.remove('correct', 'wrong');
|
|
if (input.value.trim().toLowerCase() === answer.toLowerCase()) {
|
|
input.classList.add('correct');
|
|
} else {
|
|
input.classList.add('wrong');
|
|
allCorrect = false;
|
|
}
|
|
});
|
|
|
|
if (allCorrect) {
|
|
feedback.textContent = 'All correct! Great job! +10 XP';
|
|
feedback.className = 'feedback correct';
|
|
var exId = exercise.getAttribute('data-ex-id') || exercise.querySelector('h3').textContent.trim();
|
|
markExerciseDone(exId);
|
|
} else {
|
|
feedback.textContent = 'Some answers are wrong. Try again!';
|
|
feedback.className = 'feedback wrong';
|
|
}
|
|
feedback.style.display = 'block';
|
|
}
|
|
|
|
// --- Tests ---
|
|
function checkTest() {
|
|
let score = 0;
|
|
const total = document.querySelectorAll('.test-question').length;
|
|
const questions = document.querySelectorAll('.test-question');
|
|
|
|
questions.forEach(function(q) {
|
|
const selected = q.querySelector('input[type="radio"]:checked');
|
|
if (selected && selected.value === 'true') {
|
|
score++;
|
|
}
|
|
});
|
|
|
|
markTestDone(score, total);
|
|
|
|
var data = getProgress();
|
|
var info = getLevelProgress(data.xp);
|
|
|
|
const resultBox = document.getElementById('test-result');
|
|
const scoreDisplay = document.getElementById('score');
|
|
scoreDisplay.textContent = score + '/' + total;
|
|
resultBox.classList.add('show');
|
|
|
|
var detail = document.getElementById('test-detail');
|
|
if (detail) {
|
|
detail.innerHTML = 'Level: <strong>' + info.current.label + '</strong> | XP: <strong>' + data.xp + '</strong> | Best: <strong>' + data.bestTestScore + '/' + total + '</strong>';
|
|
}
|
|
|
|
resultBox.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
|
|
// --- Contact form ---
|
|
function submitForm(event) {
|
|
event.preventDefault();
|
|
const message = document.getElementById('form-message');
|
|
message.classList.add('show');
|
|
document.getElementById('contact-form').reset();
|
|
setTimeout(function() {
|
|
message.classList.remove('show');
|
|
}, 5000);
|
|
}
|
|
|
|
// --- Pronunciation ---
|
|
function speakWord(word, rate) {
|
|
if (!window.speechSynthesis) {
|
|
alert('Speech synthesis is not supported in your browser.');
|
|
return;
|
|
}
|
|
window.speechSynthesis.cancel();
|
|
var utterance = new SpeechSynthesisUtterance(word);
|
|
utterance.lang = 'en-US';
|
|
utterance.rate = rate || 0.9;
|
|
utterance.pitch = 1;
|
|
window.speechSynthesis.speak(utterance);
|
|
}
|
|
|
|
function repeatWord(btn) {
|
|
var word = btn.getAttribute('data-word');
|
|
speakWord(word, 0.8);
|
|
btn.textContent = '🔊 Playing...';
|
|
markWordListened();
|
|
setTimeout(function() { btn.textContent = '🔊 Listen'; }, 1500);
|
|
}
|
|
|
|
function speakSlow(btn) {
|
|
var word = btn.getAttribute('data-word');
|
|
speakWord(word, 0.4);
|
|
btn.textContent = '🐢 Slow...';
|
|
setTimeout(function() { btn.innerHTML = '🐢 Slow'; }, 2000);
|
|
}
|
|
|
|
function checkDictation(input) {
|
|
var exercise = input.closest('.exercise');
|
|
var answer = input.getAttribute('data-answer');
|
|
var feedback = exercise.querySelector('.feedback');
|
|
|
|
input.classList.remove('correct', 'wrong');
|
|
|
|
if (input.value.trim().toLowerCase() === answer.toLowerCase()) {
|
|
input.classList.add('correct');
|
|
feedback.textContent = 'Correct! Great listening!';
|
|
feedback.className = 'feedback correct';
|
|
} else {
|
|
input.classList.add('wrong');
|
|
feedback.textContent = 'Not quite. The correct word is: ' + answer;
|
|
feedback.className = 'feedback wrong';
|
|
}
|
|
feedback.style.display = 'block';
|
|
}
|
|
|
|
function speakPhrase(btn) {
|
|
var phrase = btn.getAttribute('data-phrase');
|
|
speakWord(phrase, 0.8);
|
|
btn.textContent = '🔊 Playing...';
|
|
setTimeout(function() { btn.innerHTML = '🔊 Hear Phrase'; }, 1500);
|
|
}
|
|
|
|
// --- Progress Tracking ---
|
|
var PROGRESS_KEY = 'english_easy_progress';
|
|
|
|
var LEVELS = [
|
|
{ min: 0, label: 'A0 Beginner', desc: 'You are just starting your English journey!' },
|
|
{ min: 50, label: 'A1 Starter', desc: 'You know basic words and phrases.' },
|
|
{ min: 150, label: 'A2 Elementary', desc: 'You can understand simple sentences.' },
|
|
{ min: 300, label: 'B1 Intermediate', desc: 'You can talk about familiar topics.' },
|
|
{ min: 500, label: 'B2 Upper Intermediate', desc: 'You can communicate fluently.' },
|
|
{ min: 800, label: 'C1 Advanced', desc: 'You understand complex texts and ideas.' }
|
|
];
|
|
|
|
function getProgress() {
|
|
var data = JSON.parse(localStorage.getItem(PROGRESS_KEY));
|
|
if (!data) {
|
|
data = {
|
|
xp: 0,
|
|
exercisesDone: [],
|
|
testsDone: 0,
|
|
bestTestScore: 0,
|
|
lastTestScore: 0,
|
|
wordsListened: 0,
|
|
readingDone: [],
|
|
joined: new Date().toISOString().split('T')[0]
|
|
};
|
|
localStorage.setItem(PROGRESS_KEY, JSON.stringify(data));
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function saveProgress(data) {
|
|
localStorage.setItem(PROGRESS_KEY, JSON.stringify(data));
|
|
}
|
|
|
|
function addXP(amount, reason) {
|
|
var data = getProgress();
|
|
data.xp += amount;
|
|
saveProgress(data);
|
|
updateLevelBadge();
|
|
return data;
|
|
}
|
|
|
|
function getLevel(xp) {
|
|
for (var i = LEVELS.length - 1; i >= 0; i--) {
|
|
if (xp >= LEVELS[i].min) return LEVELS[i];
|
|
}
|
|
return LEVELS[0];
|
|
}
|
|
|
|
function getLevelProgress(xp) {
|
|
var current = getLevel(xp);
|
|
var nextMin = 9999;
|
|
for (var i = 0; i < LEVELS.length; i++) {
|
|
if (LEVELS[i].min > current.min) {
|
|
nextMin = LEVELS[i].min;
|
|
break;
|
|
}
|
|
}
|
|
var range = nextMin - current.min;
|
|
var progress = xp - current.min;
|
|
return { current: current, nextMin: nextMin, progress: progress, range: range, percent: range > 0 ? Math.min(100, Math.round(progress / range * 100)) : 100 };
|
|
}
|
|
|
|
function updateLevelBadge() {
|
|
var data = getProgress();
|
|
var info = getLevelProgress(data.xp);
|
|
var badge = document.querySelector('.level-badge');
|
|
if (badge) {
|
|
badge.innerHTML = '<span class="level-label">' + info.current.label + '</span><span class="level-xp">' + data.xp + ' XP</span>';
|
|
}
|
|
var bar = document.querySelector('.level-bar-fill');
|
|
if (bar) bar.style.width = info.percent + '%';
|
|
}
|
|
|
|
function markExerciseDone(id) {
|
|
var data = getProgress();
|
|
if (data.exercisesDone.indexOf(id) === -1) {
|
|
data.exercisesDone.push(id);
|
|
data.xp += 10;
|
|
saveProgress(data);
|
|
}
|
|
}
|
|
|
|
function markTestDone(score, total) {
|
|
var data = getProgress();
|
|
data.testsDone++;
|
|
data.lastTestScore = score;
|
|
if (score > data.bestTestScore) data.bestTestScore = score;
|
|
data.xp += 15 + score * 2;
|
|
saveProgress(data);
|
|
}
|
|
|
|
function markWordListened() {
|
|
var data = getProgress();
|
|
data.wordsListened++;
|
|
if (data.wordsListened % 5 === 0) {
|
|
data.xp += 5;
|
|
saveProgress(data);
|
|
}
|
|
}
|
|
|
|
// --- Daily Reminder at 20:00 ---
|
|
(function() {
|
|
var LS_KEY = 'english_easy_daily';
|
|
var TOAST_SHOWN = false;
|
|
|
|
function getToday() {
|
|
return new Date().toDateString();
|
|
}
|
|
|
|
function getStreak() {
|
|
var data = JSON.parse(localStorage.getItem(LS_KEY) || '{"streak":0,"lastDate":""}');
|
|
var yesterday = new Date();
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
var yesterdayStr = yesterday.toDateString();
|
|
var today = getToday();
|
|
if (data.lastDate === today) {
|
|
// already done today
|
|
} else if (data.lastDate === yesterdayStr) {
|
|
// consecutive day
|
|
} else if (data.lastDate && data.lastDate !== today) {
|
|
data.streak = 0;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function saveStreak(data) {
|
|
data.lastDate = getToday();
|
|
localStorage.setItem(LS_KEY, JSON.stringify(data));
|
|
}
|
|
|
|
function showDailyToast() {
|
|
if (TOAST_SHOWN) return;
|
|
TOAST_SHOWN = true;
|
|
|
|
var data = getStreak();
|
|
var existing = document.querySelector('.daily-toast');
|
|
if (existing) existing.remove();
|
|
|
|
var toast = document.createElement('div');
|
|
toast.className = 'daily-toast';
|
|
|
|
var streakHTML = '';
|
|
if (data.streak > 0) {
|
|
streakHTML = '<div class="streak">🔥 Day ' + data.streak + ' streak!</div>';
|
|
}
|
|
|
|
toast.innerHTML =
|
|
'<div class="toast-header">' +
|
|
'<div class="toast-title">⏰ Daily Practice</div>' +
|
|
'<button class="toast-close" onclick="this.closest(\'.daily-toast\').remove()">✕</button>' +
|
|
'</div>' +
|
|
streakHTML +
|
|
'<p>It\'s time for your daily English practice! Spend 5 minutes on an exercise or test.</p>' +
|
|
'<div class="toast-actions">' +
|
|
'<a href="exercises.html" class="btn" onclick="saveStreakFunc()">Practice Now</a>' +
|
|
'<button class="btn btn-outline" onclick="this.closest(\'.daily-toast\').remove()">Later</button>' +
|
|
'</div>';
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
// trigger animation
|
|
setTimeout(function() { toast.classList.add('show'); }, 100);
|
|
|
|
// also try browser notification
|
|
if ('Notification' in window && Notification.permission === 'granted') {
|
|
new Notification('English Easy — Daily Practice', {
|
|
body: 'Time for your daily English practice! 5 minutes is all you need.',
|
|
icon: '/images/icon.png'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Expose saveStreak for the toast button
|
|
window.saveStreakFunc = function() {
|
|
var data = getStreak();
|
|
data.streak = (data.streak || 0) + 1;
|
|
saveStreak(data);
|
|
};
|
|
|
|
// Request notification permission
|
|
function requestNotifPermission() {
|
|
if ('Notification' in window && Notification.permission === 'default') {
|
|
Notification.requestPermission();
|
|
}
|
|
}
|
|
|
|
function checkDailyReminder() {
|
|
var now = new Date();
|
|
var hours = now.getHours();
|
|
var minutes = now.getMinutes();
|
|
var data = getStreak();
|
|
|
|
// Show if it's >= 20:00 and we haven't shown today
|
|
if (hours >= 20 && data.lastDate !== getToday()) {
|
|
showDailyToast();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Check on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
requestNotifPermission();
|
|
checkDailyReminder();
|
|
|
|
// Check every 30 seconds if it's 20:00
|
|
setInterval(function() {
|
|
var now = new Date();
|
|
if (now.getHours() === 20 && now.getMinutes() === 0) {
|
|
checkDailyReminder();
|
|
}
|
|
}, 30000);
|
|
});
|
|
})();
|
|
|
|
// --- Twinkling Stars ---
|
|
(function() {
|
|
var bg = document.getElementById('stars-bg');
|
|
if (!bg) return;
|
|
var count = window.innerWidth < 600 ? 40 : 80;
|
|
for (var i = 0; i < count; i++) {
|
|
var star = document.createElement('div');
|
|
star.className = 'star';
|
|
star.style.left = Math.random() * 100 + '%';
|
|
star.style.top = Math.random() * 100 + '%';
|
|
star.style.width = (Math.random() * 3 + 1) + 'px';
|
|
star.style.height = star.style.width;
|
|
star.style.animationDelay = (Math.random() * 5) + 's';
|
|
star.style.animationDuration = (Math.random() * 3 + 2) + 's';
|
|
bg.appendChild(star);
|
|
}
|
|
})();
|
|
|
|
// --- Vocabulary filter ---
|
|
function filterVocab(category, btn) {
|
|
const rows = document.querySelectorAll('.vocab-row');
|
|
const buttons = document.querySelectorAll('.category-btn');
|
|
|
|
buttons.forEach(function(b) { b.classList.remove('active'); });
|
|
if (btn) btn.classList.add('active');
|
|
|
|
rows.forEach(function(row) {
|
|
if (category === 'all' || row.getAttribute('data-category') === category) {
|
|
row.style.display = '';
|
|
} else {
|
|
row.style.display = 'none';
|
|
}
|
|
});
|
|
}
|