126 lines
4.0 KiB
TypeScript
126 lines
4.0 KiB
TypeScript
import { useRouter } from "expo-router";
|
|
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
import Animated, { FadeIn } from "react-native-reanimated";
|
|
import { dueMeta, friendlyDate, isDeferred } from "../lib/dates";
|
|
import { haptic } from "../lib/haptics";
|
|
import { formatEstimate, PRIORITY_COLOR } from "../lib/priority";
|
|
import { updateTask } from "../lib/store";
|
|
import { C, radius, space, type } from "../lib/theme";
|
|
import type { Task, TaskStatus } from "../lib/types";
|
|
|
|
export default function TaskRow({
|
|
task,
|
|
uncheckTo = "next",
|
|
showStatus,
|
|
onLongPress,
|
|
}: {
|
|
task: Task;
|
|
/** Status to revert to when un-checking a done task. */
|
|
uncheckTo?: TaskStatus;
|
|
showStatus?: boolean;
|
|
/** Drag handle for reorderable lists. */
|
|
onLongPress?: () => void;
|
|
}) {
|
|
const router = useRouter();
|
|
const done = task.status === "done";
|
|
// Due date is rendered separately so it can carry a tone color; the reader
|
|
// sees "due today" in amber, never does date math on an ISO string.
|
|
const due = task.due_date ? dueMeta(task.due_date) : null;
|
|
const dueColor = due?.tone === "overdue" ? C.danger : due?.tone === "today" ? C.warn : C.muted;
|
|
const base = [
|
|
showStatus ? task.status : null,
|
|
task.repeat && "↻",
|
|
task.context,
|
|
task.waiting_for && `→ ${task.waiting_for}`,
|
|
formatEstimate(task.estimate_min),
|
|
isDeferred(task.defer_date) && `⏸ until ${friendlyDate(task.defer_date!)}`,
|
|
]
|
|
.filter(Boolean)
|
|
.join(" · ");
|
|
const meta = base || due;
|
|
const flag = PRIORITY_COLOR[task.priority];
|
|
|
|
const toggle = () => {
|
|
const next = task.status !== "done";
|
|
if (next) haptic.success();
|
|
else haptic.tap();
|
|
updateTask(task.id, { status: next ? "done" : uncheckTo });
|
|
};
|
|
|
|
return (
|
|
<Animated.View entering={FadeIn.duration(150)}>
|
|
<Pressable
|
|
style={({ pressed }) => [s.row, pressed && s.rowPressed]}
|
|
onPress={() => router.push(`/task/${task.id}`)}
|
|
onLongPress={onLongPress}
|
|
delayLongPress={200}
|
|
>
|
|
{/* Rounded square rather than a circle — the Things checkbox shape.
|
|
The border carries the priority colour, so priority is legible at a
|
|
glance without spending a whole column on a flag. */}
|
|
<Pressable
|
|
style={[s.checkbox, { borderColor: done ? C.done : flag }, done && s.checkboxDone]}
|
|
hitSlop={10}
|
|
onPress={toggle}
|
|
>
|
|
{done && <Text style={s.check}>✓</Text>}
|
|
</Pressable>
|
|
<View style={{ flex: 1 }}>
|
|
<Text style={[s.title, done && s.titleDone]} numberOfLines={2}>
|
|
{task.title}
|
|
</Text>
|
|
{!!meta && (
|
|
<Text style={s.meta} numberOfLines={1}>
|
|
{base}
|
|
{!!base && !!due && " · "}
|
|
{due && <Text style={{ color: dueColor, fontWeight: "500" }}>{due.label}</Text>}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
</Pressable>
|
|
</Animated.View>
|
|
);
|
|
}
|
|
|
|
const s = StyleSheet.create({
|
|
row: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: space.md,
|
|
paddingVertical: space.md + 2,
|
|
paddingHorizontal: space.lg,
|
|
backgroundColor: C.surface,
|
|
},
|
|
rowPressed: { backgroundColor: C.surface2 },
|
|
checkbox: {
|
|
width: 22,
|
|
height: 22,
|
|
borderRadius: 7,
|
|
borderWidth: 1.5,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
checkboxDone: { backgroundColor: C.done },
|
|
check: { color: C.onAccent, fontSize: 13, lineHeight: 16, fontWeight: "700" },
|
|
title: { ...type.row, color: C.text },
|
|
titleDone: { color: C.muted, textDecorationLine: "line-through" },
|
|
meta: { ...type.meta, color: C.muted, marginTop: 3 },
|
|
});
|
|
|
|
/** Inset hairline sized to line up under the task title, not the checkbox. */
|
|
export const TASK_ROW_INSET = space.lg + 22 + space.md;
|
|
|
|
export function TaskSeparator() {
|
|
return (
|
|
<View
|
|
style={{
|
|
height: StyleSheet.hairlineWidth,
|
|
backgroundColor: C.separator,
|
|
marginLeft: TASK_ROW_INSET,
|
|
// Keeps the hairline from showing through a card's rounded corner.
|
|
borderRadius: radius.sm,
|
|
}}
|
|
/>
|
|
);
|
|
}
|