gitshark

Clone repository

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

← Commits

✨ Add base features for prototype

755f7c4216901b85c79576df638da36ce911facf · vvilip · 2025-11-04T11:55:18Z

Changes

12 files changed, +1772 -203

MODIFY app/(tabs)/_layout.tsx +19 -5
diff --git "a/app/\050tabs\051/_layout.tsx" "b/app/\050tabs\051/_layout.tsx"
index 54e11d0..63fe608 100644
--- "a/app/\050tabs\051/_layout.tsx"
+++ "b/app/\050tabs\051/_layout.tsx"
@@ -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
diff --git "a/app/\050tabs\051/explore.tsx" "b/app/\050tabs\051/explore.tsx"
deleted file mode 100644
index 71518f9..0000000
--- "a/app/\050tabs\051/explore.tsx"
+++ /dev/null
@@ -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&apos;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
diff --git "a/app/\050tabs\051/index.tsx" "b/app/\050tabs\051/index.tsx"
index 786b736..29f0f1c 100644
--- "a/app/\050tabs\051/index.tsx"
+++ "b/app/\050tabs\051/index.tsx"
@@ -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
diff --git "a/app/\050tabs\051/projects.tsx" "b/app/\050tabs\051/projects.tsx"
new file mode 100644
index 0000000..14496ab
--- /dev/null
+++ "b/app/\050tabs\051/projects.tsx"
@@ -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
diff --git "a/app/\050tabs\051/settings.tsx" "b/app/\050tabs\051/settings.tsx"
new file mode 100644
index 0000000..e96e549
--- /dev/null
+++ "b/app/\050tabs\051/settings.tsx"
@@ -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
diff --git "a/app/\050tabs\051/today.tsx" "b/app/\050tabs\051/today.tsx"
new file mode 100644
index 0000000..fb49fa0
--- /dev/null
+++ "b/app/\050tabs\051/today.tsx"
@@ -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
diff --git "a/app/project/\133id\135.tsx" "b/app/project/\133id\135.tsx"
new file mode 100644
index 0000000..d1be58f
--- /dev/null
+++ "b/app/project/\133id\135.tsx"
@@ -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
diff --git "a/app/task/\133id\135.tsx" "b/app/task/\133id\135.tsx"
new file mode 100644
index 0000000..718f9fb
--- /dev/null
+++ "b/app/task/\133id\135.tsx"
@@ -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
diff --git a/components/fab-button.tsx b/components/fab-button.tsx
new file mode 100644
index 0000000..07ea9ed
--- /dev/null
+++ b/components/fab-button.tsx
@@ -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
diff --git a/components/task-item.tsx b/components/task-item.tsx
new file mode 100644
index 0000000..0a8a175
--- /dev/null
+++ b/components/task-item.tsx
@@ -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
diff --git a/components/task-list.tsx b/components/task-list.tsx
new file mode 100644
index 0000000..3c3f499
--- /dev/null
+++ b/components/task-list.tsx
@@ -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
diff --git a/package-lock.json b/package-lock.json
index 34a9d51..15ed89c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog