math4 / app.js
matt-ritchey's picture
Upload 6 files
fd586d8 verified
Raw History Blame Contribute Delete
13.1 kB
const state = {
selectedTopic: null,
selectedDifficulty: 1,
selectedCount: 10,
practice: { questions: [], index: 0, awaitingNext: false, currentProblem: null, topic: null, difficulty: 1, count: 0 },
quiz: { questions: [], index: 0, score: 0, timer: null, timeLeft: 0, active: false }
};
function navigate(view) {
if (view === 'home' && state.quiz.active) {
clearInterval(state.quiz.timer);
state.quiz.active = false;
}
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
const el = document.getElementById('view-' + view);
if (el) el.classList.add('active');
updateHeader();
if (view === 'progress') renderProgress();
if (view === 'home') renderHome();
if (view === 'topics' || view === 'quiz-setup') resetSelection(view);
window.scrollTo(0, 0);
}
function updateHeader() {
const data = ProgressStore.load();
document.getElementById('points-header').textContent = data.points;
document.getElementById('streak-header').textContent = data.streak;
}
function renderHome() {
updateHeader();
}
function resetSelection(view) {
const container = document.getElementById('view-' + view);
container.querySelectorAll('.topic-card').forEach(c => c.classList.remove('selected'));
container.querySelectorAll('.diff-btn').forEach(b => b.classList.remove('active'));
container.querySelector('.diff-btn[data-level="1"]')?.classList.add('active');
state.selectedTopic = null;
state.selectedDifficulty = 1;
const startBtn = document.getElementById(view === 'topics' ? 'start-practice' : 'start-quiz');
if (startBtn) startBtn.disabled = true;
}
document.addEventListener('click', function(e) {
const topicCard = e.target.closest('.topic-card');
if (topicCard) {
const view = topicCard.closest('.view');
if (!view) return;
view.querySelectorAll('.topic-card').forEach(c => c.classList.remove('selected'));
topicCard.classList.add('selected');
state.selectedTopic = topicCard.dataset.topic;
const isQuiz = view.id === 'view-quiz-setup';
const btn = document.getElementById(isQuiz ? 'start-quiz' : 'start-practice');
if (btn) btn.disabled = false;
return;
}
const diffBtn = e.target.closest('.diff-btn');
if (diffBtn) {
const view = diffBtn.closest('.view');
view.querySelectorAll('.diff-btn').forEach(b => b.classList.remove('active'));
diffBtn.classList.add('active');
state.selectedDifficulty = parseInt(diffBtn.dataset.level);
return;
}
const countBtn = e.target.closest('.count-btn');
if (countBtn) {
countBtn.closest('.quiz-count-selector').querySelectorAll('.count-btn').forEach(b => b.classList.remove('active'));
countBtn.classList.add('active');
state.selectedCount = parseInt(countBtn.dataset.count);
return;
}
});
document.getElementById('start-practice')?.addEventListener('click', function() {
if (!state.selectedTopic) return;
startPractice();
});
document.getElementById('start-quiz')?.addEventListener('click', function() {
if (!state.selectedTopic) return;
startQuiz();
});
function startPractice() {
state.practice.topic = state.selectedTopic;
state.practice.difficulty = state.selectedDifficulty;
state.practice.count = 0;
nextPracticeProblem();
}
function nextPracticeProblem() {
const topic = state.practice.topic;
const diff = state.practice.difficulty;
const problem = MathEngine.generateProblem(topic, diff);
state.practice.currentProblem = problem;
state.practice.awaitingNext = false;
state.practice.count++;
navigate('practice');
showPracticeProblem();
}
function showPracticeProblem() {
const q = state.practice.currentProblem;
document.getElementById('practice-problem').textContent = q.display;
document.getElementById('practice-counter').textContent = `#${state.practice.count}`;
document.getElementById('practice-topic-label').textContent = q.type.replace('-', ' ');
document.getElementById('practice-input').value = '';
document.getElementById('practice-input').disabled = false;
document.getElementById('practice-check').disabled = false;
document.getElementById('practice-feedback').className = '';
document.getElementById('practice-feedback').textContent = '';
document.getElementById('practice-steps').style.display = 'none';
document.getElementById('practice-steps').innerHTML = '';
document.getElementById('practice-actions').style.display = 'none';
document.getElementById('practice-input').focus();
}
document.getElementById('practice-check')?.addEventListener('click', checkPracticeAnswer);
document.getElementById('practice-input')?.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
if (state.practice.awaitingNext) {
nextPracticeProblem();
} else {
checkPracticeAnswer();
}
}
});
function checkPracticeAnswer() {
if (state.practice.awaitingNext) return;
const input = document.getElementById('practice-input');
const answer = parseFloat(input.value);
const problem = state.practice.currentProblem;
if (isNaN(answer)) return;
const correct = answer === problem.answer;
const fb = document.getElementById('practice-feedback');
const steps = document.getElementById('practice-steps');
if (correct) {
fb.className = 'feedback-correct';
fb.textContent = 'โœ… Correct!';
} else {
fb.className = 'feedback-incorrect';
fb.textContent = `โŒ Not quite. The answer is ${problem.answerDisplay || problem.answer}`;
}
ProgressStore.recordAnswer(problem.type, correct);
updateHeader();
input.disabled = true;
document.getElementById('practice-check').disabled = true;
state.practice.awaitingNext = true;
document.getElementById('practice-actions').style.display = 'flex';
}
document.getElementById('practice-show-steps')?.addEventListener('click', function() {
const problem = state.practice.currentProblem;
const steps = document.getElementById('practice-steps');
if (steps.style.display === 'none') {
steps.innerHTML = problem.steps.map(s => `<p>${s}</p>`).join('');
steps.style.display = 'block';
this.textContent = 'Hide Steps';
} else {
steps.style.display = 'none';
this.textContent = 'Show Steps';
}
});
document.getElementById('practice-next')?.addEventListener('click', function() {
nextPracticeProblem();
});
function startQuiz() {
const topic = state.selectedTopic;
const diff = state.selectedDifficulty;
const count = state.selectedCount;
state.quiz.questions = MathEngine.generateQuizQuestions(topic, diff, count);
state.quiz.index = 0;
state.quiz.score = 0;
state.quiz.timeLeft = count * 15;
state.quiz.active = true;
navigate('quiz');
showQuizProblem();
startQuizTimer();
}
function showQuizProblem() {
const q = state.quiz.questions[state.quiz.index];
document.getElementById('quiz-problem').textContent = q.display;
document.getElementById('quiz-question-num').textContent = `Question ${state.quiz.index + 1} of ${state.quiz.questions.length}`;
document.getElementById('quiz-score').textContent = state.quiz.score;
document.getElementById('quiz-total').textContent = state.quiz.questions.length;
document.getElementById('quiz-input').value = '';
document.getElementById('quiz-input').disabled = false;
document.getElementById('quiz-submit').disabled = false;
document.getElementById('quiz-feedback').className = '';
document.getElementById('quiz-feedback').textContent = '';
document.getElementById('quiz-input').focus();
updateQuizProgress();
}
function updateQuizProgress() {
const pct = (state.quiz.index / state.quiz.questions.length) * 100;
document.getElementById('quiz-progress-fill').style.width = pct + '%';
}
function startQuizTimer() {
updateTimerDisplay();
state.quiz.timer = setInterval(function() {
state.quiz.timeLeft--;
updateTimerDisplay();
if (state.quiz.timeLeft <= 0) {
clearInterval(state.quiz.timer);
endQuiz();
}
}, 1000);
}
function updateTimerDisplay() {
const minutes = Math.floor(state.quiz.timeLeft / 60);
const seconds = state.quiz.timeLeft % 60;
document.getElementById('quiz-timer').textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
const timerEl = document.querySelector('.quiz-timer');
if (state.quiz.timeLeft <= 15) {
timerEl.style.color = '#e74c3c';
} else if (state.quiz.timeLeft <= 30) {
timerEl.style.color = '#f5a623';
} else {
timerEl.style.color = '#2c3e50';
}
}
document.getElementById('quiz-submit')?.addEventListener('click', submitQuizAnswer);
document.getElementById('quiz-input')?.addEventListener('keydown', function(e) {
if (e.key === 'Enter') submitQuizAnswer();
});
function submitQuizAnswer() {
const input = document.getElementById('quiz-input');
const answer = parseFloat(input.value);
const problem = state.quiz.questions[state.quiz.index];
if (isNaN(answer)) return;
const correct = answer === problem.answer;
const fb = document.getElementById('quiz-feedback');
if (correct) {
state.quiz.score++;
fb.className = 'feedback-correct';
fb.textContent = 'โœ… Correct!';
ProgressStore.recordAnswer(problem.type, true);
} else {
fb.className = 'feedback-incorrect';
const display = problem.answerDisplay || problem.answer;
fb.textContent = `โŒ Answer: ${display}`;
ProgressStore.recordAnswer(problem.type, false);
}
updateHeader();
input.disabled = true;
document.getElementById('quiz-submit').disabled = true;
setTimeout(function() {
if (!state.quiz.active) return;
state.quiz.index++;
if (state.quiz.index >= state.quiz.questions.length) {
clearInterval(state.quiz.timer);
endQuiz();
} else {
showQuizProblem();
}
}, 800);
}
function endQuiz() {
state.quiz.active = false;
const total = state.quiz.questions.length;
const score = state.quiz.score;
const pct = Math.round((score / total) * 100);
const data = ProgressStore.recordQuiz(state.selectedTopic, score, total, state.selectedDifficulty);
updateHeader();
let stars = 'โญ';
if (pct >= 100) stars = 'โญโญโญ';
else if (pct >= 80) stars = 'โญโญ';
document.getElementById('results-stars').textContent = stars;
document.getElementById('results-correct').textContent = score;
document.getElementById('results-total').textContent = total;
document.getElementById('results-pct').textContent = pct + '%';
if (pct >= 100) document.getElementById('results-title').textContent = '๐ŸŽ‰ Perfect Score!';
else if (pct >= 80) document.getElementById('results-title').textContent = 'Great Job! ๐ŸŒŸ';
else if (pct >= 60) document.getElementById('results-title').textContent = 'Good Effort! ๐Ÿ’ช';
else document.getElementById('results-title').textContent = 'Keep Practicing! ๐Ÿ“š';
const pointsEarned = pct >= 80 ? 30 : pct >= 60 ? 15 : 5;
document.getElementById('results-points').textContent = pointsEarned;
document.getElementById('results-streak').textContent = data.streak;
navigate('results');
}
function renderProgress() {
const data = ProgressStore.load();
const attempted = data.totalAttempted || 1;
document.getElementById('stat-points').textContent = data.points;
document.getElementById('stat-streak').textContent = data.bestStreak;
document.getElementById('stat-accuracy').textContent = Math.round((data.totalCorrect / attempted) * 100) + '%';
const topics = ['addition', 'subtraction', 'multiplication', 'division', 'word-problems'];
const labels = { 'addition': 'โž• Add', 'subtraction': 'โž– Sub', 'multiplication': 'โœ–๏ธ Mul', 'division': 'โž— Div', 'word-problems': '๐Ÿ“– Word' };
const breakdown = document.getElementById('topic-breakdown');
breakdown.innerHTML = '';
topics.forEach(t => {
const stats = ProgressStore.getTopicStats(t);
const pct = stats.attempted > 0 ? Math.round((stats.correct / stats.attempted) * 100) : 0;
const row = document.createElement('div');
row.className = 'topic-row';
row.innerHTML = `
<span class="topic-name">${labels[t] || t}</span>
<div class="topic-bar-bg"><div class="topic-bar-fill" style="width:${pct}%"></div></div>
<span class="topic-pct">${pct}%</span>
`;
breakdown.appendChild(row);
});
const hist = document.getElementById('quiz-history');
hist.innerHTML = '';
if (data.quizHistory.length === 0) {
hist.innerHTML = '<p style="color:#999;text-align:center">No quizzes yet. Take one to see it here!</p>';
} else {
const recent = [...data.quizHistory].reverse().slice(0, 10);
recent.forEach(h => {
const div = document.createElement('div');
div.className = 'quiz-history-item';
const date = new Date(h.date);
const dateStr = date.toLocaleDateString();
const pct = Math.round((h.score / h.total) * 100);
div.innerHTML = `
<span>${h.topic} (Lv${h.difficulty})</span>
<span>${h.score}/${h.total} (${pct}%) - ${dateStr}</span>
`;
hist.appendChild(div);
});
}
}
document.addEventListener('DOMContentLoaded', function() {
navigate('home');
});