๐ UI improvements
Changes
6 files changed, +628 -21
MODIFY
app/(tabs)/projects.tsx
+2 -2
@@ -37,11 +37,11 @@
37
37
};
38
38
39
39
const handleProjectPress = (project: Project) => {
40
- router.push(`/project/${project.id}`);
40
+ router.push({ pathname: '/project-modal', params: { projectId: project.id } });
41
41
};
42
42
43
43
const handleAddProject = () => {
44
- router.push('/modal');
44
+ router.push({ pathname: '/project-modal', params: { projectId: 'new' } });
45
45
};
46
46
47
47
const renderProject = ({ item }: { item: Project }) => (
MODIFY
app/_layout.tsx
+8 -0
@@ -29,6 +29,14 @@
29
29
headerShown: false
30
30
}}
31
31
/>
32
+ <Stack.Screen
33
+ name="project-modal"
34
+ options={{
35
+ presentation: 'transparentModal',
36
+ animation: 'fade',
37
+ headerShown: false
38
+ }}
39
+ />
32
40
<Stack.Screen name="webdav-setup" options={{ headerShown: false }} />
33
41
</Stack>
34
42
<StatusBar style="auto" />
MODIFY
app/modal.tsx
+23 -13
@@ -19,9 +19,15 @@
19
19
const translateY = useSharedValue(0);
20
20
const colorScheme = useColorScheme() ?? 'light';
21
21
const colors = Colors[colorScheme];
22
- const { taskId } = useLocalSearchParams<{ taskId?: string }>();
22
+ const { taskId, projectId } = useLocalSearchParams<{ taskId?: string; projectId?: string }>();
23
+ const taskFormRef = React.useRef<any>(null);
23
24
24
- const handleClose = () => {
25
+ const handleClose = async () => {
26
+ // Try to save before closing
27
+ if (taskFormRef.current?.handleSave) {
28
+ const saved = await taskFormRef.current.handleSave();
29
+ }
30
+
25
31
if (router.canDismiss()) {
26
32
router.dismiss();
27
33
} else if (router.canGoBack()) {
@@ -38,17 +44,20 @@
38
44
runOnJS(dismissKeyboard)();
39
45
})
40
46
.onUpdate((event) => {
41
- translateY.value = Math.max(0, event.translationY);
47
+ if (event.translationY > 0) {
48
+ translateY.value = event.translationY;
49
+ }
42
50
})
43
- .onEnd(() => {
44
- if (translateY.value > SCREEN_HEIGHT * 0.3) {
51
+ .onEnd((event) => {
52
+ if (translateY.value > SCREEN_HEIGHT * 0.3 || event.velocityY > 500) {
45
53
translateY.value = withTiming(SCREEN_HEIGHT, {}, () => {
46
54
runOnJS(handleClose)();
47
55
});
48
56
} else {
49
57
translateY.value = withTiming(0);
50
58
}
51
- });
59
+ })
60
+ .simultaneousWithExternalGesture();
52
61
53
62
const animatedStyle = useAnimatedStyle(() => {
54
63
return {
@@ -63,16 +72,16 @@
63
72
onPress={handleClose}
64
73
activeOpacity={1}
65
74
/>
66
- <Animated.View style={[styles.modal, animatedStyle]}>
67
- <GestureDetector gesture={pan}>
75
+ <GestureDetector gesture={pan}>
76
+ <Animated.View style={[styles.modal, animatedStyle]}>
68
77
<ThemedView style={styles.handleContainer}>
69
78
<ThemedView style={styles.handle} />
70
79
</ThemedView>
71
- </GestureDetector>
72
- <ThemedView style={[styles.formContainer, { backgroundColor: colors.background }]}>
73
- <TaskForm id={taskId ?? 'new'} onSave={handleClose} />
74
- </ThemedView>
75
- </Animated.View>
80
+ <ThemedView style={[styles.formContainer, { backgroundColor: colors.background }]}>
81
+ <TaskForm ref={taskFormRef} id={taskId ?? 'new'} projectId={projectId} />
82
+ </ThemedView>
83
+ </Animated.View>
84
+ </GestureDetector>
76
85
</ThemedView>
77
86
);
78
87
}
@@ -93,6 +102,7 @@
93
102
borderTopRightRadius: 30,
94
103
paddingTop: 16,
95
104
backgroundColor: 'white',
105
+ overflow: 'hidden',
96
106
},
97
107
handle: {
98
108
width: 40,
ADD
app/project-modal.tsx
+123 -0
@@ -0,0 +1,123 @@
1
+import React from 'react';
2
+import { StyleSheet, TouchableOpacity, Platform, Dimensions, Keyboard } from 'react-native';
3
+import { ThemedView } from '@/components/themed-view';
4
+import { router, useLocalSearchParams } from 'expo-router';
5
+import { Gesture, GestureDetector } from 'react-native-gesture-handler';
6
+import Animated, {
7
+ useSharedValue,
8
+ useAnimatedStyle,
9
+ withTiming,
10
+ runOnJS,
11
+} from 'react-native-reanimated';
12
+import { ProjectForm } from '@/components/project-form';
13
+import { useColorScheme } from '@/hooks/use-color-scheme';
14
+import { Colors } from '@/constants/theme';
15
+
16
+const { height: SCREEN_HEIGHT } = Dimensions.get('window');
17
+
18
+export default function ProjectModalScreen() {
19
+ const translateY = useSharedValue(0);
20
+ const colorScheme = useColorScheme() ?? 'light';
21
+ const colors = Colors[colorScheme];
22
+ const { projectId } = useLocalSearchParams<{ projectId?: string }>();
23
+ const projectFormRef = React.useRef<any>(null);
24
+
25
+ const handleClose = async () => {
26
+ // Try to save before closing
27
+ if (projectFormRef.current?.handleSave) {
28
+ await projectFormRef.current.handleSave();
29
+ }
30
+
31
+ if (router.canDismiss()) {
32
+ router.dismiss();
33
+ } else if (router.canGoBack()) {
34
+ router.back();
35
+ }
36
+ };
37
+
38
+ const dismissKeyboard = () => {
39
+ Keyboard.dismiss();
40
+ };
41
+
42
+ const pan = Gesture.Pan()
43
+ .onStart(() => {
44
+ runOnJS(dismissKeyboard)();
45
+ })
46
+ .onUpdate((event) => {
47
+ if (event.translationY > 0) {
48
+ translateY.value = event.translationY;
49
+ }
50
+ })
51
+ .onEnd((event) => {
52
+ if (translateY.value > SCREEN_HEIGHT * 0.3 || event.velocityY > 500) {
53
+ translateY.value = withTiming(SCREEN_HEIGHT, {}, () => {
54
+ runOnJS(handleClose)();
55
+ });
56
+ } else {
57
+ translateY.value = withTiming(0);
58
+ }
59
+ })
60
+ .simultaneousWithExternalGesture();
61
+
62
+ const animatedStyle = useAnimatedStyle(() => {
63
+ return {
64
+ transform: [{ translateY: translateY.value }],
65
+ };
66
+ });
67
+
68
+ return (
69
+ <ThemedView style={styles.container}>
70
+ <TouchableOpacity
71
+ style={styles.overlay}
72
+ onPress={handleClose}
73
+ activeOpacity={1}
74
+ />
75
+ <GestureDetector gesture={pan}>
76
+ <Animated.View style={[styles.modal, animatedStyle]}>
77
+ <ThemedView style={styles.handleContainer}>
78
+ <ThemedView style={styles.handle} />
79
+ </ThemedView>
80
+ <ThemedView style={[styles.formContainer, { backgroundColor: colors.background }]}>
81
+ <ProjectForm ref={projectFormRef} id={projectId ?? 'new'} onSave={handleClose} />
82
+ </ThemedView>
83
+ </Animated.View>
84
+ </GestureDetector>
85
+ </ThemedView>
86
+ );
87
+}
88
+
89
+const styles = StyleSheet.create({
90
+ container: {
91
+ flex: 1,
92
+ justifyContent: 'flex-end',
93
+ backgroundColor: 'transparent',
94
+ },
95
+ overlay: {
96
+ ...StyleSheet.absoluteFillObject,
97
+ backgroundColor: 'rgba(0,0,0,0.4)',
98
+ },
99
+ modal: {
100
+ height: '90%',
101
+ borderTopLeftRadius: 30,
102
+ borderTopRightRadius: 30,
103
+ paddingTop: 16,
104
+ backgroundColor: 'white',
105
+ overflow: 'hidden',
106
+ },
107
+ handle: {
108
+ width: 40,
109
+ height: 5,
110
+ borderRadius: 2.5,
111
+ backgroundColor: 'rgba(0,0,0,0.2)',
112
+ alignSelf: 'center',
113
+ },
114
+ handleContainer: {
115
+ paddingTop: 8,
116
+ paddingBottom: 16,
117
+ alignItems: 'center',
118
+ },
119
+ formContainer: {
120
+ flex: 1,
121
+ overflow: 'hidden',
122
+ },
123
+});
ADD
components/project-form.tsx
+466 -0
@@ -0,0 +1,466 @@
1
+import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
2
+import { StyleSheet, Alert, ScrollView, TouchableOpacity, TextInput, View } from 'react-native';
3
+import { Ionicons } from '@expo/vector-icons';
4
+import { ThemedView } from '@/components/themed-view';
5
+import { ThemedText } from '@/components/themed-text';
6
+import { TaskList } from '@/components/task-list';
7
+import { Project, Task } from '@/types/gtd';
8
+import { projectService, taskService } from '@/services';
9
+import { Colors } from '@/constants/theme';
10
+import { useColorScheme } from '@/hooks/use-color-scheme';
11
+import { router, useFocusEffect } from 'expo-router';
12
+
13
+interface ProjectFormProps {
14
+ id?: string;
15
+ onSave?: (projectId: string) => void;
16
+ onClose?: () => void;
17
+}
18
+
19
+export const ProjectForm = forwardRef(({ id, onSave, onClose }: ProjectFormProps, ref) => {
20
+ const isNew = id === 'new';
21
+ const colorScheme = useColorScheme() ?? 'light';
22
+ const colors = Colors[colorScheme];
23
+
24
+ const [project, setProject] = useState<Partial<Project>>({
25
+ name: '',
26
+ description: '',
27
+ goal: '',
28
+ color: '#0a7ea4',
29
+ archived: false,
30
+ });
31
+
32
+ const [tasks, setTasks] = useState<Task[]>([]);
33
+ const [loading, setLoading] = useState(!isNew);
34
+ const [isEditing, setIsEditing] = useState(isNew);
35
+
36
+ useImperativeHandle(ref, () => ({
37
+ handleSave,
38
+ }));
39
+
40
+ useEffect(() => {
41
+ loadData();
42
+ }, [id]);
43
+
44
+ useFocusEffect(
45
+ React.useCallback(() => {
46
+ if (!isNew && id) {
47
+ loadData();
48
+ }
49
+ }, [id, isNew])
50
+ );
51
+
52
+ const loadData = async () => {
53
+ try {
54
+ if (!isNew && id) {
55
+ const foundProject = await projectService.getProjectById(id);
56
+ if (foundProject) {
57
+ setProject(foundProject);
58
+ const projectTasks = await taskService.getTasksByProject(id);
59
+ setTasks(projectTasks);
60
+ } else {
61
+ Alert.alert('Error', 'Project not found');
62
+ onClose?.();
63
+ }
64
+ }
65
+ } catch (error) {
66
+ Alert.alert('Error', 'Failed to load project');
67
+ } finally {
68
+ setLoading(false);
69
+ }
70
+ };
71
+
72
+ const handleSave = async () => {
73
+ if (!project.name?.trim()) {
74
+ return false;
75
+ }
76
+
77
+ try {
78
+ let savedProject: Project | undefined;
79
+ if (isNew) {
80
+ savedProject = await projectService.createProject(project);
81
+ if (savedProject) {
82
+ onSave?.(savedProject.id);
83
+ }
84
+ } else if (id) {
85
+ savedProject = await projectService.updateProject(id, project);
86
+ setIsEditing(false);
87
+ await loadData();
88
+ }
89
+
90
+ return !!savedProject;
91
+ } catch (error) {
92
+ Alert.alert('Error', 'Failed to save project');
93
+ return false;
94
+ }
95
+ };
96
+
97
+ const handleDelete = async () => {
98
+ Alert.alert(
99
+ 'Delete Project',
100
+ 'Are you sure you want to delete this project? Tasks will be kept but unassigned.',
101
+ [
102
+ { text: 'Cancel', style: 'cancel' },
103
+ {
104
+ text: 'Delete',
105
+ style: 'destructive',
106
+ onPress: async () => {
107
+ try {
108
+ if (id && id !== 'new') {
109
+ await projectService.deleteProject(id);
110
+ onClose?.();
111
+ }
112
+ } catch (error) {
113
+ Alert.alert('Error', 'Failed to delete project');
114
+ }
115
+ },
116
+ },
117
+ ]
118
+ );
119
+ };
120
+
121
+ const handleArchive = async () => {
122
+ if (!id || id === 'new') return;
123
+
124
+ try {
125
+ if (project.archived) {
126
+ await projectService.unarchiveProject(id);
127
+ } else {
128
+ await projectService.archiveProject(id);
129
+ }
130
+ const updatedProject = await projectService.getProjectById(id);
131
+ if (updatedProject) {
132
+ setProject(updatedProject);
133
+ }
134
+ } catch (error) {
135
+ Alert.alert('Error', 'Failed to archive project');
136
+ }
137
+ };
138
+
139
+ const handleToggleComplete = async (taskId: string) => {
140
+ try {
141
+ const task = tasks.find(t => t.id === taskId);
142
+ if (!task) return;
143
+
144
+ if (task.completed) {
145
+ await taskService.uncompleteTask(taskId);
146
+ } else {
147
+ await taskService.completeTask(taskId);
148
+ }
149
+
150
+ await loadData();
151
+ } catch (error) {
152
+ Alert.alert('Error', 'Failed to update task');
153
+ }
154
+ };
155
+
156
+ const handleTaskPress = (task: Task) => {
157
+ router.push({ pathname: '/modal', params: { taskId: task.id } });
158
+ };
159
+
160
+ const handleAddTask = () => {
161
+ router.push({
162
+ pathname: '/modal',
163
+ params: { taskId: 'new', projectId: id },
164
+ });
165
+ };
166
+
167
+ const colorOptions = ['#0a7ea4', '#ef4444', '#f59e0b', '#10b981', '#8b5cf6', '#ec4899', '#64748b'];
168
+
169
+ if (!isNew && !isEditing) {
170
+ return (
171
+ <ScrollView
172
+ style={styles.scrollView}
173
+ contentContainerStyle={styles.scrollViewContent}
174
+ showsVerticalScrollIndicator={true}
175
+ scrollEnabled={true}
176
+ nestedScrollEnabled={true}
177
+ keyboardShouldPersistTaps="handled"
178
+ >
179
+ <ThemedView style={styles.viewHeader}>
180
+ <View style={styles.projectTitleContainer}>
181
+ {project.color && (
182
+ <View style={[styles.colorIndicator, { backgroundColor: project.color }]} />
183
+ )}
184
+ <ThemedText type="title">{project.name}</ThemedText>
185
+ </View>
186
+ <TouchableOpacity onPress={() => setIsEditing(true)} style={styles.iconButton}>
187
+ <Ionicons name="create-outline" size={24} color={colors.tint} />
188
+ </TouchableOpacity>
189
+ </ThemedView>
190
+
191
+ {project.archived && (
192
+ <ThemedView style={styles.archivedBadgeContainer}>
193
+ <View style={[styles.archivedBadge, { backgroundColor: colors.secondaryBackground }]}>
194
+ <ThemedText style={styles.archivedText}>Archived</ThemedText>
195
+ </View>
196
+ </ThemedView>
197
+ )}
198
+
199
+ {project.description && (
200
+ <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
201
+ <ThemedText style={styles.infoLabel}>Description</ThemedText>
202
+ <ThemedText style={styles.infoText}>{project.description}</ThemedText>
203
+ </ThemedView>
204
+ )}
205
+
206
+ {project.goal && (
207
+ <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
208
+ <ThemedText style={styles.infoLabel}>Goal</ThemedText>
209
+ <ThemedText style={styles.infoText}>{project.goal}</ThemedText>
210
+ </ThemedView>
211
+ )}
212
+
213
+ <ThemedView style={styles.tasksSection}>
214
+ <View style={styles.tasksSectionHeader}>
215
+ <ThemedText type="subtitle">Tasks</ThemedText>
216
+ <TouchableOpacity onPress={handleAddTask} style={styles.iconButton}>
217
+ <Ionicons name="add-circle-outline" size={24} color={colors.tint} />
218
+ </TouchableOpacity>
219
+ </View>
220
+
221
+ <TaskList
222
+ tasks={tasks}
223
+ onTaskPress={handleTaskPress}
224
+ onToggleComplete={handleToggleComplete}
225
+ emptyMessage="No tasks in this project yet"
226
+ scrollable={false}
227
+ />
228
+ </ThemedView>
229
+ </ScrollView>
230
+ );
231
+ }
232
+
233
+ return (
234
+ <ScrollView
235
+ style={styles.scrollView}
236
+ contentContainerStyle={styles.scrollViewContent}
237
+ showsVerticalScrollIndicator={true}
238
+ scrollEnabled={true}
239
+ nestedScrollEnabled={true}
240
+ keyboardShouldPersistTaps="handled"
241
+ >
242
+ <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
243
+ <ThemedText style={styles.label}>Project Name *</ThemedText>
244
+ <TextInput
245
+ style={[styles.input, {
246
+ backgroundColor: colors.inputBackground,
247
+ borderColor: colors.inputBorder,
248
+ color: colors.text
249
+ }]}
250
+ value={project.name}
251
+ onChangeText={(text) => setProject({ ...project, name: text })}
252
+ placeholder="Enter project name"
253
+ placeholderTextColor={colors.placeholder}
254
+ autoFocus={isNew}
255
+ />
256
+ </ThemedView>
257
+
258
+ <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
259
+ <ThemedText style={styles.label}>Description</ThemedText>
260
+ <TextInput
261
+ style={[styles.input, styles.textArea, {
262
+ backgroundColor: colors.inputBackground,
263
+ borderColor: colors.inputBorder,
264
+ color: colors.text
265
+ }]}
266
+ value={project.description}
267
+ onChangeText={(text) => setProject({ ...project, description: text })}
268
+ placeholder="Add description"
269
+ placeholderTextColor={colors.placeholder}
270
+ multiline
271
+ numberOfLines={3}
272
+ textAlignVertical="top"
273
+ />
274
+ </ThemedView>
275
+
276
+ <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
277
+ <ThemedText style={styles.label}>Goal</ThemedText>
278
+ <TextInput
279
+ style={[styles.input, styles.textArea, {
280
+ backgroundColor: colors.inputBackground,
281
+ borderColor: colors.inputBorder,
282
+ color: colors.text
283
+ }]}
284
+ value={project.goal}
285
+ onChangeText={(text) => setProject({ ...project, goal: text })}
286
+ placeholder="What's the desired outcome?"
287
+ placeholderTextColor={colors.placeholder}
288
+ multiline
289
+ numberOfLines={3}
290
+ textAlignVertical="top"
291
+ />
292
+ </ThemedView>
293
+
294
+ <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
295
+ <ThemedText style={styles.label}>Color</ThemedText>
296
+ <View style={styles.colorContainer}>
297
+ {colorOptions.map((color) => (
298
+ <TouchableOpacity
299
+ key={color}
300
+ style={[
301
+ styles.colorButton,
302
+ { backgroundColor: color },
303
+ project.color === color && styles.colorButtonActive,
304
+ ]}
305
+ onPress={() => setProject({ ...project, color })}
306
+ />
307
+ ))}
308
+ </View>
309
+ </ThemedView>
310
+
311
+ {!isNew && (
312
+ <>
313
+ <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
314
+ <TouchableOpacity
315
+ style={[styles.archiveButton, { backgroundColor: colors.secondaryBackground }]}
316
+ onPress={handleArchive}
317
+ >
318
+ <ThemedText style={styles.archiveButtonText}>
319
+ {project.archived ? 'Unarchive Project' : 'Archive Project'}
320
+ </ThemedText>
321
+ </TouchableOpacity>
322
+ </ThemedView>
323
+
324
+ <ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
325
+ <TouchableOpacity
326
+ style={[styles.deleteButton, { backgroundColor: colors.dangerBackground }]}
327
+ onPress={handleDelete}
328
+ >
329
+ <ThemedText style={[styles.deleteButtonText, { color: colors.danger }]}>
330
+ Delete Project
331
+ </ThemedText>
332
+ </TouchableOpacity>
333
+ </ThemedView>
334
+ </>
335
+ )}
336
+ </ScrollView>
337
+ );
338
+});
339
+
340
+const styles = StyleSheet.create({
341
+ scrollView: {
342
+ flex: 1,
343
+ },
344
+ scrollViewContent: {
345
+ paddingBottom: 120,
346
+ },
347
+ section: {
348
+ paddingHorizontal: 16,
349
+ paddingVertical: 16,
350
+ borderBottomWidth: StyleSheet.hairlineWidth,
351
+ },
352
+ label: {
353
+ fontSize: 14,
354
+ fontWeight: '600',
355
+ marginBottom: 8,
356
+ opacity: 0.7,
357
+ },
358
+ input: {
359
+ fontSize: 16,
360
+ paddingVertical: 8,
361
+ paddingHorizontal: 12,
362
+ borderWidth: 1,
363
+ borderRadius: 8,
364
+ minHeight: 44,
365
+ },
366
+ textArea: {
367
+ minHeight: 80,
368
+ paddingTop: 12,
369
+ },
370
+ colorContainer: {
371
+ flexDirection: 'row',
372
+ gap: 12,
373
+ flexWrap: 'wrap',
374
+ },
375
+ colorButton: {
376
+ width: 40,
377
+ height: 40,
378
+ borderRadius: 20,
379
+ borderWidth: 3,
380
+ borderColor: 'transparent',
381
+ },
382
+ colorButtonActive: {
383
+ borderColor: '#fff',
384
+ shadowColor: '#000',
385
+ shadowOffset: { width: 0, height: 2 },
386
+ shadowOpacity: 0.2,
387
+ shadowRadius: 4,
388
+ elevation: 4,
389
+ },
390
+ archiveButton: {
391
+ paddingVertical: 16,
392
+ borderRadius: 8,
393
+ alignItems: 'center',
394
+ },
395
+ archiveButtonText: {
396
+ fontSize: 16,
397
+ fontWeight: '600',
398
+ },
399
+ deleteButton: {
400
+ paddingVertical: 16,
401
+ borderRadius: 8,
402
+ alignItems: 'center',
403
+ },
404
+ deleteButtonText: {
405
+ fontSize: 16,
406
+ fontWeight: '600',
407
+ },
408
+ viewHeader: {
409
+ paddingHorizontal: 16,
410
+ paddingTop: 16,
411
+ paddingBottom: 16,
412
+ flexDirection: 'row',
413
+ justifyContent: 'space-between',
414
+ alignItems: 'center',
415
+ },
416
+ projectTitleContainer: {
417
+ flexDirection: 'row',
418
+ alignItems: 'center',
419
+ gap: 12,
420
+ flex: 1,
421
+ },
422
+ colorIndicator: {
423
+ width: 6,
424
+ height: 32,
425
+ borderRadius: 3,
426
+ },
427
+ iconButton: {
428
+ padding: 4,
429
+ },
430
+ archivedBadgeContainer: {
431
+ paddingHorizontal: 16,
432
+ paddingBottom: 12,
433
+ },
434
+ archivedBadge: {
435
+ paddingHorizontal: 12,
436
+ paddingVertical: 4,
437
+ borderRadius: 12,
438
+ alignSelf: 'flex-start',
439
+ },
440
+ archivedText: {
441
+ fontSize: 12,
442
+ fontWeight: '600',
443
+ opacity: 0.6,
444
+ },
445
+ infoLabel: {
446
+ fontSize: 12,
447
+ fontWeight: '600',
448
+ opacity: 0.5,
449
+ marginBottom: 4,
450
+ textTransform: 'uppercase',
451
+ },
452
+ infoText: {
453
+ fontSize: 16,
454
+ lineHeight: 22,
455
+ },
456
+ tasksSection: {
457
+ paddingTop: 8,
458
+ },
459
+ tasksSectionHeader: {
460
+ flexDirection: 'row',
461
+ justifyContent: 'space-between',
462
+ alignItems: 'center',
463
+ paddingHorizontal: 16,
464
+ paddingBottom: 8,
465
+ },
466
+});
MODIFY
components/task-form.tsx
+6 -6
@@ -11,11 +11,12 @@
11
11
12
12
interface TaskFormProps {
13
13
id?: string;
14
+ projectId?: string;
14
15
onSave?: (taskId: string) => void;
15
16
onClose?: () => void;
16
17
}
17
18
18
-export const TaskForm = forwardRef(({ id, onSave, onClose }: TaskFormProps, ref) => {
19
+export const TaskForm = forwardRef(({ id, projectId, onSave, onClose }: TaskFormProps, ref) => {
19
20
const isNew = id === 'new';
20
21
const colorScheme = useColorScheme() ?? 'light';
21
22
const colors = Colors[colorScheme];
@@ -26,7 +27,7 @@
26
27
status: undefined,
27
28
priority: undefined,
28
29
dueDate: undefined,
29
- projectId: undefined,
30
+ projectId: projectId,
30
31
tagIds: [],
31
32
completed: false,
32
33
});
@@ -73,7 +74,7 @@
73
74
74
75
const handleSave = async () => {
75
76
if (!task.title?.trim()) {
76
- return;
77
+ return false;
77
78
}
78
79
79
80
try {
@@ -84,11 +85,10 @@
84
85
savedTask = await taskService.updateTask(id, task);
85
86
}
86
87
87
- if (savedTask) {
88
- onSave?.(savedTask.id);
89
- }
88
+ return !!savedTask;
90
89
} catch (error) {
91
90
Alert.alert('Error', 'Failed to save task');
91
+ return false;
92
92
}
93
93
};
94
94