Inline app/ (was an embedded git repo)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 16:36:14 -07:00
parent df4068bb3b
commit dea5160e0b
51 changed files with 9014 additions and 1 deletions

View File

@@ -0,0 +1,36 @@
import { Link, Tabs } from "expo-router";
import { Text } from "react-native";
import { C } from "../../lib/theme";
function icon(glyph: string) {
return ({ focused }: { focused: boolean }) => (
<Text style={{ fontSize: 20, opacity: focused ? 1 : 0.45 }}>{glyph}</Text>
);
}
export default function TabsLayout() {
return (
<Tabs
screenOptions={{
headerStyle: { backgroundColor: C.bg },
headerTintColor: C.text,
headerShadowVisible: false,
sceneStyle: { backgroundColor: C.bg },
tabBarStyle: { backgroundColor: C.bg, borderTopColor: C.border },
tabBarActiveTintColor: C.text,
tabBarInactiveTintColor: C.muted,
headerRight: () => (
<Link href="/settings" style={{ paddingHorizontal: 16, fontSize: 18 }}>
</Link>
),
}}
>
<Tabs.Screen name="index" options={{ title: "Inbox", tabBarIcon: icon("📥") }} />
<Tabs.Screen name="next" options={{ title: "Next", tabBarIcon: icon("▶️") }} />
<Tabs.Screen name="waiting" options={{ title: "Waiting", tabBarIcon: icon("⏳") }} />
<Tabs.Screen name="someday" options={{ title: "Someday", tabBarIcon: icon("💭") }} />
<Tabs.Screen name="projects" options={{ title: "Projects", tabBarIcon: icon("📁") }} />
</Tabs>
);
}

View File

@@ -0,0 +1,5 @@
import TaskListScreen from "../../components/TaskListScreen";
export default function Inbox() {
return <TaskListScreen status="inbox" capture emptyHint="Inbox zero. Capture anything above." />;
}

View File

@@ -0,0 +1,5 @@
import TaskListScreen from "../../components/TaskListScreen";
export default function Next() {
return <TaskListScreen status="next" capture emptyHint="No next actions. Clarify your inbox." />;
}

View File

@@ -0,0 +1,95 @@
import { useFocusEffect, useRouter } from "expo-router";
import { useCallback, useState } from "react";
import { FlatList, Pressable, StyleSheet, Text, TextInput, View } from "react-native";
import { api } from "../../lib/api";
import { C } from "../../lib/theme";
import type { Project } from "../../lib/types";
export default function Projects() {
const router = useRouter();
const [projects, setProjects] = useState<Project[]>([]);
const [draft, setDraft] = useState("");
const [error, setError] = useState<string | null>(null);
const load = useCallback(async () => {
try {
setProjects(await api.projects.list());
setError(null);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
}
}, []);
useFocusEffect(
useCallback(() => {
load();
}, [load]),
);
const add = async () => {
const name = draft.trim();
if (!name) return;
setDraft("");
await api.projects.create({ name });
load();
};
return (
<View style={s.screen}>
<TextInput
style={s.capture}
placeholder="New project…"
placeholderTextColor={C.muted}
value={draft}
onChangeText={setDraft}
onSubmitEditing={add}
returnKeyType="done"
/>
{error && <Text style={s.error}>{error}</Text>}
<FlatList
data={projects}
keyExtractor={(p) => p.id}
renderItem={({ item }) => (
<Pressable style={s.row} onPress={() => router.push(`/project/${item.id}`)}>
<View style={{ flex: 1 }}>
<Text style={s.title}>{item.name}</Text>
<Text style={s.meta}>
{item.status}
{item.open_tasks != null && ` · ${item.open_tasks} open`}
</Text>
</View>
<Text style={{ color: C.muted }}></Text>
</Pressable>
)}
ListEmptyComponent={<Text style={s.empty}>No projects yet.</Text>}
/>
</View>
);
}
const s = StyleSheet.create({
screen: { flex: 1, backgroundColor: C.bg },
capture: {
margin: 12,
marginBottom: 4,
padding: 14,
borderRadius: 12,
backgroundColor: C.surface,
color: C.text,
fontSize: 16,
borderWidth: 1,
borderColor: C.border,
},
row: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 14,
paddingHorizontal: 16,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: C.border,
},
title: { color: C.text, fontSize: 16 },
meta: { color: C.muted, fontSize: 13, marginTop: 2 },
empty: { color: C.muted, textAlign: "center", marginTop: 48 },
error: { color: C.danger, marginHorizontal: 16, marginTop: 8 },
});

View File

@@ -0,0 +1,5 @@
import TaskListScreen from "../../components/TaskListScreen";
export default function Someday() {
return <TaskListScreen status="someday" emptyHint="No someday/maybe items." />;
}

View File

@@ -0,0 +1,5 @@
import TaskListScreen from "../../components/TaskListScreen";
export default function Waiting() {
return <TaskListScreen status="waiting" emptyHint="Not waiting on anyone." />;
}

24
app/src/app/_layout.tsx Normal file
View File

@@ -0,0 +1,24 @@
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { C } from "../lib/theme";
export default function RootLayout() {
return (
<>
<StatusBar style="light" />
<Stack
screenOptions={{
headerStyle: { backgroundColor: C.bg },
headerTintColor: C.text,
headerShadowVisible: false,
contentStyle: { backgroundColor: C.bg },
}}
>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="task/[id]" options={{ title: "Clarify", presentation: "modal" }} />
<Stack.Screen name="project/[id]" options={{ title: "Project" }} />
<Stack.Screen name="settings" options={{ title: "Settings", presentation: "modal" }} />
</Stack>
</>
);
}

View File

@@ -0,0 +1,122 @@
import { Stack, useFocusEffect, useLocalSearchParams, useRouter } from "expo-router";
import { useCallback, useState } from "react";
import { FlatList, Pressable, StyleSheet, Text, TextInput, View } from "react-native";
import { Button, Chips } from "../../components/ui";
import { api } from "../../lib/api";
import { C } from "../../lib/theme";
import type { Project, ProjectStatus, Task } from "../../lib/types";
const STATUSES: ProjectStatus[] = ["active", "someday", "completed"];
export default function ProjectDetail() {
const { id } = useLocalSearchParams<{ id: string }>();
const router = useRouter();
const [project, setProject] = useState<Project | null>(null);
const [tasks, setTasks] = useState<Task[]>([]);
const [draft, setDraft] = useState("");
const load = useCallback(async () => {
const [ps, ts] = await Promise.all([api.projects.list(), api.tasks.list({ project_id: id })]);
setProject(ps.find((p) => p.id === id) ?? null);
setTasks(ts.filter((t) => t.status !== "done"));
}, [id]);
useFocusEffect(
useCallback(() => {
load();
}, [load]),
);
if (!project) return <Text style={{ color: C.muted, margin: 16 }}>Loading</Text>;
const add = async () => {
const title = draft.trim();
if (!title) return;
setDraft("");
await api.tasks.create({ title, status: "next", project_id: id });
load();
};
const complete = async (t: Task) => {
setTasks((cur) => cur.filter((x) => x.id !== t.id));
await api.tasks.update(t.id, { status: "done" });
load();
};
return (
<View style={{ flex: 1, backgroundColor: C.bg }}>
<Stack.Screen options={{ title: project.name }} />
<View style={{ padding: 16, paddingBottom: 0 }}>
<Chips
label="Status"
options={STATUSES}
value={project.status}
onChange={async (status) => {
setProject({ ...project, status });
await api.projects.update(project.id, { status });
}}
/>
</View>
<TextInput
style={s.capture}
placeholder="Add next action…"
placeholderTextColor={C.muted}
value={draft}
onChangeText={setDraft}
onSubmitEditing={add}
returnKeyType="done"
/>
<FlatList
data={tasks}
keyExtractor={(t) => t.id}
renderItem={({ item }) => (
<Pressable style={s.row} onPress={() => router.push(`/task/${item.id}`)}>
<Pressable style={s.checkbox} hitSlop={10} onPress={() => complete(item)} />
<View style={{ flex: 1 }}>
<Text style={s.title}>{item.title}</Text>
<Text style={s.meta}>{[item.status, item.context].filter(Boolean).join(" · ")}</Text>
</View>
</Pressable>
)}
ListEmptyComponent={<Text style={s.empty}>No open tasks in this project.</Text>}
/>
<View style={{ padding: 16 }}>
<Button
title="Drop project"
danger
onPress={async () => {
await api.projects.drop(project.id);
router.back();
}}
/>
</View>
</View>
);
}
const s = StyleSheet.create({
capture: {
margin: 12,
marginBottom: 4,
padding: 14,
borderRadius: 12,
backgroundColor: C.surface,
color: C.text,
fontSize: 16,
borderWidth: 1,
borderColor: C.border,
},
row: {
flexDirection: "row",
alignItems: "center",
gap: 12,
paddingVertical: 14,
paddingHorizontal: 16,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: C.border,
},
checkbox: { width: 22, height: 22, borderRadius: 11, borderWidth: 2, borderColor: C.muted },
title: { color: C.text, fontSize: 16 },
meta: { color: C.muted, fontSize: 13, marginTop: 2 },
empty: { color: C.muted, textAlign: "center", marginTop: 48 },
});

47
app/src/app/settings.tsx Normal file
View File

@@ -0,0 +1,47 @@
import { useRouter } from "expo-router";
import { useEffect, useState } from "react";
import { ScrollView, Text } from "react-native";
import { Button, Field } from "../components/ui";
import { api } from "../lib/api";
import { DEFAULT_URL, getConfig, setConfig } from "../lib/config";
import { C } from "../lib/theme";
export default function Settings() {
const router = useRouter();
const [url, setUrl] = useState(DEFAULT_URL);
const [token, setToken] = useState("");
const [status, setStatus] = useState<string | null>(null);
useEffect(() => {
getConfig().then((c) => {
setUrl(c.url);
setToken(c.token);
});
}, []);
const save = async () => {
await setConfig(url, token);
try {
await api.tasks.list({ status: "inbox" });
router.back();
} catch (e) {
setStatus(`Connection failed: ${e instanceof Error ? e.message : e}`);
}
};
return (
<ScrollView style={{ flex: 1, backgroundColor: C.bg }} contentContainerStyle={{ padding: 16 }}>
<Field label="Server" value={url} onChangeText={setUrl} autoCapitalize="none" autoCorrect={false} />
<Field
label="API token"
value={token}
onChangeText={setToken}
autoCapitalize="none"
autoCorrect={false}
secureTextEntry
/>
<Button title="Save & test" onPress={save} />
{status && <Text style={{ color: C.danger }}>{status}</Text>}
</ScrollView>
);
}

109
app/src/app/task/[id].tsx Normal file
View File

@@ -0,0 +1,109 @@
import { useLocalSearchParams, useRouter } from "expo-router";
import { useEffect, useState } from "react";
import { ScrollView, Text } from "react-native";
import { Button, Chips, Field } from "../../components/ui";
import { api } from "../../lib/api";
import { C } from "../../lib/theme";
import type { Project, Task, TaskStatus } from "../../lib/types";
const STATUSES: TaskStatus[] = ["inbox", "next", "waiting", "scheduled", "someday", "done"];
export default function TaskDetail() {
const { id } = useLocalSearchParams<{ id: string }>();
const router = useRouter();
const [task, setTask] = useState<Task | null>(null);
const [projects, setProjects] = useState<Project[]>([]);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
Promise.all([api.tasks.get(id), api.projects.list()])
.then(([t, ps]) => {
setTask(t);
setProjects(ps.filter((p) => p.status === "active" || p.id === t.project_id));
})
.catch((e) => setError(e instanceof Error ? e.message : String(e)));
}, [id]);
if (error) return <Text style={{ color: C.danger, margin: 16 }}>{error}</Text>;
if (!task) return <Text style={{ color: C.muted, margin: 16 }}>Loading</Text>;
const set = (patch: Partial<Task>) => setTask({ ...task, ...patch });
const save = async () => {
try {
await api.tasks.update(task.id, {
title: task.title,
notes: task.notes,
status: task.status,
project_id: task.project_id,
context: task.context,
waiting_for: task.waiting_for,
due_date: task.due_date,
defer_date: task.defer_date,
});
router.back();
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
}
};
const trash = async () => {
await api.tasks.trash(task.id);
router.back();
};
const projectOptions = ["none", ...projects.map((p) => p.id)];
const projectLabels = Object.fromEntries([["none", "None"], ...projects.map((p) => [p.id, p.name])]);
return (
<ScrollView style={{ flex: 1, backgroundColor: C.bg }} contentContainerStyle={{ padding: 16 }}>
<Field label="Title" value={task.title} onChangeText={(v) => set({ title: v })} />
<Chips label="List" options={STATUSES} value={task.status} onChange={(status) => set({ status })} />
<Chips
label="Project"
options={projectOptions}
labels={projectLabels}
value={task.project_id ?? "none"}
onChange={(v) => set({ project_id: v === "none" ? null : v })}
/>
<Field
label="Context"
value={task.context ?? ""}
onChangeText={(v) => set({ context: v || null })}
placeholder="@home, @computer, @errands…"
autoCapitalize="none"
/>
{task.status === "waiting" && (
<Field
label="Waiting for"
value={task.waiting_for ?? ""}
onChangeText={(v) => set({ waiting_for: v || null })}
placeholder="Who or what?"
/>
)}
<Field
label="Due date"
value={task.due_date ?? ""}
onChangeText={(v) => set({ due_date: v || null })}
placeholder="YYYY-MM-DD"
autoCapitalize="none"
/>
<Field
label="Defer until"
value={task.defer_date ?? ""}
onChangeText={(v) => set({ defer_date: v || null })}
placeholder="YYYY-MM-DD"
autoCapitalize="none"
/>
<Field
label="Notes"
value={task.notes}
onChangeText={(v) => set({ notes: v })}
multiline
style={{ minHeight: 80, textAlignVertical: "top" }}
/>
<Button title="Save" onPress={save} />
<Button title="Delete" onPress={trash} danger />
</ScrollView>
);
}

View File

@@ -0,0 +1,160 @@
import { useFocusEffect, useRouter } from "expo-router";
import { useCallback, useState } from "react";
import {
FlatList,
Pressable,
RefreshControl,
StyleSheet,
Text,
TextInput,
View,
} from "react-native";
import { api } from "../lib/api";
import { C } from "../lib/theme";
import type { Task, TaskStatus } from "../lib/types";
function TaskRow({ task, onToggle }: { task: Task; onToggle: (t: Task) => void }) {
const router = useRouter();
const meta = [task.context, task.waiting_for && `${task.waiting_for}`, task.due_date && `due ${task.due_date}`]
.filter(Boolean)
.join(" · ");
return (
<Pressable style={s.row} onPress={() => router.push(`/task/${task.id}`)}>
<Pressable style={s.checkbox} hitSlop={10} onPress={() => onToggle(task)}>
{task.status === "done" && <View style={s.checkboxFill} />}
</Pressable>
<View style={{ flex: 1 }}>
<Text style={s.title} numberOfLines={2}>
{task.title}
</Text>
{!!meta && <Text style={s.meta}>{meta}</Text>}
</View>
</Pressable>
);
}
export default function TaskListScreen({
status,
capture,
emptyHint,
}: {
status: TaskStatus;
capture?: boolean;
emptyHint: string;
}) {
const [tasks, setTasks] = useState<Task[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [draft, setDraft] = useState("");
const load = useCallback(async () => {
try {
setTasks(await api.tasks.list({ status }));
setError(null);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
}
}, [status]);
useFocusEffect(
useCallback(() => {
load();
}, [load]),
);
const add = async () => {
const title = draft.trim();
if (!title) return;
setDraft("");
setTasks((cur) => [{ id: `tmp-${title}`, title, status } as Task, ...cur]);
try {
await api.tasks.create({ title, status });
} finally {
load();
}
};
const toggle = async (task: Task) => {
const done = task.status !== "done";
setTasks((cur) => cur.filter((t) => t.id !== task.id));
try {
await api.tasks.update(task.id, { status: done ? "done" : status });
} finally {
load();
}
};
return (
<View style={s.screen}>
{capture && (
<TextInput
style={s.capture}
placeholder="Capture anything…"
placeholderTextColor={C.muted}
value={draft}
onChangeText={setDraft}
onSubmitEditing={add}
submitBehavior="submit"
returnKeyType="done"
/>
)}
{error && <Text style={s.error}>{error}</Text>}
<FlatList
data={tasks}
keyExtractor={(t) => t.id}
renderItem={({ item }) => <TaskRow task={item} onToggle={toggle} />}
refreshControl={
<RefreshControl
refreshing={refreshing}
tintColor={C.muted}
onRefresh={async () => {
setRefreshing(true);
await load();
setRefreshing(false);
}}
/>
}
ListEmptyComponent={<Text style={s.empty}>{emptyHint}</Text>}
contentContainerStyle={{ paddingBottom: 32 }}
/>
</View>
);
}
const s = StyleSheet.create({
screen: { flex: 1, backgroundColor: C.bg },
capture: {
margin: 12,
marginBottom: 4,
padding: 14,
borderRadius: 12,
backgroundColor: C.surface,
color: C.text,
fontSize: 16,
borderWidth: 1,
borderColor: C.border,
},
row: {
flexDirection: "row",
alignItems: "center",
gap: 12,
paddingVertical: 14,
paddingHorizontal: 16,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: C.border,
},
checkbox: {
width: 22,
height: 22,
borderRadius: 11,
borderWidth: 2,
borderColor: C.muted,
alignItems: "center",
justifyContent: "center",
},
checkboxFill: { width: 12, height: 12, borderRadius: 6, backgroundColor: C.done },
title: { color: C.text, fontSize: 16 },
meta: { color: C.muted, fontSize: 13, marginTop: 2 },
empty: { color: C.muted, textAlign: "center", marginTop: 48, fontSize: 15 },
error: { color: C.danger, marginHorizontal: 16, marginTop: 8 },
});

94
app/src/components/ui.tsx Normal file
View File

@@ -0,0 +1,94 @@
import { Pressable, StyleSheet, Text, TextInput, View, type TextInputProps } from "react-native";
import { C } from "../lib/theme";
export function Field({ label, ...props }: TextInputProps & { label: string }) {
return (
<View style={{ marginBottom: 14 }}>
<Text style={u.label}>{label}</Text>
<TextInput style={u.input} placeholderTextColor={C.muted} {...props} />
</View>
);
}
export function Chips<T extends string>({
label,
options,
value,
onChange,
labels,
}: {
label: string;
options: T[];
value: T | null;
onChange: (v: T) => void;
labels?: Record<string, string>;
}) {
return (
<View style={{ marginBottom: 14 }}>
<Text style={u.label}>{label}</Text>
<View style={u.chipRow}>
{options.map((o) => (
<Pressable
key={o}
style={[u.chip, value === o && u.chipActive]}
onPress={() => onChange(o)}
>
<Text style={[u.chipText, value === o && { color: C.text }]}>{labels?.[o] ?? o}</Text>
</Pressable>
))}
</View>
</View>
);
}
export function Button({
title,
onPress,
danger,
}: {
title: string;
onPress: () => void;
danger?: boolean;
}) {
return (
<Pressable
style={[u.button, danger && { backgroundColor: "transparent", borderColor: C.danger, borderWidth: 1 }]}
onPress={onPress}
>
<Text style={[u.buttonText, danger && { color: C.danger }]}>{title}</Text>
</Pressable>
);
}
const u = StyleSheet.create({
label: { color: C.muted, fontSize: 13, marginBottom: 6, textTransform: "uppercase", letterSpacing: 0.5 },
input: {
backgroundColor: C.surface,
color: C.text,
borderRadius: 10,
borderWidth: 1,
borderColor: C.border,
padding: 12,
fontSize: 16,
},
chipRow: { flexDirection: "row", flexWrap: "wrap", gap: 8 },
chip: {
paddingVertical: 8,
paddingHorizontal: 14,
borderRadius: 20,
backgroundColor: C.surface,
borderWidth: 1,
borderColor: C.border,
},
chipActive: { backgroundColor: C.surface2, borderColor: C.accent },
chipText: { color: C.muted, fontSize: 14 },
button: {
backgroundColor: C.accent,
borderRadius: 10,
padding: 14,
alignItems: "center",
marginTop: 8,
marginBottom: 12,
},
buttonText: { color: C.text, fontSize: 16, fontWeight: "600" },
});

52
app/src/lib/api.ts Normal file
View File

@@ -0,0 +1,52 @@
import { getConfig } from "./config";
import type { Project, Task } from "./types";
class ApiError extends Error {
constructor(public status: number, message: string) {
super(message);
}
}
async function req<T>(path: string, init?: RequestInit): Promise<T> {
const { url, token } = await getConfig();
if (!token) throw new ApiError(401, "No API token set — open Settings");
const res = await fetch(`${url}${path}`, {
...init,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
...init?.headers,
},
});
if (!res.ok) {
const body = await res.json().catch(() => ({ error: res.statusText }));
throw new ApiError(res.status, (body as { error?: string }).error ?? `HTTP ${res.status}`);
}
return res.json();
}
export const api = {
tasks: {
list: (params: { status?: string; project_id?: string } = {}) => {
const q = new URLSearchParams(
Object.entries(params).filter(([, v]) => v != null) as [string, string][],
).toString();
return req<Task[]>(`/v1/tasks${q ? `?${q}` : ""}`);
},
get: (id: string) => req<Task>(`/v1/tasks/${id}`),
create: (data: Partial<Task> & { title: string }) =>
req<Task>("/v1/tasks", { method: "POST", body: JSON.stringify(data) }),
update: (id: string, data: Partial<Task>) =>
req<Task>(`/v1/tasks/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
trash: (id: string) => req<Task>(`/v1/tasks/${id}`, { method: "DELETE" }),
},
projects: {
list: () => req<Project[]>("/v1/projects"),
create: (data: Partial<Project> & { name: string }) =>
req<Project>("/v1/projects", { method: "POST", body: JSON.stringify(data) }),
update: (id: string, data: Partial<Project>) =>
req<Project>(`/v1/projects/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
drop: (id: string) => req<Project>(`/v1/projects/${id}`, { method: "DELETE" }),
},
contexts: { list: () => req<string[]>("/v1/contexts") },
};

24
app/src/lib/config.ts Normal file
View File

@@ -0,0 +1,24 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
export const DEFAULT_URL = "https://gtd.rehbock.xyz/api";
let cached: { url: string; token: string } | null = null;
export async function getConfig() {
if (!cached) {
const [url, token] = await Promise.all([
AsyncStorage.getItem("api_url"),
AsyncStorage.getItem("api_token"),
]);
cached = { url: url || DEFAULT_URL, token: token || "" };
}
return cached;
}
export async function setConfig(url: string, token: string) {
cached = { url: url.trim().replace(/\/$/, "") || DEFAULT_URL, token: token.trim() };
await Promise.all([
AsyncStorage.setItem("api_url", cached.url),
AsyncStorage.setItem("api_token", cached.token),
]);
}

11
app/src/lib/theme.ts Normal file
View File

@@ -0,0 +1,11 @@
export const C = {
bg: "#0F1115",
surface: "#1A1D24",
surface2: "#232733",
border: "#2C313D",
text: "#E6E8EC",
muted: "#8A909C",
accent: "#5B8DEF",
done: "#4CAF7D",
danger: "#E05B5B",
};

29
app/src/lib/types.ts Normal file
View File

@@ -0,0 +1,29 @@
export type TaskStatus = "inbox" | "next" | "waiting" | "scheduled" | "someday" | "done" | "trashed";
export type ProjectStatus = "active" | "someday" | "completed" | "dropped";
export interface Task {
id: string;
title: string;
notes: string;
status: TaskStatus;
project_id: string | null;
context: string | null;
waiting_for: string | null;
due_date: string | null;
defer_date: string | null;
completed_at: string | null;
sort_order: number;
created_at: string;
updated_at: string;
}
export interface Project {
id: string;
name: string;
status: ProjectStatus;
notes: string;
sort_order: number;
created_at: string;
updated_at: string;
open_tasks?: number;
}