(function () { "use strict"; let currentUser = null; let pendingScore = null; function diffLabel(d) { return I18N.t('difficulty.' + d); } // ---------- API helper ---------- async function api(path, options) { const opts = Object.assign({ credentials: 'include', headers: {} }, options); if (opts.body) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(opts.body); } const res = await fetch(path, opts); let data = null; try { data = await res.json(); } catch (e) { data = null; } if (!res.ok) { const code = (data && data.error) || 'UNKNOWN'; throw new Error(I18N.t('errors.' + code)); } return data; } // ---------- Modal helpers ---------- function openModal(id) { document.getElementById(id).classList.add('show'); } function closeModal(id) { document.getElementById(id).classList.remove('show'); } document.querySelectorAll('.modal-overlay').forEach(overlay => { overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.classList.remove('show'); }); }); document.querySelectorAll('.modal-close').forEach(btn => { btn.addEventListener('click', () => btn.closest('.modal-overlay').classList.remove('show')); }); // ---------- Theme ---------- const THEMES = ['blue', 'green', 'amber', 'mono']; function applyTheme(name) { if (!THEMES.includes(name)) name = 'blue'; document.documentElement.dataset.theme = name; try { localStorage.setItem('sudoku-theme', name); } catch (e) {} document.querySelectorAll('.theme-swatch').forEach(btn => { btn.classList.toggle('active', btn.dataset.theme === name); }); } document.querySelectorAll('.theme-swatch').forEach(btn => { btn.addEventListener('click', () => applyTheme(btn.dataset.theme)); }); // ---------- Language ---------- function syncLangButtons() { const lang = I18N.getLang(); document.querySelectorAll('.lang-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.lang === lang); }); } document.querySelectorAll('.lang-btn').forEach(btn => { btn.addEventListener('click', () => I18N.setLang(btn.dataset.lang)); }); window.onLangChange = function () { syncLangButtons(); renderAuthbar(); // Re-render whichever modal body is currently open, since their content is JS-generated if (document.getElementById('leaderboardModal').classList.contains('show')) loadLeaderboard(); if (document.getElementById('historyModal').classList.contains('show')) loadHistory(); }; // ---------- Auth bar ---------- function renderAuthbar() { const bar = document.getElementById('authbar'); const banner = document.getElementById('anonBanner'); bar.innerHTML = ''; if (currentUser) { banner.hidden = true; const who = document.createElement('div'); who.className = 'who'; who.innerHTML = `๐ค ${escapeHtml(currentUser.pseudo)} ยท ${currentUser.totalPoints || 0} pts`; const actions = document.createElement('div'); actions.className = 'actions-inline'; const logoutBtn = document.createElement('button'); logoutBtn.textContent = I18N.t('auth.logout'); logoutBtn.addEventListener('click', logout); actions.appendChild(logoutBtn); bar.appendChild(who); bar.appendChild(actions); } else { const dismissed = localStorage.getItem('sudoku-anon-banner-dismissed') === '1'; banner.hidden = dismissed; const who = document.createElement('div'); who.className = 'who'; who.textContent = '๐ ' + (I18N.getLang() === 'en' ? 'Guest mode' : 'Mode anonyme'); const actions = document.createElement('div'); actions.className = 'actions-inline'; const loginBtn = document.createElement('button'); loginBtn.textContent = I18N.t('auth.login'); loginBtn.addEventListener('click', () => openAuthModal('login')); const registerBtn = document.createElement('button'); registerBtn.textContent = I18N.t('auth.register'); registerBtn.className = 'primary'; registerBtn.addEventListener('click', () => openAuthModal('register')); actions.appendChild(loginBtn); actions.appendChild(registerBtn); bar.appendChild(who); bar.appendChild(actions); } } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } async function refreshAuth() { try { const data = await api('/api/auth/me'); currentUser = data.user; } catch (e) { currentUser = null; } renderAuthbar(); } async function logout() { try { await api('/api/auth/logout', { method: 'POST' }); } catch (e) {} currentUser = null; renderAuthbar(); setMsg(I18N.t('msg.loggedOut')); } function setMsg(text) { document.getElementById('msg').textContent = text || ''; } // ---------- Anonymous banner ---------- document.getElementById('anonBannerClose').addEventListener('click', () => { localStorage.setItem('sudoku-anon-banner-dismissed', '1'); document.getElementById('anonBanner').hidden = true; }); document.getElementById('anonBannerLogin').addEventListener('click', () => openAuthModal('login')); document.getElementById('anonBannerRegister').addEventListener('click', () => openAuthModal('register')); // ---------- Auth modal ---------- function openAuthModal(tab) { setAuthTab(tab || 'login'); document.getElementById('authError').textContent = ''; openModal('authModal'); } function setAuthTab(tab) { const isLogin = tab === 'login'; document.getElementById('authTabLogin').classList.toggle('active', isLogin); document.getElementById('authTabRegister').classList.toggle('active', !isLogin); document.getElementById('loginForm').hidden = !isLogin; document.getElementById('registerForm').hidden = isLogin; document.getElementById('authError').textContent = ''; } document.getElementById('authTabLogin').addEventListener('click', () => setAuthTab('login')); document.getElementById('authTabRegister').addEventListener('click', () => setAuthTab('register')); document.getElementById('loginForm').addEventListener('submit', async (e) => { e.preventDefault(); const identifier = document.getElementById('loginIdentifier').value.trim(); const password = document.getElementById('loginPassword').value; const errEl = document.getElementById('authError'); errEl.textContent = ''; try { const user = await api('/api/auth/login', { method: 'POST', body: { identifier, password } }); await refreshAuth(); closeModal('authModal'); e.target.reset(); setMsg(I18N.t('msg.welcome', { pseudo: user.pseudo })); await maybeSubmitPendingScore(); } catch (err) { errEl.textContent = err.message; } }); document.getElementById('registerForm').addEventListener('submit', async (e) => { e.preventDefault(); const pseudo = document.getElementById('registerPseudo').value.trim(); const email = document.getElementById('registerEmail').value.trim(); const password = document.getElementById('registerPassword').value; const errEl = document.getElementById('authError'); errEl.textContent = ''; try { const user = await api('/api/auth/register', { method: 'POST', body: { pseudo, email, password } }); await refreshAuth(); closeModal('authModal'); e.target.reset(); setMsg(I18N.t('msg.accountCreated', { pseudo: user.pseudo })); await maybeSubmitPendingScore(); } catch (err) { errEl.textContent = err.message; } }); async function maybeSubmitPendingScore() { if (!pendingScore || !currentUser) return; try { const saved = await api('/api/games', { method: 'POST', body: { difficulty: pendingScore.difficulty, time_seconds: pendingScore.timeSeconds, mistakes: pendingScore.mistakes, won: true, }}); setMsg(I18N.t('msg.scoreSaved', { points: saved.points })); pendingScore = null; await refreshAuth(); } catch (e) { setMsg(I18N.t('msg.scoreSaveError', { msg: e.message })); } } // ---------- History modal ---------- async function loadHistory() { const body = document.getElementById('historyBody'); body.innerHTML = `
${I18N.t('history.loading')}
`; try { const data = await api('/api/games/history'); if (!data.history.length) { body.innerHTML = `${I18N.t('history.empty')}
`; return; } const rows = data.history.map(g => `| ${I18N.t('history.colDate')} | ${I18N.t('history.colDifficulty')} | ${I18N.t('history.colTime')} | ${I18N.t('history.colMistakes')} | ${I18N.t('history.colResult')} | ${I18N.t('history.colPoints')} |
|---|
${I18N.t('history.error', { msg: e.message })}
`; } } document.getElementById('historyBtn').addEventListener('click', async () => { if (!currentUser) { openAuthModal('login'); setMsg(I18N.t('msg.loginRequiredHistory')); return; } openModal('historyModal'); loadHistory(); }); function formatTime(s) { const m = Math.floor(s / 60).toString().padStart(2, '0'); const sec = (s % 60).toString().padStart(2, '0'); return `${m}:${sec}`; } // ---------- Rules modal ---------- document.getElementById('rulesBtn').addEventListener('click', () => openModal('rulesModal')); // ---------- Leaderboard modal ---------- let leaderboardMode = 'times'; let leaderboardDiff = 'medium'; document.getElementById('leaderboardBtn').addEventListener('click', () => { openModal('leaderboardModal'); loadLeaderboard(); }); document.getElementById('lbTabTimes').addEventListener('click', () => { leaderboardMode = 'times'; syncLbTabs(); loadLeaderboard(); }); document.getElementById('lbTabPoints').addEventListener('click', () => { leaderboardMode = 'points'; syncLbTabs(); loadLeaderboard(); }); function syncLbTabs() { document.getElementById('lbTabTimes').classList.toggle('active', leaderboardMode === 'times'); document.getElementById('lbTabPoints').classList.toggle('active', leaderboardMode === 'points'); document.getElementById('lbDiffTabs').hidden = leaderboardMode !== 'times'; } document.querySelectorAll('#lbDiffTabs button').forEach(btn => { btn.addEventListener('click', () => { leaderboardDiff = btn.dataset.diff; document.querySelectorAll('#lbDiffTabs button').forEach(b => b.classList.toggle('active', b === btn)); loadLeaderboard(); }); }); async function loadLeaderboard() { const body = document.getElementById('leaderboardBody'); body.innerHTML = `${I18N.t('history.loading')}
`; try { if (leaderboardMode === 'times') { const data = await api(`/api/leaderboard/best-times?difficulty=${leaderboardDiff}`); if (!data.entries.length) { body.innerHTML = `${I18N.t('leaderboard.emptyTimes')}
`; return; } const rows = data.entries.map((e, i) => `| ${I18N.t('leaderboard.colRank')} | ${I18N.t('leaderboard.colPlayer')} | ${I18N.t('leaderboard.colTime')} | ${I18N.t('leaderboard.colMistakes')} |
|---|
${I18N.t('leaderboard.emptyPoints')}
`; return; } const rows = data.entries.map((e, i) => `| ${I18N.t('leaderboard.colRank')} | ${I18N.t('leaderboard.colPlayer')} | ${I18N.t('leaderboard.colPoints')} | ${I18N.t('leaderboard.colWins')} |
|---|
${I18N.t('leaderboard.error', { msg: e.message })}
`; } } // ---------- Win modal ---------- function showWinModal({ difficulty, timeSeconds, mistakes }) { document.getElementById('winSubtitle').textContent = I18N.t('win.subtitle', { difficulty: diffLabel(difficulty) }); document.getElementById('winTime').textContent = formatTime(timeSeconds); document.getElementById('winMistakes').textContent = mistakes; const cta = document.getElementById('winCta'); const pointsLine = document.getElementById('winPoints'); pointsLine.textContent = ''; if (currentUser) { cta.hidden = true; api('/api/games', { method: 'POST', body: { difficulty, time_seconds: timeSeconds, mistakes, won: true } }) .then(saved => { pointsLine.textContent = I18N.t('win.pointsSaved', { points: saved.points }); return refreshAuth(); }) .catch(err => { pointsLine.textContent = I18N.t('win.saveError', { msg: err.message }); }); } else { pendingScore = { difficulty, timeSeconds, mistakes }; cta.hidden = false; } openModal('winModal'); } document.getElementById('winRegisterBtn').addEventListener('click', () => { closeModal('winModal'); openAuthModal('register'); }); document.getElementById('winCloseBtn').addEventListener('click', () => { closeModal('winModal'); window.SudokuGame.reset(); }); // ---------- Abandon ---------- async function handleAbandon({ difficulty, timeSeconds, mistakes }) { if (!currentUser) return; try { await api('/api/games', { method: 'POST', body: { difficulty, time_seconds: timeSeconds, mistakes, won: false } }); } catch (e) { /* silent */ } } // ---------- Boot ---------- applyTheme(localStorage.getItem('sudoku-theme') || 'blue'); syncLangButtons(); window.SudokuGame.init({ onWin: showWinModal, onAbandon: handleAbandon }); refreshAuth(); })();