From d8162c3e2dcd27c24947e2c6c4bad68c01498e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20K=C3=BChn?= Date: Fri, 8 May 2026 11:48:06 +0200 Subject: [PATCH] Add app.js entry point module --- public/js/app.js | 106 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 public/js/app.js diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 0000000..236e7b3 --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,106 @@ +import { state } from './state.js'; +import { applyLocale } from './i18n.js'; +import { api, loadGoals } from './api.js'; +import { updateHeader } from './ui.js'; +import { showLogin, showRegister, showResetPassword } from './auth.js'; +import { openNew, openData } from './sheets.js'; +import { render } from './render.js'; + +document.addEventListener('session-expired', () => showLogin()); + +document.getElementById('btnNew').onclick = openNew; +document.getElementById('btnData').onclick = openData; +document.querySelector('.hdr-logo').onclick = () => { + loadGoals().then(g => { state.goals = g; render(); }).catch(() => {}); +}; + +updateHeader(); + +const _qs = new URLSearchParams(window.location.search); +const inviteToken = _qs.get('invite'); +const resetSelector = _qs.get('reset_selector'); +const resetToken = _qs.get('reset_token'); +if (inviteToken || resetSelector) history.replaceState(null, '', location.pathname); + +if (resetSelector && resetToken) { + applyLocale(null); render(); showResetPassword(resetSelector, resetToken); +} else { + api('GET', 'me') + .then(r => { + state.userName = r.name || ''; state.isAdmin = r.is_admin || false; + applyLocale(r.locale); updateHeader(); + return loadGoals(); + }) + .then(g => { state.goals = g; render(); }) + .catch(() => { + applyLocale(null); render(); + if (inviteToken) showRegister(inviteToken); + else showLogin(); + }); +} + +function scheduleMidnight() { + const n = new Date(); + const ms = new Date(n.getFullYear(), n.getMonth(), n.getDate() + 1, 0, 0, 5).getTime() - n.getTime(); + setTimeout(() => { + state.TODAY = new Date(); state.TODAY.setHours(0, 0, 0, 0); + state.selDay = {}; state.collapsed = {}; + updateHeader(); render(); scheduleMidnight(); + }, ms); +} +scheduleMidnight(); + +document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + const n = new Date(); n.setHours(0, 0, 0, 0); + if (n.getTime() !== state.TODAY.getTime()) { + state.TODAY = n; state.selDay = {}; state.collapsed = {}; + render(); scheduleMidnight(); + } + loadGoals().then(g => { state.goals = g; render(); }).catch(() => {}); + } +}); + +(function() { + const swEl = document.getElementById('sw'); + let swState = 0, start = 0, elapsed = 0, raf = null, wakeLock = null; + + function acquireWakeLock() { + if (!('wakeLock' in navigator)) return; + navigator.wakeLock.request('screen').then(s => { wakeLock = s; }).catch(() => {}); + } + function releaseWakeLock() { + if (wakeLock) { wakeLock.release(); wakeLock = null; } + } + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible' && swState === 1) acquireWakeLock(); + }); + + function getMs() { return swState === 1 ? elapsed + (Date.now() - start) : elapsed; } + function updateFillBtns() { + const show = getMs() >= 1000; + document.querySelectorAll('.btn-sw-fill').forEach(b => { b.style.display = show ? '' : 'none'; }); + } + function fmt(ms) { return (ms / 1000).toFixed(2) + 's'; } + function tick() { swEl.textContent = fmt(Date.now() - start + elapsed); updateFillBtns(); raf = requestAnimationFrame(tick); } + + swEl.addEventListener('click', () => { + if (swState === 0) { + start = Date.now(); elapsed = 0; swEl.classList.add('running'); + swState = 1; tick(); acquireWakeLock(); + } else if (swState === 1) { + cancelAnimationFrame(raf); elapsed += Date.now() - start; + swEl.textContent = fmt(elapsed); swEl.classList.remove('running'); + swState = 2; updateFillBtns(); releaseWakeLock(); + } else { + cancelAnimationFrame(raf); elapsed = 0; swEl.textContent = '0.00s'; + swEl.classList.remove('running'); swState = 0; updateFillBtns(); releaseWakeLock(); + } + }); + + document.addEventListener('click', e => { + if (!e.target.classList.contains('btn-sw-fill')) return; + const inp = e.target.closest('.add-row, .qb-row').querySelector('.num-in'); + if (inp) { inp.value = Math.floor(getMs() / 1000); inp.dispatchEvent(new Event('input')); } + }); +})();