gitshark

Clone repository

git clone https://git.vilip.de/git/vilip/taskflow.git
git clone git@git.vilip.de:vilip/taskflow.git

← Commits

๐Ÿ’„ First improvement

ba1a39c8aa5bcab5b3a4c89ea1ca0e3a9d3cb12f ยท vvilip ยท 2025-11-11T10:26:18Z

Changes

8 files changed, +575 -533

MODIFY app/(tabs)/calendar.tsx +1 -4
diff --git "a/app/\050tabs\051/calendar.tsx" "b/app/\050tabs\051/calendar.tsx"
index e8fd969..91ba195 100644
--- "a/app/\050tabs\051/calendar.tsx"
+++ "b/app/\050tabs\051/calendar.tsx"
@@ -117,10 +117,7 @@
117 117 };
118 118
119 119 const handleAddTask = () => {
120 - router.push({
121 - pathname: '/task/new',
122 - params: { dueDate: selectedDate.getTime() },
123 - });
120 + router.push('/modal');
124 121 };
125 122
126 123 const renderCalendar = () => {
MODIFY app/(tabs)/index.tsx +1 -1
diff --git "a/app/\050tabs\051/index.tsx" "b/app/\050tabs\051/index.tsx"
index 9f2fdbe..6004065 100644
--- "a/app/\050tabs\051/index.tsx"
+++ "b/app/\050tabs\051/index.tsx"
@@ -64,7 +64,7 @@
64 64 };
65 65
66 66 const handleAddTask = () => {
67 - router.push('/task/new');
67 + router.push('/modal');
68 68 };
69 69
70 70 const colorScheme = useColorScheme();
MODIFY app/(tabs)/projects.tsx +1 -1
diff --git "a/app/\050tabs\051/projects.tsx" "b/app/\050tabs\051/projects.tsx"
index 6b79a48..b164c49 100644
--- "a/app/\050tabs\051/projects.tsx"
+++ "b/app/\050tabs\051/projects.tsx"
@@ -41,7 +41,7 @@
41 41 };
42 42
43 43 const handleAddProject = () => {
44 - router.push('/project/new');
44 + router.push('/modal');
45 45 };
46 46
47 47 const renderProject = ({ item }: { item: Project }) => (
MODIFY app/(tabs)/today.tsx +1 -6
diff --git "a/app/\050tabs\051/today.tsx" "b/app/\050tabs\051/today.tsx"
index d15303e..5f6d3f6 100644
--- "a/app/\050tabs\051/today.tsx"
+++ "b/app/\050tabs\051/today.tsx"
@@ -52,12 +52,7 @@
52 52 };
53 53
54 54 const handleAddTask = () => {
55 - const today = new Date();
56 - today.setHours(23, 59, 59, 999);
57 - router.push({
58 - pathname: '/task/new',
59 - params: { dueDate: today.getTime().toString() },
60 - });
55 + router.push('/modal');
61 56 };
62 57
63 58 return (
MODIFY app/_layout.tsx +9 -6
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 13831e9..8b20a07 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -3,6 +3,7 @@
3 3 import { StatusBar } from 'expo-status-bar';
4 4 import { SafeAreaProvider } from 'react-native-safe-area-context';
5 5 import 'react-native-reanimated';
6 +import { GestureHandlerRootView } from 'react-native-gesture-handler';
6 7
7 8 import { useColorScheme } from '@/hooks/use-color-scheme';
8 9 import { ThemeProvider } from '@/contexts/theme-context';
@@ -31,10 +32,12 @@
31 32
32 33 export default function RootLayout() {
33 34 return (
34 - <SafeAreaProvider>
35 - <ThemeProvider>
36 - <RootLayoutNav />
37 - </ThemeProvider>
38 - </SafeAreaProvider>
35 + <GestureHandlerRootView style={{ flex: 1 }}>
36 + <SafeAreaProvider>
37 + <ThemeProvider>
38 + <RootLayoutNav />
39 + </ThemeProvider>
40 + </SafeAreaProvider>
41 + </GestureHandlerRootView>
39 42 );
40 -}
43 +}
\ No newline at end of file
MODIFY app/modal.tsx +39 -17
diff --git a/app/modal.tsx b/app/modal.tsx
index 6dfbc1a..b1b4c4a 100644
--- a/app/modal.tsx
+++ b/app/modal.tsx
@@ -1,29 +1,51 @@
1 -import { Link } from 'expo-router';
2 -import { StyleSheet } from 'react-native';
3 -
4 -import { ThemedText } from '@/components/themed-text';
1 +import React, { useRef } from 'react';
2 +import { StyleSheet, TouchableOpacity, Platform } from 'react-native';
5 3 import { ThemedView } from '@/components/themed-view';
4 +import { router } from 'expo-router';
5 +import { Gesture, GestureDetector } from 'react-native-gesture-handler';
6 +import { runOnJS } from 'react-native-reanimated';
7 +import { TaskForm } from '@/components/task-form';
6 8
7 9 export default function ModalScreen() {
10 + const taskFormRef = useRef<{ handleSave: () => void }>(null);
11 +
12 + const handleClose = () => {
13 + taskFormRef.current?.handleSave();
14 + router.back();
15 + };
16 +
17 + const pan = Gesture.Pan()
18 + .onEnd((e) => {
19 + if (e.translationY > 100) {
20 + runOnJS(handleClose)();
21 + }
22 + });
23 +
8 24 return (
9 - <ThemedView style={styles.container}>
10 - <ThemedText type="title">This is a modal</ThemedText>
11 - <Link href="/" dismissTo style={styles.link}>
12 - <ThemedText type="link">Go to home screen</ThemedText>
13 - </Link>
14 - </ThemedView>
25 + <GestureDetector gesture={pan}>
26 + <ThemedView style={styles.container}>
27 + <TouchableOpacity style={styles.overlay} onPress={handleClose} />
28 + <ThemedView style={styles.modal}>
29 + <TaskForm ref={taskFormRef} id="new" onSave={router.back} />
30 + </ThemedView>
31 + </ThemedView>
32 + </GestureDetector>
15 33 );
16 34 }
17 35
18 36 const styles = StyleSheet.create({
19 37 container: {
20 38 flex: 1,
21 - alignItems: 'center',
22 - justifyContent: 'center',
23 - padding: 20,
39 + justifyContent: 'flex-end',
24 40 },
25 - link: {
26 - marginTop: 15,
27 - paddingVertical: 15,
41 + overlay: {
42 + ...StyleSheet.absoluteFillObject,
43 + backgroundColor: 'rgba(0,0,0,0.5)',
28 44 },
29 -});
45 + modal: {
46 + height: '80%',
47 + borderTopLeftRadius: 20,
48 + borderTopRightRadius: 20,
49 + overflow: 'hidden',
50 + },
51 +});
\ No newline at end of file
MODIFY app/task/[id].tsx +7 -498
diff --git "a/app/task/\133id\135.tsx" "b/app/task/\133id\135.tsx"
index 0df1d83..d432c7a 100644
--- "a/app/task/\133id\135.tsx"
+++ "b/app/task/\133id\135.tsx"
@@ -1,191 +1,21 @@
1 -import React, { useEffect, useState } from 'react';
2 -import { StyleSheet, Alert, ScrollView, TouchableOpacity, TextInput, Platform } from 'react-native';
1 +import React from 'react';
2 +import { StyleSheet, TouchableOpacity } from 'react-native';
3 3 import { SafeAreaView } from 'react-native-safe-area-context';
4 4 import { router, useLocalSearchParams } from 'expo-router';
5 -import DateTimePicker from '@react-native-community/datetimepicker';
6 5 import { ThemedView } from '@/components/themed-view';
7 6 import { ThemedText } from '@/components/themed-text';
8 -import { Task, Priority, TaskStatus, Project, Tag } from '@/types/gtd';
9 -import { taskService, projectService, tagService } from '@/services';
7 +import { TaskForm } from '@/components/task-form';
10 8 import { Colors } from '@/constants/theme';
11 9 import { useColorScheme } from '@/hooks/use-color-scheme';
12 -import { parseTaskTitle } from '@/utils/date-parser';
13 10
14 11 export default function TaskDetailScreen() {
15 - const { id, dueDate: dueDateParam, projectId: projectIdParam } = useLocalSearchParams<{
16 - id: string;
17 - dueDate?: string;
18 - projectId?: string;
19 - }>();
12 + const { id } = useLocalSearchParams<{ id: string }>();
20 13 const isNew = id === 'new';
21 14 const colorScheme = useColorScheme() ?? 'light';
22 15 const colors = Colors[colorScheme];
23 -
24 - const [task, setTask] = useState<Partial<Task>>({
25 - title: '',
26 - description: '',
27 - status: undefined,
28 - priority: undefined,
29 - dueDate: dueDateParam ? parseInt(dueDateParam) : undefined,
30 - projectId: projectIdParam,
31 - tagIds: [],
32 - completed: false,
33 - });
34 -
35 - const [projects, setProjects] = useState<Project[]>([]);
36 - const [tags, setTags] = useState<Tag[]>([]);
37 - const [loading, setLoading] = useState(!isNew);
38 - const [showDatePicker, setShowDatePicker] = useState(false);
39 16
40 - useEffect(() => {
41 - loadData();
42 - }, [id]);
43 -
44 - const loadData = async () => {
45 - try {
46 - const [allProjects, allTags] = await Promise.all([
47 - projectService.getAllProjects(),
48 - tagService.getAllTags(),
49 - ]);
50 -
51 - setProjects(allProjects);
52 - setTags(allTags);
53 -
54 - if (!isNew && id) {
55 - const tasks = await taskService.getAllTasks();
56 - const foundTask = tasks.find(t => t.id === id);
57 - if (foundTask) {
58 - setTask(foundTask);
59 - } else {
60 - Alert.alert('Error', 'Task not found');
61 - router.back();
62 - }
63 - }
64 - } catch (error) {
65 - Alert.alert('Error', 'Failed to load data');
66 - } finally {
67 - setLoading(false);
68 - }
69 - };
70 -
71 - const handleSave = async () => {
72 - if (!task.title?.trim()) {
73 - Alert.alert('Error', 'Please enter a task title');
74 - return;
75 - }
76 -
77 - try {
78 - if (isNew) {
79 - await taskService.createTask(task);
80 - } else if (id) {
81 - await taskService.updateTask(id, task);
82 - }
83 -
84 - router.back();
85 - } catch (error) {
86 - Alert.alert('Error', 'Failed to save task');
87 - }
88 - };
89 -
90 - const handleDelete = async () => {
91 - Alert.alert(
92 - 'Delete Task',
93 - 'Are you sure you want to delete this task?',
94 - [
95 - { text: 'Cancel', style: 'cancel' },
96 - {
97 - text: 'Delete',
98 - style: 'destructive',
99 - onPress: async () => {
100 - try {
101 - if (id && id !== 'new') {
102 - await taskService.deleteTask(id);
103 - router.back();
104 - }
105 - } catch (error) {
106 - Alert.alert('Error', 'Failed to delete task');
107 - }
108 - },
109 - },
110 - ]
111 - );
112 - };
113 -
114 - const formatDate = (timestamp?: number) => {
115 - if (!timestamp) return 'No date';
116 - return new Date(timestamp).toLocaleDateString('de-DE', {
117 - weekday: 'short',
118 - day: '2-digit',
119 - month: 'short',
120 - year: 'numeric',
121 - });
122 - };
123 -
124 - const isDueDateToday = (timestamp: number, daysOffset: number) => {
125 - const taskDate = new Date(timestamp);
126 - taskDate.setHours(0, 0, 0, 0);
127 - const compareDate = new Date();
128 - compareDate.setDate(compareDate.getDate() + daysOffset);
129 - compareDate.setHours(0, 0, 0, 0);
130 - return taskDate.getTime() === compareDate.getTime();
131 - };
132 -
133 - const setDueDate = (days: number) => {
134 - const date = new Date();
135 - date.setDate(date.getDate() + days);
136 - date.setHours(23, 59, 59, 999);
137 - const newDate = date.getTime();
138 -
139 - // Toggle: if the same date is already set, remove it
140 - if (task.dueDate) {
141 - const existingDate = new Date(task.dueDate);
142 - existingDate.setHours(0, 0, 0, 0);
143 - const compareDate = new Date();
144 - compareDate.setDate(compareDate.getDate() + days);
145 - compareDate.setHours(0, 0, 0, 0);
146 -
147 - if (existingDate.getTime() === compareDate.getTime()) {
148 - setTask({ ...task, dueDate: undefined });
149 - return;
150 - }
151 - }
152 -
153 - setTask({ ...task, dueDate: newDate });
154 - };
155 -
156 - const handleDateChange = (event: any, selectedDate?: Date) => {
157 - setShowDatePicker(Platform.OS === 'ios');
158 -
159 - if (selectedDate) {
160 - selectedDate.setHours(23, 59, 59, 999);
161 - setTask({ ...task, dueDate: selectedDate.getTime() });
162 - }
163 - };
164 -
165 - const openDatePicker = () => {
166 - setShowDatePicker(true);
167 - };
168 -
169 - const handleTitleChange = (text: string) => {
170 - // Only parse if the text ends with a space (user finished typing a word)
171 - const shouldParse = text.endsWith(' ');
172 -
173 - if (shouldParse && text.trim()) {
174 - const parsed = parseTaskTitle(text.trim());
175 -
176 - // Only update if we actually found a date
177 - if (parsed.detectedDate) {
178 - setTask({
179 - ...task,
180 - title: '', // Clear the field so user can type the actual task
181 - dueDate: parsed.detectedDate,
182 - });
183 - return;
184 - }
185 - }
186 -
187 - // Otherwise just update the title
188 - setTask({ ...task, title: text });
17 + const handleSave = () => {
18 + // This will be handled by the TaskForm's internal save
189 19 };
190 20
191 21 return (
@@ -200,195 +30,7 @@
200 30 </TouchableOpacity>
201 31 </ThemedView>
202 32
203 - <ScrollView style={styles.scrollView}>
204 - <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
205 - <ThemedText style={styles.label}>Title *</ThemedText>
206 - <TextInput
207 - style={[styles.input, {
208 - backgroundColor: colors.inputBackground,
209 - borderColor: colors.inputBorder,
210 - color: colors.text
211 - }]}
212 - value={task.title}
213 - onChangeText={handleTitleChange}
214 - placeholder="Enter task title"
215 - placeholderTextColor={colors.placeholder}
216 - autoFocus={isNew}
217 - />
218 - </ThemedView>
219 -
220 - <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
221 - <ThemedText style={styles.label}>Description</ThemedText>
222 - <TextInput
223 - style={[styles.input, styles.textArea, {
224 - backgroundColor: colors.inputBackground,
225 - borderColor: colors.inputBorder,
226 - color: colors.text
227 - }]}
228 - value={task.description}
229 - onChangeText={(text) => setTask({ ...task, description: text })}
230 - placeholder="Add description"
231 - placeholderTextColor={colors.placeholder}
232 - multiline
233 - numberOfLines={4}
234 - textAlignVertical="top"
235 - />
236 - </ThemedView>
237 -
238 - <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
239 - <ThemedText style={styles.label}>Priority</ThemedText>
240 - <ThemedView style={styles.priorityContainer}>
241 - {(['high', 'medium', 'low'] as Priority[]).map((priority) => (
242 - <TouchableOpacity
243 - key={priority}
244 - style={[
245 - styles.priorityButton,
246 - { borderColor: colors.border },
247 - task.priority === priority && [styles.priorityButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }],
248 - ]}
249 - onPress={() => setTask({ ...task, priority: task.priority === priority ? undefined : priority })}
250 - >
251 - <ThemedText
252 - style={[
253 - styles.priorityText,
254 - task.priority === priority && styles.priorityTextActive,
255 - ]}
256 - >
257 - {priority === 'high' ? '๐Ÿ”ด High' : priority === 'medium' ? '๐ŸŸก Medium' : '๐ŸŸข Low'}
258 - </ThemedText>
259 - </TouchableOpacity>
260 - ))}
261 - </ThemedView>
262 - </ThemedView>
263 -
264 - <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
265 - <ThemedText style={styles.label}>Due Date</ThemedText>
266 - <ThemedView style={styles.dateContainer}>
267 - <TouchableOpacity
268 - style={[
269 - styles.dateButton,
270 - { borderColor: colors.border },
271 - task.dueDate && isDueDateToday(task.dueDate, 0) && [styles.dateButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }]
272 - ]}
273 - onPress={() => setDueDate(0)}
274 - >
275 - <ThemedText style={[
276 - styles.dateButtonText,
277 - task.dueDate && isDueDateToday(task.dueDate, 0) && styles.dateButtonTextActive
278 - ]}>Today</ThemedText>
279 - </TouchableOpacity>
280 - <TouchableOpacity
281 - style={[
282 - styles.dateButton,
283 - { borderColor: colors.border },
284 - task.dueDate && isDueDateToday(task.dueDate, 1) && [styles.dateButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }]
285 - ]}
286 - onPress={() => setDueDate(1)}
287 - >
288 - <ThemedText style={[
289 - styles.dateButtonText,
290 - task.dueDate && isDueDateToday(task.dueDate, 1) && styles.dateButtonTextActive
291 - ]}>Tomorrow</ThemedText>
292 - </TouchableOpacity>
293 - <TouchableOpacity
294 - style={[
295 - styles.dateButton,
296 - { borderColor: colors.border },
297 - task.dueDate && isDueDateToday(task.dueDate, 7) && [styles.dateButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }]
298 - ]}
299 - onPress={() => setDueDate(7)}
300 - >
301 - <ThemedText style={[
302 - styles.dateButtonText,
303 - task.dueDate && isDueDateToday(task.dueDate, 7) && styles.dateButtonTextActive
304 - ]}>Next Week</ThemedText>
305 - </TouchableOpacity>
306 - <TouchableOpacity
307 - style={[
308 - styles.dateButton,
309 - { borderColor: colors.border },
310 - ]}
311 - onPress={openDatePicker}
312 - >
313 - <ThemedText style={styles.dateButtonText}>๐Ÿ“… Pick Date</ThemedText>
314 - </TouchableOpacity>
315 - </ThemedView>
316 - {task.dueDate && (
317 - <ThemedView style={styles.selectedDateContainer}>
318 - <ThemedText style={styles.selectedDate}>
319 - ๐Ÿ“… {formatDate(task.dueDate)}
320 - </ThemedText>
321 - <TouchableOpacity onPress={() => setTask({ ...task, dueDate: undefined })}>
322 - <ThemedText style={[styles.clearDateButton, { color: colors.danger }]}>Clear</ThemedText>
323 - </TouchableOpacity>
324 - </ThemedView>
325 - )}
326 - {showDatePicker && (
327 - <DateTimePicker
328 - value={task.dueDate ? new Date(task.dueDate) : new Date()}
329 - mode="date"
330 - display={Platform.OS === 'ios' ? 'spinner' : 'default'}
331 - onChange={handleDateChange}
332 - minimumDate={new Date()}
333 - />
334 - )}
335 - </ThemedView>
336 -
337 - <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
338 - <ThemedText style={styles.label}>Status</ThemedText>
339 - <ThemedView style={styles.statusContainer}>
340 - {(['waiting', 'someday'] as TaskStatus[]).map((status) => (
341 - <TouchableOpacity
342 - key={status}
343 - style={[
344 - styles.statusButton,
345 - { borderColor: colors.border },
346 - task.status === status && [styles.statusButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }],
347 - ]}
348 - onPress={() => setTask({ ...task, status: task.status === status ? undefined : status })}
349 - >
350 - <ThemedText
351 - style={[
352 - styles.statusText,
353 - task.status === status && styles.statusTextActive,
354 - ]}
355 - >
356 - {status === 'waiting' ? 'โณ Waiting' : '๐Ÿ’ญ Someday'}
357 - </ThemedText>
358 - </TouchableOpacity>
359 - ))}
360 - </ThemedView>
361 - </ThemedView>
362 -
363 - <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
364 - <ThemedText style={styles.label}>Project</ThemedText>
365 - <ThemedView style={styles.projectContainer}>
366 - {projects.map((project) => (
367 - <TouchableOpacity
368 - key={project.id}
369 - style={[
370 - styles.projectButton,
371 - { borderColor: colors.border },
372 - task.projectId === project.id && [styles.projectButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }],
373 - ]}
374 - onPress={() => setTask({ ...task, projectId: task.projectId === project.id ? undefined : project.id })}
375 - >
376 - <ThemedText style={[styles.projectText, task.projectId === project.id && styles.projectTextActive]}>{project.name}</ThemedText>
377 - </TouchableOpacity>
378 - ))}
379 - </ThemedView>
380 - </ThemedView>
381 -
382 - {!isNew && (
383 - <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
384 - <TouchableOpacity style={[styles.deleteButton, { backgroundColor: colors.dangerBackground }]} onPress={handleDelete}>
385 - <ThemedText style={[styles.deleteButtonText, { color: colors.danger }]}>Delete Task</ThemedText>
386 - </TouchableOpacity>
387 - </ThemedView>
388 - )}
389 -
390 - <ThemedView style={styles.bottomPadding} />
391 - </ScrollView>
33 + <TaskForm id={id} onSave={() => router.back()} onClose={() => router.back()} />
392 34 </SafeAreaView>
393 35 );
394 36 }
@@ -411,137 +53,4 @@
411 53 saveButton: {
412 54 fontWeight: '600',
413 55 },
414 - scrollView: {
415 - flex: 1,
416 - },
417 - section: {
418 - paddingHorizontal: 16,
419 - paddingVertical: 16,
420 - borderBottomWidth: StyleSheet.hairlineWidth,
421 - },
422 - label: {
423 - fontSize: 14,
424 - fontWeight: '600',
425 - marginBottom: 8,
426 - opacity: 0.7,
427 - },
428 - input: {
429 - fontSize: 16,
430 - paddingVertical: 8,
431 - paddingHorizontal: 12,
432 - borderWidth: 1,
433 - borderRadius: 8,
434 - minHeight: 44,
435 - },
436 - textArea: {
437 - minHeight: 100,
438 - paddingTop: 12,
439 - },
440 - priorityContainer: {
441 - flexDirection: 'row',
442 - gap: 8,
443 - flexWrap: 'wrap',
444 - },
445 - priorityButton: {
446 - paddingVertical: 8,
447 - paddingHorizontal: 16,
448 - borderRadius: 8,
449 - borderWidth: 1,
450 - },
451 - priorityButtonActive: {
452 - },
453 - priorityText: {
454 - fontSize: 14,
455 - },
456 - priorityTextActive: {
457 - color: '#fff',
458 - fontWeight: '600',
459 - },
460 - dateContainer: {
461 - flexDirection: 'row',
462 - gap: 8,
463 - flexWrap: 'wrap',
464 - },
465 - dateButton: {
466 - paddingVertical: 8,
467 - paddingHorizontal: 16,
468 - borderRadius: 8,
469 - borderWidth: 1,
470 - },
471 - dateButtonActive: {
472 - },
473 - dateButtonText: {
474 - fontSize: 14,
475 - },
476 - dateButtonTextActive: {
477 - color: '#fff',
478 - fontWeight: '600',
479 - },
480 - selectedDate: {
481 - fontSize: 14,
482 - opacity: 0.6,
483 - flex: 1,
484 - },
485 - selectedDateContainer: {
486 - flexDirection: 'row',
487 - alignItems: 'center',
488 - justifyContent: 'space-between',
489 - marginTop: 8,
490 - },
491 - clearDateButton: {
492 - fontSize: 14,
493 - fontWeight: '600',
494 - },
495 - statusContainer: {
496 - flexDirection: 'row',
497 - gap: 8,
498 - flexWrap: 'wrap',
499 - },
500 - statusButton: {
501 - paddingVertical: 8,
502 - paddingHorizontal: 16,
503 - borderRadius: 8,
504 - borderWidth: 1,
505 - },
506 - statusButtonActive: {
507 - },
508 - statusText: {
509 - fontSize: 14,
510 - },
511 - statusTextActive: {
512 - color: '#fff',
513 - fontWeight: '600',
514 - },
515 - projectContainer: {
516 - flexDirection: 'row',
517 - gap: 8,
518 - flexWrap: 'wrap',
519 - },
520 - projectButton: {
521 - paddingVertical: 8,
522 - paddingHorizontal: 16,
523 - borderRadius: 8,
524 - borderWidth: 1,
525 - },
526 - projectButtonActive: {
527 - },
528 - projectText: {
529 - fontSize: 14,
530 - },
531 - projectTextActive: {
532 - color: '#fff',
533 - fontWeight: '600',
534 - },
535 - deleteButton: {
536 - paddingVertical: 16,
537 - borderRadius: 8,
538 - alignItems: 'center',
539 - },
540 - deleteButtonText: {
541 - fontSize: 16,
542 - fontWeight: '600',
543 - },
544 - bottomPadding: {
545 - height: 40,
546 - },
547 56 });
ADD components/task-form.tsx +516 -0
diff --git a/components/task-form.tsx b/components/task-form.tsx
new file mode 100644
index 0000000..d4258d8
--- /dev/null
+++ b/components/task-form.tsx
@@ -0,0 +1,516 @@
1 +import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
2 +import { StyleSheet, Alert, ScrollView, TouchableOpacity, TextInput, Platform } from 'react-native';
3 +import { ThemedView } from '@/components/themed-view';
4 +import { ThemedText } from '@/components/themed-text';
5 +import { Task, Priority, TaskStatus, Project, Tag } from '@/types/gtd';
6 +import { taskService, projectService, tagService } from '@/services';
7 +import { Colors } from '@/constants/theme';
8 +import { useColorScheme } from '@/hooks/use-color-scheme';
9 +import { parseTaskTitle } from '@/utils/date-parser';
10 +import DateTimePicker from '@react-native-community/datetimepicker';
11 +
12 +interface TaskFormProps {
13 + id?: string;
14 + onSave?: (task: Partial<Task>) => void;
15 + onClose?: () => void;
16 +}
17 +
18 +export const TaskForm = forwardRef(({ id, onSave, onClose }: TaskFormProps, ref) => {
19 + const isNew = id === 'new';
20 + const colorScheme = useColorScheme() ?? 'light';
21 + const colors = Colors[colorScheme];
22 +
23 + const [task, setTask] = useState<Partial<Task>>({
24 + title: '',
25 + description: '',
26 + status: undefined,
27 + priority: undefined,
28 + dueDate: undefined,
29 + projectId: undefined,
30 + tagIds: [],
31 + completed: false,
32 + });
33 +
34 + const [projects, setProjects] = useState<Project[]>([]);
35 + const [tags, setTags] = useState<Tag[]>([]);
36 + const [loading, setLoading] = useState(!isNew);
37 + const [showDatePicker, setShowDatePicker] = useState(false);
38 +
39 + useImperativeHandle(ref, () => ({
40 + handleSave,
41 + }));
42 +
43 + useEffect(() => {
44 + loadData();
45 + }, [id]);
46 +
47 + const loadData = async () => {
48 + try {
49 + const [allProjects, allTags] = await Promise.all([
50 + projectService.getAllProjects(),
51 + tagService.getAllTags(),
52 + ]);
53 +
54 + setProjects(allProjects);
55 + setTags(allTags);
56 +
57 + if (!isNew && id) {
58 + const tasks = await taskService.getAllTasks();
59 + const foundTask = tasks.find(t => t.id === id);
60 + if (foundTask) {
61 + setTask(foundTask);
62 + } else {
63 + Alert.alert('Error', 'Task not found');
64 + onClose?.();
65 + }
66 + }
67 + } catch (error) {
68 + Alert.alert('Error', 'Failed to load data');
69 + } finally {
70 + setLoading(false);
71 + }
72 + };
73 +
74 + const handleSave = async () => {
75 + if (!task.title?.trim()) {
76 + return;
77 + }
78 +
79 + try {
80 + if (isNew) {
81 + await taskService.createTask(task);
82 + } else if (id) {
83 + await taskService.updateTask(id, task);
84 + }
85 +
86 + onSave?.(task);
87 + } catch (error) {
88 + Alert.alert('Error', 'Failed to save task');
89 + }
90 + };
91 +
92 + const handleDelete = async () => {
93 + Alert.alert(
94 + 'Delete Task',
95 + 'Are you sure you want to delete this task?',
96 + [
97 + { text: 'Cancel', style: 'cancel' },
98 + {
99 + text: 'Delete',
100 + style: 'destructive',
101 + onPress: async () => {
102 + try {
103 + if (id && id !== 'new') {
104 + await taskService.deleteTask(id);
105 + onClose?.();
106 + }
107 + } catch (error) {
108 + Alert.alert('Error', 'Failed to delete task');
109 + }
110 + },
111 + },
112 + ]
113 + );
114 + };
115 +
116 + const formatDate = (timestamp?: number) => {
117 + if (!timestamp) return 'No date';
118 + return new Date(timestamp).toLocaleDateString('de-DE', {
119 + weekday: 'short',
120 + day: '2-digit',
121 + month: 'short',
122 + year: 'numeric',
123 + });
124 + };
125 +
126 + const isDueDateToday = (timestamp: number, daysOffset: number) => {
127 + const taskDate = new Date(timestamp);
128 + taskDate.setHours(0, 0, 0, 0);
129 + const compareDate = new Date();
130 + compareDate.setDate(compareDate.getDate() + daysOffset);
131 + compareDate.setHours(0, 0, 0, 0);
132 + return taskDate.getTime() === compareDate.getTime();
133 + };
134 +
135 + const setDueDate = (days: number) => {
136 + const date = new Date();
137 + date.setDate(date.getDate() + days);
138 + date.setHours(23, 59, 59, 999);
139 + const newDate = date.getTime();
140 +
141 + if (task.dueDate) {
142 + const existingDate = new Date(task.dueDate);
143 + existingDate.setHours(0, 0, 0, 0);
144 + const compareDate = new Date();
145 + compareDate.setDate(compareDate.getDate() + days);
146 + compareDate.setHours(0, 0, 0, 0);
147 +
148 + if (existingDate.getTime() === compareDate.getTime()) {
149 + setTask({ ...task, dueDate: undefined });
150 + return;
151 + }
152 + }
153 +
154 + setTask({ ...task, dueDate: newDate });
155 + };
156 +
157 + const handleDateChange = (event: any, selectedDate?: Date) => {
158 + setShowDatePicker(Platform.OS === 'ios');
159 +
160 + if (selectedDate) {
161 + selectedDate.setHours(23, 59, 59, 999);
162 + setTask({ ...task, dueDate: selectedDate.getTime() });
163 + }
164 + };
165 +
166 + const openDatePicker = () => {
167 + setShowDatePicker(true);
168 + };
169 +
170 + const handleTitleChange = (text: string) => {
171 + const shouldParse = text.endsWith(' ');
172 +
173 + if (shouldParse && text.trim()) {
174 + const parsed = parseTaskTitle(text.trim());
175 +
176 + if (parsed.detectedDate) {
177 + setTask({
178 + ...task,
179 + title: '',
180 + dueDate: parsed.detectedDate,
181 + });
182 + return;
183 + }
184 + }
185 +
186 + setTask({ ...task, title: text });
187 + };
188 +
189 + return (
190 + <ScrollView style={styles.scrollView}>
191 + <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
192 + <ThemedText style={styles.label}>Title *</ThemedText>
193 + <TextInput
194 + style={[styles.input, {
195 + backgroundColor: colors.inputBackground,
196 + borderColor: colors.inputBorder,
197 + color: colors.text
198 + }]}
199 + value={task.title}
200 + onChangeText={handleTitleChange}
201 + placeholder="Enter task title"
202 + placeholderTextColor={colors.placeholder}
203 + autoFocus={isNew}
204 + />
205 + </ThemedView>
206 +
207 + <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
208 + <ThemedText style={styles.label}>Description</ThemedText>
209 + <TextInput
210 + style={[styles.input, styles.textArea, {
211 + backgroundColor: colors.inputBackground,
212 + borderColor: colors.inputBorder,
213 + color: colors.text
214 + }]}
215 + value={task.description}
216 + onChangeText={(text) => setTask({ ...task, description: text })}
217 + placeholder="Add description"
218 + placeholderTextColor={colors.placeholder}
219 + multiline
220 + numberOfLines={4}
221 + textAlignVertical="top"
222 + />
223 + </ThemedView>
224 +
225 + <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
226 + <ThemedText style={styles.label}>Priority</ThemedText>
227 + <ThemedView style={styles.priorityContainer}>
228 + {(['high', 'medium', 'low'] as Priority[]).map((priority) => (
229 + <TouchableOpacity
230 + key={priority}
231 + style={[
232 + styles.priorityButton,
233 + { borderColor: colors.border },
234 + task.priority === priority && [styles.priorityButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }],
235 + ]}
236 + onPress={() => setTask({ ...task, priority: task.priority === priority ? undefined : priority })}
237 + >
238 + <ThemedText
239 + style={[
240 + styles.priorityText,
241 + task.priority === priority && styles.priorityTextActive,
242 + ]}
243 + >
244 + {priority === 'high' ? '๐Ÿ”ด High' : priority === 'medium' ? '๐ŸŸก Medium' : '๐ŸŸข Low'}
245 + </ThemedText>
246 + </TouchableOpacity>
247 + ))}
248 + </ThemedView>
249 + </ThemedView>
250 +
251 + <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
252 + <ThemedText style={styles.label}>Due Date</ThemedText>
253 + <ThemedView style={styles.dateContainer}>
254 + <TouchableOpacity
255 + style={[
256 + styles.dateButton,
257 + { borderColor: colors.border },
258 + task.dueDate && isDueDateToday(task.dueDate, 0) && [styles.dateButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }]
259 + ]}
260 + onPress={() => setDueDate(0)}
261 + >
262 + <ThemedText style={[
263 + styles.dateButtonText,
264 + task.dueDate && isDueDateToday(task.dueDate, 0) && styles.dateButtonTextActive
265 + ]}>Today</ThemedText>
266 + </TouchableOpacity>
267 + <TouchableOpacity
268 + style={[
269 + styles.dateButton,
270 + { borderColor: colors.border },
271 + task.dueDate && isDueDateToday(task.dueDate, 1) && [styles.dateButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }]
272 + ]}
273 + onPress={() => setDueDate(1)}
274 + >
275 + <ThemedText style={[
276 + styles.dateButtonText,
277 + task.dueDate && isDueDateToday(task.dueDate, 1) && styles.dateButtonTextActive
278 + ]}>Tomorrow</ThemedText>
279 + </TouchableOpacity>
280 + <TouchableOpacity
281 + style={[
282 + styles.dateButton,
283 + { borderColor: colors.border },
284 + task.dueDate && isDueDateToday(task.dueDate, 7) && [styles.dateButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }]
285 + ]}
286 + onPress={() => setDueDate(7)}
287 + >
288 + <ThemedText style={[
289 + styles.dateButtonText,
290 + task.dueDate && isDueDateToday(task.dueDate, 7) && styles.dateButtonTextActive
291 + ]}>Next Week</ThemedText>
292 + </TouchableOpacity>
293 + <TouchableOpacity
294 + style={[
295 + styles.dateButton,
296 + { borderColor: colors.border },
297 + ]}
298 + onPress={openDatePicker}
299 + >
300 + <ThemedText style={styles.dateButtonText}>๐Ÿ“… Pick Date</ThemedText>
301 + </TouchableOpacity>
302 + </ThemedView>
303 + {task.dueDate && (
304 + <ThemedView style={styles.selectedDateContainer}>
305 + <ThemedText style={styles.selectedDate}>
306 + ๐Ÿ“… {formatDate(task.dueDate)}
307 + </ThemedText>
308 + <TouchableOpacity onPress={() => setTask({ ...task, dueDate: undefined })}>
309 + <ThemedText style={[styles.clearDateButton, { color: colors.danger }]}>Clear</ThemedText>
310 + </TouchableOpacity>
311 + </ThemedView>
312 + )}
313 + {showDatePicker && (
314 + <DateTimePicker
315 + value={task.dueDate ? new Date(task.dueDate) : new Date()}
316 + mode="date"
317 + display={Platform.OS === 'ios' ? 'spinner' : 'default'}
318 + onChange={handleDateChange}
319 + minimumDate={new Date()}
320 + />
321 + )}
322 + </ThemedView>
323 +
324 + <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
325 + <ThemedText style={styles.label}>Status</ThemedText>
326 + <ThemedView style={styles.statusContainer}>
327 + {(['waiting', 'someday'] as TaskStatus[]).map((status) => (
328 + <TouchableOpacity
329 + key={status}
330 + style={[
331 + styles.statusButton,
332 + { borderColor: colors.border },
333 + task.status === status && [styles.statusButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }],
334 + ]}
335 + onPress={() => setTask({ ...task, status: task.status === status ? undefined : status })}
336 + >
337 + <ThemedText
338 + style={[
339 + styles.statusText,
340 + task.status === status && styles.statusTextActive,
341 + ]}
342 + >
343 + {status === 'waiting' ? 'โณ Waiting' : '๐Ÿ’ญ Someday'}
344 + </ThemedText>
345 + </TouchableOpacity>
346 + ))}
347 + </ThemedView>
348 + </ThemedView>
349 +
350 + <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
351 + <ThemedText style={styles.label}>Project</ThemedText>
352 + <ThemedView style={styles.projectContainer}>
353 + {projects.map((project) => (
354 + <TouchableOpacity
355 + key={project.id}
356 + style={[
357 + styles.projectButton,
358 + { borderColor: colors.border },
359 + task.projectId === project.id && [styles.projectButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }],
360 + ]}
361 + onPress={() => setTask({ ...task, projectId: task.projectId === project.id ? undefined : project.id })}
362 + >
363 + <ThemedText style={[styles.projectText, task.projectId === project.id && styles.projectTextActive]}>{project.name}</ThemedText>
364 + </TouchableOpacity>
365 + ))}
366 + </ThemedView>
367 + </ThemedView>
368 +
369 + {!isNew && (
370 + <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
371 + <TouchableOpacity style={[styles.deleteButton, { backgroundColor: colors.dangerBackground }]} onPress={handleDelete}>
372 + <ThemedText style={[styles.deleteButtonText, { color: colors.danger }]}>Delete Task</ThemedText>
373 + </TouchableOpacity>
374 + </ThemedView>
375 + )}
376 +
377 + <ThemedView style={styles.bottomPadding} />
378 + </ScrollView>
379 + );
380 +});
381 +
382 +const styles = StyleSheet.create({
383 + scrollView: {
384 + flex: 1,
385 + },
386 + section: {
387 + paddingHorizontal: 16,
388 + paddingVertical: 16,
389 + borderBottomWidth: StyleSheet.hairlineWidth,
390 + },
391 + label: {
392 + fontSize: 14,
393 + fontWeight: '600',
394 + marginBottom: 8,
395 + opacity: 0.7,
396 + },
397 + input: {
398 + fontSize: 16,
399 + paddingVertical: 8,
400 + paddingHorizontal: 12,
401 + borderWidth: 1,
402 + borderRadius: 8,
403 + minHeight: 44,
404 + },
405 + textArea: {
406 + minHeight: 100,
407 + paddingTop: 12,
408 + },
409 + priorityContainer: {
410 + flexDirection: 'row',
411 + gap: 8,
412 + flexWrap: 'wrap',
413 + },
414 + priorityButton: {
415 + paddingVertical: 8,
416 + paddingHorizontal: 16,
417 + borderRadius: 8,
418 + borderWidth: 1,
419 + },
420 + priorityButtonActive: {
421 + },
422 + priorityText: {
423 + fontSize: 14,
424 + },
425 + priorityTextActive: {
426 + color: '#fff',
427 + fontWeight: '600',
428 + },
429 + dateContainer: {
430 + flexDirection: 'row',
431 + gap: 8,
432 + flexWrap: 'wrap',
433 + },
434 + dateButton: {
435 + paddingVertical: 8,
436 + paddingHorizontal: 16,
437 + borderRadius: 8,
438 + borderWidth: 1,
439 + },
440 + dateButtonActive: {
441 + },
442 + dateButtonText: {
443 + fontSize: 14,
444 + },
445 + dateButtonTextActive: {
446 + color: '#fff',
447 + fontWeight: '600',
448 + },
449 + selectedDate: {
450 + fontSize: 14,
451 + opacity: 0.6,
452 + flex: 1,
453 + },
454 + selectedDateContainer: {
455 + flexDirection: 'row',
456 + alignItems: 'center',
457 + justifyContent: 'space-between',
458 + marginTop: 8,
459 + },
460 + clearDateButton: {
461 + fontSize: 14,
462 + fontWeight: '600',
463 + },
464 + statusContainer: {
465 + flexDirection: 'row',
466 + gap: 8,
467 + flexWrap: 'wrap',
468 + },
469 + statusButton: {
470 + paddingVertical: 8,
471 + paddingHorizontal: 16,
472 + borderRadius: 8,
473 + borderWidth: 1,
474 + },
475 + statusButtonActive: {
476 + },
477 + statusText: {
478 + fontSize: 14,
479 + },
480 + statusTextActive: {
481 + color: '#fff',
482 + fontWeight: '600',
483 + },
484 + projectContainer: {
485 + flexDirection: 'row',
486 + gap: 8,
487 + flexWrap: 'wrap',
488 + },
489 + projectButton: {
490 + paddingVertical: 8,
491 + paddingHorizontal: 16,
492 + borderRadius: 8,
493 + borderWidth: 1,
494 + },
495 + projectButtonActive: {
496 + },
497 + projectText: {
498 + fontSize: 14,
499 + },
500 + projectTextActive: {
501 + color: '#fff',
502 + fontWeight: '600',
503 + },
504 + deleteButton: {
505 + paddingVertical: 16,
506 + borderRadius: 8,
507 + alignItems: 'center',
508 + },
509 + deleteButtonText: {
510 + fontSize: 16,
511 + fontWeight: '600',
512 + },
513 + bottomPadding: {
514 + height: 40,
515 + },
516 +});

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog