Quadrant home screen: Next/Waiting/Someday/Projects grid replaces bottom tabs
Some checks failed
Build & Release APK / build (push) Has been cancelled
Some checks failed
Build & Release APK / build (push) Has been cancelled
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,36 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import TaskListScreen from "../../components/TaskListScreen";
|
|
||||||
|
|
||||||
export default function Inbox() {
|
|
||||||
return <TaskListScreen status="inbox" capture emptyHint="Inbox zero. Capture anything above." />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import TaskListScreen from "../../components/TaskListScreen";
|
|
||||||
|
|
||||||
export default function Next() {
|
|
||||||
return <TaskListScreen status="next" capture emptyHint="No next actions. Clarify your inbox." />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import TaskListScreen from "../../components/TaskListScreen";
|
|
||||||
|
|
||||||
export default function Someday() {
|
|
||||||
return <TaskListScreen status="someday" emptyHint="No someday/maybe items." />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import TaskListScreen from "../../components/TaskListScreen";
|
|
||||||
|
|
||||||
export default function Waiting() {
|
|
||||||
return <TaskListScreen status="waiting" emptyHint="Not waiting on anyone." />;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Stack } from "expo-router";
|
import { Link, Stack } from "expo-router";
|
||||||
import { StatusBar } from "expo-status-bar";
|
import { StatusBar } from "expo-status-bar";
|
||||||
import { C } from "../lib/theme";
|
import { C } from "../lib/theme";
|
||||||
|
|
||||||
@@ -14,7 +14,19 @@ export default function RootLayout() {
|
|||||||
contentStyle: { backgroundColor: C.bg },
|
contentStyle: { backgroundColor: C.bg },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
<Stack.Screen
|
||||||
|
name="index"
|
||||||
|
options={{
|
||||||
|
title: "GTD",
|
||||||
|
headerRight: () => (
|
||||||
|
<Link href="/settings" style={{ paddingHorizontal: 8, fontSize: 18 }}>
|
||||||
|
⚙️
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen name="list/[status]" />
|
||||||
|
<Stack.Screen name="projects" options={{ title: "Projects" }} />
|
||||||
<Stack.Screen name="task/[id]" options={{ title: "Clarify", presentation: "modal" }} />
|
<Stack.Screen name="task/[id]" options={{ title: "Clarify", presentation: "modal" }} />
|
||||||
<Stack.Screen name="project/[id]" options={{ title: "Project" }} />
|
<Stack.Screen name="project/[id]" options={{ title: "Project" }} />
|
||||||
<Stack.Screen name="settings" options={{ title: "Settings", presentation: "modal" }} />
|
<Stack.Screen name="settings" options={{ title: "Settings", presentation: "modal" }} />
|
||||||
|
|||||||
157
app/src/app/index.tsx
Normal file
157
app/src/app/index.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { useFocusEffect, useRouter } from "expo-router";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { Pressable, StyleSheet, Text, TextInput, View } from "react-native";
|
||||||
|
import { api } from "../lib/api";
|
||||||
|
import { C } from "../lib/theme";
|
||||||
|
import type { Project, Task } from "../lib/types";
|
||||||
|
|
||||||
|
const Q = {
|
||||||
|
next: { title: "Next up", color: "#3DBF77", bg: "#15301F", border: "#1E4D33" },
|
||||||
|
waiting: { title: "Waiting for", color: "#E0A93E", bg: "#33270F", border: "#544019" },
|
||||||
|
someday: { title: "Someday", color: "#9D8FE8", bg: "#221E38", border: "#38315C" },
|
||||||
|
projects: { title: "Projects", color: "#5B9DE8", bg: "#152538", border: "#1F3D5C" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function Quadrant({
|
||||||
|
kind,
|
||||||
|
count,
|
||||||
|
preview,
|
||||||
|
onPress,
|
||||||
|
}: {
|
||||||
|
kind: keyof typeof Q;
|
||||||
|
count: number;
|
||||||
|
preview: string[];
|
||||||
|
onPress: () => void;
|
||||||
|
}) {
|
||||||
|
const q = Q[kind];
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
style={[s.quad, { backgroundColor: q.bg, borderColor: q.border }]}
|
||||||
|
onPress={onPress}
|
||||||
|
>
|
||||||
|
<View style={s.quadHead}>
|
||||||
|
<Text style={[s.quadTitle, { color: q.color }]}>{q.title}</Text>
|
||||||
|
<Text style={[s.quadCount, { color: q.color }]}>{count}</Text>
|
||||||
|
</View>
|
||||||
|
{preview.map((p, i) => (
|
||||||
|
<Text key={i} style={s.quadItem} numberOfLines={1}>
|
||||||
|
{p}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [tasks, setTasks] = useState<Task[]>([]);
|
||||||
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
|
const [draft, setDraft] = useState("");
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const [ts, ps] = await Promise.all([api.tasks.list(), api.projects.list()]);
|
||||||
|
setTasks(ts);
|
||||||
|
setProjects(ps.filter((p) => p.status === "active"));
|
||||||
|
setError(null);
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : String(e));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
load();
|
||||||
|
}, [load]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const capture = async () => {
|
||||||
|
const title = draft.trim();
|
||||||
|
if (!title) return;
|
||||||
|
setDraft("");
|
||||||
|
try {
|
||||||
|
await api.tasks.create({ title });
|
||||||
|
} finally {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const byStatus = (st: string) => tasks.filter((t) => t.status === st);
|
||||||
|
const inboxCount = byStatus("inbox").length;
|
||||||
|
const titles = (st: string) => byStatus(st).slice(0, 3).map((t) => t.title);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={s.screen}>
|
||||||
|
<TextInput
|
||||||
|
style={s.capture}
|
||||||
|
placeholder="Capture anything…"
|
||||||
|
placeholderTextColor={C.muted}
|
||||||
|
value={draft}
|
||||||
|
onChangeText={setDraft}
|
||||||
|
onSubmitEditing={capture}
|
||||||
|
submitBehavior="submit"
|
||||||
|
returnKeyType="done"
|
||||||
|
/>
|
||||||
|
<Pressable style={s.inboxPill} onPress={() => router.push("/list/inbox")}>
|
||||||
|
<Text style={s.inboxText}>📥 Inbox</Text>
|
||||||
|
<Text style={[s.inboxText, { color: inboxCount ? C.text : C.muted }]}>{inboxCount}</Text>
|
||||||
|
</Pressable>
|
||||||
|
{error && <Text style={s.error}>{error}</Text>}
|
||||||
|
<View style={s.grid}>
|
||||||
|
<View style={s.gridRow}>
|
||||||
|
<Quadrant kind="next" count={byStatus("next").length} preview={titles("next")} onPress={() => router.push("/list/next")} />
|
||||||
|
<Quadrant kind="waiting" count={byStatus("waiting").length} preview={titles("waiting")} onPress={() => router.push("/list/waiting")} />
|
||||||
|
</View>
|
||||||
|
<View style={s.gridRow}>
|
||||||
|
<Quadrant kind="someday" count={byStatus("someday").length} preview={titles("someday")} onPress={() => router.push("/list/someday")} />
|
||||||
|
<Quadrant
|
||||||
|
kind="projects"
|
||||||
|
count={projects.length}
|
||||||
|
preview={projects.slice(0, 3).map((p) => p.name)}
|
||||||
|
onPress={() => router.push("/projects")}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = StyleSheet.create({
|
||||||
|
screen: { flex: 1, backgroundColor: C.bg, padding: 12 },
|
||||||
|
capture: {
|
||||||
|
padding: 14,
|
||||||
|
borderRadius: 12,
|
||||||
|
backgroundColor: C.surface,
|
||||||
|
color: C.text,
|
||||||
|
fontSize: 16,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: C.border,
|
||||||
|
},
|
||||||
|
inboxPill: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 10,
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
borderRadius: 12,
|
||||||
|
backgroundColor: C.surface,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: C.border,
|
||||||
|
},
|
||||||
|
inboxText: { color: C.text, fontSize: 16, fontWeight: "600" },
|
||||||
|
grid: { flex: 1, marginTop: 10, gap: 10 },
|
||||||
|
gridRow: { flex: 1, flexDirection: "row", gap: 10 },
|
||||||
|
quad: {
|
||||||
|
flex: 1,
|
||||||
|
borderRadius: 16,
|
||||||
|
borderWidth: 1,
|
||||||
|
padding: 14,
|
||||||
|
},
|
||||||
|
quadHead: { flexDirection: "row", justifyContent: "space-between", marginBottom: 8 },
|
||||||
|
quadTitle: { fontSize: 17, fontWeight: "700" },
|
||||||
|
quadCount: { fontSize: 17, fontWeight: "700" },
|
||||||
|
quadItem: { color: C.muted, fontSize: 13, marginBottom: 4 },
|
||||||
|
error: { color: C.danger, marginTop: 8 },
|
||||||
|
});
|
||||||
32
app/src/app/list/[status].tsx
Normal file
32
app/src/app/list/[status].tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Stack, useLocalSearchParams } from "expo-router";
|
||||||
|
import TaskListScreen from "../../components/TaskListScreen";
|
||||||
|
import type { TaskStatus } from "../../lib/types";
|
||||||
|
|
||||||
|
const TITLES: Record<string, string> = {
|
||||||
|
inbox: "Inbox",
|
||||||
|
next: "Next up",
|
||||||
|
waiting: "Waiting for",
|
||||||
|
scheduled: "Scheduled",
|
||||||
|
someday: "Someday",
|
||||||
|
done: "Done",
|
||||||
|
};
|
||||||
|
|
||||||
|
const HINTS: Record<string, string> = {
|
||||||
|
inbox: "Inbox zero. Capture from the home screen.",
|
||||||
|
next: "No next actions. Clarify your inbox.",
|
||||||
|
waiting: "Not waiting on anyone.",
|
||||||
|
scheduled: "Nothing scheduled.",
|
||||||
|
someday: "No someday/maybe items.",
|
||||||
|
done: "Nothing done yet.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ListByStatus() {
|
||||||
|
const { status } = useLocalSearchParams<{ status: string }>();
|
||||||
|
const st = (status in TITLES ? status : "inbox") as TaskStatus;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen options={{ title: TITLES[st] }} />
|
||||||
|
<TaskListScreen status={st} capture={st === "inbox" || st === "next"} emptyHint={HINTS[st]} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useFocusEffect, useRouter } from "expo-router";
|
import { useFocusEffect, useRouter } from "expo-router";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { FlatList, Pressable, StyleSheet, Text, TextInput, View } from "react-native";
|
import { FlatList, Pressable, StyleSheet, Text, TextInput, View } from "react-native";
|
||||||
import { api } from "../../lib/api";
|
import { api } from "../lib/api";
|
||||||
import { C } from "../../lib/theme";
|
import { C } from "../lib/theme";
|
||||||
import type { Project } from "../../lib/types";
|
import type { Project } from "../lib/types";
|
||||||
|
|
||||||
export default function Projects() {
|
export default function Projects() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
Reference in New Issue
Block a user