diff --git a/app/static/app.js b/app/static/app.js index 3281921..56e9472 100644 --- a/app/static/app.js +++ b/app/static/app.js @@ -1,6 +1,7 @@ 'use strict'; const VIEWS = [ + { id: 'overview', label: 'Overview', icon: '🧭' }, { id: 'today', label: 'Today', icon: '☀️' }, { id: 'upcoming', label: 'Upcoming', icon: '📅' }, { id: 'calendar', label: 'Calendar', icon: '🗓️' }, @@ -12,10 +13,12 @@ const PRIORITY_LABELS = ['', 'Low', 'Medium', 'High']; const REPEAT_UNITS = { daily: 'day', weekly: 'week', monthly: 'month', yearly: 'year' }; const HOUR_PX = 48; const BACKLOG_SUGGESTIONS = 2; +const DEFAULT_VIEW = 'overview'; +const OVERVIEW_DAYS = 7; // days after today that the overview lists one by one const state = { tasks: [], categories: [], projects: [], - view: 'today', categoryId: null, projectId: null, + view: DEFAULT_VIEW, categoryId: null, projectId: null, search: '', calDate: null, showDoneProjects: false, }; let editing = null; // task open in the dialog; null when creating @@ -198,10 +201,12 @@ async function act(fn) { // --- Persisted UI prefs ------------------------------------------------------- +const PREFS_VERSION = 2; + function savePrefs() { try { localStorage.setItem('whattodo.view', JSON.stringify({ - view: state.view, categoryId: state.categoryId, projectId: state.projectId, + view: state.view, categoryId: state.categoryId, projectId: state.projectId, v: PREFS_VERSION, })); } catch { /* ignore */ } } @@ -212,6 +217,8 @@ function loadPrefs() { state.view = p.view; state.categoryId = p.categoryId ?? null; state.projectId = p.projectId ?? null; + // Today used to be the start page; move people who were left on it to the overview once. + if (!p.v && p.view === 'today') state.view = DEFAULT_VIEW; } } catch { /* ignore */ } } @@ -289,6 +296,16 @@ function buildGroups(tasks) { { title: 'From your backlog', tasks: state.search ? [] : backlogSuggestions(), hint: 'No date yet. Plan one for today?' }, ]; } + if (state.view === 'overview') { + const shown = new Set(open.filter((x) => overdue(x) || today(x)).map((x) => x.id)); + const days = Array.from({ length: OVERVIEW_DAYS }, (_, i) => addDays(t, i + 1)); + return [ + group('Overdue', overdue), + group('Today', today), + group('High priority, not planned', (x) => x.priority === 3 && !shown.has(x.id) && !x.planned_date), + ...days.map((day) => group(dayName(day), (x) => later(x) && whenDate(x) === day)), + ]; + } if (state.view === 'upcoming') { return [ group('Tomorrow', (x) => later(x) && whenDate(x) === tomorrow), @@ -323,7 +340,9 @@ function counts() { const t = todayStr(); const open = state.tasks.filter((x) => !x.done); const overdue = open.filter((x) => isOverdue(x, t)).length; + const laterThan = addDays(t, OVERVIEW_DAYS); const c = { + later: open.filter((x) => !isOverdue(x, t) && !isForToday(x, t) && whenDate(x) > laterThan).length, today: open.filter((x) => isOverdue(x, t) || isForToday(x, t) || (x.priority === 3 && !x.planned_date)).length, upcoming: open.filter((x) => !isOverdue(x, t) && !isForToday(x, t) && whenDate(x)).length, backlog: open.filter(isBacklog).length, @@ -334,6 +353,7 @@ function counts() { cats: {}, projects: {}, }; + c.overview = c.today; for (const x of open) { c.cats[x.category_id || 0] = (c.cats[x.category_id || 0] || 0) + 1; if (x.project_id) c.projects[x.project_id] = (c.projects[x.project_id] || 0) + 1; @@ -402,7 +422,7 @@ function navItem({ active, icon, label, count, onclick, color, alert, muted }) { function renderNav(c) { $('#views').replaceChildren(...VIEWS.map((v) => navItem({ active: state.view === v.id, icon: v.icon, label: v.label, count: c[v.id], - onclick: () => setView(v.id), alert: v.id === 'today' && c.overdue > 0, + onclick: () => setView(v.id), alert: ['overview', 'today'].includes(v.id) && c.overdue > 0, }))); const projects = sortedProjects(); @@ -438,11 +458,12 @@ function renderNav(c) { } function render() { - if (state.view === 'cat' && state.categoryId && !catById(state.categoryId)) state.view = 'today'; - if (state.view === 'project' && !projectById(state.projectId)) state.view = 'today'; - if (!['cat', 'project'].includes(state.view) && !VIEWS.some((v) => v.id === state.view)) state.view = 'today'; + if (state.view === 'cat' && state.categoryId && !catById(state.categoryId)) state.view = DEFAULT_VIEW; + if (state.view === 'project' && !projectById(state.projectId)) state.view = DEFAULT_VIEW; + if (!['cat', 'project'].includes(state.view) && !VIEWS.some((v) => v.id === state.view)) state.view = DEFAULT_VIEW; - renderNav(counts()); + const c = counts(); + renderNav(c); let title = (VIEWS.find((v) => v.id === state.view) || {}).label; if (state.view === 'cat') title = state.categoryId ? catById(state.categoryId).name : 'No category'; @@ -479,11 +500,16 @@ function render() { const msg = state.search ? 'No tasks match your search.' : state.view === 'done' ? 'Nothing completed yet.' : state.view === 'today' ? 'Nothing planned for today. 🎉' + : state.view === 'overview' ? 'Nothing planned for today or the coming week. 🎉' : state.view === 'backlog' ? 'Backlog is empty: everything has a date.' : state.view === 'project' ? 'No tasks in this project yet. Add the first step above.' : 'No tasks here.'; sections.push(h('div', { class: 'empty' }, msg)); } + if (state.view === 'overview' && !state.search && c.later) { + sections.push(h('button', { type: 'button', class: 'more-link', onclick: () => setView('upcoming') }, + `${c.later} more planned later →`)); + } $('#list').replaceChildren(...sections); } @@ -759,7 +785,7 @@ function viewDefaults() { d.project_id = p.id; if (p.category_id) d.category_id = p.category_id; } - if (state.view === 'today') d.planned_date = todayStr(); + if (['overview', 'today'].includes(state.view)) d.planned_date = todayStr(); if (state.view === 'upcoming') d.planned_date = addDays(todayStr(), 1); if (state.view === 'calendar') d.planned_date = state.calDate || todayStr(); return d; diff --git a/app/static/style.css b/app/static/style.css index 2f39d40..fd272b6 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -324,6 +324,8 @@ input:disabled { opacity: 0.5; } .link-chip { cursor: pointer; } .link-chip:hover { color: var(--accent); } .plan-btn { align-self: center; font-size: 16px; } +.more-link { display: block; margin: 18px auto 0; padding: 8px 12px; border: 0; background: none; color: var(--muted); font: inherit; font-size: 14px; cursor: pointer; } +.more-link:hover { color: var(--accent); } .group-hint { margin: -4px 0 8px; font-size: 13px; color: var(--muted); } /* Projects */