Files
math/static/app.js
Dmitry Bikulov d1bb3559bb Initial commit
2025-09-11 13:42:53 +03:00

178 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(() => {
const startBtn = document.getElementById('startBtn');
const timeEl = document.getElementById('time');
const progressEl = document.getElementById('progress');
const problemEl = document.getElementById('problem');
const optionsEl = document.getElementById('options');
const gameEl = document.getElementById('game');
const statusEl = document.getElementById('status');
const resultEl = document.getElementById('result');
const fxLayer = document.getElementById('fxLayer');
let sessionId = null;
let totalQuestions = 20;
let remaining = 60;
let countdown = null;
let inFlight = false;
function setStatus(msg) {
statusEl.textContent = msg || '';
}
function showGame(show) {
gameEl.classList.toggle('hidden', !show);
}
function showResult(score) {
const { correct, total_answered, total } = score;
resultEl.innerHTML = `
<div class="score">Результат: <strong>${correct}</strong> из <strong>${total}</strong></div>
<div class="small">Отвечено: ${total_answered}</div>
<button class="again" id="againBtn">Сыграть ещё</button>
`;
resultEl.classList.remove('hidden');
document.getElementById('againBtn')?.addEventListener('click', () => {
resultEl.classList.add('hidden');
start();
});
}
function renderQuestion(q) {
if (!q) return;
problemEl.textContent = `${q.a} × ${q.b}`;
optionsEl.innerHTML = '';
q.options.forEach(value => {
const btn = document.createElement('button');
btn.className = 'option-btn';
btn.textContent = String(value);
btn.addEventListener('click', () => answer(value, btn));
optionsEl.appendChild(btn);
});
progressEl.textContent = `${q.index}/${totalQuestions}`;
}
function stopTimer() {
if (countdown) {
clearInterval(countdown);
countdown = null;
}
}
function startTimer(seconds) {
remaining = seconds;
timeEl.textContent = String(remaining);
stopTimer();
countdown = setInterval(() => {
remaining -= 1;
if (remaining <= 0) {
remaining = 0;
timeEl.textContent = '0';
stopTimer();
} else {
timeEl.textContent = String(remaining);
}
}, 1000);
}
async function start() {
if (inFlight) return;
inFlight = true;
setStatus('');
showGame(false);
startBtn.disabled = true;
resultEl.classList.add('hidden');
try {
const res = await fetch('/api/session/start', { method: 'POST' });
if (!res.ok) throw new Error('Не удалось начать сессию');
const data = await res.json();
sessionId = data.session_id;
totalQuestions = data.total_questions;
startTimer(data.remaining_seconds);
renderQuestion(data.question);
showGame(true);
setStatus('Отвечайте как можно быстрее!');
} catch (e) {
console.error(e);
setStatus('Ошибка старта. Попробуйте ещё раз.');
} finally {
startBtn.disabled = false;
inFlight = false;
}
}
function celebrateFromElement(el) {
if (!fxLayer) return;
const rect = el.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
const count = 40;
for (let i = 0; i < count; i++) {
const p = document.createElement('span');
p.className = 'confetti';
// origin
p.style.left = `${x}px`;
p.style.top = `${y}px`;
// random trajectory
const angle = Math.random() * Math.PI * 2;
const distance = 60 + Math.random() * 80;
const tx = Math.cos(angle) * distance;
const ty = Math.sin(angle) * distance + 40 + Math.random() * 60; // gravity pull
const rot = (Math.random() * 720 - 360).toFixed(0) + 'deg';
p.style.setProperty('--tx', `${tx.toFixed(0)}px`);
p.style.setProperty('--ty', `${ty.toFixed(0)}px`);
p.style.setProperty('--rot', rot);
const hue = Math.floor(Math.random() * 360);
p.style.backgroundColor = `hsl(${hue} 90% 55%)`;
p.style.animation = `confetti ${650 + Math.random() * 400}ms ease-out forwards`;
fxLayer.appendChild(p);
setTimeout(() => p.remove(), 1200);
}
}
async function answer(value, clickedBtn) {
if (!sessionId || inFlight) return;
inFlight = true;
// Disable all buttons to prevent double clicks
const buttons = Array.from(optionsEl.querySelectorAll('button'));
buttons.forEach(b => b.disabled = true);
try {
const res = await fetch(`/api/session/${sessionId}/answer`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ answer: value }),
});
if (!res.ok) throw new Error('Не удалось отправить ответ');
const data = await res.json();
// feedback
if (data.correct) {
clickedBtn.classList.add('correct');
celebrateFromElement(clickedBtn);
} else {
clickedBtn.classList.add('wrong');
}
// Small pause to show feedback
await new Promise(r => setTimeout(r, data.correct ? 350 : 250));
if (data.finished) {
stopTimer();
showGame(false);
const reason = data.reason === 'timeout' ? 'Время вышло.' : 'Завершено!';
setStatus(reason);
showResult(data.score);
} else {
timeEl.textContent = String(data.remaining_seconds);
renderQuestion(data.question);
}
} catch (e) {
console.error(e);
setStatus('Ошибка ответа. Перезапустите сессию.');
} finally {
inFlight = false;
}
}
startBtn.addEventListener('click', start);
})();