Files
Daily-review-page/templates/index.html
2026-04-12 18:55:00 +03:00

391 lines
14 KiB
HTML
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.
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Daily Check-in</title>
<link rel="stylesheet" href="/static/css/style.css">
<script src="/static/js/theme.js"></script>
</head>
<body>
<nav class="topbar">
<span class="topbar-title">Daily Check-in</span>
<div class="topbar-nav">
<a href="/" class="active">Form</a>
<a href="/history">History</a>
</div>
<div id="status" title="Save status"></div>
<button class="theme-toggle" id="theme-toggle" title="Toggle theme"></button>
</nav>
<div class="page">
<!-- Period banner -->
<div class="period-banner" id="period-banner">
<span class="period-icon">🕐</span>
<div class="period-text" id="period-text">Loading period…</div>
<div class="period-progress">
<div class="period-pct" id="period-pct"></div>
<div class="period-bar-wrap"><div class="period-bar" id="period-bar" style="width:0%"></div></div>
</div>
</div>
<!-- Questions -->
<div id="form"></div>
<!-- Actions row -->
<div style="display:flex;gap:10px;align-items:center;margin-bottom:12px;flex-wrap:wrap;">
<a class="download-btn" href="#" onclick="downloadCurrentMd(event)">⬇ Download as .md</a>
<button class="early-submit-btn" onclick="earlySubmit()">⏭ Start new day</button>
</div>
<!-- Confirmation dialog -->
<div id="confirm-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:300;backdrop-filter:blur(4px);">
<div style="display:flex;align-items:center;justify-content:center;height:100%;">
<div style="background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:28px 24px;max-width:340px;width:90%;box-shadow:var(--shadow);">
<div style="font-size:17px;font-weight:600;margin-bottom:8px;">Start new day?</div>
<div style="font-size:13px;color:var(--text2);margin-bottom:24px;line-height:1.5;">This will snapshot current answers and reset the form — same as the automatic 10:00 submission, just now.</div>
<div style="display:flex;gap:10px;">
<button onclick="confirmEarlySubmit()" style="flex:1;padding:10px;background:var(--accent);border:none;border-radius:var(--radius);color:white;font-size:14px;font-weight:600;cursor:pointer;">Submit now</button>
<button onclick="closeConfirm()" style="flex:1;padding:10px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);font-size:14px;cursor:pointer;">Cancel</button>
</div>
</div>
</div>
</div>
<!-- Done checkbox -->
<div class="done-card" id="done-card" onclick="toggleDone()">
<div class="done-checkbox" id="done-checkbox"></div>
<div>
<div class="done-label">Mark as fully filled</div>
<div class="done-sub" id="done-sub">Not marked yet</div>
</div>
</div>
</div>
<script>
const QUESTIONS = {{ questions | tojson }};
const PERIOD = {{ period | tojson }};
let answers = {};
let saveTimer = null;
// ── Status ──
function setStatus(s) {
const el = document.getElementById('status');
el.className = s;
if (s === 'saving') el.textContent = 'saving';
else if (s === 'saved') el.textContent = 'saved';
else if (s === 'error') el.textContent = 'error';
else el.textContent = '';
}
// ── Period banner ──
function renderPeriod() {
if (!PERIOD.start || !PERIOD.end) {
document.getElementById('period-text').innerHTML = '<strong>No active period configured</strong>';
return;
}
const start = new Date(PERIOD.start);
const end = new Date(PERIOD.end);
const now = new Date();
const fmt = d => d.toLocaleString('en-GB', {day:'numeric', month:'short', hour:'2-digit', minute:'2-digit', hour12:false});
document.getElementById('period-text').innerHTML =
`<strong>${fmt(start)}</strong> → <strong>${fmt(end)}</strong>`;
const total = end - start;
const elapsed = Math.max(0, Math.min(now - start, total));
const pct = total > 0 ? Math.round(elapsed / total * 100) : 0;
document.getElementById('period-pct').textContent = pct + '%';
document.getElementById('period-bar').style.width = pct + '%';
}
renderPeriod();
// ── Load & save ──
async function loadAnswers() {
const res = await fetch('/api/answers');
answers = await res.json();
renderAll();
renderDone();
}
function scheduleSave() {
clearTimeout(saveTimer);
setStatus('saving');
saveTimer = setTimeout(async () => {
try {
await fetch('/api/answers', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(answers)
});
setStatus('saved');
setTimeout(() => setStatus('idle'), 2000);
} catch(e) { setStatus('error'); }
}, 600);
}
function set(id, val) { answers[id] = val; scheduleSave(); }
// ── Done checkbox ──
async function toggleDone() {
const current = answers.__done__ || false;
const next = !current;
answers.__done__ = next;
answers.__done_at__ = next ? new Date().toISOString() : null;
scheduleSave();
renderDone();
}
function renderDone() {
const card = document.getElementById('done-card');
const box = document.getElementById('done-checkbox');
const sub = document.getElementById('done-sub');
const done = answers.__done__;
card.classList.toggle('checked', !!done);
box.textContent = done ? '✓' : '';
if (done && answers.__done_at__) {
const d = new Date(answers.__done_at__);
sub.textContent = 'Marked at ' + d.toLocaleTimeString('en-GB', {hour:'2-digit', minute:'2-digit'});
} else {
sub.textContent = 'Not marked yet';
}
}
// ── Render questions ──
function renderAll() {
const form = document.getElementById('form');
form.innerHTML = '';
QUESTIONS.forEach(q => {
const wrap = document.createElement('div');
wrap.className = 'question';
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `<div class="question-label">${escHtml(q.label)}</div>` + renderField(q);
wrap.appendChild(card);
form.appendChild(wrap);
attachListeners(q, card);
});
}
function renderField(q) {
const val = answers[q.id];
switch(q.type) {
case 'yesno': {
const y = val === true, n = val === false;
return `<div class="yesno">
<button data-id="${q.id}" data-val="yes" class="${y?'active-yes':''}">Yes</button>
<button data-id="${q.id}" data-val="no" class="${n?'active-no':''}">No</button>
</div>`;
}
case 'scale': {
const v = val ?? Math.round(((q.min??1)+(q.max??10))/2);
return `<div class="scale-wrap">
<div class="scale-top">
<span class="scale-value" id="sv-${q.id}">${v}</span>
<span class="scale-range">${q.min??1} ${q.max??10}</span>
</div>
<input type="range" data-id="${q.id}" min="${q.min??1}" max="${q.max??10}" step="${q.step??1}" value="${v}">
</div>`;
}
case 'number': {
return `<input type="number" data-id="${q.id}" min="${q.min??''}" max="${q.max??''}" value="${val??''}" placeholder="—">`;
}
case 'duration': {
const h = val != null ? Math.floor(val/60) : '';
const m = val != null ? String(val%60).padStart(2,'0') : '';
return `<div class="duration-wrap">
<input type="number" data-id="${q.id}" data-part="h" min="0" max="23" value="${h}" placeholder="h">
<span class="time-sep">:</span>
<input type="number" data-id="${q.id}" data-part="m" min="0" max="59" value="${m}" placeholder="mm">
</div>`;
}
case 'time': {
const h = val != null ? String(Math.floor(val/60)).padStart(2,'0') : '';
const m = val != null ? String(val%60).padStart(2,'0') : '';
return `<div class="time-wrap">
<input type="number" data-id="${q.id}" data-part="h" min="0" max="23" value="${h}" placeholder="HH">
<span class="time-sep">:</span>
<input type="number" data-id="${q.id}" data-part="m" min="0" max="59" value="${m}" placeholder="MM">
</div>`;
}
case 'counter': {
const v = val ?? 0;
return `<div class="counter-wrap">
<button class="counter-btn" data-id="${q.id}" data-action="dec"></button>
<div class="counter-val" id="cv-${q.id}">${v}</div>
<button class="counter-btn" data-id="${q.id}" data-action="inc">+</button>
</div>`;
}
case 'text':
return `<input type="text" data-id="${q.id}" value="${escHtml(val??'')}" placeholder="…">`;
case 'textarea':
return `<textarea data-id="${q.id}" placeholder="…">${escHtml(val??'')}</textarea>`;
case 'choice': {
return `<div class="choice-wrap">${q.options.map(o =>
`<button class="choice-btn${val===o?' active':''}" data-id="${q.id}" data-val="${escHtml(o)}">${escHtml(o)}</button>`
).join('')}</div>`;
}
case 'multichoice': {
const sel = Array.isArray(val) ? val : [];
return `<div class="multichoice-wrap">${q.options.map(o =>
`<button class="choice-btn${sel.includes(o)?' active':''}" data-id="${q.id}" data-val="${escHtml(o)}">${escHtml(o)}</button>`
).join('')}</div>`;
}
default: return `<span style="color:var(--text3);font-size:12px">unknown type: ${q.type}</span>`;
}
}
function attachListeners(q, div) {
switch(q.type) {
case 'yesno':
div.querySelectorAll('button[data-id]').forEach(btn => btn.addEventListener('click', () => {
const v = btn.dataset.val === 'yes';
set(q.id, v);
div.querySelectorAll('button[data-id]').forEach(b => b.className = '');
btn.className = v ? 'active-yes' : 'active-no';
}));
break;
case 'scale': {
const inp = div.querySelector('input[type=range]');
const display = div.querySelector(`#sv-${q.id}`);
inp.addEventListener('input', () => { display.textContent = inp.value; set(q.id, Number(inp.value)); });
break;
}
case 'number':
div.querySelector('input[type=number]').addEventListener('input', e => {
set(q.id, e.target.value === '' ? null : Number(e.target.value));
});
break;
case 'duration':
case 'time':
div.querySelectorAll('input[type=number]').forEach(inp => {
inp.addEventListener('input', () => {
const h = parseInt(div.querySelector('[data-part=h]').value) || 0;
const m = parseInt(div.querySelector('[data-part=m]').value) || 0;
set(q.id, h * 60 + m);
});
});
break;
case 'counter': {
const display = div.querySelector(`#cv-${q.id}`);
const cfg = QUESTIONS.find(x => x.id === q.id);
div.querySelectorAll('.counter-btn').forEach(btn => btn.addEventListener('click', () => {
let v = answers[q.id] ?? 0;
if (btn.dataset.action === 'inc') v = Math.min(v + (cfg.step||1), cfg.max ?? Infinity);
else v = Math.max(v - (cfg.step||1), cfg.min ?? -Infinity);
display.textContent = v;
set(q.id, v);
}));
break;
}
case 'text':
div.querySelector('input[type=text]').addEventListener('input', e => set(q.id, e.target.value));
break;
case 'textarea':
div.querySelector('textarea').addEventListener('input', e => set(q.id, e.target.value));
break;
case 'choice':
div.querySelectorAll('.choice-btn').forEach(btn => btn.addEventListener('click', () => {
set(q.id, btn.dataset.val);
div.querySelectorAll('.choice-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
}));
break;
case 'multichoice':
div.querySelectorAll('.choice-btn').forEach(btn => btn.addEventListener('click', () => {
let sel = Array.isArray(answers[q.id]) ? [...answers[q.id]] : [];
const v = btn.dataset.val;
sel = sel.includes(v) ? sel.filter(x => x !== v) : [...sel, v];
set(q.id, sel);
btn.classList.toggle('active');
}));
break;
}
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function formatValMd(q, val) {
if (val == null) return '_(no answer)_';
switch(q.type) {
case 'yesno': return val ? 'Yes' : 'No';
case 'duration':
case 'time': { const h = Math.floor(val/60), m = val%60; return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`; }
case 'multichoice': return Array.isArray(val) ? val.join(', ') : String(val);
default: return String(val);
}
}
function downloadCurrentMd(e) {
e.preventDefault();
const periodStart = PERIOD.start ? new Date(PERIOD.start) : new Date();
const dateStr = periodStart.toISOString().slice(0, 10);
const now = new Date();
const lines = [
`# Daily Check-in — ${dateStr}`,
'',
`_Downloaded at: ${now.toLocaleString('en-GB')}_`,
'',
'---',
'',
];
QUESTIONS.forEach(q => {
lines.push(`## ${q.label}`, '', formatValMd(q, answers[q.id]), '');
});
const blob = new Blob([lines.join('\n')], {type: 'text/markdown'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `checkin-${dateStr}-draft.md`;
a.click();
}
function earlySubmit() {
document.getElementById('confirm-overlay').style.display = 'block';
}
function closeConfirm() {
document.getElementById('confirm-overlay').style.display = 'none';
}
async function confirmEarlySubmit() {
closeConfirm();
setStatus('saving');
try {
const res = await fetch('/api/snapshot/now', {method: 'POST'});
if (!res.ok) throw new Error();
answers = {};
renderAll();
renderDone();
setStatus('saved');
setTimeout(() => { setStatus('idle'); window.location.reload(); }, 1000);
} catch(e) { setStatus('error'); }
}
loadAnswers();
initThemeToggle();
</script>
</body>
</html>