diff --git a/frontend/src/app/(app)/layout.tsx b/frontend/src/app/(app)/layout.tsx
index 5d5ef75..ba60413 100644
--- a/frontend/src/app/(app)/layout.tsx
+++ b/frontend/src/app/(app)/layout.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useState, useRef } from "react";
+import { useEffect, useState, useRef, Suspense } from "react";
import { usePathname, useRouter } from "next/navigation";
import { signOut } from "next-auth/react";
import type { TriageQueue, User } from "@/lib/api-types";
@@ -9,6 +9,7 @@ import { cn } from "@/lib/utils";
import { useTheme } from "@/components/theme-provider";
import { TriageModal } from "@/components/triage-modal";
import { WinddownModal } from "@/components/winddown-modal";
+import { DomainNav } from "@/components/domain-nav";
type NavIconName = "review" | "today" | "soon" | "later" | "someday" | "done" | "settings";
@@ -234,6 +235,11 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
+ {/* Domain nav section */}
+
+
+
+
{/* Bottom section — upgrade CTA + theme toggle + settings */}
{user && !user.is_pro && (
diff --git a/frontend/src/app/(app)/today/page.tsx b/frontend/src/app/(app)/today/page.tsx
index 145f0a0..47953d8 100644
--- a/frontend/src/app/(app)/today/page.tsx
+++ b/frontend/src/app/(app)/today/page.tsx
@@ -1,6 +1,7 @@
"use client";
-import { useEffect, useState, useCallback, useRef } from "react";
+import { useEffect, useState, useCallback, useRef, Suspense } from "react";
+import { useSearchParams } from "next/navigation";
import {
DndContext,
closestCenter,
@@ -24,15 +25,15 @@ import { SortableTaskItem } from "@/components/sortable-task-item";
import { TaskInput } from "@/components/task-input";
import type { TaskInputHandle } from "@/components/task-input";
import { useGlobalShortcut } from "@/hooks/useGlobalShortcut";
-import { cn } from "@/lib/utils";
const BUCKET: BucketType = "today";
-export default function TodayPage() {
+function TodayContent() {
+ const searchParams = useSearchParams();
+ const domainFilter = searchParams.get("domain_id");
const taskInputRef = useRef
(null);
const [tasks, setTasks] = useState([]);
const [domains, setDomains] = useState([]);
- const [domainFilter, setDomainFilter] = useState(null);
const [loading, setLoading] = useState(true);
useGlobalShortcut("n", useCallback(() => {
@@ -207,41 +208,6 @@ export default function TodayPage() {
return (
- {/* Domain filters */}
- {domains.length > 0 && (
-
-
- {domains.map((d) => (
-
- ))}
-
- )}
-
{/* Task input */}
@@ -309,3 +275,11 @@ export default function TodayPage() {
);
}
+
+export default function TodayPage() {
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/components/domain-nav.tsx b/frontend/src/components/domain-nav.tsx
new file mode 100644
index 0000000..f8bead6
--- /dev/null
+++ b/frontend/src/components/domain-nav.tsx
@@ -0,0 +1,97 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useRouter, useSearchParams, usePathname } from "next/navigation";
+import type { Domain, Task } from "@/lib/api-types";
+import { getDomains, getTasks } from "@/lib/api";
+import { cn } from "@/lib/utils";
+
+export function DomainNav() {
+ const router = useRouter();
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+ const activeDomainId = searchParams.get("domain_id");
+
+ const [domains, setDomains] = useState([]);
+ const [pendingTasks, setPendingTasks] = useState([]);
+ const [loaded, setLoaded] = useState(false);
+
+ useEffect(() => {
+ Promise.all([getDomains(), getTasks({ status: "pending" })])
+ .then(([d, t]) => {
+ setDomains(d);
+ setPendingTasks(t);
+ setLoaded(true);
+ })
+ .catch((err) => {
+ console.error("DomainNav: failed to load", err);
+ setLoaded(true);
+ });
+ }, [pathname]);
+
+ if (!loaded || domains.length === 0) return null;
+
+ function countForDomain(domainId: string): number {
+ return pendingTasks.filter((t) => t.domain?.id === domainId).length;
+ }
+
+ function handleClick(domainId: string | null) {
+ const params = new URLSearchParams(searchParams.toString());
+ if (domainId === null || activeDomainId === domainId) {
+ params.delete("domain_id");
+ } else {
+ params.set("domain_id", domainId);
+ }
+ const qs = params.toString();
+ router.push(`${pathname}${qs ? `?${qs}` : ""}`);
+ }
+
+ return (
+
+
+ Domains
+
+
+ {/* All — clears filter */}
+
+
+ {domains.map((d) => {
+ const isActive = activeDomainId === d.id;
+ const count = countForDomain(d.id);
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/frontend/src/components/task-item.tsx b/frontend/src/components/task-item.tsx
index 73a9a56..d7bf6e7 100644
--- a/frontend/src/components/task-item.tsx
+++ b/frontend/src/components/task-item.tsx
@@ -3,7 +3,7 @@
import { useState, useRef, useEffect, useCallback } from "react";
import { StickyNote } from "lucide-react";
import type { Task, Domain, SubTask } from "@/lib/api-types";
-import { completeTask, createTask, deleteTask, updateTask } from "@/lib/api";
+import { completeTask, createTask, deleteTask, updateTask, setPriority } from "@/lib/api";
import { formatAge, ageColor, cn } from "@/lib/utils";
import { DomainPicker } from "@/components/domain-picker";
@@ -143,11 +143,22 @@ export function TaskItem({ task, domains, onMutate, isMIT, onSetMIT }: TaskItemP
const [noteSaving, setNoteSaving] = useState(false);
const [noteError, setNoteError] = useState(null);
const noteTextareaRef = useRef(null);
+ // Priority state — optimistic local copies, kept in sync with incoming props
+ const [important, setImportant] = useState(task.important);
+ const [urgent, setUrgent] = useState(task.urgent);
+ const mountedRef = useRef(true);
+ useEffect(() => {
+ mountedRef.current = true;
+ return () => { mountedRef.current = false; };
+ }, []);
+ useEffect(() => { setImportant(task.important); }, [task.important]);
+ useEffect(() => { setUrgent(task.urgent); }, [task.urgent]);
const isComplete = task.status === "complete";
const isSubtask = !!task.parent_id;
const hasChildren = task.children.length > 0;
const hasNote = !!task.notes;
const completedChildren = task.children.filter((c) => c.status === "complete").length;
+ const isQ1 = important && urgent;
useEffect(() => {
if (isEditing && inputRef.current) {
@@ -258,6 +269,30 @@ export function TaskItem({ task, domains, onMutate, isMIT, onSetMIT }: TaskItemP
}
}
+ async function handleToggleImportant() {
+ const prev = important;
+ setImportant(!prev);
+ try {
+ await setPriority(task.id, { important: !prev });
+ onMutate();
+ } catch (err) {
+ console.error("Failed to set important:", err);
+ if (mountedRef.current) setImportant(prev);
+ }
+ }
+
+ async function handleToggleUrgent() {
+ const prev = urgent;
+ setUrgent(!prev);
+ try {
+ await setPriority(task.id, { urgent: !prev });
+ onMutate();
+ } catch (err) {
+ console.error("Failed to set urgent:", err);
+ if (mountedRef.current) setUrgent(prev);
+ }
+ }
+
function startNoteEdit() {
setNoteDraft(task.notes ?? "");
setNoteEditing(true);
@@ -313,6 +348,7 @@ export function TaskItem({ task, domains, onMutate, isMIT, onSetMIT }: TaskItemP
{/* Complete checkbox */}