Todoist-style: priorities P1-P4, estimates, FAB add form, Today view, natural quick add, drag-and-drop with haptics
All checks were successful
Build & Release APK / build (push) Successful in 1m53s

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 19:46:11 -07:00
parent 035a114e58
commit 0487bc7e4e
17 changed files with 752 additions and 123 deletions

View File

@@ -7,7 +7,7 @@ if (!API_TOKEN) throw new Error("API_TOKEN not set");
const TASK_COLS = `id, title, notes, status, project_id, context, waiting_for,
due_date::text as due_date, defer_date::text as defer_date,
completed_at, sort_order, created_at, updated_at`;
priority, estimate_min, completed_at, sort_order, created_at, updated_at`;
const PROJECT_COLS = `id, name, status, notes, sort_order, created_at, updated_at`;
const TASK_STATUSES = ["inbox", "next", "waiting", "scheduled", "someday", "done", "trashed"];
@@ -28,7 +28,7 @@ function err(message: string, status: number) {
}
// Fields a client may set. completed_at is derived from status server-side.
const TASK_FIELDS = ["title", "notes", "status", "project_id", "context", "waiting_for", "due_date", "defer_date", "sort_order"];
const TASK_FIELDS = ["title", "notes", "status", "project_id", "context", "waiting_for", "due_date", "defer_date", "priority", "estimate_min", "sort_order"];
const PROJECT_FIELDS = ["name", "status", "notes", "sort_order"];
function pick(body: Record<string, unknown>, fields: string[]) {
@@ -73,12 +73,13 @@ async function handle(req: Request): Promise<Response> {
const id = typeof body.id === "string" && UUID_RE.test(body.id) ? body.id : null;
// Client-supplied id + on-conflict no-op make offline-queue retries idempotent.
const rows = await sql.unsafe(
`insert into tasks (id, title, notes, status, project_id, context, waiting_for, due_date, defer_date, sort_order)
values (coalesce($1::uuid, gen_random_uuid()), $2, $3, $4, $5, $6, $7, $8, $9, $10)
`insert into tasks (id, title, notes, status, project_id, context, waiting_for, due_date, defer_date, priority, estimate_min, sort_order)
values (coalesce($1::uuid, gen_random_uuid()), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
on conflict (id) do nothing
returning ${TASK_COLS}`,
[id, t.title, t.notes ?? "", t.status ?? "inbox", t.project_id ?? null, t.context ?? null,
t.waiting_for ?? null, t.due_date ?? null, t.defer_date ?? null, t.sort_order ?? 0]);
t.waiting_for ?? null, t.due_date ?? null, t.defer_date ?? null, t.priority ?? 4,
t.estimate_min ?? null, t.sort_order ?? 0]);
if (rows.length) return json(rows[0], 201);
const existing = await sql.unsafe(`select ${TASK_COLS} from tasks where id = $1`, [id]);
return json(existing[0], 200);
@@ -99,10 +100,11 @@ async function handle(req: Request): Promise<Response> {
const completedAt = cur.status === "done" ? (existing[0].completed_at ?? new Date()) : null;
const rows = await sql.unsafe(
`update tasks set title=$2, notes=$3, status=$4, project_id=$5, context=$6, waiting_for=$7,
due_date=$8, defer_date=$9, completed_at=$10, sort_order=$11, updated_at=now()
due_date=$8, defer_date=$9, priority=$10, estimate_min=$11, completed_at=$12, sort_order=$13,
updated_at=now()
where id=$1 returning ${TASK_COLS}`,
[id, cur.title, cur.notes, cur.status, cur.project_id, cur.context, cur.waiting_for,
cur.due_date, cur.defer_date, completedAt, cur.sort_order]);
cur.due_date, cur.defer_date, cur.priority, cur.estimate_min, completedAt, cur.sort_order]);
return json(rows[0]);
}

View File

@@ -23,6 +23,8 @@ create table if not exists tasks (
waiting_for text, -- who/what is being waited on when status = 'waiting'
due_date date,
defer_date date, -- hide from lists until this date
priority int not null default 4 check (priority between 1 and 4), -- 1=urgent … 4=low
estimate_min int check (estimate_min > 0),
completed_at timestamptz,
sort_order double precision not null default 0,
created_at timestamptz not null default now(),