๐ First improvement
Changes
8 files changed, +575 -533
MODIFY
app/(tabs)/calendar.tsx
+1 -4
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+}
MODIFY
app/modal.tsx
+39 -17
@@ -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
+});
MODIFY
app/task/[id].tsx
+7 -498
@@ -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
@@ -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
+});