gitshark

Clone repository

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

← Commits

♻️ Clean up code and harden stability

6dfd7dd682aeede92af3bd64c45b7422ceb8f0c7 · Phillip Souza Furtner · 2026-06-18T17:34:34Z

Changes

16 files changed, +156 -195

MODIFY README.md +71 -32
diff --git a/README.md b/README.md
index 48dd63f..606f527 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git "a/app/\050tabs\051/_layout.tsx" "b/app/\050tabs\051/_layout.tsx"
index 4602d65..4986964 100644
--- "a/app/\050tabs\051/_layout.tsx"
+++ "b/app/\050tabs\051/_layout.tsx"
@@ -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
diff --git "a/app/\050tabs\051/calendar.tsx" "b/app/\050tabs\051/calendar.tsx"
index d46b560..e6664ce 100644
--- "a/app/\050tabs\051/calendar.tsx"
+++ "b/app/\050tabs\051/calendar.tsx"
@@ -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
diff --git "a/app/\050tabs\051/index.tsx" "b/app/\050tabs\051/index.tsx"
index b7eb655..8cf5366 100644
--- "a/app/\050tabs\051/index.tsx"
+++ "b/app/\050tabs\051/index.tsx"
@@ -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
diff --git "a/app/\050tabs\051/projects.tsx" "b/app/\050tabs\051/projects.tsx"
index 0723bac..0b00191 100644
--- "a/app/\050tabs\051/projects.tsx"
+++ "b/app/\050tabs\051/projects.tsx"
@@ -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
diff --git "a/app/\050tabs\051/settings.tsx" "b/app/\050tabs\051/settings.tsx"
index 9bcc2d7..e3455fe 100644
--- "a/app/\050tabs\051/settings.tsx"
+++ "b/app/\050tabs\051/settings.tsx"
@@ -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
diff --git "a/app/\050tabs\051/today.tsx" "b/app/\050tabs\051/today.tsx"
index b708be0..42b36cf 100644
--- "a/app/\050tabs\051/today.tsx"
+++ "b/app/\050tabs\051/today.tsx"
@@ -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
diff --git a/app/modal.tsx b/app/modal.tsx
index 87d6ed0..2b6f5f8 100644
--- a/app/modal.tsx
+++ b/app/modal.tsx
@@ -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
diff --git a/app/project-modal.tsx b/app/project-modal.tsx
index 3545cc2..17fdbba 100644
--- a/app/project-modal.tsx
+++ b/app/project-modal.tsx
@@ -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
diff --git "a/app/project/\133id\135.tsx" "b/app/project/\133id\135.tsx"
index ec4cf00..ed55e21 100644
--- "a/app/project/\133id\135.tsx"
+++ "b/app/project/\133id\135.tsx"
@@ -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
diff --git a/app/webdav-setup.tsx b/app/webdav-setup.tsx
index 310fb5f..e1314c2 100644
--- a/app/webdav-setup.tsx
+++ b/app/webdav-setup.tsx
@@ -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
diff --git a/components/project-form.tsx b/components/project-form.tsx
index 1ccd08b..8bf92dc 100644
--- a/components/project-form.tsx
+++ b/components/project-form.tsx
@@ -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
diff --git a/components/swipeable-tab-bar.tsx b/components/swipeable-tab-bar.tsx
deleted file mode 100644
index 2e2b298..0000000
--- a/components/swipeable-tab-bar.tsx
+++ /dev/null
@@ -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
diff --git a/components/task-form.tsx b/components/task-form.tsx
index be1bb9b..2c6f5e4 100644
--- a/components/task-form.tsx
+++ b/components/task-form.tsx
@@ -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
diff --git a/components/task-item.tsx b/components/task-item.tsx
index 00b1642..c0167ca 100644
--- a/components/task-item.tsx
+++ b/components/task-item.tsx
@@ -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
diff --git a/services/notification.service.ts b/services/notification.service.ts
index d2babf1..9e221e5 100644
--- a/services/notification.service.ts
+++ b/services/notification.service.ts
@@ -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) {

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog