/* Focus Desk — vanilla task manager */ (function () { 'use strict'; var KEY = 'focusdesk.tasks.v1'; var PRIOS = ['low', 'medium', 'high']; var PRIO_LABEL = { low: 'Low', medium: 'Medium', high: 'High' }; var PRIO_RANK = { high: 0, medium: 1, low: 2 }; var tasks = []; var editingId = null; var $ = function (sel) { return document.querySelector(sel); }; var form = $('#create-form'); var fTitle = $('#f-title'), fNotes = $('#f-notes'), fPrio = $('#f-priority'), fDue = $('#f-due'); var errBox = $('#title-err'); var fSearch = $('#f-search'), fStatus = $('#f-status'), fPrioFilter = $('#f-prio'); var listEl = $('#list'), emptyEl = $('#empty'), resultLine = $('#result-line'); var emptyTitle = $('#empty-title'), emptyText = $('#empty-text'); var cTotal = document.querySelector('[data-testid="count-total"]'); var cActive = document.querySelector('[data-testid="count-active"]'); var cDone = document.querySelector('[data-testid="count-completed"]'); /* ---------- storage ---------- */ function isDate(s) { return typeof s === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(s); } function normalize(raw) { if (!raw || typeof raw !== 'object') return null; var title = typeof raw.title === 'string' ? raw.title.trim() : ''; if (!title) return null; return { id: (typeof raw.id === 'string' || typeof raw.id === 'number') ? String(raw.id) : uid(), title: title, notes: typeof raw.notes === 'string' ? raw.notes : '', priority: PRIOS.indexOf(raw.priority) > -1 ? raw.priority : 'medium', due: isDate(raw.due) ? raw.due : '', completed: raw.completed === true, created: typeof raw.created === 'number' ? raw.created : Date.now() }; } function load() { var text; try { text = localStorage.getItem(KEY); } catch (e) { return []; } if (!text) return []; var data; try { data = JSON.parse(text); } catch (e) { console.warn('Focus Desk: saved data was invalid JSON; starting empty.'); try { localStorage.removeItem(KEY); } catch (e2) {} return []; } if (!Array.isArray(data)) return []; var out = []; for (var i = 0; i < data.length; i++) { var t = normalize(data[i]); if (t) out.push(t); } return out; } function save() { try { localStorage.setItem(KEY, JSON.stringify(tasks)); } catch (e) {} } function uid() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 8); } /* ---------- helpers ---------- */ function el(tag, props, kids) { var n = document.createElement(tag); if (props) { for (var k in props) { if (k === 'text') n.textContent = props[k]; else if (k === 'cls') n.className = props[k]; else if (k in n && k !== 'list') n[k] = props[k]; else n.setAttribute(k, props[k]); } } (kids || []).forEach(function (c) { if (c) n.appendChild(c); }); return n; } function todayISO() { var d = new Date(); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); } function prettyDate(iso) { var p = iso.split('-'); var d = new Date(Number(p[0]), Number(p[1]) - 1, Number(p[2])); if (isNaN(d.getTime())) return iso; var M = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return M[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear(); } function plural(n, w) { return n + ' ' + w + (n === 1 ? '' : 's'); } /* ---------- filtering ---------- */ function visible() { var q = fSearch.value.trim().toLowerCase(); var status = fStatus.value, prio = fPrioFilter.value; return tasks.filter(function (t) { if (status === 'active' && t.completed) return false; if (status === 'completed' && !t.completed) return false; if (prio !== 'all' && t.priority !== prio) return false; if (q) { var hay = (t.title + ' ' + t.notes).toLowerCase(); if (hay.indexOf(q) === -1) return false; } return true; }).sort(function (a, b) { if (a.completed !== b.completed) return a.completed ? 1 : -1; if (a.priority !== b.priority) return PRIO_RANK[a.priority] - PRIO_RANK[b.priority]; if (a.due !== b.due) { if (!a.due) return 1; if (!b.due) return -1; return a.due < b.due ? -1 : 1; } return b.created - a.created; }); } /* ---------- rendering ---------- */ function readRow(t) { var kids = [ el('h3', { cls: 'title', text: t.title, 'data-testid': 'task-title-text' }) ]; if (t.notes) kids.push(el('p', { cls: 'notes', text: t.notes, 'data-testid': 'task-notes-text' })); var meta = [el('span', { cls: 'tag ' + t.priority, text: PRIO_LABEL[t.priority] })]; if (t.due) { var over = !t.completed && t.due < todayISO(); meta.push(el('span', { cls: 'due' + (over ? ' over' : ''), text: (over ? 'Overdue · ' : 'Due ') + prettyDate(t.due) })); } if (t.completed) meta.push(el('span', { cls: 'due', text: 'Completed' })); kids.push(el('div', { cls: 'meta' }, meta)); var actions = el('div', { cls: 'actions' }, [ el('button', { cls: 'btn small', type: 'button', 'data-testid': 'task-edit', 'data-act': 'edit', 'aria-label': 'Edit task: ' + t.title, text: 'Edit' }), el('button', { cls: 'btn small danger', type: 'button', 'data-testid': 'task-delete', 'data-act': 'delete', 'aria-label': 'Delete task: ' + t.title, text: 'Delete' }) ]); return [el('div', { cls: 'body' }, kids), actions]; } function editRow(t) { var tid = 'edit-title-' + t.id, nid = 'edit-notes-' + t.id, pid = 'edit-prio-' + t.id, did = 'edit-due-' + t.id; var titleIn = el('input', { id: tid, type: 'text', value: t.title, required: true, 'data-testid': 'edit-title', autocomplete: 'off' }); var notesIn = el('textarea', { id: nid, rows: 3, 'data-testid': 'edit-notes' }); notesIn.value = t.notes; var prioSel = el('select', { id: pid, 'data-testid': 'edit-priority' }, PRIOS.map(function (p) { return el('option', { value: p, text: PRIO_LABEL[p], selected: p === t.priority }); })); prioSel.value = t.priority; var dueIn = el('input', { id: did, type: 'date', value: t.due, 'data-testid': 'edit-due' }); var err = el('p', { cls: 'err', role: 'alert', 'data-testid': 'edit-error', hidden: true }); var grid = el('form', { cls: 'edit-grid', 'data-act': 'save-form', novalidate: true }, [ el('div', { cls: 'field' }, [el('label', { htmlFor: tid, text: 'Title' }), titleIn, err]), el('div', { cls: 'field' }, [el('label', { htmlFor: nid, text: 'Notes' }), notesIn]), el('div', { cls: 'row' }, [ el('div', { cls: 'field' }, [el('label', { htmlFor: pid, text: 'Priority' }), prioSel]), el('div', { cls: 'field' }, [el('label', { htmlFor: did, text: 'Due date' }), dueIn]) ]), el('div', { cls: 'edit-actions' }, [ el('button', { cls: 'btn primary small', type: 'submit', 'data-testid': 'save-edit', text: 'Save' }), el('button', { cls: 'btn small', type: 'button', 'data-act': 'cancel', 'data-testid': 'cancel-edit', text: 'Cancel' }) ]) ]); return [el('div', { cls: 'body editing' }, [grid])]; } function row(t) { var cb = el('input', { type: 'checkbox', checked: t.completed, 'data-testid': 'task-toggle', 'data-act': 'toggle', id: 'cb-' + t.id }); cb.setAttribute('aria-label', (t.completed ? 'Mark as active: ' : 'Mark as completed: ') + t.title); var li = el('li', { cls: 'item' + (t.completed ? ' done' : ''), 'data-testid': 'task-item', 'data-id': t.id, 'data-priority': t.priority, 'data-completed': String(t.completed) }, [el('div', { cls: 'check' }, [cb])].concat(editingId === t.id ? editRow(t) : readRow(t))); return li; } function render() { var total = tasks.length; var done = tasks.filter(function (t) { return t.completed; }).length; cTotal.textContent = String(total); cActive.textContent = String(total - done); cDone.textContent = String(done); var shown = visible(); listEl.textContent = ''; var frag = document.createDocumentFragment(); shown.forEach(function (t) { frag.appendChild(row(t)); }); listEl.appendChild(frag); var filtering = fSearch.value.trim() !== '' || fStatus.value !== 'all' || fPrioFilter.value !== 'all'; if (shown.length === 0) { emptyEl.hidden = false; listEl.hidden = true; if (total === 0) { emptyTitle.textContent = 'No tasks yet'; emptyText.textContent = 'Add your first task using the form to get started.'; } else { emptyTitle.textContent = 'No matching tasks'; emptyText.textContent = 'No tasks match your search and filters. Try clearing the search box or resetting the filters.'; } } else { emptyEl.hidden = true; listEl.hidden = false; } resultLine.textContent = total === 0 ? 'No tasks yet.' : (filtering ? 'Showing ' + plural(shown.length, 'task') + ' of ' + total : 'Showing all ' + plural(total, 'task')); } /* ---------- actions ---------- */ function showError(msg) { errBox.textContent = msg; errBox.hidden = false; fTitle.setAttribute('aria-invalid', 'true'); } function clearError() { errBox.hidden = true; errBox.textContent = ''; fTitle.removeAttribute('aria-invalid'); } form.addEventListener('submit', function (e) { e.preventDefault(); var title = fTitle.value.trim(); if (!title) { showError('Please enter a task title.'); fTitle.focus(); return; } clearError(); tasks.push({ id: uid(), title: title, notes: fNotes.value.trim(), priority: PRIOS.indexOf(fPrio.value) > -1 ? fPrio.value : 'medium', due: isDate(fDue.value) ? fDue.value : '', completed: false, created: Date.now() }); save(); form.reset(); fPrio.value = 'medium'; render(); fTitle.focus(); }); fTitle.addEventListener('input', function () { if (!errBox.hidden && fTitle.value.trim()) clearError(); }); [fSearch, fStatus, fPrioFilter].forEach(function (n) { n.addEventListener('input', render); n.addEventListener('change', render); }); function find(id) { for (var i = 0; i < tasks.length; i++) if (tasks[i].id === id) return tasks[i]; return null; } listEl.addEventListener('click', function (e) { var btn = e.target.closest('[data-act]'); if (!btn || btn.tagName === 'FORM' || btn.type === 'checkbox') return; var li = btn.closest('[data-id]'); if (!li) return; var id = li.getAttribute('data-id'); var act = btn.getAttribute('data-act'); if (act === 'edit') { editingId = id; render(); var input = listEl.querySelector('[data-id="' + id + '"] [data-testid="edit-title"]'); if (input) { input.focus(); input.select(); } } else if (act === 'cancel') { editingId = null; render(); } else if (act === 'delete') { var t = find(id); if (!t) return; if (!window.confirm('Delete "' + t.title + '"? This cannot be undone.')) return; tasks = tasks.filter(function (x) { return x.id !== id; }); if (editingId === id) editingId = null; save(); render(); } }); listEl.addEventListener('change', function (e) { if (e.target.getAttribute && e.target.getAttribute('data-act') === 'toggle') { var li = e.target.closest('[data-id]'); var t = find(li && li.getAttribute('data-id')); if (!t) return; t.completed = e.target.checked; save(); render(); } }); listEl.addEventListener('submit', function (e) { var f = e.target; if (!f.matches('[data-act="save-form"]')) return; e.preventDefault(); var li = f.closest('[data-id]'); var t = find(li && li.getAttribute('data-id')); if (!t) return; var titleIn = f.querySelector('[data-testid="edit-title"]'); var err = f.querySelector('[data-testid="edit-error"]'); var title = titleIn.value.trim(); if (!title) { err.textContent = 'Title cannot be empty.'; err.hidden = false; titleIn.setAttribute('aria-invalid', 'true'); titleIn.focus(); return; } var dueVal = f.querySelector('[data-testid="edit-due"]').value; t.title = title; t.notes = f.querySelector('[data-testid="edit-notes"]').value.trim(); t.priority = f.querySelector('[data-testid="edit-priority"]').value; t.due = isDate(dueVal) ? dueVal : ''; editingId = null; save(); render(); }); listEl.addEventListener('keydown', function (e) { if (e.key === 'Escape' && editingId && e.target.closest('[data-act="save-form"]')) { editingId = null; render(); } }); tasks = load(); render(); })();