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:
+34
-8
@@ -1,6 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const VIEWS = [
|
const VIEWS = [
|
||||||
|
{ id: 'overview', label: 'Overview', icon: '🧭' },
|
||||||
{ id: 'today', label: 'Today', icon: '☀️' },
|
{ id: 'today', label: 'Today', icon: '☀️' },
|
||||||
{ id: 'upcoming', label: 'Upcoming', icon: '📅' },
|
{ id: 'upcoming', label: 'Upcoming', icon: '📅' },
|
||||||
{ id: 'calendar', label: 'Calendar', 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 REPEAT_UNITS = { daily: 'day', weekly: 'week', monthly: 'month', yearly: 'year' };
|
||||||
const HOUR_PX = 48;
|
const HOUR_PX = 48;
|
||||||
const BACKLOG_SUGGESTIONS = 2;
|
const BACKLOG_SUGGESTIONS = 2;
|
||||||
|
const DEFAULT_VIEW = 'overview';
|
||||||
|
const OVERVIEW_DAYS = 7; // days after today that the overview lists one by one
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
tasks: [], categories: [], projects: [],
|
tasks: [], categories: [], projects: [],
|
||||||
view: 'today', categoryId: null, projectId: null,
|
view: DEFAULT_VIEW, categoryId: null, projectId: null,
|
||||||
search: '', calDate: null, showDoneProjects: false,
|
search: '', calDate: null, showDoneProjects: false,
|
||||||
};
|
};
|
||||||
let editing = null; // task open in the dialog; null when creating
|
let editing = null; // task open in the dialog; null when creating
|
||||||
@@ -198,10 +201,12 @@ async function act(fn) {
|
|||||||
|
|
||||||
// --- Persisted UI prefs -------------------------------------------------------
|
// --- Persisted UI prefs -------------------------------------------------------
|
||||||
|
|
||||||
|
const PREFS_VERSION = 2;
|
||||||
|
|
||||||
function savePrefs() {
|
function savePrefs() {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('whattodo.view', JSON.stringify({
|
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 */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
@@ -212,6 +217,8 @@ function loadPrefs() {
|
|||||||
state.view = p.view;
|
state.view = p.view;
|
||||||
state.categoryId = p.categoryId ?? null;
|
state.categoryId = p.categoryId ?? null;
|
||||||
state.projectId = p.projectId ?? 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 */ }
|
} 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?' },
|
{ 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') {
|
if (state.view === 'upcoming') {
|
||||||
return [
|
return [
|
||||||
group('Tomorrow', (x) => later(x) && whenDate(x) === tomorrow),
|
group('Tomorrow', (x) => later(x) && whenDate(x) === tomorrow),
|
||||||
@@ -323,7 +340,9 @@ function counts() {
|
|||||||
const t = todayStr();
|
const t = todayStr();
|
||||||
const open = state.tasks.filter((x) => !x.done);
|
const open = state.tasks.filter((x) => !x.done);
|
||||||
const overdue = open.filter((x) => isOverdue(x, t)).length;
|
const overdue = open.filter((x) => isOverdue(x, t)).length;
|
||||||
|
const laterThan = addDays(t, OVERVIEW_DAYS);
|
||||||
const c = {
|
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,
|
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,
|
upcoming: open.filter((x) => !isOverdue(x, t) && !isForToday(x, t) && whenDate(x)).length,
|
||||||
backlog: open.filter(isBacklog).length,
|
backlog: open.filter(isBacklog).length,
|
||||||
@@ -334,6 +353,7 @@ function counts() {
|
|||||||
cats: {},
|
cats: {},
|
||||||
projects: {},
|
projects: {},
|
||||||
};
|
};
|
||||||
|
c.overview = c.today;
|
||||||
for (const x of open) {
|
for (const x of open) {
|
||||||
c.cats[x.category_id || 0] = (c.cats[x.category_id || 0] || 0) + 1;
|
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;
|
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) {
|
function renderNav(c) {
|
||||||
$('#views').replaceChildren(...VIEWS.map((v) => navItem({
|
$('#views').replaceChildren(...VIEWS.map((v) => navItem({
|
||||||
active: state.view === v.id, icon: v.icon, label: v.label, count: c[v.id],
|
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();
|
const projects = sortedProjects();
|
||||||
@@ -438,11 +458,12 @@ function renderNav(c) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
if (state.view === 'cat' && state.categoryId && !catById(state.categoryId)) 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 = 'today';
|
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 = 'today';
|
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;
|
let title = (VIEWS.find((v) => v.id === state.view) || {}).label;
|
||||||
if (state.view === 'cat') title = state.categoryId ? catById(state.categoryId).name : 'No category';
|
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.'
|
const msg = state.search ? 'No tasks match your search.'
|
||||||
: state.view === 'done' ? 'Nothing completed yet.'
|
: state.view === 'done' ? 'Nothing completed yet.'
|
||||||
: state.view === 'today' ? 'Nothing planned for today. 🎉'
|
: 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 === 'backlog' ? 'Backlog is empty: everything has a date.'
|
||||||
: state.view === 'project' ? 'No tasks in this project yet. Add the first step above.'
|
: state.view === 'project' ? 'No tasks in this project yet. Add the first step above.'
|
||||||
: 'No tasks here.';
|
: 'No tasks here.';
|
||||||
sections.push(h('div', { class: 'empty' }, msg));
|
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);
|
$('#list').replaceChildren(...sections);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,7 +785,7 @@ function viewDefaults() {
|
|||||||
d.project_id = p.id;
|
d.project_id = p.id;
|
||||||
if (p.category_id) d.category_id = p.category_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 === 'upcoming') d.planned_date = addDays(todayStr(), 1);
|
||||||
if (state.view === 'calendar') d.planned_date = state.calDate || todayStr();
|
if (state.view === 'calendar') d.planned_date = state.calDate || todayStr();
|
||||||
return d;
|
return d;
|
||||||
|
|||||||
@@ -324,6 +324,8 @@ input:disabled { opacity: 0.5; }
|
|||||||
.link-chip { cursor: pointer; }
|
.link-chip { cursor: pointer; }
|
||||||
.link-chip:hover { color: var(--accent); }
|
.link-chip:hover { color: var(--accent); }
|
||||||
.plan-btn { align-self: center; font-size: 16px; }
|
.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); }
|
.group-hint { margin: -4px 0 8px; font-size: 13px; color: var(--muted); }
|
||||||
|
|
||||||
/* Projects */
|
/* Projects */
|
||||||
|
|||||||
Reference in New Issue
Block a user