diff --git a/app/package-lock.json b/app/package-lock.json index bf0c3aa..4f35b1a 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -16,6 +16,7 @@ "expo-device": "~57.0.1", "expo-font": "~57.0.1", "expo-glass-effect": "~57.0.1", + "expo-haptics": "~57.0.1", "expo-image": "~57.0.2", "expo-linking": "~57.0.5", "expo-router": "~57.0.11", @@ -3771,6 +3772,15 @@ "react-native": "*" } }, + "node_modules/expo-haptics": { + "version": "57.0.1", + "resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-57.0.1.tgz", + "integrity": "sha512-8VhbnxlIrfXjP0syZr1JT197nafYicQu9119adOJnX62osU9Cw+PdDnAx/6LxuKJRzQdwxOMq7b7eWjhNL5zAQ==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-image": { "version": "57.0.2", "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-57.0.2.tgz", diff --git a/app/package.json b/app/package.json index efb79b6..84c41ca 100644 --- a/app/package.json +++ b/app/package.json @@ -11,6 +11,7 @@ "expo-device": "~57.0.1", "expo-font": "~57.0.1", "expo-glass-effect": "~57.0.1", + "expo-haptics": "~57.0.1", "expo-image": "~57.0.2", "expo-linking": "~57.0.5", "expo-router": "~57.0.11", diff --git a/app/src/app/_layout.tsx b/app/src/app/_layout.tsx index 539275a..47e62c6 100644 --- a/app/src/app/_layout.tsx +++ b/app/src/app/_layout.tsx @@ -2,6 +2,7 @@ import { Link, Stack } from "expo-router"; import { StatusBar } from "expo-status-bar"; import { useEffect } from "react"; import { AppState } from "react-native"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; import { hydrate, sync } from "../lib/store"; import { C } from "../lib/theme"; @@ -16,7 +17,7 @@ export default function RootLayout() { }, []); return ( - <> + + + - + ); } diff --git a/app/src/app/add.tsx b/app/src/app/add.tsx new file mode 100644 index 0000000..f4bc907 --- /dev/null +++ b/app/src/app/add.tsx @@ -0,0 +1,146 @@ +import { useRouter } from "expo-router"; +import { useState } from "react"; +import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native"; +import PriorityChips from "../components/PriorityChips"; +import { Button, Chips, Field } from "../components/ui"; +import { haptic } from "../lib/haptics"; +import { activeProjects, createTask } from "../lib/store"; +import { C } from "../lib/theme"; +import { useStore } from "../lib/useStore"; +import type { Task, TaskStatus } from "../lib/types"; + +const BOXES: { key: TaskStatus; label: string }[] = [ + { key: "inbox", label: "Inbox" }, + { key: "next", label: "Next up" }, + { key: "waiting", label: "Waiting" }, + { key: "someday", label: "Someday" }, +]; + +const ESTIMATES = [15, 30, 45, 60, 90, 120]; + +function isoToday(offset = 0): string { + const d = new Date(); + d.setDate(d.getDate() + offset); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; +} + +export default function AddTask() { + const router = useRouter(); + const store = useStore(); + const [title, setTitle] = useState(""); + const [notes, setNotes] = useState(""); + const [priority, setPriority] = useState(4); + const [estimate, setEstimate] = useState(null); + const [due, setDue] = useState(""); + const [box, setBox] = useState("inbox"); + const [projectId, setProjectId] = useState(null); + + const projects = activeProjects(store).filter((p) => p.status === "active"); + const projectOptions = ["none", ...projects.map((p) => p.id)]; + const projectLabels = Object.fromEntries([["none", "None"], ...projects.map((p) => [p.id, p.name])]); + + const save = () => { + if (!title.trim()) return; + createTask({ + title: title.trim(), + notes, + status: box, + priority, + estimate_min: estimate, + due_date: due.trim() || null, + project_id: projectId, + }); + haptic.success(); + router.back(); + }; + + return ( + + + + + + Estimated time + + {ESTIMATES.map((m) => { + const active = estimate === m; + return ( + { + haptic.select(); + setEstimate(active ? null : m); + }} + > + + {m < 60 ? `${m}m` : m % 60 ? `${Math.floor(m / 60)}h${m % 60}` : `${m / 60}h`} + + + ); + })} + + + + + {[ + { label: "Due today", v: isoToday() }, + { label: "Tomorrow", v: isoToday(1) }, + ].map(({ label, v }) => ( + { + haptic.select(); + setDue(due === v ? "" : v); + }} + > + {label} + + ))} + + + + b.key)} + labels={Object.fromEntries(BOXES.map((b) => [b.key, b.label]))} + value={box} + onChange={(b) => { + haptic.select(); + setBox(b); + }} + /> + setProjectId(v === "none" ? null : v)} + /> +