✨ Add base features for prototype
Changes
12 files changed, +1772 -203
MODIFY
app/(tabs)/_layout.tsx
+19 -5
@@ -19,15 +19,29 @@
19
19
<Tabs.Screen
20
20
name="index"
21
21
options={{
22
- title: 'Home',
23
- tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />,
22
+ title: 'Inbox',
23
+ tabBarIcon: ({ color }) => <IconSymbol size={28} name="tray.fill" color={color} />,
24
24
}}
25
25
/>
26
26
<Tabs.Screen
27
- name="explore"
27
+ name="today"
28
28
options={{
29
- title: 'Explore',
30
- tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />,
29
+ title: 'Today',
30
+ tabBarIcon: ({ color }) => <IconSymbol size={28} name="calendar" color={color} />,
31
+ }}
32
+ />
33
+ <Tabs.Screen
34
+ name="projects"
35
+ options={{
36
+ title: 'Projects',
37
+ tabBarIcon: ({ color }) => <IconSymbol size={28} name="folder.fill" color={color} />,
38
+ }}
39
+ />
40
+ <Tabs.Screen
41
+ name="settings"
42
+ options={{
43
+ title: 'Settings',
44
+ tabBarIcon: ({ color }) => <IconSymbol size={28} name="gear" color={color} />,
31
45
}}
32
46
/>
33
47
</Tabs>
DELETE
app/(tabs)/explore.tsx
+0 -112
@@ -1,112 +0,0 @@
1
-import { Image } from 'expo-image';
2
-import { Platform, StyleSheet } from 'react-native';
3
-
4
-import { Collapsible } from '@/components/ui/collapsible';
5
-import { ExternalLink } from '@/components/external-link';
6
-import ParallaxScrollView from '@/components/parallax-scroll-view';
7
-import { ThemedText } from '@/components/themed-text';
8
-import { ThemedView } from '@/components/themed-view';
9
-import { IconSymbol } from '@/components/ui/icon-symbol';
10
-import { Fonts } from '@/constants/theme';
11
-
12
-export default function TabTwoScreen() {
13
- return (
14
- <ParallaxScrollView
15
- headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
16
- headerImage={
17
- <IconSymbol
18
- size={310}
19
- color="#808080"
20
- name="chevron.left.forwardslash.chevron.right"
21
- style={styles.headerImage}
22
- />
23
- }>
24
- <ThemedView style={styles.titleContainer}>
25
- <ThemedText
26
- type="title"
27
- style={{
28
- fontFamily: Fonts.rounded,
29
- }}>
30
- Explore
31
- </ThemedText>
32
- </ThemedView>
33
- <ThemedText>This app includes example code to help you get started.</ThemedText>
34
- <Collapsible title="File-based routing">
35
- <ThemedText>
36
- This app has two screens:{' '}
37
- <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '}
38
- <ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText>
39
- </ThemedText>
40
- <ThemedText>
41
- The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '}
42
- sets up the tab navigator.
43
- </ThemedText>
44
- <ExternalLink href="https://docs.expo.dev/router/introduction">
45
- <ThemedText type="link">Learn more</ThemedText>
46
- </ExternalLink>
47
- </Collapsible>
48
- <Collapsible title="Android, iOS, and web support">
49
- <ThemedText>
50
- You can open this project on Android, iOS, and the web. To open the web version, press{' '}
51
- <ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project.
52
- </ThemedText>
53
- </Collapsible>
54
- <Collapsible title="Images">
55
- <ThemedText>
56
- For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '}
57
- <ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
58
- different screen densities
59
- </ThemedText>
60
- <Image
61
- source={require('@/assets/images/react-logo.png')}
62
- style={{ width: 100, height: 100, alignSelf: 'center' }}
63
- />
64
- <ExternalLink href="https://reactnative.dev/docs/images">
65
- <ThemedText type="link">Learn more</ThemedText>
66
- </ExternalLink>
67
- </Collapsible>
68
- <Collapsible title="Light and dark mode components">
69
- <ThemedText>
70
- This template has light and dark mode support. The{' '}
71
- <ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect
72
- what the user's current color scheme is, and so you can adjust UI colors accordingly.
73
- </ThemedText>
74
- <ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
75
- <ThemedText type="link">Learn more</ThemedText>
76
- </ExternalLink>
77
- </Collapsible>
78
- <Collapsible title="Animations">
79
- <ThemedText>
80
- This template includes an example of an animated component. The{' '}
81
- <ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses
82
- the powerful{' '}
83
- <ThemedText type="defaultSemiBold" style={{ fontFamily: Fonts.mono }}>
84
- react-native-reanimated
85
- </ThemedText>{' '}
86
- library to create a waving hand animation.
87
- </ThemedText>
88
- {Platform.select({
89
- ios: (
90
- <ThemedText>
91
- The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '}
92
- component provides a parallax effect for the header image.
93
- </ThemedText>
94
- ),
95
- })}
96
- </Collapsible>
97
- </ParallaxScrollView>
98
- );
99
-}
100
-
101
-const styles = StyleSheet.create({
102
- headerImage: {
103
- color: '#808080',
104
- bottom: -90,
105
- left: -35,
106
- position: 'absolute',
107
- },
108
- titleContainer: {
109
- flexDirection: 'row',
110
- gap: 8,
111
- },
112
-});
MODIFY
app/(tabs)/index.tsx
+76 -86
@@ -1,98 +1,88 @@
1
-import { Image } from 'expo-image';
2
-import { Platform, StyleSheet } from 'react-native';
3
-
4
-import { HelloWave } from '@/components/hello-wave';
5
-import ParallaxScrollView from '@/components/parallax-scroll-view';
6
-import { ThemedText } from '@/components/themed-text';
1
+import React, { useEffect, useState } from 'react';
2
+import { StyleSheet, SafeAreaView, Alert } from 'react-native';
3
+import { router } from 'expo-router';
7
4
import { ThemedView } from '@/components/themed-view';
8
-import { Link } from 'expo-router';
5
+import { ThemedText } from '@/components/themed-text';
6
+import { TaskList } from '@/components/task-list';
7
+import { FabButton } from '@/components/fab-button';
8
+import { Task } from '@/types/gtd';
9
+import { taskService } from '@/services';
9
10
10
-export default function HomeScreen() {
11
+export default function InboxScreen() {
12
+ const [tasks, setTasks] = useState<Task[]>([]);
13
+ const [loading, setLoading] = useState(true);
14
+
15
+ useEffect(() => {
16
+ loadTasks();
17
+ }, []);
18
+
19
+ const loadTasks = async () => {
20
+ try {
21
+ const inboxTasks = await taskService.getInboxTasks();
22
+ setTasks(inboxTasks);
23
+ } catch (error) {
24
+ Alert.alert('Error', 'Failed to load tasks');
25
+ } finally {
26
+ setLoading(false);
27
+ }
28
+ };
29
+
30
+ const handleToggleComplete = async (taskId: string) => {
31
+ try {
32
+ const task = tasks.find(t => t.id === taskId);
33
+ if (!task) return;
34
+
35
+ if (task.completed) {
36
+ await taskService.uncompleteTask(taskId);
37
+ } else {
38
+ await taskService.completeTask(taskId);
39
+ }
40
+
41
+ await loadTasks();
42
+ } catch (error) {
43
+ Alert.alert('Error', 'Failed to update task');
44
+ }
45
+ };
46
+
47
+ const handleTaskPress = (task: Task) => {
48
+ router.push(`/task/${task.id}`);
49
+ };
50
+
51
+ const handleAddTask = () => {
52
+ router.push('/task/new');
53
+ };
54
+
11
55
return (
12
- <ParallaxScrollView
13
- headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
14
- headerImage={
15
- <Image
16
- source={require('@/assets/images/partial-react-logo.png')}
17
- style={styles.reactLogo}
18
- />
19
- }>
20
- <ThemedView style={styles.titleContainer}>
21
- <ThemedText type="title">Welcome!</ThemedText>
22
- <HelloWave />
56
+ <SafeAreaView style={styles.container}>
57
+ <ThemedView style={styles.header}>
58
+ <ThemedText type="title">Inbox</ThemedText>
59
+ <ThemedText style={styles.count}>{tasks.length} tasks</ThemedText>
23
60
</ThemedView>
24
- <ThemedView style={styles.stepContainer}>
25
- <ThemedText type="subtitle">Step 1: Try it</ThemedText>
26
- <ThemedText>
27
- Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
28
- Press{' '}
29
- <ThemedText type="defaultSemiBold">
30
- {Platform.select({
31
- ios: 'cmd + d',
32
- android: 'cmd + m',
33
- web: 'F12',
34
- })}
35
- </ThemedText>{' '}
36
- to open developer tools.
37
- </ThemedText>
38
- </ThemedView>
39
- <ThemedView style={styles.stepContainer}>
40
- <Link href="/modal">
41
- <Link.Trigger>
42
- <ThemedText type="subtitle">Step 2: Explore</ThemedText>
43
- </Link.Trigger>
44
- <Link.Preview />
45
- <Link.Menu>
46
- <Link.MenuAction title="Action" icon="cube" onPress={() => alert('Action pressed')} />
47
- <Link.MenuAction
48
- title="Share"
49
- icon="square.and.arrow.up"
50
- onPress={() => alert('Share pressed')}
51
- />
52
- <Link.Menu title="More" icon="ellipsis">
53
- <Link.MenuAction
54
- title="Delete"
55
- icon="trash"
56
- destructive
57
- onPress={() => alert('Delete pressed')}
58
- />
59
- </Link.Menu>
60
- </Link.Menu>
61
- </Link>
62
-
63
- <ThemedText>
64
- {`Tap the Explore tab to learn more about what's included in this starter app.`}
65
- </ThemedText>
66
- </ThemedView>
67
- <ThemedView style={styles.stepContainer}>
68
- <ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText>
69
- <ThemedText>
70
- {`When you're ready, run `}
71
- <ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '}
72
- <ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '}
73
- <ThemedText type="defaultSemiBold">app</ThemedText> to{' '}
74
- <ThemedText type="defaultSemiBold">app-example</ThemedText>.
75
- </ThemedText>
76
- </ThemedView>
77
- </ParallaxScrollView>
61
+
62
+ <TaskList
63
+ tasks={tasks}
64
+ onTaskPress={handleTaskPress}
65
+ onToggleComplete={handleToggleComplete}
66
+ emptyMessage="🎉 Inbox is empty!"
67
+ />
68
+
69
+ <FabButton onPress={handleAddTask} />
70
+ </SafeAreaView>
78
71
);
79
72
}
80
73
81
74
const styles = StyleSheet.create({
82
- titleContainer: {
83
- flexDirection: 'row',
84
- alignItems: 'center',
85
- gap: 8,
75
+ container: {
76
+ flex: 1,
86
77
},
87
- stepContainer: {
88
- gap: 8,
89
- marginBottom: 8,
78
+ header: {
79
+ paddingHorizontal: 16,
80
+ paddingTop: 16,
81
+ paddingBottom: 12,
90
82
},
91
- reactLogo: {
92
- height: 178,
93
- width: 290,
94
- bottom: 0,
95
- left: 0,
96
- position: 'absolute',
83
+ count: {
84
+ fontSize: 14,
85
+ opacity: 0.6,
86
+ marginTop: 4,
97
87
},
98
88
});
ADD
app/(tabs)/projects.tsx
+166 -0
@@ -0,0 +1,166 @@
1
+import React, { useEffect, useState } from 'react';
2
+import { StyleSheet, SafeAreaView, Alert, TouchableOpacity, FlatList, View } from 'react-native';
3
+import { router } from 'expo-router';
4
+import { ThemedView } from '@/components/themed-view';
5
+import { ThemedText } from '@/components/themed-text';
6
+import { FabButton } from '@/components/fab-button';
7
+import { Project } from '@/types/gtd';
8
+import { projectService, taskService } from '@/services';
9
+
10
+export default function ProjectsScreen() {
11
+ const [projects, setProjects] = useState<Project[]>([]);
12
+ const [taskCounts, setTaskCounts] = useState<Record<string, number>>({});
13
+ const [loading, setLoading] = useState(true);
14
+
15
+ useEffect(() => {
16
+ loadProjects();
17
+ }, []);
18
+
19
+ const loadProjects = async () => {
20
+ try {
21
+ const allProjects = await projectService.getAllProjects();
22
+ setProjects(allProjects);
23
+
24
+ const counts: Record<string, number> = {};
25
+ for (const project of allProjects) {
26
+ counts[project.id] = await projectService.getProjectTaskCount(project.id);
27
+ }
28
+ setTaskCounts(counts);
29
+ } catch (error) {
30
+ Alert.alert('Error', 'Failed to load projects');
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ };
35
+
36
+ const handleProjectPress = (project: Project) => {
37
+ router.push(`/project/${project.id}`);
38
+ };
39
+
40
+ const handleAddProject = () => {
41
+ router.push('/project/new');
42
+ };
43
+
44
+ const renderProject = ({ item }: { item: Project }) => (
45
+ <TouchableOpacity
46
+ style={styles.projectCard}
47
+ onPress={() => handleProjectPress(item)}
48
+ activeOpacity={0.7}
49
+ >
50
+ <ThemedView style={styles.projectContent}>
51
+ {item.color && (
52
+ <View style={[styles.colorIndicator, { backgroundColor: item.color }]} />
53
+ )}
54
+ <View style={styles.projectInfo}>
55
+ <ThemedText type="defaultSemiBold" style={styles.projectName}>
56
+ {item.name}
57
+ </ThemedText>
58
+ {item.description && (
59
+ <ThemedText style={styles.projectDescription} numberOfLines={2}>
60
+ {item.description}
61
+ </ThemedText>
62
+ )}
63
+ <ThemedText style={styles.taskCount}>
64
+ {taskCounts[item.id] || 0} tasks
65
+ </ThemedText>
66
+ </View>
67
+ </ThemedView>
68
+ </TouchableOpacity>
69
+ );
70
+
71
+ return (
72
+ <SafeAreaView style={styles.container}>
73
+ <ThemedView style={styles.header}>
74
+ <ThemedText type="title">Projects</ThemedText>
75
+ <ThemedText style={styles.count}>{projects.length} projects</ThemedText>
76
+ </ThemedView>
77
+
78
+ {projects.length === 0 ? (
79
+ <ThemedView style={styles.emptyContainer}>
80
+ <ThemedText style={styles.emptyText}>
81
+ 📁 No projects yet
82
+ </ThemedText>
83
+ <ThemedText style={styles.emptySubtext}>
84
+ Create a project to organize your tasks
85
+ </ThemedText>
86
+ </ThemedView>
87
+ ) : (
88
+ <FlatList
89
+ data={projects}
90
+ keyExtractor={(item) => item.id}
91
+ renderItem={renderProject}
92
+ contentContainerStyle={styles.list}
93
+ />
94
+ )}
95
+
96
+ <FabButton onPress={handleAddProject} />
97
+ </SafeAreaView>
98
+ );
99
+}
100
+
101
+const styles = StyleSheet.create({
102
+ container: {
103
+ flex: 1,
104
+ },
105
+ header: {
106
+ paddingHorizontal: 16,
107
+ paddingTop: 16,
108
+ paddingBottom: 12,
109
+ },
110
+ count: {
111
+ fontSize: 14,
112
+ opacity: 0.6,
113
+ marginTop: 4,
114
+ },
115
+ list: {
116
+ padding: 16,
117
+ gap: 12,
118
+ },
119
+ projectCard: {
120
+ borderRadius: 12,
121
+ overflow: 'hidden',
122
+ marginBottom: 12,
123
+ },
124
+ projectContent: {
125
+ flexDirection: 'row',
126
+ padding: 16,
127
+ gap: 12,
128
+ },
129
+ colorIndicator: {
130
+ width: 4,
131
+ borderRadius: 2,
132
+ },
133
+ projectInfo: {
134
+ flex: 1,
135
+ gap: 4,
136
+ },
137
+ projectName: {
138
+ fontSize: 18,
139
+ },
140
+ projectDescription: {
141
+ fontSize: 14,
142
+ opacity: 0.7,
143
+ },
144
+ taskCount: {
145
+ fontSize: 12,
146
+ opacity: 0.5,
147
+ marginTop: 4,
148
+ },
149
+ emptyContainer: {
150
+ flex: 1,
151
+ justifyContent: 'center',
152
+ alignItems: 'center',
153
+ paddingHorizontal: 32,
154
+ },
155
+ emptyText: {
156
+ fontSize: 16,
157
+ opacity: 0.5,
158
+ textAlign: 'center',
159
+ },
160
+ emptySubtext: {
161
+ fontSize: 14,
162
+ opacity: 0.4,
163
+ textAlign: 'center',
164
+ marginTop: 8,
165
+ },
166
+});
ADD
app/(tabs)/settings.tsx
+213 -0
@@ -0,0 +1,213 @@
1
+import React, { useState } from 'react';
2
+import { StyleSheet, SafeAreaView, Alert, TouchableOpacity, ScrollView, Switch } from 'react-native';
3
+import { ThemedView } from '@/components/themed-view';
4
+import { ThemedText } from '@/components/themed-text';
5
+import { storageService, taskService } from '@/services';
6
+
7
+export default function SettingsScreen() {
8
+ const [syncing, setSyncing] = useState(false);
9
+ const [autoArchive, setAutoArchive] = useState(false);
10
+
11
+ const handleExport = async () => {
12
+ try {
13
+ const fileUri = await storageService.exportToFile();
14
+ Alert.alert(
15
+ 'Export Successful',
16
+ `Data exported to:\n${fileUri}`,
17
+ [{ text: 'OK' }]
18
+ );
19
+ } catch (error) {
20
+ Alert.alert('Export Failed', 'Failed to export data');
21
+ }
22
+ };
23
+
24
+ const handleClearCompleted = async () => {
25
+ Alert.alert(
26
+ 'Archive Completed Tasks',
27
+ 'Archive all completed tasks older than 30 days?',
28
+ [
29
+ { text: 'Cancel', style: 'cancel' },
30
+ {
31
+ text: 'Archive',
32
+ style: 'destructive',
33
+ onPress: async () => {
34
+ try {
35
+ const count = await taskService.archiveOldCompletedTasks(30);
36
+ Alert.alert('Success', `Archived ${count} tasks`);
37
+ } catch (error) {
38
+ Alert.alert('Error', 'Failed to archive tasks');
39
+ }
40
+ },
41
+ },
42
+ ]
43
+ );
44
+ };
45
+
46
+ const handleClearAll = async () => {
47
+ Alert.alert(
48
+ 'Clear All Data',
49
+ 'This will delete all tasks, projects, and tags. This action cannot be undone!',
50
+ [
51
+ { text: 'Cancel', style: 'cancel' },
52
+ {
53
+ text: 'Delete All',
54
+ style: 'destructive',
55
+ onPress: async () => {
56
+ try {
57
+ await storageService.clearAll();
58
+ Alert.alert('Success', 'All data cleared');
59
+ } catch (error) {
60
+ Alert.alert('Error', 'Failed to clear data');
61
+ }
62
+ },
63
+ },
64
+ ]
65
+ );
66
+ };
67
+
68
+ const handleSync = async () => {
69
+ setSyncing(true);
70
+ try {
71
+ await new Promise(resolve => setTimeout(resolve, 1000));
72
+ Alert.alert(
73
+ 'Sync Coming Soon',
74
+ 'WebDAV/Nextcloud sync will be available in a future version'
75
+ );
76
+ } finally {
77
+ setSyncing(false);
78
+ }
79
+ };
80
+
81
+ return (
82
+ <SafeAreaView style={styles.container}>
83
+ <ThemedView style={styles.header}>
84
+ <ThemedText type="title">Settings</ThemedText>
85
+ </ThemedView>
86
+
87
+ <ScrollView style={styles.scrollView}>
88
+ <ThemedView style={styles.section}>
89
+ <ThemedText type="subtitle" style={styles.sectionTitle}>Data</ThemedText>
90
+
91
+ <TouchableOpacity style={styles.option} onPress={handleExport}>
92
+ <ThemedText style={styles.optionText}>Export Data</ThemedText>
93
+ <ThemedText style={styles.optionDescription}>
94
+ Export all data as JSON file
95
+ </ThemedText>
96
+ </TouchableOpacity>
97
+
98
+ <TouchableOpacity style={styles.option} onPress={handleClearCompleted}>
99
+ <ThemedText style={styles.optionText}>Archive Completed Tasks</ThemedText>
100
+ <ThemedText style={styles.optionDescription}>
101
+ Remove old completed tasks (30+ days)
102
+ </ThemedText>
103
+ </TouchableOpacity>
104
+ </ThemedView>
105
+
106
+ <ThemedView style={styles.section}>
107
+ <ThemedText type="subtitle" style={styles.sectionTitle}>Sync</ThemedText>
108
+
109
+ <TouchableOpacity
110
+ style={styles.option}
111
+ onPress={handleSync}
112
+ disabled={syncing}
113
+ >
114
+ <ThemedText style={styles.optionText}>
115
+ {syncing ? 'Syncing...' : 'Sync Now'}
116
+ </ThemedText>
117
+ <ThemedText style={styles.optionDescription}>
118
+ WebDAV/Nextcloud sync (Coming Soon)
119
+ </ThemedText>
120
+ </TouchableOpacity>
121
+
122
+ <ThemedView style={styles.option}>
123
+ <ThemedView style={styles.optionWithSwitch}>
124
+ <ThemedView style={styles.optionTextContainer}>
125
+ <ThemedText style={styles.optionText}>Auto Archive</ThemedText>
126
+ <ThemedText style={styles.optionDescription}>
127
+ Automatically archive old completed tasks
128
+ </ThemedText>
129
+ </ThemedView>
130
+ <Switch
131
+ value={autoArchive}
132
+ onValueChange={setAutoArchive}
133
+ />
134
+ </ThemedView>
135
+ </ThemedView>
136
+ </ThemedView>
137
+
138
+ <ThemedView style={styles.section}>
139
+ <ThemedText type="subtitle" style={styles.sectionTitle}>About</ThemedText>
140
+
141
+ <ThemedView style={styles.option}>
142
+ <ThemedText style={styles.optionText}>Version</ThemedText>
143
+ <ThemedText style={styles.optionDescription}>1.0.0</ThemedText>
144
+ </ThemedView>
145
+
146
+ <ThemedView style={styles.option}>
147
+ <ThemedText style={styles.optionText}>Storage Format</ThemedText>
148
+ <ThemedText style={styles.optionDescription}>JSON (Human-readable)</ThemedText>
149
+ </ThemedView>
150
+ </ThemedView>
151
+
152
+ <ThemedView style={styles.section}>
153
+ <TouchableOpacity style={styles.dangerOption} onPress={handleClearAll}>
154
+ <ThemedText style={styles.dangerText}>Clear All Data</ThemedText>
155
+ </TouchableOpacity>
156
+ </ThemedView>
157
+ </ScrollView>
158
+ </SafeAreaView>
159
+ );
160
+}
161
+
162
+const styles = StyleSheet.create({
163
+ container: {
164
+ flex: 1,
165
+ },
166
+ header: {
167
+ paddingHorizontal: 16,
168
+ paddingTop: 16,
169
+ paddingBottom: 12,
170
+ },
171
+ scrollView: {
172
+ flex: 1,
173
+ },
174
+ section: {
175
+ marginBottom: 32,
176
+ paddingHorizontal: 16,
177
+ },
178
+ sectionTitle: {
179
+ marginBottom: 12,
180
+ },
181
+ option: {
182
+ paddingVertical: 12,
183
+ borderBottomWidth: StyleSheet.hairlineWidth,
184
+ borderBottomColor: '#e2e8f0',
185
+ },
186
+ optionWithSwitch: {
187
+ flexDirection: 'row',
188
+ justifyContent: 'space-between',
189
+ alignItems: 'center',
190
+ },
191
+ optionTextContainer: {
192
+ flex: 1,
193
+ },
194
+ optionText: {
195
+ fontSize: 16,
196
+ marginBottom: 4,
197
+ },
198
+ optionDescription: {
199
+ fontSize: 14,
200
+ opacity: 0.6,
201
+ },
202
+ dangerOption: {
203
+ paddingVertical: 16,
204
+ alignItems: 'center',
205
+ borderRadius: 8,
206
+ backgroundColor: '#fef2f2',
207
+ },
208
+ dangerText: {
209
+ color: '#ef4444',
210
+ fontSize: 16,
211
+ fontWeight: '600',
212
+ },
213
+});
ADD
app/(tabs)/today.tsx
+160 -0
@@ -0,0 +1,160 @@
1
+import React, { useEffect, useState } from 'react';
2
+import { StyleSheet, SafeAreaView, Alert, ScrollView } from 'react-native';
3
+import { router } from 'expo-router';
4
+import { ThemedView } from '@/components/themed-view';
5
+import { ThemedText } from '@/components/themed-text';
6
+import { TaskList } from '@/components/task-list';
7
+import { FabButton } from '@/components/fab-button';
8
+import { Task } from '@/types/gtd';
9
+import { taskService } from '@/services';
10
+
11
+export default function TodayScreen() {
12
+ const [todayTasks, setTodayTasks] = useState<Task[]>([]);
13
+ const [tomorrowTasks, setTomorrowTasks] = useState<Task[]>([]);
14
+ const [overdueTasks, setOverdueTasks] = useState<Task[]>([]);
15
+ const [loading, setLoading] = useState(true);
16
+
17
+ useEffect(() => {
18
+ loadTasks();
19
+ }, []);
20
+
21
+ const loadTasks = async () => {
22
+ try {
23
+ const [today, tomorrow, overdue] = await Promise.all([
24
+ taskService.getTodayTasks(),
25
+ taskService.getTomorrowTasks(),
26
+ taskService.getOverdueTasks(),
27
+ ]);
28
+
29
+ setTodayTasks(today);
30
+ setTomorrowTasks(tomorrow);
31
+ setOverdueTasks(overdue);
32
+ } catch (error) {
33
+ Alert.alert('Error', 'Failed to load tasks');
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ };
38
+
39
+ const handleToggleComplete = async (taskId: string) => {
40
+ try {
41
+ const allTasks = [...overdueTasks, ...todayTasks, ...tomorrowTasks];
42
+ const task = allTasks.find(t => t.id === taskId);
43
+ if (!task) return;
44
+
45
+ if (task.completed) {
46
+ await taskService.uncompleteTask(taskId);
47
+ } else {
48
+ await taskService.completeTask(taskId);
49
+ }
50
+
51
+ await loadTasks();
52
+ } catch (error) {
53
+ Alert.alert('Error', 'Failed to update task');
54
+ }
55
+ };
56
+
57
+ const handleTaskPress = (task: Task) => {
58
+ router.push(`/task/${task.id}`);
59
+ };
60
+
61
+ const handleAddTask = () => {
62
+ router.push('/task/new');
63
+ };
64
+
65
+ return (
66
+ <SafeAreaView style={styles.container}>
67
+ <ThemedView style={styles.header}>
68
+ <ThemedText type="title">Today</ThemedText>
69
+ <ThemedText style={styles.date}>
70
+ {new Date().toLocaleDateString('de-DE', {
71
+ weekday: 'long',
72
+ day: 'numeric',
73
+ month: 'long'
74
+ })}
75
+ </ThemedText>
76
+ </ThemedView>
77
+
78
+ <ScrollView style={styles.scrollView}>
79
+ {overdueTasks.length > 0 && (
80
+ <ThemedView style={styles.section}>
81
+ <ThemedText type="subtitle" style={styles.overdueTitle}>⚠️ Overdue</ThemedText>
82
+ <TaskList
83
+ tasks={overdueTasks}
84
+ onTaskPress={handleTaskPress}
85
+ onToggleComplete={handleToggleComplete}
86
+ />
87
+ </ThemedView>
88
+ )}
89
+
90
+ {todayTasks.length > 0 && (
91
+ <ThemedView style={styles.section}>
92
+ <ThemedText type="subtitle">Today</ThemedText>
93
+ <TaskList
94
+ tasks={todayTasks}
95
+ onTaskPress={handleTaskPress}
96
+ onToggleComplete={handleToggleComplete}
97
+ />
98
+ </ThemedView>
99
+ )}
100
+
101
+ {tomorrowTasks.length > 0 && (
102
+ <ThemedView style={styles.section}>
103
+ <ThemedText type="subtitle">Tomorrow</ThemedText>
104
+ <TaskList
105
+ tasks={tomorrowTasks}
106
+ onTaskPress={handleTaskPress}
107
+ onToggleComplete={handleToggleComplete}
108
+ />
109
+ </ThemedView>
110
+ )}
111
+
112
+ {overdueTasks.length === 0 && todayTasks.length === 0 && tomorrowTasks.length === 0 && (
113
+ <ThemedView style={styles.emptyContainer}>
114
+ <ThemedText style={styles.emptyText}>
115
+ ☀️ All clear! No scheduled tasks.
116
+ </ThemedText>
117
+ </ThemedView>
118
+ )}
119
+ </ScrollView>
120
+
121
+ <FabButton onPress={handleAddTask} />
122
+ </SafeAreaView>
123
+ );
124
+}
125
+
126
+const styles = StyleSheet.create({
127
+ container: {
128
+ flex: 1,
129
+ },
130
+ header: {
131
+ paddingHorizontal: 16,
132
+ paddingTop: 16,
133
+ paddingBottom: 12,
134
+ },
135
+ date: {
136
+ fontSize: 14,
137
+ opacity: 0.6,
138
+ marginTop: 4,
139
+ },
140
+ scrollView: {
141
+ flex: 1,
142
+ },
143
+ section: {
144
+ marginBottom: 24,
145
+ },
146
+ overdueTitle: {
147
+ color: '#ef4444',
148
+ },
149
+ emptyContainer: {
150
+ flex: 1,
151
+ justifyContent: 'center',
152
+ alignItems: 'center',
153
+ paddingVertical: 60,
154
+ },
155
+ emptyText: {
156
+ fontSize: 16,
157
+ opacity: 0.5,
158
+ textAlign: 'center',
159
+ },
160
+});
ADD
app/project/[id].tsx
+438 -0
@@ -0,0 +1,438 @@
1
+import React, { useEffect, useState } from 'react';
2
+import { StyleSheet, SafeAreaView, Alert, ScrollView, TouchableOpacity, TextInput } from 'react-native';
3
+import { router, useLocalSearchParams } from 'expo-router';
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
+
10
+export default function ProjectDetailScreen() {
11
+ const { id } = useLocalSearchParams<{ id: string }>();
12
+ const isNew = id === 'new';
13
+
14
+ const [project, setProject] = useState<Partial<Project>>({
15
+ name: '',
16
+ description: '',
17
+ goal: '',
18
+ color: '#0a7ea4',
19
+ archived: false,
20
+ });
21
+
22
+ const [tasks, setTasks] = useState<Task[]>([]);
23
+ const [isEditing, setIsEditing] = useState(isNew);
24
+ const [loading, setLoading] = useState(!isNew);
25
+
26
+ useEffect(() => {
27
+ loadData();
28
+ }, [id]);
29
+
30
+ const loadData = async () => {
31
+ try {
32
+ if (!isNew && id) {
33
+ const foundProject = await projectService.getProjectById(id);
34
+ if (foundProject) {
35
+ setProject(foundProject);
36
+ const projectTasks = await taskService.getTasksByProject(id);
37
+ setTasks(projectTasks);
38
+ } else {
39
+ Alert.alert('Error', 'Project not found');
40
+ router.back();
41
+ }
42
+ }
43
+ } catch (error) {
44
+ Alert.alert('Error', 'Failed to load project');
45
+ } finally {
46
+ setLoading(false);
47
+ }
48
+ };
49
+
50
+ const handleSave = async () => {
51
+ if (!project.name?.trim()) {
52
+ Alert.alert('Error', 'Please enter a project name');
53
+ return;
54
+ }
55
+
56
+ try {
57
+ if (isNew) {
58
+ await projectService.createProject(project);
59
+ router.back();
60
+ } else if (id) {
61
+ await projectService.updateProject(id, project);
62
+ setIsEditing(false);
63
+ await loadData();
64
+ }
65
+ } catch (error) {
66
+ Alert.alert('Error', 'Failed to save project');
67
+ }
68
+ };
69
+
70
+ const handleDelete = async () => {
71
+ Alert.alert(
72
+ 'Delete Project',
73
+ 'Are you sure you want to delete this project? Tasks will be kept but unassigned.',
74
+ [
75
+ { text: 'Cancel', style: 'cancel' },
76
+ {
77
+ text: 'Delete',
78
+ style: 'destructive',
79
+ onPress: async () => {
80
+ try {
81
+ if (id && id !== 'new') {
82
+ await projectService.deleteProject(id);
83
+ router.back();
84
+ }
85
+ } catch (error) {
86
+ Alert.alert('Error', 'Failed to delete project');
87
+ }
88
+ },
89
+ },
90
+ ]
91
+ );
92
+ };
93
+
94
+ const handleArchive = async () => {
95
+ if (!id || id === 'new') return;
96
+
97
+ try {
98
+ if (project.archived) {
99
+ await projectService.unarchiveProject(id);
100
+ } else {
101
+ await projectService.archiveProject(id);
102
+ }
103
+ await loadData();
104
+ } catch (error) {
105
+ Alert.alert('Error', 'Failed to archive project');
106
+ }
107
+ };
108
+
109
+ const handleToggleComplete = async (taskId: string) => {
110
+ try {
111
+ const task = tasks.find(t => t.id === taskId);
112
+ if (!task) return;
113
+
114
+ if (task.completed) {
115
+ await taskService.uncompleteTask(taskId);
116
+ } else {
117
+ await taskService.completeTask(taskId);
118
+ }
119
+
120
+ await loadData();
121
+ } catch (error) {
122
+ Alert.alert('Error', 'Failed to update task');
123
+ }
124
+ };
125
+
126
+ const handleTaskPress = (task: Task) => {
127
+ router.push(`/task/${task.id}`);
128
+ };
129
+
130
+ const handleAddTask = () => {
131
+ router.push({
132
+ pathname: '/task/new',
133
+ params: { projectId: id },
134
+ });
135
+ };
136
+
137
+ const colors = ['#0a7ea4', '#ef4444', '#f59e0b', '#10b981', '#8b5cf6', '#ec4899', '#64748b'];
138
+
139
+ return (
140
+ <SafeAreaView style={styles.container}>
141
+ <ThemedView style={styles.header}>
142
+ <TouchableOpacity onPress={() => router.back()}>
143
+ <ThemedText style={styles.backButton}>Back</ThemedText>
144
+ </TouchableOpacity>
145
+ {isEditing ? (
146
+ <TouchableOpacity onPress={handleSave}>
147
+ <ThemedText style={styles.saveButton}>Save</ThemedText>
148
+ </TouchableOpacity>
149
+ ) : (
150
+ <TouchableOpacity onPress={() => setIsEditing(true)}>
151
+ <ThemedText style={styles.editButton}>Edit</ThemedText>
152
+ </TouchableOpacity>
153
+ )}
154
+ </ThemedView>
155
+
156
+ <ScrollView style={styles.scrollView}>
157
+ {isEditing ? (
158
+ <>
159
+ <ThemedView style={styles.section}>
160
+ <ThemedText style={styles.label}>Project Name *</ThemedText>
161
+ <TextInput
162
+ style={styles.input}
163
+ value={project.name}
164
+ onChangeText={(text) => setProject({ ...project, name: text })}
165
+ placeholder="Enter project name"
166
+ placeholderTextColor="#94a3b8"
167
+ autoFocus={isNew}
168
+ />
169
+ </ThemedView>
170
+
171
+ <ThemedView style={styles.section}>
172
+ <ThemedText style={styles.label}>Description</ThemedText>
173
+ <TextInput
174
+ style={[styles.input, styles.textArea]}
175
+ value={project.description}
176
+ onChangeText={(text) => setProject({ ...project, description: text })}
177
+ placeholder="Add description"
178
+ placeholderTextColor="#94a3b8"
179
+ multiline
180
+ numberOfLines={3}
181
+ textAlignVertical="top"
182
+ />
183
+ </ThemedView>
184
+
185
+ <ThemedView style={styles.section}>
186
+ <ThemedText style={styles.label}>Goal</ThemedText>
187
+ <TextInput
188
+ style={[styles.input, styles.textArea]}
189
+ value={project.goal}
190
+ onChangeText={(text) => setProject({ ...project, goal: text })}
191
+ placeholder="What's the desired outcome?"
192
+ placeholderTextColor="#94a3b8"
193
+ multiline
194
+ numberOfLines={3}
195
+ textAlignVertical="top"
196
+ />
197
+ </ThemedView>
198
+
199
+ <ThemedView style={styles.section}>
200
+ <ThemedText style={styles.label}>Color</ThemedText>
201
+ <ThemedView style={styles.colorContainer}>
202
+ {colors.map((color) => (
203
+ <TouchableOpacity
204
+ key={color}
205
+ style={[
206
+ styles.colorButton,
207
+ { backgroundColor: color },
208
+ project.color === color && styles.colorButtonActive,
209
+ ]}
210
+ onPress={() => setProject({ ...project, color })}
211
+ />
212
+ ))}
213
+ </ThemedView>
214
+ </ThemedView>
215
+
216
+ {!isNew && (
217
+ <ThemedView style={styles.section}>
218
+ <TouchableOpacity style={styles.archiveButton} onPress={handleArchive}>
219
+ <ThemedText style={styles.archiveButtonText}>
220
+ {project.archived ? 'Unarchive Project' : 'Archive Project'}
221
+ </ThemedText>
222
+ </TouchableOpacity>
223
+
224
+ <TouchableOpacity style={styles.deleteButton} onPress={handleDelete}>
225
+ <ThemedText style={styles.deleteButtonText}>Delete Project</ThemedText>
226
+ </TouchableOpacity>
227
+ </ThemedView>
228
+ )}
229
+ </>
230
+ ) : (
231
+ <>
232
+ <ThemedView style={styles.projectHeader}>
233
+ <ThemedView style={styles.projectTitleContainer}>
234
+ {project.color && (
235
+ <ThemedView style={[styles.colorIndicator, { backgroundColor: project.color }]} />
236
+ )}
237
+ <ThemedText type="title">{project.name}</ThemedText>
238
+ </ThemedView>
239
+ {project.archived && (
240
+ <ThemedView style={styles.archivedBadge}>
241
+ <ThemedText style={styles.archivedText}>Archived</ThemedText>
242
+ </ThemedView>
243
+ )}
244
+ </ThemedView>
245
+
246
+ {project.description && (
247
+ <ThemedView style={styles.infoSection}>
248
+ <ThemedText style={styles.infoLabel}>Description</ThemedText>
249
+ <ThemedText style={styles.infoText}>{project.description}</ThemedText>
250
+ </ThemedView>
251
+ )}
252
+
253
+ {project.goal && (
254
+ <ThemedView style={styles.infoSection}>
255
+ <ThemedText style={styles.infoLabel}>Goal</ThemedText>
256
+ <ThemedText style={styles.infoText}>{project.goal}</ThemedText>
257
+ </ThemedView>
258
+ )}
259
+
260
+ <ThemedView style={styles.tasksSection}>
261
+ <ThemedView style={styles.tasksSectionHeader}>
262
+ <ThemedText type="subtitle">Tasks</ThemedText>
263
+ <TouchableOpacity onPress={handleAddTask}>
264
+ <ThemedText style={styles.addTaskButton}>+ Add Task</ThemedText>
265
+ </TouchableOpacity>
266
+ </ThemedView>
267
+
268
+ <TaskList
269
+ tasks={tasks}
270
+ onTaskPress={handleTaskPress}
271
+ onToggleComplete={handleToggleComplete}
272
+ emptyMessage="No tasks in this project yet"
273
+ />
274
+ </ThemedView>
275
+ </>
276
+ )}
277
+
278
+ <ThemedView style={styles.bottomPadding} />
279
+ </ScrollView>
280
+ </SafeAreaView>
281
+ );
282
+}
283
+
284
+const styles = StyleSheet.create({
285
+ container: {
286
+ flex: 1,
287
+ },
288
+ header: {
289
+ flexDirection: 'row',
290
+ justifyContent: 'space-between',
291
+ alignItems: 'center',
292
+ paddingHorizontal: 16,
293
+ paddingVertical: 12,
294
+ borderBottomWidth: StyleSheet.hairlineWidth,
295
+ borderBottomColor: '#e2e8f0',
296
+ },
297
+ backButton: {
298
+ color: '#0a7ea4',
299
+ },
300
+ editButton: {
301
+ color: '#0a7ea4',
302
+ fontWeight: '600',
303
+ },
304
+ saveButton: {
305
+ color: '#0a7ea4',
306
+ fontWeight: '600',
307
+ },
308
+ scrollView: {
309
+ flex: 1,
310
+ },
311
+ section: {
312
+ paddingHorizontal: 16,
313
+ paddingVertical: 16,
314
+ borderBottomWidth: StyleSheet.hairlineWidth,
315
+ borderBottomColor: '#e2e8f0',
316
+ },
317
+ label: {
318
+ fontSize: 14,
319
+ fontWeight: '600',
320
+ marginBottom: 8,
321
+ opacity: 0.7,
322
+ },
323
+ input: {
324
+ fontSize: 16,
325
+ paddingVertical: 8,
326
+ paddingHorizontal: 12,
327
+ borderWidth: 1,
328
+ borderColor: '#e2e8f0',
329
+ borderRadius: 8,
330
+ minHeight: 44,
331
+ },
332
+ textArea: {
333
+ minHeight: 80,
334
+ paddingTop: 12,
335
+ },
336
+ colorContainer: {
337
+ flexDirection: 'row',
338
+ gap: 12,
339
+ flexWrap: 'wrap',
340
+ },
341
+ colorButton: {
342
+ width: 40,
343
+ height: 40,
344
+ borderRadius: 20,
345
+ borderWidth: 3,
346
+ borderColor: 'transparent',
347
+ },
348
+ colorButtonActive: {
349
+ borderColor: '#fff',
350
+ shadowColor: '#000',
351
+ shadowOffset: { width: 0, height: 2 },
352
+ shadowOpacity: 0.2,
353
+ shadowRadius: 4,
354
+ elevation: 4,
355
+ },
356
+ archiveButton: {
357
+ paddingVertical: 16,
358
+ borderRadius: 8,
359
+ backgroundColor: '#f1f5f9',
360
+ alignItems: 'center',
361
+ marginBottom: 12,
362
+ },
363
+ archiveButtonText: {
364
+ fontSize: 16,
365
+ fontWeight: '600',
366
+ },
367
+ deleteButton: {
368
+ paddingVertical: 16,
369
+ borderRadius: 8,
370
+ backgroundColor: '#fef2f2',
371
+ alignItems: 'center',
372
+ },
373
+ deleteButtonText: {
374
+ color: '#ef4444',
375
+ fontSize: 16,
376
+ fontWeight: '600',
377
+ },
378
+ projectHeader: {
379
+ paddingHorizontal: 16,
380
+ paddingTop: 24,
381
+ paddingBottom: 16,
382
+ },
383
+ projectTitleContainer: {
384
+ flexDirection: 'row',
385
+ alignItems: 'center',
386
+ gap: 12,
387
+ },
388
+ colorIndicator: {
389
+ width: 6,
390
+ height: 32,
391
+ borderRadius: 3,
392
+ },
393
+ archivedBadge: {
394
+ marginTop: 12,
395
+ paddingHorizontal: 12,
396
+ paddingVertical: 4,
397
+ backgroundColor: '#f1f5f9',
398
+ borderRadius: 12,
399
+ alignSelf: 'flex-start',
400
+ },
401
+ archivedText: {
402
+ fontSize: 12,
403
+ fontWeight: '600',
404
+ opacity: 0.6,
405
+ },
406
+ infoSection: {
407
+ paddingHorizontal: 16,
408
+ paddingVertical: 12,
409
+ },
410
+ infoLabel: {
411
+ fontSize: 12,
412
+ fontWeight: '600',
413
+ opacity: 0.5,
414
+ marginBottom: 4,
415
+ textTransform: 'uppercase',
416
+ },
417
+ infoText: {
418
+ fontSize: 16,
419
+ lineHeight: 22,
420
+ },
421
+ tasksSection: {
422
+ marginTop: 24,
423
+ },
424
+ tasksSectionHeader: {
425
+ flexDirection: 'row',
426
+ justifyContent: 'space-between',
427
+ alignItems: 'center',
428
+ paddingHorizontal: 16,
429
+ paddingBottom: 8,
430
+ },
431
+ addTaskButton: {
432
+ color: '#0a7ea4',
433
+ fontWeight: '600',
434
+ },
435
+ bottomPadding: {
436
+ height: 40,
437
+ },
438
+});
ADD
app/task/[id].tsx
+441 -0
@@ -0,0 +1,441 @@
1
+import React, { useEffect, useState } from 'react';
2
+import { StyleSheet, SafeAreaView, Alert, ScrollView, TouchableOpacity, TextInput, Platform } from 'react-native';
3
+import { router, useLocalSearchParams } from 'expo-router';
4
+import { ThemedView } from '@/components/themed-view';
5
+import { ThemedText } from '@/components/themed-text';
6
+import { Task, Priority, TaskStatus, Project, Tag } from '@/types/gtd';
7
+import { taskService, projectService, tagService } from '@/services';
8
+
9
+export default function TaskDetailScreen() {
10
+ const { id } = useLocalSearchParams<{ id: string }>();
11
+ const isNew = id === 'new';
12
+
13
+ const [task, setTask] = useState<Partial<Task>>({
14
+ title: '',
15
+ description: '',
16
+ status: 'inbox',
17
+ priority: undefined,
18
+ dueDate: undefined,
19
+ projectId: undefined,
20
+ tagIds: [],
21
+ completed: false,
22
+ });
23
+
24
+ const [projects, setProjects] = useState<Project[]>([]);
25
+ const [tags, setTags] = useState<Tag[]>([]);
26
+ const [loading, setLoading] = useState(!isNew);
27
+
28
+ useEffect(() => {
29
+ loadData();
30
+ }, [id]);
31
+
32
+ const loadData = async () => {
33
+ try {
34
+ const [allProjects, allTags] = await Promise.all([
35
+ projectService.getAllProjects(),
36
+ tagService.getAllTags(),
37
+ ]);
38
+
39
+ setProjects(allProjects);
40
+ setTags(allTags);
41
+
42
+ if (!isNew && id) {
43
+ const tasks = await taskService.getAllTasks();
44
+ const foundTask = tasks.find(t => t.id === id);
45
+ if (foundTask) {
46
+ setTask(foundTask);
47
+ } else {
48
+ Alert.alert('Error', 'Task not found');
49
+ router.back();
50
+ }
51
+ }
52
+ } catch (error) {
53
+ Alert.alert('Error', 'Failed to load data');
54
+ } finally {
55
+ setLoading(false);
56
+ }
57
+ };
58
+
59
+ const handleSave = async () => {
60
+ if (!task.title?.trim()) {
61
+ Alert.alert('Error', 'Please enter a task title');
62
+ return;
63
+ }
64
+
65
+ try {
66
+ if (isNew) {
67
+ await taskService.createTask(task);
68
+ } else if (id) {
69
+ await taskService.updateTask(id, task);
70
+ }
71
+
72
+ router.back();
73
+ } catch (error) {
74
+ Alert.alert('Error', 'Failed to save task');
75
+ }
76
+ };
77
+
78
+ const handleDelete = async () => {
79
+ Alert.alert(
80
+ 'Delete Task',
81
+ 'Are you sure you want to delete this task?',
82
+ [
83
+ { text: 'Cancel', style: 'cancel' },
84
+ {
85
+ text: 'Delete',
86
+ style: 'destructive',
87
+ onPress: async () => {
88
+ try {
89
+ if (id && id !== 'new') {
90
+ await taskService.deleteTask(id);
91
+ router.back();
92
+ }
93
+ } catch (error) {
94
+ Alert.alert('Error', 'Failed to delete task');
95
+ }
96
+ },
97
+ },
98
+ ]
99
+ );
100
+ };
101
+
102
+ const formatDate = (timestamp?: number) => {
103
+ if (!timestamp) return 'No date';
104
+ return new Date(timestamp).toLocaleDateString('de-DE', {
105
+ weekday: 'short',
106
+ day: '2-digit',
107
+ month: 'short',
108
+ year: 'numeric',
109
+ });
110
+ };
111
+
112
+ const setDueDate = (days: number | null) => {
113
+ if (days === null) {
114
+ setTask({ ...task, dueDate: undefined });
115
+ } else {
116
+ const date = new Date();
117
+ date.setDate(date.getDate() + days);
118
+ date.setHours(23, 59, 59, 999);
119
+ setTask({ ...task, dueDate: date.getTime() });
120
+ }
121
+ };
122
+
123
+ return (
124
+ <SafeAreaView style={styles.container}>
125
+ <ThemedView style={styles.header}>
126
+ <TouchableOpacity onPress={() => router.back()}>
127
+ <ThemedText style={styles.cancelButton}>Cancel</ThemedText>
128
+ </TouchableOpacity>
129
+ <ThemedText type="defaultSemiBold">{isNew ? 'New Task' : 'Edit Task'}</ThemedText>
130
+ <TouchableOpacity onPress={handleSave}>
131
+ <ThemedText style={styles.saveButton}>Save</ThemedText>
132
+ </TouchableOpacity>
133
+ </ThemedView>
134
+
135
+ <ScrollView style={styles.scrollView}>
136
+ <ThemedView style={styles.section}>
137
+ <ThemedText style={styles.label}>Title *</ThemedText>
138
+ <TextInput
139
+ style={styles.input}
140
+ value={task.title}
141
+ onChangeText={(text) => setTask({ ...task, title: text })}
142
+ placeholder="Enter task title"
143
+ placeholderTextColor="#94a3b8"
144
+ autoFocus={isNew}
145
+ />
146
+ </ThemedView>
147
+
148
+ <ThemedView style={styles.section}>
149
+ <ThemedText style={styles.label}>Description</ThemedText>
150
+ <TextInput
151
+ style={[styles.input, styles.textArea]}
152
+ value={task.description}
153
+ onChangeText={(text) => setTask({ ...task, description: text })}
154
+ placeholder="Add description"
155
+ placeholderTextColor="#94a3b8"
156
+ multiline
157
+ numberOfLines={4}
158
+ textAlignVertical="top"
159
+ />
160
+ </ThemedView>
161
+
162
+ <ThemedView style={styles.section}>
163
+ <ThemedText style={styles.label}>Priority</ThemedText>
164
+ <ThemedView style={styles.priorityContainer}>
165
+ {(['high', 'medium', 'low'] as Priority[]).map((priority) => (
166
+ <TouchableOpacity
167
+ key={priority}
168
+ style={[
169
+ styles.priorityButton,
170
+ task.priority === priority && styles.priorityButtonActive,
171
+ ]}
172
+ onPress={() => setTask({ ...task, priority })}
173
+ >
174
+ <ThemedText
175
+ style={[
176
+ styles.priorityText,
177
+ task.priority === priority && styles.priorityTextActive,
178
+ ]}
179
+ >
180
+ {priority.charAt(0).toUpperCase() + priority.slice(1)}
181
+ </ThemedText>
182
+ </TouchableOpacity>
183
+ ))}
184
+ <TouchableOpacity
185
+ style={styles.priorityButton}
186
+ onPress={() => setTask({ ...task, priority: undefined })}
187
+ >
188
+ <ThemedText style={styles.priorityText}>None</ThemedText>
189
+ </TouchableOpacity>
190
+ </ThemedView>
191
+ </ThemedView>
192
+
193
+ <ThemedView style={styles.section}>
194
+ <ThemedText style={styles.label}>Due Date</ThemedText>
195
+ <ThemedView style={styles.dateContainer}>
196
+ <TouchableOpacity
197
+ style={styles.dateButton}
198
+ onPress={() => setDueDate(0)}
199
+ >
200
+ <ThemedText style={styles.dateButtonText}>Today</ThemedText>
201
+ </TouchableOpacity>
202
+ <TouchableOpacity
203
+ style={styles.dateButton}
204
+ onPress={() => setDueDate(1)}
205
+ >
206
+ <ThemedText style={styles.dateButtonText}>Tomorrow</ThemedText>
207
+ </TouchableOpacity>
208
+ <TouchableOpacity
209
+ style={styles.dateButton}
210
+ onPress={() => setDueDate(7)}
211
+ >
212
+ <ThemedText style={styles.dateButtonText}>Next Week</ThemedText>
213
+ </TouchableOpacity>
214
+ <TouchableOpacity
215
+ style={styles.dateButton}
216
+ onPress={() => setDueDate(null)}
217
+ >
218
+ <ThemedText style={styles.dateButtonText}>Clear</ThemedText>
219
+ </TouchableOpacity>
220
+ </ThemedView>
221
+ {task.dueDate && (
222
+ <ThemedText style={styles.selectedDate}>
223
+ {formatDate(task.dueDate)}
224
+ </ThemedText>
225
+ )}
226
+ </ThemedView>
227
+
228
+ <ThemedView style={styles.section}>
229
+ <ThemedText style={styles.label}>Status</ThemedText>
230
+ <ThemedView style={styles.statusContainer}>
231
+ {(['inbox', 'next', 'waiting', 'someday'] as TaskStatus[]).map((status) => (
232
+ <TouchableOpacity
233
+ key={status}
234
+ style={[
235
+ styles.statusButton,
236
+ task.status === status && styles.statusButtonActive,
237
+ ]}
238
+ onPress={() => setTask({ ...task, status })}
239
+ >
240
+ <ThemedText
241
+ style={[
242
+ styles.statusText,
243
+ task.status === status && styles.statusTextActive,
244
+ ]}
245
+ >
246
+ {status.charAt(0).toUpperCase() + status.slice(1)}
247
+ </ThemedText>
248
+ </TouchableOpacity>
249
+ ))}
250
+ </ThemedView>
251
+ </ThemedView>
252
+
253
+ <ThemedView style={styles.section}>
254
+ <ThemedText style={styles.label}>Project</ThemedText>
255
+ <ThemedView style={styles.projectContainer}>
256
+ <TouchableOpacity
257
+ style={[
258
+ styles.projectButton,
259
+ !task.projectId && styles.projectButtonActive,
260
+ ]}
261
+ onPress={() => setTask({ ...task, projectId: undefined })}
262
+ >
263
+ <ThemedText style={styles.projectText}>None</ThemedText>
264
+ </TouchableOpacity>
265
+ {projects.map((project) => (
266
+ <TouchableOpacity
267
+ key={project.id}
268
+ style={[
269
+ styles.projectButton,
270
+ task.projectId === project.id && styles.projectButtonActive,
271
+ ]}
272
+ onPress={() => setTask({ ...task, projectId: project.id })}
273
+ >
274
+ <ThemedText style={styles.projectText}>{project.name}</ThemedText>
275
+ </TouchableOpacity>
276
+ ))}
277
+ </ThemedView>
278
+ </ThemedView>
279
+
280
+ {!isNew && (
281
+ <ThemedView style={styles.section}>
282
+ <TouchableOpacity style={styles.deleteButton} onPress={handleDelete}>
283
+ <ThemedText style={styles.deleteButtonText}>Delete Task</ThemedText>
284
+ </TouchableOpacity>
285
+ </ThemedView>
286
+ )}
287
+
288
+ <ThemedView style={styles.bottomPadding} />
289
+ </ScrollView>
290
+ </SafeAreaView>
291
+ );
292
+}
293
+
294
+const styles = StyleSheet.create({
295
+ container: {
296
+ flex: 1,
297
+ },
298
+ header: {
299
+ flexDirection: 'row',
300
+ justifyContent: 'space-between',
301
+ alignItems: 'center',
302
+ paddingHorizontal: 16,
303
+ paddingVertical: 12,
304
+ borderBottomWidth: StyleSheet.hairlineWidth,
305
+ borderBottomColor: '#e2e8f0',
306
+ },
307
+ cancelButton: {
308
+ color: '#64748b',
309
+ },
310
+ saveButton: {
311
+ color: '#0a7ea4',
312
+ fontWeight: '600',
313
+ },
314
+ scrollView: {
315
+ flex: 1,
316
+ },
317
+ section: {
318
+ paddingHorizontal: 16,
319
+ paddingVertical: 16,
320
+ borderBottomWidth: StyleSheet.hairlineWidth,
321
+ borderBottomColor: '#e2e8f0',
322
+ },
323
+ label: {
324
+ fontSize: 14,
325
+ fontWeight: '600',
326
+ marginBottom: 8,
327
+ opacity: 0.7,
328
+ },
329
+ input: {
330
+ fontSize: 16,
331
+ paddingVertical: 8,
332
+ paddingHorizontal: 12,
333
+ borderWidth: 1,
334
+ borderColor: '#e2e8f0',
335
+ borderRadius: 8,
336
+ minHeight: 44,
337
+ },
338
+ textArea: {
339
+ minHeight: 100,
340
+ paddingTop: 12,
341
+ },
342
+ priorityContainer: {
343
+ flexDirection: 'row',
344
+ gap: 8,
345
+ flexWrap: 'wrap',
346
+ },
347
+ priorityButton: {
348
+ paddingVertical: 8,
349
+ paddingHorizontal: 16,
350
+ borderRadius: 8,
351
+ borderWidth: 1,
352
+ borderColor: '#e2e8f0',
353
+ },
354
+ priorityButtonActive: {
355
+ backgroundColor: '#0a7ea4',
356
+ borderColor: '#0a7ea4',
357
+ },
358
+ priorityText: {
359
+ fontSize: 14,
360
+ },
361
+ priorityTextActive: {
362
+ color: '#fff',
363
+ fontWeight: '600',
364
+ },
365
+ dateContainer: {
366
+ flexDirection: 'row',
367
+ gap: 8,
368
+ flexWrap: 'wrap',
369
+ },
370
+ dateButton: {
371
+ paddingVertical: 8,
372
+ paddingHorizontal: 16,
373
+ borderRadius: 8,
374
+ borderWidth: 1,
375
+ borderColor: '#e2e8f0',
376
+ },
377
+ dateButtonText: {
378
+ fontSize: 14,
379
+ },
380
+ selectedDate: {
381
+ marginTop: 8,
382
+ fontSize: 14,
383
+ opacity: 0.6,
384
+ },
385
+ statusContainer: {
386
+ flexDirection: 'row',
387
+ gap: 8,
388
+ flexWrap: 'wrap',
389
+ },
390
+ statusButton: {
391
+ paddingVertical: 8,
392
+ paddingHorizontal: 16,
393
+ borderRadius: 8,
394
+ borderWidth: 1,
395
+ borderColor: '#e2e8f0',
396
+ },
397
+ statusButtonActive: {
398
+ backgroundColor: '#0a7ea4',
399
+ borderColor: '#0a7ea4',
400
+ },
401
+ statusText: {
402
+ fontSize: 14,
403
+ },
404
+ statusTextActive: {
405
+ color: '#fff',
406
+ fontWeight: '600',
407
+ },
408
+ projectContainer: {
409
+ flexDirection: 'row',
410
+ gap: 8,
411
+ flexWrap: 'wrap',
412
+ },
413
+ projectButton: {
414
+ paddingVertical: 8,
415
+ paddingHorizontal: 16,
416
+ borderRadius: 8,
417
+ borderWidth: 1,
418
+ borderColor: '#e2e8f0',
419
+ },
420
+ projectButtonActive: {
421
+ backgroundColor: '#0a7ea4',
422
+ borderColor: '#0a7ea4',
423
+ },
424
+ projectText: {
425
+ fontSize: 14,
426
+ },
427
+ deleteButton: {
428
+ paddingVertical: 16,
429
+ borderRadius: 8,
430
+ backgroundColor: '#fef2f2',
431
+ alignItems: 'center',
432
+ },
433
+ deleteButtonText: {
434
+ color: '#ef4444',
435
+ fontSize: 16,
436
+ fontWeight: '600',
437
+ },
438
+ bottomPadding: {
439
+ height: 40,
440
+ },
441
+});
ADD
components/fab-button.tsx
+48 -0
@@ -0,0 +1,48 @@
1
+import React from 'react';
2
+import { StyleSheet, TouchableOpacity, Platform } from 'react-native';
3
+import { ThemedText } from './themed-text';
4
+import { Colors } from '@/constants/theme';
5
+import { useColorScheme } from '@/hooks/use-color-scheme';
6
+
7
+interface FabButtonProps {
8
+ onPress: () => void;
9
+}
10
+
11
+export function FabButton({ onPress }: FabButtonProps) {
12
+ const colorScheme = useColorScheme() ?? 'light';
13
+ const colors = Colors[colorScheme];
14
+
15
+ return (
16
+ <TouchableOpacity
17
+ style={[styles.fab, { backgroundColor: colors.tint }]}
18
+ onPress={onPress}
19
+ activeOpacity={0.8}
20
+ >
21
+ <ThemedText style={styles.fabText}>+</ThemedText>
22
+ </TouchableOpacity>
23
+ );
24
+}
25
+
26
+const styles = StyleSheet.create({
27
+ fab: {
28
+ position: 'absolute',
29
+ right: 20,
30
+ bottom: Platform.OS === 'ios' ? 90 : 80,
31
+ width: 56,
32
+ height: 56,
33
+ borderRadius: 28,
34
+ justifyContent: 'center',
35
+ alignItems: 'center',
36
+ elevation: 8,
37
+ shadowColor: '#000',
38
+ shadowOffset: { width: 0, height: 4 },
39
+ shadowOpacity: 0.3,
40
+ shadowRadius: 8,
41
+ },
42
+ fabText: {
43
+ color: '#fff',
44
+ fontSize: 32,
45
+ fontWeight: '300',
46
+ marginTop: -2,
47
+ },
48
+});
ADD
components/task-item.tsx
+146 -0
@@ -0,0 +1,146 @@
1
+import React from 'react';
2
+import { StyleSheet, TouchableOpacity, View } from 'react-native';
3
+import { Task } from '@/types/gtd';
4
+import { ThemedText } from './themed-text';
5
+import { ThemedView } from './themed-view';
6
+import { Colors } from '@/constants/theme';
7
+import { useColorScheme } from '@/hooks/use-color-scheme';
8
+
9
+interface TaskItemProps {
10
+ task: Task;
11
+ onPress: () => void;
12
+ onToggleComplete: () => void;
13
+}
14
+
15
+export function TaskItem({ task, onPress, onToggleComplete }: TaskItemProps) {
16
+ const colorScheme = useColorScheme() ?? 'light';
17
+ const colors = Colors[colorScheme];
18
+
19
+ const priorityColor = task.priority === 'high' ? '#ef4444' :
20
+ task.priority === 'medium' ? '#f59e0b' :
21
+ '#6b7280';
22
+
23
+ const formatDate = (timestamp?: number) => {
24
+ if (!timestamp) return null;
25
+ const date = new Date(timestamp);
26
+ const today = new Date();
27
+ today.setHours(0, 0, 0, 0);
28
+ const tomorrow = new Date(today);
29
+ tomorrow.setDate(tomorrow.getDate() + 1);
30
+
31
+ if (date >= today && date < tomorrow) return 'Today';
32
+ if (date >= tomorrow && date < new Date(tomorrow.getTime() + 86400000)) return 'Tomorrow';
33
+ if (date < today) return '⚠️ Overdue';
34
+
35
+ return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
36
+ };
37
+
38
+ return (
39
+ <ThemedView style={styles.container}>
40
+ <TouchableOpacity
41
+ style={[styles.checkbox, task.completed && styles.checkboxCompleted]}
42
+ onPress={onToggleComplete}
43
+ >
44
+ {task.completed && <ThemedText style={styles.checkmark}>✓</ThemedText>}
45
+ </TouchableOpacity>
46
+
47
+ <TouchableOpacity style={styles.content} onPress={onPress}>
48
+ <View style={styles.header}>
49
+ {task.priority && (
50
+ <View style={[styles.priorityBadge, { backgroundColor: priorityColor }]} />
51
+ )}
52
+ <ThemedText
53
+ style={[
54
+ styles.title,
55
+ task.completed && styles.completedText
56
+ ]}
57
+ numberOfLines={1}
58
+ >
59
+ {task.title}
60
+ </ThemedText>
61
+ </View>
62
+
63
+ {(task.description || task.dueDate) && (
64
+ <View style={styles.meta}>
65
+ {task.description && (
66
+ <ThemedText style={styles.description} numberOfLines={1}>
67
+ {task.description}
68
+ </ThemedText>
69
+ )}
70
+ {task.dueDate && (
71
+ <ThemedText style={[styles.dueDate, task.dueDate < Date.now() && styles.overdue]}>
72
+ {formatDate(task.dueDate)}
73
+ </ThemedText>
74
+ )}
75
+ </View>
76
+ )}
77
+ </TouchableOpacity>
78
+ </ThemedView>
79
+ );
80
+}
81
+
82
+const styles = StyleSheet.create({
83
+ container: {
84
+ flexDirection: 'row',
85
+ alignItems: 'flex-start',
86
+ paddingVertical: 12,
87
+ paddingHorizontal: 16,
88
+ gap: 12,
89
+ },
90
+ checkbox: {
91
+ width: 24,
92
+ height: 24,
93
+ borderRadius: 12,
94
+ borderWidth: 2,
95
+ borderColor: '#94a3b8',
96
+ justifyContent: 'center',
97
+ alignItems: 'center',
98
+ marginTop: 2,
99
+ },
100
+ checkboxCompleted: {
101
+ backgroundColor: '#0a7ea4',
102
+ borderColor: '#0a7ea4',
103
+ },
104
+ checkmark: {
105
+ color: '#fff',
106
+ fontSize: 16,
107
+ fontWeight: 'bold',
108
+ },
109
+ content: {
110
+ flex: 1,
111
+ },
112
+ header: {
113
+ flexDirection: 'row',
114
+ alignItems: 'center',
115
+ gap: 8,
116
+ },
117
+ priorityBadge: {
118
+ width: 8,
119
+ height: 8,
120
+ borderRadius: 4,
121
+ },
122
+ title: {
123
+ fontSize: 16,
124
+ flex: 1,
125
+ },
126
+ completedText: {
127
+ textDecorationLine: 'line-through',
128
+ opacity: 0.5,
129
+ },
130
+ meta: {
131
+ marginTop: 4,
132
+ gap: 4,
133
+ },
134
+ description: {
135
+ fontSize: 14,
136
+ opacity: 0.7,
137
+ },
138
+ dueDate: {
139
+ fontSize: 12,
140
+ opacity: 0.6,
141
+ },
142
+ overdue: {
143
+ color: '#ef4444',
144
+ fontWeight: '600',
145
+ },
146
+});
ADD
components/task-list.tsx
+62 -0
@@ -0,0 +1,62 @@
1
+import React from 'react';
2
+import { FlatList, StyleSheet, View } from 'react-native';
3
+import { Task } from '@/types/gtd';
4
+import { TaskItem } from './task-item';
5
+import { ThemedText } from './themed-text';
6
+import { ThemedView } from './themed-view';
7
+
8
+interface TaskListProps {
9
+ tasks: Task[];
10
+ onTaskPress: (task: Task) => void;
11
+ onToggleComplete: (taskId: string) => void;
12
+ emptyMessage?: string;
13
+}
14
+
15
+export function TaskList({ tasks, onTaskPress, onToggleComplete, emptyMessage }: TaskListProps) {
16
+ if (tasks.length === 0) {
17
+ return (
18
+ <ThemedView style={styles.emptyContainer}>
19
+ <ThemedText style={styles.emptyText}>
20
+ {emptyMessage || 'No tasks'}
21
+ </ThemedText>
22
+ </ThemedView>
23
+ );
24
+ }
25
+
26
+ return (
27
+ <FlatList
28
+ data={tasks}
29
+ keyExtractor={(item) => item.id}
30
+ renderItem={({ item }) => (
31
+ <TaskItem
32
+ task={item}
33
+ onPress={() => onTaskPress(item)}
34
+ onToggleComplete={() => onToggleComplete(item.id)}
35
+ />
36
+ )}
37
+ ItemSeparatorComponent={() => <View style={styles.separator} />}
38
+ contentContainerStyle={styles.list}
39
+ />
40
+ );
41
+}
42
+
43
+const styles = StyleSheet.create({
44
+ list: {
45
+ paddingVertical: 8,
46
+ },
47
+ separator: {
48
+ height: StyleSheet.hairlineWidth,
49
+ backgroundColor: '#e2e8f0',
50
+ marginHorizontal: 16,
51
+ },
52
+ emptyContainer: {
53
+ flex: 1,
54
+ justifyContent: 'center',
55
+ alignItems: 'center',
56
+ paddingVertical: 60,
57
+ },
58
+ emptyText: {
59
+ fontSize: 16,
60
+ opacity: 0.5,
61
+ },
62
+});
MODIFY
package-lock.json
+3 -0
@@ -1467,6 +1467,7 @@
1467
1467
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
1468
1468
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
1469
1469
"license": "MIT",
1470
+ "peer": true,
1470
1471
"engines": {
1471
1472
"node": ">=6.9.0"
1472
1473
}
@@ -10622,6 +10623,7 @@
10622
10623
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.3.tgz",
10623
10624
"integrity": "sha512-GP8wsi1u3nqvC1fMab/m8gfFwFyldawElCcUSBJQgfrXeLmsPPUOpDw44lbLeCpcwUuLa05WTVePdTEwCLTUZg==",
10624
10625
"license": "MIT",
10626
+ "peer": true,
10625
10627
"dependencies": {
10626
10628
"react-native-is-edge-to-edge": "^1.2.1",
10627
10629
"semver": "7.7.2"
@@ -10710,6 +10712,7 @@
10710
10712
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
10711
10713
"integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==",
10712
10714
"license": "MIT",
10715
+ "peer": true,
10713
10716
"dependencies": {
10714
10717
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",
10715
10718
"@babel/plugin-transform-class-properties": "^7.0.0-0",