Add an Overview start page combining today and the coming week

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
goyban
2026-10-12 10:45:49 +02:00
parent 0060cd11bb
commit c015e5663b
2 changed files with 36 additions and 8 deletions
+34 -8
View File
@@ -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;
+2
View File
@@ -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 */