✨ Add calender
Changes
3 files changed, +337 -3
MODIFY
app/(tabs)/_layout.tsx
+7 -0
@@ -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
@@ -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
@@ -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
});