♻️ Clean up code and harden stability
Changes
16 files changed, +156 -195
MODIFY
README.md
+71 -32
@@ -1,50 +1,89 @@
1
-# Welcome to your Expo app 👋
1
+# taskflow
2
2
3
-This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
3
+A GTD-style (Getting Things Done) task manager for iOS and Android, built with Expo and React Native.
4
4
5
-## Get started
5
+## Features
6
6
7
-1. Install dependencies
7
+- **Inbox, Today, Calendar, Projects, Settings** tabs with swipe navigation
8
+- **Tasks** with priority (low/medium/high), due dates, status (waiting/someday), project assignment, and tags
9
+- **Natural-language date parsing** in task titles: type "today", "tomorrow", "monday", or German equivalents ("heute", "morgen", "montag", ...) and the due date is set automatically
10
+- **Projects** with name, description, goal, color, and archive support
11
+- **Tags** with optional color
12
+- **Local persistence** via AsyncStorage (JSON)
13
+- **Export / import** as a JSON file using the native share sheet
14
+- **WebDAV / Nextcloud sync** with last-write-wins conflict resolution; supports force-push and force-pull
15
+- **Daily task reminder** notification at noon (native platforms only, includes today's task count)
16
+- **Light / dark / system** theme, persisted across sessions
8
17
9
- ```bash
10
- npm install
11
- ```
18
+## Tech Stack
12
19
13
-2. Start the app
20
+- React Native 0.81 / React 19
21
+- Expo 54 (new architecture enabled, React Compiler enabled)
22
+- expo-router 6 (file-based routing, typed routes)
23
+- TypeScript 5.9
24
+- `@react-native-async-storage/async-storage` for local storage
25
+- `webdav` library for WebDAV/Nextcloud sync
26
+- `expo-notifications` for push notifications
27
+- `expo-sharing` + `expo-file-system` for data export/import
28
+- `@react-navigation/material-top-tabs` for swipeable tab navigation
14
29
15
- ```bash
16
- npx expo start
17
- ```
30
+## Prerequisites
18
31
19
-In the output, you'll find options to open the app in a
32
+- Node.js 18+
33
+- npm
34
+- Expo Go app (for quick preview) or a development build for full native features (notifications, file sharing)
20
35
21
-- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
22
-- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
23
-- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
24
-- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
25
-
26
-You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
27
-
28
-## Get a fresh project
29
-
30
-When you're ready, run:
36
+## Getting Started
31
37
32
38
```bash
33
-npm run reset-project
39
+npm install
34
40
```
35
41
36
-This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
42
+| Script | Command | Description |
43
+|---|---|---|
44
+| start | `npm start` | Start Expo dev server |
45
+| android | `npm run android` | Open on Android emulator/device |
46
+| ios | `npm run ios` | Open on iOS simulator/device |
47
+| web | `npm run web` | Open in browser |
48
+| lint | `npm run lint` | Run ESLint |
37
49
38
-## Learn more
50
+## Project Structure
39
51
40
-To learn more about developing your project with Expo, look at the following resources:
52
+```
53
+app/
54
+ (tabs)/
55
+ index.tsx # Inbox
56
+ today.tsx # Today's tasks
57
+ calendar.tsx # Calendar view
58
+ projects.tsx # Projects list
59
+ settings.tsx # Settings (theme, notifications, sync, export)
60
+ archive.tsx # Archived projects
61
+ modal.tsx # Task create/edit modal
62
+ project-modal.tsx # Project create/edit modal
63
+ project/[id].tsx # Project detail
64
+ webdav-setup.tsx # WebDAV/Nextcloud connection setup
65
+components/ # Shared UI components
66
+constants/theme.ts # Color tokens for light/dark
67
+contexts/
68
+ theme-context.tsx # ThemeProvider + useTheme hook
69
+hooks/ # useColorScheme, useThemeColor
70
+services/
71
+ storage.service.ts # AsyncStorage persistence + export/import
72
+ webdav.service.ts # WebDAV sync
73
+ notification.service.ts # Daily reminders
74
+ task.service.ts
75
+ project.service.ts
76
+ tag.service.ts
77
+types/gtd.ts # Task, Project, Tag, GTDData types
78
+utils/
79
+ date-parser.ts # Natural-language date extraction from task titles
80
+ id-generator.ts
81
+```
41
82
42
-- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
43
-- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
83
+## Data and Sync
44
84
45
-## Join the community
85
+All data is stored locally as a single JSON document (`GTDData`) in AsyncStorage. The schema includes tasks, projects, tags, a version field, and sync metadata (timestamp + hash).
46
86
47
-Join our community of developers creating universal apps.
87
+**Export/Import:** The settings screen lets you export the full dataset as `taskflow-export-<timestamp>.json` via the native share sheet and import from any `.json` file on device.
48
88
49
-- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
50
-- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
89
+**WebDAV/Nextcloud sync:** Configure a server URL, username, and password in the WebDAV setup screen. Syncing uses a last-write-wins strategy based on the `lastSync` timestamp stored in the JSON file. Force-push and force-pull are available for manual conflict resolution. WebDAV is not supported on web.
MODIFY
app/(tabs)/_layout.tsx
+14 -14
@@ -1,5 +1,5 @@
1
1
import { withLayoutContext } from 'expo-router';
2
-import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
2
+import { createMaterialTopTabNavigator, MaterialTopTabBarProps } from '@react-navigation/material-top-tabs';
3
3
import { Ionicons } from '@expo/vector-icons';
4
4
import { View, TouchableOpacity, StyleSheet } from 'react-native';
5
5
import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -9,19 +9,21 @@
9
9
const { Navigator } = createMaterialTopTabNavigator();
10
10
const MaterialTopTabs = withLayoutContext(Navigator);
11
11
12
-function CustomTabBar({ state, descriptors, navigation }) {
12
+type TabIcon = { focused: keyof typeof Ionicons.glyphMap; unfocused: keyof typeof Ionicons.glyphMap };
13
+
14
+const iconMap: Record<string, TabIcon> = {
15
+ index: { focused: 'mail', unfocused: 'mail-outline' },
16
+ today: { focused: 'today', unfocused: 'today-outline' },
17
+ calendar: { focused: 'calendar', unfocused: 'calendar-outline' },
18
+ projects: { focused: 'folder', unfocused: 'folder-outline' },
19
+ settings: { focused: 'settings', unfocused: 'settings-outline' },
20
+};
21
+
22
+function CustomTabBar({ state, navigation }: MaterialTopTabBarProps) {
13
23
const colorScheme = useColorScheme();
14
24
const colors = Colors[colorScheme];
15
25
const insets = useSafeAreaInsets();
16
26
17
- const iconMap = {
18
- index: { focused: 'mail', unfocused: 'mail-outline' },
19
- today: { focused: 'today', unfocused: 'today-outline' },
20
- calendar: { focused: 'calendar', unfocused: 'calendar-outline' },
21
- projects: { focused: 'folder', unfocused: 'folder-outline' },
22
- settings: { focused: 'settings', unfocused: 'settings-outline' },
23
- };
24
-
25
27
return (
26
28
<View style={[styles.tabBar, {
27
29
height: 60 + insets.bottom,
@@ -32,7 +34,8 @@
32
34
}]}>
33
35
{state.routes.map((route, index) => {
34
36
const isFocused = state.index === index;
35
- const iconName = isFocused ? iconMap[route.name]?.focused : iconMap[route.name]?.unfocused;
37
+ const icon = iconMap[route.name];
38
+ const iconName = (isFocused ? icon?.focused : icon?.unfocused) ?? 'ellipse-outline';
36
39
const color = isFocused ? colors.tint : colors.tabIconDefault;
37
40
38
41
const onPress = () => {
@@ -62,9 +65,6 @@
62
65
}
63
66
64
67
export default function TabLayout() {
65
- const colorScheme = useColorScheme();
66
- const colors = Colors[colorScheme];
67
-
68
68
return (
69
69
<MaterialTopTabs
70
70
tabBarPosition="bottom"
MODIFY
app/(tabs)/calendar.tsx
+3 -4
@@ -1,4 +1,4 @@
1
-import React, { useEffect, useState } from 'react';
1
+import React, { useState } from 'react';
2
2
import { StyleSheet, Alert, ScrollView, TouchableOpacity, View } from 'react-native';
3
3
import { SafeAreaView } from 'react-native-safe-area-context';
4
4
import { router, useFocusEffect } from 'expo-router';
@@ -42,13 +42,12 @@
42
42
43
43
// Filter tasks for selected date
44
44
filterTasksForDate(selectedDate, allTasks);
45
- } catch (error) {
45
+ } catch {
46
46
Alert.alert('Error', 'Failed to load tasks');
47
47
}
48
48
};
49
49
50
50
const filterTasksForDate = (date: Date, allTasks?: Task[]) => {
51
- const dateKey = getDateKey(date);
52
51
const startOfDay = new Date(date);
53
52
startOfDay.setHours(0, 0, 0, 0);
54
53
const endOfDay = new Date(date);
@@ -114,7 +113,7 @@
114
113
loadTasks();
115
114
}, 1700);
116
115
}
117
- } catch (error) {
116
+ } catch {
118
117
Alert.alert('Error', 'Failed to update task');
119
118
await loadTasks();
120
119
}
MODIFY
app/(tabs)/index.tsx
+4 -7
@@ -1,5 +1,5 @@
1
-import React, { useEffect, useState } from 'react';
2
-import { StyleSheet, Alert, TouchableOpacity, ScrollView, View } from 'react-native';
1
+import React, { useState } from 'react';
2
+import { StyleSheet, Alert, TouchableOpacity, ScrollView } from 'react-native';
3
3
import { SafeAreaView } from 'react-native-safe-area-context';
4
4
import { router, useFocusEffect } from 'expo-router';
5
5
import { ThemedView } from '@/components/themed-view';
@@ -13,7 +13,6 @@
13
13
14
14
export default function InboxScreen() {
15
15
const [tasks, setTasks] = useState<Task[]>([]);
16
- const [loading, setLoading] = useState(true);
17
16
const [statusFilter, setStatusFilter] = useState<'all' | 'waiting' | 'someday'>('all');
18
17
19
18
useFocusEffect(
@@ -35,10 +34,8 @@
35
34
: inboxTasks.filter(t => t.status === statusFilter);
36
35
setTasks(filtered);
37
36
}
38
- } catch (error) {
37
+ } catch {
39
38
Alert.alert('Error', 'Failed to load tasks');
40
- } finally {
41
- setLoading(false);
42
39
}
43
40
};
44
41
@@ -63,7 +60,7 @@
63
60
loadTasks();
64
61
}, 1700);
65
62
}
66
- } catch (error) {
63
+ } catch {
67
64
Alert.alert('Error', 'Failed to update task');
68
65
await loadTasks(); // Reload on error to revert optimistic update
69
66
}
MODIFY
app/(tabs)/projects.tsx
+3 -6
@@ -1,4 +1,4 @@
1
-import React, { useEffect, useState } from 'react';
1
+import React, { useState } from 'react';
2
2
import { StyleSheet, Alert, TouchableOpacity, FlatList, View } from 'react-native';
3
3
import { SafeAreaView } from 'react-native-safe-area-context';
4
4
import { router, useFocusEffect } from 'expo-router';
@@ -6,12 +6,11 @@
6
6
import { ThemedText } from '@/components/themed-text';
7
7
import { FabButton } from '@/components/fab-button';
8
8
import { Project } from '@/types/gtd';
9
-import { projectService, taskService } from '@/services';
9
+import { projectService } from '@/services';
10
10
11
11
export default function ProjectsScreen() {
12
12
const [projects, setProjects] = useState<Project[]>([]);
13
13
const [taskCounts, setTaskCounts] = useState<Record<string, number>>({});
14
- const [loading, setLoading] = useState(true);
15
14
16
15
useFocusEffect(
17
16
React.useCallback(() => {
@@ -29,10 +28,8 @@
29
28
counts[project.id] = await projectService.getProjectTaskCount(project.id);
30
29
}
31
30
setTaskCounts(counts);
32
- } catch (error) {
31
+ } catch {
33
32
Alert.alert('Error', 'Failed to load projects');
34
- } finally {
35
- setLoading(false);
36
33
}
37
34
};
38
35
MODIFY
app/(tabs)/settings.tsx
+6 -11
@@ -14,7 +14,6 @@
14
14
const [syncing, setSyncing] = useState(false);
15
15
const [autoArchive, setAutoArchive] = useState(false);
16
16
const [dailyNotifications, setDailyNotifications] = useState(false);
17
- const [notificationsEnabled, setNotificationsEnabled] = useState(false);
18
17
const [webdavConfigured, setWebdavConfigured] = useState(false);
19
18
const [webdavInfo, setWebdavInfo] = useState<{ url: string; username: string } | null>(null);
20
19
const { themeMode, setThemeMode } = useTheme();
@@ -38,9 +37,6 @@
38
37
};
39
38
40
39
const checkNotificationStatus = async () => {
41
- const enabled = await notificationService.isEnabled();
42
- setNotificationsEnabled(enabled);
43
-
44
40
// Check if there are scheduled notifications
45
41
const scheduled = await notificationService.getScheduledNotifications();
46
42
setDailyNotifications(scheduled.length > 0);
@@ -54,7 +50,7 @@
54
50
'Your data has been exported as JSON. You can now save or share the file.',
55
51
[{ text: 'OK' }]
56
52
);
57
- } catch (error) {
53
+ } catch {
58
54
Alert.alert('Export Failed', 'Failed to export data. Please try again.');
59
55
}
60
56
};
@@ -94,7 +90,7 @@
94
90
}
95
91
]
96
92
);
97
- } catch (error) {
93
+ } catch {
98
94
Alert.alert(
99
95
'Import Failed',
100
96
'Failed to import data. Please make sure the file is a valid Taskflow export.',
@@ -105,7 +101,7 @@
105
101
},
106
102
]
107
103
);
108
- } catch (error) {
104
+ } catch {
109
105
Alert.alert('Import Failed', 'Failed to select file. Please try again.');
110
106
}
111
107
};
@@ -123,7 +119,7 @@
123
119
try {
124
120
const count = await taskService.archiveOldCompletedTasks(30);
125
121
Alert.alert('Success', `Archived ${count} tasks`);
126
- } catch (error) {
122
+ } catch {
127
123
Alert.alert('Error', 'Failed to archive tasks');
128
124
}
129
125
},
@@ -145,7 +141,7 @@
145
141
try {
146
142
await storageService.clearAll();
147
143
Alert.alert('Success', 'All data cleared');
148
- } catch (error) {
144
+ } catch {
149
145
Alert.alert('Error', 'Failed to clear data');
150
146
}
151
147
},
@@ -174,7 +170,7 @@
174
170
result.success ? 'Sync Complete' : 'Sync Failed',
175
171
result.message
176
172
);
177
- } catch (error) {
173
+ } catch {
178
174
Alert.alert('Sync Failed', 'An error occurred during sync');
179
175
} finally {
180
176
setSyncing(false);
@@ -214,7 +210,6 @@
214
210
const success = await notificationService.scheduleDailyNotificationWithCount();
215
211
if (success) {
216
212
setDailyNotifications(true);
217
- setNotificationsEnabled(true);
218
213
Alert.alert(
219
214
'Notifications Enabled',
220
215
'You will receive a daily reminder at 12:00 PM'
MODIFY
app/(tabs)/today.tsx
+4 -7
@@ -1,5 +1,5 @@
1
-import React, { useEffect, useState } from 'react';
2
-import { StyleSheet, Alert, ScrollView } from 'react-native';
1
+import React, { useState } from 'react';
2
+import { StyleSheet, Alert } from 'react-native';
3
3
import { SafeAreaView } from 'react-native-safe-area-context';
4
4
import { router, useFocusEffect } from 'expo-router';
5
5
import { ThemedView } from '@/components/themed-view';
@@ -11,7 +11,6 @@
11
11
12
12
export default function TodayScreen() {
13
13
const [todayTasks, setTodayTasks] = useState<Task[]>([]);
14
- const [loading, setLoading] = useState(true);
15
14
16
15
useFocusEffect(
17
16
React.useCallback(() => {
@@ -23,10 +22,8 @@
23
22
try {
24
23
const today = await taskService.getTodayTasks();
25
24
setTodayTasks(today);
26
- } catch (error) {
25
+ } catch {
27
26
Alert.alert('Error', 'Failed to load tasks');
28
- } finally {
29
- setLoading(false);
30
27
}
31
28
};
32
29
@@ -49,7 +46,7 @@
49
46
loadTasks();
50
47
}, 1700);
51
48
}
52
- } catch (error) {
49
+ } catch {
53
50
Alert.alert('Error', 'Failed to update task');
54
51
await loadTasks();
55
52
}
MODIFY
app/modal.tsx
+4 -5
@@ -1,7 +1,7 @@
1
-import React, { useRef } from 'react';
2
-import { StyleSheet, TouchableOpacity, Platform, Dimensions, Keyboard } from 'react-native';
1
+import React from 'react';
2
+import { StyleSheet, TouchableOpacity, Dimensions, Keyboard } from 'react-native';
3
3
import { ThemedView } from '@/components/themed-view';
4
-import { router, useRouter, useLocalSearchParams, useNavigation } from 'expo-router';
4
+import { router, useLocalSearchParams } from 'expo-router';
5
5
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
6
6
import Animated, {
7
7
useSharedValue,
@@ -21,7 +21,6 @@
21
21
const colors = Colors[colorScheme];
22
22
const { taskId, projectId } = useLocalSearchParams<{ taskId?: string; projectId?: string }>();
23
23
const taskFormRef = React.useRef<any>(null);
24
- const navigation = useNavigation();
25
24
26
25
// Check if this modal is nested (opened from another modal)
27
26
const isNested = !!projectId;
@@ -31,7 +30,7 @@
31
30
const handleClose = async () => {
32
31
// Try to save before closing
33
32
if (taskFormRef.current?.handleSave) {
34
- const saved = await taskFormRef.current.handleSave();
33
+ await taskFormRef.current.handleSave();
35
34
}
36
35
37
36
if (router.canDismiss()) {
MODIFY
app/project-modal.tsx
+2 -2
@@ -1,5 +1,5 @@
1
1
import React from 'react';
2
-import { StyleSheet, TouchableOpacity, Platform, Dimensions, Keyboard } from 'react-native';
2
+import { StyleSheet, TouchableOpacity, Dimensions, Keyboard } from 'react-native';
3
3
import { ThemedView } from '@/components/themed-view';
4
4
import { router, useLocalSearchParams } from 'expo-router';
5
5
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
@@ -25,7 +25,7 @@
25
25
const handleClose = async () => {
26
26
// Try to save before closing (only if there's content)
27
27
if (projectFormRef.current?.handleSave) {
28
- const saved = await projectFormRef.current.handleSave();
28
+ await projectFormRef.current.handleSave();
29
29
// If save returned false, it means validation failed - just close modal without saving
30
30
}
31
31
MODIFY
app/project/[id].tsx
+6 -9
@@ -1,4 +1,4 @@
1
-import React, { useEffect, useState } from 'react';
1
+import React, { useState } from 'react';
2
2
import { StyleSheet, Alert, ScrollView, TouchableOpacity, TextInput, View } from 'react-native';
3
3
import { SafeAreaView } from 'react-native-safe-area-context';
4
4
import { router, useLocalSearchParams, useFocusEffect } from 'expo-router';
@@ -22,7 +22,6 @@
22
22
23
23
const [tasks, setTasks] = useState<Task[]>([]);
24
24
const [isEditing, setIsEditing] = useState(isNew);
25
- const [loading, setLoading] = useState(!isNew);
26
25
27
26
useFocusEffect(
28
27
React.useCallback(() => {
@@ -43,10 +42,8 @@
43
42
router.back();
44
43
}
45
44
}
46
- } catch (error) {
45
+ } catch {
47
46
Alert.alert('Error', 'Failed to load project');
48
- } finally {
49
- setLoading(false);
50
47
}
51
48
};
52
49
@@ -65,7 +62,7 @@
65
62
setIsEditing(false);
66
63
await loadData();
67
64
}
68
- } catch (error) {
65
+ } catch {
69
66
Alert.alert('Error', 'Failed to save project');
70
67
}
71
68
};
@@ -85,7 +82,7 @@
85
82
await projectService.deleteProject(id);
86
83
router.back();
87
84
}
88
- } catch (error) {
85
+ } catch {
89
86
Alert.alert('Error', 'Failed to delete project');
90
87
}
91
88
},
@@ -104,7 +101,7 @@
104
101
await projectService.archiveProject(id);
105
102
}
106
103
await loadData();
107
- } catch (error) {
104
+ } catch {
108
105
Alert.alert('Error', 'Failed to archive project');
109
106
}
110
107
};
@@ -128,7 +125,7 @@
128
125
loadData();
129
126
}, 1700);
130
127
}
131
- } catch (error) {
128
+ } catch {
132
129
Alert.alert('Error', 'Failed to update task');
133
130
await loadData();
134
131
}
MODIFY
app/webdav-setup.tsx
+1 -1
@@ -117,7 +117,7 @@
117
117
autoCorrect={false}
118
118
/>
119
119
<ThemedText style={[styles.hint, { color: colors.subtitle }]}>
120
- For Nextcloud, it's recommended to use an app password
120
+ For Nextcloud, an app password is recommended
121
121
</ThemedText>
122
122
123
123
<TouchableOpacity
MODIFY
components/project-form.tsx
+8 -11
@@ -16,7 +16,7 @@
16
16
onClose?: () => void;
17
17
}
18
18
19
-export const ProjectForm = forwardRef(({ id, onSave, onClose }: ProjectFormProps, ref) => {
19
+export const ProjectForm = forwardRef(function ProjectForm({ id, onSave, onClose }: ProjectFormProps, ref) {
20
20
const isNew = id === 'new';
21
21
const colorScheme = useColorScheme() ?? 'light';
22
22
const colors = Colors[colorScheme];
@@ -30,7 +30,6 @@
30
30
});
31
31
32
32
const [tasks, setTasks] = useState<Task[]>([]);
33
- const [loading, setLoading] = useState(!isNew);
34
33
const [isEditing, setIsEditing] = useState(isNew);
35
34
36
35
useImperativeHandle(ref, () => ({
@@ -62,10 +61,8 @@
62
61
onClose?.();
63
62
}
64
63
}
65
- } catch (error) {
64
+ } catch {
66
65
Alert.alert('Error', 'Failed to load project');
67
- } finally {
68
- setLoading(false);
69
66
}
70
67
};
71
68
@@ -85,7 +82,7 @@
85
82
}
86
83
87
84
return !!savedProject;
88
- } catch (error) {
85
+ } catch {
89
86
Alert.alert('Error', 'Failed to save project');
90
87
return false;
91
88
}
@@ -106,7 +103,7 @@
106
103
await projectService.deleteProject(id);
107
104
onClose?.();
108
105
}
109
- } catch (error) {
106
+ } catch {
110
107
Alert.alert('Error', 'Failed to delete project');
111
108
}
112
109
},
@@ -128,7 +125,7 @@
128
125
if (updatedProject) {
129
126
setProject(updatedProject);
130
127
}
131
- } catch (error) {
128
+ } catch {
132
129
Alert.alert('Error', 'Failed to archive project');
133
130
}
134
131
};
@@ -145,7 +142,7 @@
145
142
}
146
143
147
144
await loadData();
148
- } catch (error) {
145
+ } catch {
149
146
Alert.alert('Error', 'Failed to update task');
150
147
}
151
148
};
@@ -187,7 +184,7 @@
187
184
188
185
{project.archived && (
189
186
<ThemedView style={styles.archivedBadgeContainer}>
190
- <View style={[styles.archivedBadge, { backgroundColor: colors.secondaryBackground }]}>
187
+ <View style={[styles.archivedBadge, { backgroundColor: colors.archiveBackground }]}>
191
188
<ThemedText style={styles.archivedText}>Archived</ThemedText>
192
189
</View>
193
190
</ThemedView>
@@ -309,7 +306,7 @@
309
306
<>
310
307
<ThemedView style={[styles.section, { borderBottomColor: colors.border }]}>
311
308
<TouchableOpacity
312
- style={[styles.archiveButton, { backgroundColor: colors.secondaryBackground }]}
309
+ style={[styles.archiveButton, { backgroundColor: colors.archiveBackground }]}
313
310
onPress={handleArchive}
314
311
>
315
312
<ThemedText style={styles.archiveButtonText}>
DELETE
components/swipeable-tab-bar.tsx
+0 -58
@@ -1,58 +0,0 @@
1
-import React from 'react';
2
-import { View, TouchableOpacity, StyleSheet } from 'react-native';
3
-import { Ionicons } from '@expo/vector-icons';
4
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
-import { ThemedText } from './themed-text';
6
-import { Colors } from '@/constants/theme';
7
-import { useColorScheme } from '@/hooks/use-color-scheme';
8
-
9
-const iconMap: { [key: string]: { focused: keyof typeof Ionicons.glyphMap; unfocused: keyof typeof Ionicons.glyphMap } } = {
10
- inbox: { focused: 'mail', unfocused: 'mail-outline' },
11
- today: { focused: 'today', unfocused: 'today-outline' },
12
- calendar: { focused: 'calendar', unfocused: 'calendar-outline' },
13
- projects: { focused: 'folder', unfocused: 'folder-outline' },
14
- settings: { focused: 'settings', unfocused: 'settings-outline' },
15
-};
16
-
17
-export function SwipeableTabBar({ navigationState, position, jumpTo }) {
18
- const colorScheme = useColorScheme();
19
- const colors = Colors[colorScheme];
20
- const insets = useSafeAreaInsets();
21
-
22
- return (
23
- <View style={[styles.tabBar, {
24
- height: 60 + insets.bottom,
25
- paddingBottom: insets.bottom,
26
- backgroundColor: colors.background,
27
- borderTopWidth: 1,
28
- borderTopColor: colors.border,
29
- }]}>
30
- {navigationState.routes.map((route, i) => {
31
- const isFocused = navigationState.index === i;
32
- const iconName = isFocused ? iconMap[route.key].focused : iconMap[route.key].unfocused;
33
- const color = isFocused ? colors.tint : colors.tabIconDefault;
34
-
35
- return (
36
- <TouchableOpacity
37
- key={route.key}
38
- onPress={() => jumpTo(route.key)}
39
- style={styles.tabItem}
40
- >
41
- <Ionicons name={iconName} size={24} color={color} />
42
- </TouchableOpacity>
43
- );
44
- })}
45
- </View>
46
- );
47
-}
48
-
49
-const styles = StyleSheet.create({
50
- tabBar: {
51
- flexDirection: 'row',
52
- },
53
- tabItem: {
54
- flex: 1,
55
- alignItems: 'center',
56
- justifyContent: 'center',
57
- },
58
-});
MODIFY
components/task-form.tsx
+13 -22
@@ -2,8 +2,8 @@
2
2
import { StyleSheet, Alert, ScrollView, TouchableOpacity, TextInput, Platform } from 'react-native';
3
3
import { ThemedView } from '@/components/themed-view';
4
4
import { ThemedText } from '@/components/themed-text';
5
-import { Task, Priority, TaskStatus, Project, Tag } from '@/types/gtd';
6
-import { taskService, projectService, tagService } from '@/services';
5
+import { Task, Priority, TaskStatus, Project } from '@/types/gtd';
6
+import { taskService, projectService } from '@/services';
7
7
import { Colors } from '@/constants/theme';
8
8
import { useColorScheme } from '@/hooks/use-color-scheme';
9
9
import { parseTaskTitle } from '@/utils/date-parser';
@@ -16,7 +16,7 @@
16
16
onClose?: () => void;
17
17
}
18
18
19
-export const TaskForm = forwardRef(({ id, projectId, onSave, onClose }: TaskFormProps, ref) => {
19
+export const TaskForm = forwardRef(function TaskForm({ id, projectId, onSave, onClose }: TaskFormProps, ref) {
20
20
const isNew = id === 'new';
21
21
const colorScheme = useColorScheme() ?? 'light';
22
22
const colors = Colors[colorScheme];
@@ -33,8 +33,6 @@
33
33
});
34
34
35
35
const [projects, setProjects] = useState<Project[]>([]);
36
- const [tags, setTags] = useState<Tag[]>([]);
37
- const [loading, setLoading] = useState(!isNew);
38
36
const [showDatePicker, setShowDatePicker] = useState(false);
39
37
40
38
useImperativeHandle(ref, () => ({
@@ -47,13 +45,8 @@
47
45
48
46
const loadData = async () => {
49
47
try {
50
- const [allProjects, allTags] = await Promise.all([
51
- projectService.getAllProjects(),
52
- tagService.getAllTags(),
53
- ]);
54
-
48
+ const allProjects = await projectService.getAllProjects();
55
49
setProjects(allProjects);
56
- setTags(allTags);
57
50
58
51
if (!isNew && id) {
59
52
const tasks = await taskService.getAllTasks();
@@ -65,10 +58,8 @@
65
58
onClose?.();
66
59
}
67
60
}
68
- } catch (error) {
61
+ } catch {
69
62
Alert.alert('Error', 'Failed to load data');
70
- } finally {
71
- setLoading(false);
72
63
}
73
64
};
74
65
@@ -86,7 +77,7 @@
86
77
}
87
78
88
79
return !!savedTask;
89
- } catch (error) {
80
+ } catch {
90
81
Alert.alert('Error', 'Failed to save task');
91
82
return false;
92
83
}
@@ -107,7 +98,7 @@
107
98
await taskService.deleteTask(id);
108
99
onClose?.();
109
100
}
110
- } catch (error) {
101
+ } catch {
111
102
Alert.alert('Error', 'Failed to delete task');
112
103
}
113
104
},
@@ -265,39 +256,39 @@
265
256
style={[
266
257
styles.dateButton,
267
258
{ borderColor: colors.border },
268
- task.dueDate && isDueDateToday(task.dueDate, 0) && [styles.dateButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }]
259
+ task.dueDate != null && isDueDateToday(task.dueDate, 0) && [styles.dateButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }]
269
260
]}
270
261
onPress={() => setDueDate(0)}
271
262
>
272
263
<ThemedText style={[
273
264
styles.dateButtonText,
274
- task.dueDate && isDueDateToday(task.dueDate, 0) && styles.dateButtonTextActive
265
+ task.dueDate != null && isDueDateToday(task.dueDate, 0) && styles.dateButtonTextActive
275
266
]}>Today</ThemedText>
276
267
</TouchableOpacity>
277
268
<TouchableOpacity
278
269
style={[
279
270
styles.dateButton,
280
271
{ borderColor: colors.border },
281
- task.dueDate && isDueDateToday(task.dueDate, 1) && [styles.dateButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }]
272
+ task.dueDate != null && isDueDateToday(task.dueDate, 1) && [styles.dateButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }]
282
273
]}
283
274
onPress={() => setDueDate(1)}
284
275
>
285
276
<ThemedText style={[
286
277
styles.dateButtonText,
287
- task.dueDate && isDueDateToday(task.dueDate, 1) && styles.dateButtonTextActive
278
+ task.dueDate != null && isDueDateToday(task.dueDate, 1) && styles.dateButtonTextActive
288
279
]}>Tomorrow</ThemedText>
289
280
</TouchableOpacity>
290
281
<TouchableOpacity
291
282
style={[
292
283
styles.dateButton,
293
284
{ borderColor: colors.border },
294
- task.dueDate && isDueDateToday(task.dueDate, 7) && [styles.dateButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }]
285
+ task.dueDate != null && isDueDateToday(task.dueDate, 7) && [styles.dateButtonActive, { backgroundColor: colors.tint, borderColor: colors.tint }]
295
286
]}
296
287
onPress={() => setDueDate(7)}
297
288
>
298
289
<ThemedText style={[
299
290
styles.dateButtonText,
300
- task.dueDate && isDueDateToday(task.dueDate, 7) && styles.dateButtonTextActive
291
+ task.dueDate != null && isDueDateToday(task.dueDate, 7) && styles.dateButtonTextActive
301
292
]}>Next Week</ThemedText>
302
293
</TouchableOpacity>
303
294
<TouchableOpacity
MODIFY
components/task-item.tsx
+15 -5
@@ -2,7 +2,6 @@
2
2
import { StyleSheet, TouchableOpacity, View, Animated } from 'react-native';
3
3
import { Task } from '@/types/gtd';
4
4
import { ThemedText } from './themed-text';
5
-import { ThemedView } from './themed-view';
6
5
import { Colors } from '@/constants/theme';
7
6
import { useColorScheme } from '@/hooks/use-color-scheme';
8
7
@@ -26,8 +25,9 @@
26
25
27
26
useEffect(() => {
28
27
if (task.completed) {
28
+ let fadeTimeout: ReturnType<typeof setTimeout> | undefined;
29
29
// Animate strikethrough
30
- Animated.parallel([
30
+ const animation = Animated.parallel([
31
31
Animated.timing(strikeAnim, {
32
32
toValue: 1,
33
33
duration: 400,
@@ -45,9 +45,10 @@
45
45
useNativeDriver: true,
46
46
}),
47
47
])
48
- ]).start(() => {
48
+ ]);
49
+ animation.start(() => {
49
50
// Wait 1 second, then fade out
50
- setTimeout(() => {
51
+ fadeTimeout = setTimeout(() => {
51
52
Animated.timing(fadeAnim, {
52
53
toValue: 0,
53
54
duration: 300,
@@ -55,13 +56,22 @@
55
56
}).start();
56
57
}, 1000);
57
58
});
59
+
60
+ // Cancel the pending animation/timeout if the task is toggled back
61
+ // or the component unmounts, so we never call start() after unmount.
62
+ return () => {
63
+ animation.stop();
64
+ if (fadeTimeout) {
65
+ clearTimeout(fadeTimeout);
66
+ }
67
+ };
58
68
} else {
59
69
// Reset animations when uncompleted
60
70
fadeAnim.setValue(1);
61
71
scaleAnim.setValue(1);
62
72
strikeAnim.setValue(0);
63
73
}
64
- }, [task.completed]);
74
+ }, [task.completed, fadeAnim, scaleAnim, strikeAnim]);
65
75
66
76
const formatDate = (timestamp?: number) => {
67
77
if (!timestamp) return null;
MODIFY
services/notification.service.ts
+2 -1
@@ -4,6 +4,7 @@
4
4
*/
5
5
6
6
import { Platform } from 'react-native';
7
+import type { NotificationRequest } from 'expo-notifications';
7
8
import taskService from './task.service';
8
9
9
10
// Only import notifications on native platforms
@@ -187,7 +188,7 @@
187
188
/**
188
189
* Get all scheduled notifications
189
190
*/
190
- async getScheduledNotifications(): Promise<Notifications.NotificationRequest[]> {
191
+ async getScheduledNotifications(): Promise<NotificationRequest[]> {
191
192
try {
192
193
return await Notifications.getAllScheduledNotificationsAsync();
193
194
} catch (error) {