gitshark

Clone repository

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

← Commits

๐Ÿ› Fix calendar skipping Saturday column

032a208e6ab7ed64a66216bc309fa10f93ed91d7 ยท Phillip Souza Furtner ยท 2026-06-18T19:28:10Z

Changes

3 files changed, +135 -86

MODIFY app/(tabs)/calendar.tsx +71 -86
diff --git "a/app/\050tabs\051/calendar.tsx" "b/app/\050tabs\051/calendar.tsx"
index e6664ce..c7cbb8d 100644
--- "a/app/\050tabs\051/calendar.tsx"
+++ "b/app/\050tabs\051/calendar.tsx"
@@ -10,13 +10,14 @@
10 10 import { taskService } from '@/services';
11 11 import { Colors } from '@/constants/theme';
12 12 import { useColorScheme } from '@/hooks/use-color-scheme';
13 +import { buildCalendarWeeks } from '@/utils/calendar';
13 14
14 15 export default function CalendarScreen() {
15 16 const [selectedDate, setSelectedDate] = useState(new Date());
16 17 const [currentMonth, setCurrentMonth] = useState(new Date());
17 18 const [tasks, setTasks] = useState<Task[]>([]);
18 19 const [tasksByDate, setTasksByDate] = useState<Record<string, number>>({});
19 - const colorScheme = useColorScheme();
20 + const colorScheme = useColorScheme() ?? 'light';
20 21 const colors = Colors[colorScheme];
21 22
22 23 useFocusEffect(
@@ -39,45 +40,33 @@
39 40 }
40 41 });
41 42 setTasksByDate(counts);
42 -
43 - // Filter tasks for selected date
43 +
44 + // Filter tasks for selected date (reuse the tasks we already loaded)
44 45 filterTasksForDate(selectedDate, allTasks);
45 46 } catch {
46 47 Alert.alert('Error', 'Failed to load tasks');
47 48 }
48 49 };
49 50
50 - const filterTasksForDate = (date: Date, allTasks?: Task[]) => {
51 + const filterTasksForDate = async (date: Date, allTasks?: Task[]) => {
51 52 const startOfDay = new Date(date);
52 53 startOfDay.setHours(0, 0, 0, 0);
53 54 const endOfDay = new Date(date);
54 55 endOfDay.setHours(23, 59, 59, 999);
55 56
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);
57 + const source = allTasks ?? (await taskService.getAllTasks());
58 + const filtered = source.filter(task => {
59 + if (!task.dueDate || task.completed) return false;
60 + const taskDate = new Date(task.dueDate);
61 + return taskDate >= startOfDay && taskDate <= endOfDay;
63 62 });
63 + setTasks(filtered);
64 64 };
65 65
66 66 const getDateKey = (date: Date) => {
67 67 return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
68 68 };
69 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 70 const handleDateSelect = (day: number) => {
82 71 const newDate = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day);
83 72 setSelectedDate(newDate);
@@ -128,67 +117,68 @@
128 117 };
129 118
130 119 const renderCalendar = () => {
131 - const { daysInMonth, startingDayOfWeek } = getDaysInMonth(currentMonth);
132 - const days = [];
133 120 const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
134 -
135 - // Week day headers
136 - const headers = weekDays.map(day => (
137 - <ThemedView key={day} style={styles.dayHeader}>
138 - <ThemedText style={styles.dayHeaderText}>{day}</ThemedText>
139 - </ThemedView>
140 - ));
141 -
142 - // Empty cells before first day
143 - for (let i = 0; i < startingDayOfWeek; i++) {
144 - days.push(<View key={`empty-${i}`} style={styles.dayCell} />);
145 - }
146 -
147 - // Day cells
148 - for (let day = 1; day <= daysInMonth; day++) {
149 - const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day);
150 - const dateKey = getDateKey(date);
151 - const taskCount = tasksByDate[dateKey] || 0;
152 - const isSelected = selectedDate.getDate() === day &&
153 - selectedDate.getMonth() === currentMonth.getMonth() &&
154 - selectedDate.getFullYear() === currentMonth.getFullYear();
155 - const isToday = new Date().getDate() === day &&
156 - new Date().getMonth() === currentMonth.getMonth() &&
157 - new Date().getFullYear() === currentMonth.getFullYear();
158 -
159 - days.push(
160 - <TouchableOpacity
161 - key={day}
162 - style={[
163 - styles.dayCell,
164 - isToday && [styles.today, { borderColor: colors.tint }],
165 - isSelected && [styles.selectedDay, { backgroundColor: colors.tint }],
166 - ]}
167 - onPress={() => handleDateSelect(day)}
168 - >
169 - <View style={styles.dayCellContent}>
170 - <ThemedText style={[
171 - styles.dayText,
172 - isSelected && styles.selectedDayText,
173 - ]}>
174 - {day}
175 - </ThemedText>
176 - {taskCount > 0 && (
177 - <View style={[styles.taskBadge, { backgroundColor: isSelected ? '#fff' : colors.tint }]}>
178 - <ThemedText style={[styles.taskBadgeText, { color: isSelected ? colors.tint : '#fff' }]}>
179 - {taskCount}
180 - </ThemedText>
181 - </View>
182 - )}
183 - </View>
184 - </TouchableOpacity>
185 - );
186 - }
121 + const weeks = buildCalendarWeeks(currentMonth.getFullYear(), currentMonth.getMonth());
122 + const now = new Date();
187 123
188 124 return (
189 125 <View style={styles.calendar}>
190 - <View style={styles.weekDays}>{headers}</View>
191 - <View style={styles.daysGrid}>{days}</View>
126 + <View style={styles.week}>
127 + {weekDays.map(day => (
128 + <ThemedView key={day} style={styles.dayHeader}>
129 + <ThemedText style={styles.dayHeaderText}>{day}</ThemedText>
130 + </ThemedView>
131 + ))}
132 + </View>
133 +
134 + {weeks.map((week, weekIndex) => (
135 + <View key={`week-${weekIndex}`} style={styles.week}>
136 + {week.map((day, dayIndex) => {
137 + if (day === null) {
138 + return <View key={`empty-${weekIndex}-${dayIndex}`} style={styles.dayCell} />;
139 + }
140 +
141 + const dateKey = getDateKey(
142 + new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day)
143 + );
144 + const taskCount = tasksByDate[dateKey] || 0;
145 + const isSelected = selectedDate.getDate() === day &&
146 + selectedDate.getMonth() === currentMonth.getMonth() &&
147 + selectedDate.getFullYear() === currentMonth.getFullYear();
148 + const isToday = now.getDate() === day &&
149 + now.getMonth() === currentMonth.getMonth() &&
150 + now.getFullYear() === currentMonth.getFullYear();
151 +
152 + return (
153 + <TouchableOpacity
154 + key={day}
155 + style={[
156 + styles.dayCell,
157 + isToday && [styles.today, { borderColor: colors.tint }],
158 + isSelected && [styles.selectedDay, { backgroundColor: colors.tint }],
159 + ]}
160 + onPress={() => handleDateSelect(day)}
161 + >
162 + <View style={styles.dayCellContent}>
163 + <ThemedText style={[
164 + styles.dayText,
165 + isSelected && styles.selectedDayText,
166 + ]}>
167 + {day}
168 + </ThemedText>
169 + {taskCount > 0 && (
170 + <View style={[styles.taskBadge, { backgroundColor: isSelected ? '#fff' : colors.tint }]}>
171 + <ThemedText style={[styles.taskBadgeText, { color: isSelected ? colors.tint : '#fff' }]}>
172 + {taskCount}
173 + </ThemedText>
174 + </View>
175 + )}
176 + </View>
177 + </TouchableOpacity>
178 + );
179 + })}
180 + </View>
181 + ))}
192 182 </View>
193 183 );
194 184 };
@@ -270,9 +260,8 @@
270 260 paddingHorizontal: 8,
271 261 paddingBottom: 16,
272 262 },
273 - weekDays: {
263 + week: {
274 264 flexDirection: 'row',
275 - marginBottom: 8,
276 265 },
277 266 dayHeader: {
278 267 flex: 1,
@@ -284,12 +273,8 @@
284 273 fontWeight: '600',
285 274 opacity: 0.6,
286 275 },
287 - daysGrid: {
288 - flexDirection: 'row',
289 - flexWrap: 'wrap',
290 - },
291 276 dayCell: {
292 - width: `${100 / 7}%`,
277 + flex: 1,
293 278 aspectRatio: 1,
294 279 padding: 4,
295 280 },
ADD utils/__tests__/calendar.test.ts +27 -0
diff --git a/utils/__tests__/calendar.test.ts b/utils/__tests__/calendar.test.ts
new file mode 100644
index 0000000..d4f4991
--- /dev/null
+++ b/utils/__tests__/calendar.test.ts
@@ -0,0 +1,27 @@
1 +import { buildCalendarWeeks } from '../calendar';
2 +
3 +describe('buildCalendarWeeks', () => {
4 + it('places each weekday in the correct column (June 2026 starts on Monday)', () => {
5 + // Month is 0-indexed: 5 = June. June 1 2026 is a Monday, June 6 is a Saturday.
6 + const weeks = buildCalendarWeeks(2026, 5);
7 + // Sunday column is empty, Saturday column holds the 6th โ€” not skipped.
8 + expect(weeks[0]).toEqual([null, 1, 2, 3, 4, 5, 6]);
9 + });
10 +
11 + it('always returns full 7-column weeks', () => {
12 + const weeks = buildCalendarWeeks(2026, 5);
13 + weeks.forEach((week) => expect(week).toHaveLength(7));
14 + });
15 +
16 + it('starts in the first column when the month begins on a Sunday (Feb 2026)', () => {
17 + // Feb 1 2026 is a Sunday.
18 + const weeks = buildCalendarWeeks(2026, 1);
19 + expect(weeks[0][0]).toBe(1);
20 + });
21 +
22 + it('includes every day of the month exactly once and in order', () => {
23 + const weeks = buildCalendarWeeks(2026, 5);
24 + const days = weeks.flat().filter((d): d is number => d !== null);
25 + expect(days).toEqual(Array.from({ length: 30 }, (_, i) => i + 1));
26 + });
27 +});
ADD utils/calendar.ts +37 -0
diff --git a/utils/calendar.ts b/utils/calendar.ts
new file mode 100644
index 0000000..59bc97b
--- /dev/null
+++ b/utils/calendar.ts
@@ -0,0 +1,37 @@
1 +/**
2 + * Calendar grid helpers
3 + *
4 + * Pure logic for laying out a month as weeks, so the calendar grid can be
5 + * rendered as fixed 7-column rows (avoiding sub-pixel width rounding that
6 + * would otherwise wrap the last column).
7 + */
8 +
9 +/**
10 + * Build the weeks of a month as rows of 7 cells. Each cell is the day number
11 + * (1-based) or `null` for padding before the first / after the last day.
12 + * Week starts on Sunday (index 0), matching the weekday headers.
13 + *
14 + * @param year Full year, e.g. 2026
15 + * @param month Month index, 0 = January โ€ฆ 11 = December
16 + */
17 +export function buildCalendarWeeks(year: number, month: number): (number | null)[][] {
18 + const startingDayOfWeek = new Date(year, month, 1).getDay(); // 0 = Sunday
19 + const daysInMonth = new Date(year, month + 1, 0).getDate();
20 +
21 + const cells: (number | null)[] = [];
22 + for (let i = 0; i < startingDayOfWeek; i++) {
23 + cells.push(null);
24 + }
25 + for (let day = 1; day <= daysInMonth; day++) {
26 + cells.push(day);
27 + }
28 + while (cells.length % 7 !== 0) {
29 + cells.push(null);
30 + }
31 +
32 + const weeks: (number | null)[][] = [];
33 + for (let i = 0; i < cells.length; i += 7) {
34 + weeks.push(cells.slice(i, i + 7));
35 + }
36 + return weeks;
37 +}

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog