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 { C } from "../lib/theme";
|
||||
|
||||
@@ -14,7 +14,19 @@ export default function RootLayout() {
|
||||
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="project/[id]" options={{ title: "Project" }} />
|
||||
<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 { 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";
|
||||
import { api } from "../lib/api";
|
||||
import { C } from "../lib/theme";
|
||||
import type { Project } from "../lib/types";
|
||||
|
||||
export default function Projects() {
|
||||
const router = useRouter();
|
||||
Reference in New Issue
Block a user