gitshark

Clone repository

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

← Commits

✨ Add calender

760c24eae92708fc1d30d46a239d60b6873647f2 · vvilip · 2025-11-04T16:20:48Z

Changes

3 files changed, +337 -3

MODIFY app/(tabs)/_layout.tsx +7 -0
diff --git "a/app/\050tabs\051/_layout.tsx" "b/app/\050tabs\051/_layout.tsx"
index 63fe608..a50f036 100644
--- "a/app/\050tabs\051/_layout.tsx"
+++ "b/app/\050tabs\051/_layout.tsx"
@@ -27,6 +27,13 @@
27 27 name="today"
28 28 options={{
29 29 title: 'Today',
30 + tabBarIcon: ({ color }) => <IconSymbol size={28} name="calendar.badge.clock" color={color} />,
31 + }}
32 + />
33 + <Tabs.Screen
34 + name="calendar"
35 + options={{
36 + title: 'Calendar',
30 37 tabBarIcon: ({ color }) => <IconSymbol size={28} name="calendar" color={color} />,
31 38 }}
32 39 />
ADD app/(tabs)/calendar.tsx +323 -0
diff --git "a/app/\050tabs\051/calendar.tsx" "b/app/\050tabs\051/calendar.tsx"
new file mode 100644
index 0000000..d34e0ab
--- /dev/null
+++ "b/app/\050tabs\051/calendar.tsx"
@@ -0,0 +1,323 @@
1 +import React, { useEffect, useState } from 'react';
2 +import { StyleSheet, SafeAreaView, Alert, ScrollView, TouchableOpacity, View } from 'react-native';
3 +import { router, useFocusEffect } 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 +import { Colors } from '@/constants/theme';
11 +import { useColorScheme } from '@/hooks/use-color-scheme';
12 +
13 +export default function CalendarScreen() {
14 + const [selectedDate, setSelectedDate] = useState(new Date());
15 + const [currentMonth, setCurrentMonth] = useState(new Date());
16 + const [tasks, setTasks] = useState<Task[]>([]);
17 + const [tasksByDate, setTasksByDate] = useState<Record<string, number>>({});
18 + const colorScheme = useColorScheme();
19 + const colors = Colors[colorScheme];
20 +
21 + useFocusEffect(
22 + React.useCallback(() => {
23 + loadTasks();
24 + }, [currentMonth])
25 + );
26 +
27 + const loadTasks = async () => {
28 + try {
29 + const allTasks = await taskService.getAllTasks();
30 + const tasksWithDates = allTasks.filter(t => t.dueDate && !t.completed);
31 +
32 + // Count tasks per date
33 + const counts: Record<string, number> = {};
34 + tasksWithDates.forEach(task => {
35 + if (task.dueDate) {
36 + const dateKey = getDateKey(new Date(task.dueDate));
37 + counts[dateKey] = (counts[dateKey] || 0) + 1;
38 + }
39 + });
40 + setTasksByDate(counts);
41 +
42 + // Filter tasks for selected date
43 + filterTasksForDate(selectedDate, allTasks);
44 + } catch (error) {
45 + Alert.alert('Error', 'Failed to load tasks');
46 + }
47 + };
48 +
49 + const filterTasksForDate = (date: Date, allTasks?: Task[]) => {
50 + const dateKey = getDateKey(date);
51 + const startOfDay = new Date(date);
52 + startOfDay.setHours(0, 0, 0, 0);
53 + const endOfDay = new Date(date);
54 + endOfDay.setHours(23, 59, 59, 999);
55 +
56 + taskService.getAllTasks().then(tasks => {
57 + const filtered = tasks.filter(task => {
58 + if (!task.dueDate || task.completed) return false;
59 + const taskDate = new Date(task.dueDate);
60 + return taskDate >= startOfDay && taskDate <= endOfDay;
61 + });
62 + setTasks(filtered);
63 + });
64 + };
65 +
66 + const getDateKey = (date: Date) => {
67 + return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
68 + };
69 +
70 + const getDaysInMonth = (date: Date) => {
71 + const year = date.getFullYear();
72 + const month = date.getMonth();
73 + const firstDay = new Date(year, month, 1);
74 + const lastDay = new Date(year, month + 1, 0);
75 + const daysInMonth = lastDay.getDate();
76 + const startingDayOfWeek = firstDay.getDay();
77 +
78 + return { daysInMonth, startingDayOfWeek };
79 + };
80 +
81 + const handleDateSelect = (day: number) => {
82 + const newDate = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day);
83 + setSelectedDate(newDate);
84 + filterTasksForDate(newDate);
85 + };
86 +
87 + const handlePrevMonth = () => {
88 + const newMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1);
89 + setCurrentMonth(newMonth);
90 + };
91 +
92 + const handleNextMonth = () => {
93 + const newMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1);
94 + setCurrentMonth(newMonth);
95 + };
96 +
97 + const handleToggleComplete = async (taskId: string) => {
98 + try {
99 + const task = tasks.find(t => t.id === taskId);
100 + if (!task) return;
101 +
102 + if (task.completed) {
103 + await taskService.uncompleteTask(taskId);
104 + } else {
105 + await taskService.completeTask(taskId);
106 + }
107 +
108 + await loadTasks();
109 + } catch (error) {
110 + Alert.alert('Error', 'Failed to update task');
111 + }
112 + };
113 +
114 + const handleTaskPress = (task: Task) => {
115 + router.push(`/task/${task.id}`);
116 + };
117 +
118 + const handleAddTask = () => {
119 + router.push({
120 + pathname: '/task/new',
121 + params: { dueDate: selectedDate.getTime() },
122 + });
123 + };
124 +
125 + const renderCalendar = () => {
126 + const { daysInMonth, startingDayOfWeek } = getDaysInMonth(currentMonth);
127 + const days = [];
128 + const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
129 +
130 + // Week day headers
131 + const headers = weekDays.map(day => (
132 + <ThemedView key={day} style={styles.dayHeader}>
133 + <ThemedText style={styles.dayHeaderText}>{day}</ThemedText>
134 + </ThemedView>
135 + ));
136 +
137 + // Empty cells before first day
138 + for (let i = 0; i < startingDayOfWeek; i++) {
139 + days.push(<View key={`empty-${i}`} style={styles.dayCell} />);
140 + }
141 +
142 + // Day cells
143 + for (let day = 1; day <= daysInMonth; day++) {
144 + const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day);
145 + const dateKey = getDateKey(date);
146 + const taskCount = tasksByDate[dateKey] || 0;
147 + const isSelected = selectedDate.getDate() === day &&
148 + selectedDate.getMonth() === currentMonth.getMonth() &&
149 + selectedDate.getFullYear() === currentMonth.getFullYear();
150 + const isToday = new Date().getDate() === day &&
151 + new Date().getMonth() === currentMonth.getMonth() &&
152 + new Date().getFullYear() === currentMonth.getFullYear();
153 +
154 + days.push(
155 + <TouchableOpacity
156 + key={day}
157 + style={[
158 + styles.dayCell,
159 + isToday && [styles.today, { borderColor: colors.tint }],
160 + isSelected && [styles.selectedDay, { backgroundColor: colors.tint }],
161 + ]}
162 + onPress={() => handleDateSelect(day)}
163 + >
164 + <ThemedText style={[
165 + styles.dayText,
166 + isSelected && styles.selectedDayText,
167 + ]}>
168 + {day}
169 + </ThemedText>
170 + {taskCount > 0 && (
171 + <View style={[styles.taskBadge, { backgroundColor: isSelected ? '#fff' : colors.tint }]}>
172 + <ThemedText style={[styles.taskBadgeText, { color: isSelected ? colors.tint : '#fff' }]}>
173 + {taskCount}
174 + </ThemedText>
175 + </View>
176 + )}
177 + </TouchableOpacity>
178 + );
179 + }
180 +
181 + return (
182 + <View style={styles.calendar}>
183 + <View style={styles.weekDays}>{headers}</View>
184 + <View style={styles.daysGrid}>{days}</View>
185 + </View>
186 + );
187 + };
188 +
189 + return (
190 + <SafeAreaView style={styles.container}>
191 + <ThemedView style={styles.header}>
192 + <ThemedText type="title">Calendar</ThemedText>
193 + </ThemedView>
194 +
195 + <ThemedView style={styles.monthSelector}>
196 + <TouchableOpacity onPress={handlePrevMonth}>
197 + <ThemedText style={[styles.monthButton, { color: colors.tint }]}>←</ThemedText>
198 + </TouchableOpacity>
199 + <ThemedText type="subtitle">
200 + {currentMonth.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
201 + </ThemedText>
202 + <TouchableOpacity onPress={handleNextMonth}>
203 + <ThemedText style={[styles.monthButton, { color: colors.tint }]}>→</ThemedText>
204 + </TouchableOpacity>
205 + </ThemedView>
206 +
207 + <ScrollView style={styles.scrollView}>
208 + {renderCalendar()}
209 +
210 + <ThemedView style={styles.tasksSection}>
211 + <ThemedText type="subtitle" style={styles.tasksSectionTitle}>
212 + {selectedDate.toLocaleDateString('en-US', {
213 + weekday: 'long',
214 + month: 'long',
215 + day: 'numeric'
216 + })}
217 + </ThemedText>
218 + <TaskList
219 + tasks={tasks}
220 + onTaskPress={handleTaskPress}
221 + onToggleComplete={handleToggleComplete}
222 + emptyMessage="No tasks for this day"
223 + scrollable={false}
224 + />
225 + </ThemedView>
226 + </ScrollView>
227 +
228 + <FabButton onPress={handleAddTask} />
229 + </SafeAreaView>
230 + );
231 +}
232 +
233 +const styles = StyleSheet.create({
234 + container: {
235 + flex: 1,
236 + },
237 + header: {
238 + paddingHorizontal: 16,
239 + paddingTop: 16,
240 + paddingBottom: 12,
241 + },
242 + monthSelector: {
243 + flexDirection: 'row',
244 + justifyContent: 'space-between',
245 + alignItems: 'center',
246 + paddingHorizontal: 16,
247 + paddingVertical: 16,
248 + },
249 + monthButton: {
250 + fontSize: 32,
251 + fontWeight: 'bold',
252 + paddingHorizontal: 16,
253 + },
254 + scrollView: {
255 + flex: 1,
256 + },
257 + calendar: {
258 + paddingHorizontal: 8,
259 + paddingBottom: 16,
260 + },
261 + weekDays: {
262 + flexDirection: 'row',
263 + marginBottom: 8,
264 + },
265 + dayHeader: {
266 + flex: 1,
267 + alignItems: 'center',
268 + paddingVertical: 8,
269 + },
270 + dayHeaderText: {
271 + fontSize: 12,
272 + fontWeight: '600',
273 + opacity: 0.6,
274 + },
275 + daysGrid: {
276 + flexDirection: 'row',
277 + flexWrap: 'wrap',
278 + },
279 + dayCell: {
280 + width: `${100 / 7}%`,
281 + aspectRatio: 1,
282 + justifyContent: 'center',
283 + alignItems: 'center',
284 + padding: 4,
285 + position: 'relative',
286 + },
287 + today: {
288 + borderWidth: 2,
289 + borderRadius: 8,
290 + },
291 + selectedDay: {
292 + borderRadius: 8,
293 + },
294 + dayText: {
295 + fontSize: 16,
296 + },
297 + selectedDayText: {
298 + color: '#fff',
299 + fontWeight: 'bold',
300 + },
301 + taskBadge: {
302 + position: 'absolute',
303 + top: 4,
304 + right: 4,
305 + minWidth: 18,
306 + height: 18,
307 + borderRadius: 9,
308 + justifyContent: 'center',
309 + alignItems: 'center',
310 + paddingHorizontal: 4,
311 + },
312 + taskBadgeText: {
313 + fontSize: 10,
314 + fontWeight: 'bold',
315 + },
316 + tasksSection: {
317 + marginTop: 24,
318 + paddingHorizontal: 16,
319 + },
320 + tasksSectionTitle: {
321 + marginBottom: 12,
322 + },
323 +});
MODIFY app/task/[id].tsx +7 -3
diff --git "a/app/task/\133id\135.tsx" "b/app/task/\133id\135.tsx"
index 9f3d7f4..56c6425 100644
--- "a/app/task/\133id\135.tsx"
+++ "b/app/task/\133id\135.tsx"
@@ -9,7 +9,11 @@
9 9 import { useColorScheme } from '@/hooks/use-color-scheme';
10 10
11 11 export default function TaskDetailScreen() {
12 - const { id } = useLocalSearchParams<{ id: string }>();
12 + const { id, dueDate: dueDateParam, projectId: projectIdParam } = useLocalSearchParams<{
13 + id: string;
14 + dueDate?: string;
15 + projectId?: string;
16 + }>();
13 17 const isNew = id === 'new';
14 18 const colorScheme = useColorScheme() ?? 'light';
15 19 const colors = Colors[colorScheme];
@@ -19,8 +23,8 @@
19 23 description: '',
20 24 status: 'inbox',
21 25 priority: undefined,
22 - dueDate: undefined,
23 - projectId: undefined,
26 + dueDate: dueDateParam ? parseInt(dueDateParam) : undefined,
27 + projectId: projectIdParam,
24 28 tagIds: [],
25 29 completed: false,
26 30 });

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog