First commit
This commit is contained in:
198
templates/history.html
Normal file
198
templates/history.html
Normal file
@@ -0,0 +1,198 @@
|
||||
<!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>History — 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="/">Form</a>
|
||||
<a href="/history" class="active">History</a>
|
||||
</div>
|
||||
<button class="theme-toggle" id="theme-toggle" title="Toggle theme"></button>
|
||||
</nav>
|
||||
|
||||
<div class="page-wide">
|
||||
|
||||
<div class="cal-header">
|
||||
<button class="cal-nav-btn" id="prev-btn">‹</button>
|
||||
<h2 id="month-label"></h2>
|
||||
<button class="cal-nav-btn" id="next-btn">›</button>
|
||||
</div>
|
||||
|
||||
<div class="cal-grid" id="cal-grid"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Overlay -->
|
||||
<div class="overlay" id="overlay" onclick="closePanel()"></div>
|
||||
|
||||
<!-- Snapshot panel -->
|
||||
<div class="snapshot-panel" id="snapshot-panel">
|
||||
<div class="snapshot-panel-header">
|
||||
<span class="snapshot-panel-title" id="panel-title">—</span>
|
||||
<button class="panel-close-btn" onclick="closePanel()">✕</button>
|
||||
</div>
|
||||
<div class="snapshot-panel-body" id="panel-body">
|
||||
<p style="color:var(--text3);font-size:13px">Select a day to view answers.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const QUESTIONS = {{ questions | tojson }};
|
||||
let snapshots = {}; // date string → snapshot doc
|
||||
let curYear, curMonth;
|
||||
|
||||
const DAYS = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
|
||||
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
|
||||
|
||||
async function loadSnapshots() {
|
||||
const res = await fetch('/api/snapshots?limit=365');
|
||||
const list = await res.json();
|
||||
snapshots = {};
|
||||
list.forEach(s => { snapshots[s.date] = s; });
|
||||
renderCalendar();
|
||||
}
|
||||
|
||||
function renderCalendar() {
|
||||
const label = document.getElementById('month-label');
|
||||
label.textContent = MONTHS[curMonth] + ' ' + curYear;
|
||||
|
||||
const grid = document.getElementById('cal-grid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
// Day of week headers
|
||||
DAYS.forEach(d => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'cal-dow';
|
||||
el.textContent = d;
|
||||
grid.appendChild(el);
|
||||
});
|
||||
|
||||
const today = new Date();
|
||||
const todayStr = fmtDate(today);
|
||||
|
||||
// First day of month — Monday-based offset
|
||||
const firstDay = new Date(curYear, curMonth, 1);
|
||||
let offset = firstDay.getDay(); // 0=Sun
|
||||
offset = offset === 0 ? 6 : offset - 1; // convert to Mon=0
|
||||
|
||||
for (let i = 0; i < offset; i++) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'cal-cell empty';
|
||||
grid.appendChild(el);
|
||||
}
|
||||
|
||||
const daysInMonth = new Date(curYear, curMonth + 1, 0).getDate();
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
const dateStr = `${curYear}-${String(curMonth+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
|
||||
const snap = snapshots[dateStr];
|
||||
const isToday = dateStr === todayStr;
|
||||
const isDone = snap && snap.answers && snap.answers.__done__;
|
||||
const hasSnap = !!snap;
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = 'cal-cell' +
|
||||
(hasSnap ? ' has-snapshot' : '') +
|
||||
(isDone ? ' done' : '') +
|
||||
(isToday ? ' today' : '');
|
||||
|
||||
el.innerHTML = `<span class="cal-day-num">${d}</span>`;
|
||||
if (hasSnap && !isDone) el.innerHTML += `<span class="cal-dot"></span>`;
|
||||
if (isDone) el.innerHTML += `<span class="cal-done-dot"></span>`;
|
||||
|
||||
if (hasSnap) el.addEventListener('click', () => openPanel(dateStr, snap));
|
||||
grid.appendChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
function fmtDate(d) {
|
||||
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
|
||||
}
|
||||
|
||||
function openPanel(dateStr, snap) {
|
||||
document.getElementById('panel-title').textContent = formatDateNice(dateStr);
|
||||
const body = document.getElementById('panel-body');
|
||||
|
||||
// Download button
|
||||
let html = `<a class="download-btn" href="/api/snapshots/${dateStr}/md" download="checkin-${dateStr}.md">⬇ Download .md</a>`;
|
||||
|
||||
// Done badge
|
||||
if (snap.answers && snap.answers.__done__) {
|
||||
const at = snap.answers.__done_at__ ? new Date(snap.answers.__done_at__).toLocaleTimeString('en-GB', {hour:'2-digit',minute:'2-digit'}) : '';
|
||||
html += `<div style="display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--success-bg);border:1px solid var(--success);border-radius:100px;font-size:12px;color:var(--success);margin-bottom:20px;margin-left:8px;">✓ Filled ${at ? 'at ' + at : ''}</div>`;
|
||||
}
|
||||
|
||||
html += `<div style="border-top:1px solid var(--border);padding-top:20px;">`;
|
||||
QUESTIONS.forEach(q => {
|
||||
const val = snap.answers ? snap.answers[q.id] : null;
|
||||
html += `<div class="snapshot-qa">
|
||||
<div class="snapshot-q">${escHtml(q.label)}</div>
|
||||
<div class="snapshot-a">${formatVal(q, val)}</div>
|
||||
</div>`;
|
||||
});
|
||||
html += `</div>`;
|
||||
|
||||
body.innerHTML = html;
|
||||
document.getElementById('snapshot-panel').classList.add('open');
|
||||
document.getElementById('overlay').classList.add('visible');
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
document.getElementById('snapshot-panel').classList.remove('open');
|
||||
document.getElementById('overlay').classList.remove('visible');
|
||||
}
|
||||
|
||||
function formatVal(q, val) {
|
||||
if (val == null) return `<span style="color:var(--text3)">—</span>`;
|
||||
switch(q.type) {
|
||||
case 'yesno': return val ? '✅ Yes' : '❌ No';
|
||||
case 'duration': {
|
||||
const h = Math.floor(val/60), m = val%60;
|
||||
return `${h}h ${String(m).padStart(2,'0')}m`;
|
||||
}
|
||||
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 escHtml(String(val));
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateNice(dateStr) {
|
||||
const d = new Date(dateStr + 'T12:00:00');
|
||||
return d.toLocaleDateString('en-GB', {weekday:'long', day:'numeric', month:'long', year:'numeric'});
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// Nav
|
||||
const now = new Date();
|
||||
curYear = now.getFullYear();
|
||||
curMonth = now.getMonth();
|
||||
|
||||
document.getElementById('prev-btn').addEventListener('click', () => {
|
||||
curMonth--;
|
||||
if (curMonth < 0) { curMonth = 11; curYear--; }
|
||||
renderCalendar();
|
||||
});
|
||||
document.getElementById('next-btn').addEventListener('click', () => {
|
||||
curMonth++;
|
||||
if (curMonth > 11) { curMonth = 0; curYear++; }
|
||||
renderCalendar();
|
||||
});
|
||||
|
||||
loadSnapshots();
|
||||
initThemeToggle();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
390
templates/index.html
Normal file
390
templates/index.html
Normal file
@@ -0,0 +1,390 @@
|
||||
<!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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user