✨ Make the reminder time configurable per weekday
Changes
5 files changed, +290 -57
MODIFY
README.md
+1 -1
@@ -13,7 +13,7 @@
13
13
- **Local persistence** via AsyncStorage (JSON)
14
14
- **Export / import** as a JSON file using the native share sheet
15
15
- **WebDAV / Nextcloud sync** with last-write-wins conflict resolution; supports force-push and force-pull
16
-- **Daily task reminder** notification at a configurable time (native platforms only); the reminder shows how many tasks are scheduled for today, counted per day and re-counted shortly before delivery via a background task
16
+- **Daily task reminder** notification at a time configurable per weekday (native platforms only); the reminder shows how many tasks are scheduled for that day, counted per day and re-counted shortly before delivery via a background task
17
17
- **Light / dark / system** theme, persisted across sessions
18
18
19
19
## Tech Stack
MODIFY
app/(tabs)/settings.tsx
+48 -30
@@ -10,14 +10,22 @@
10
10
import { useTheme } from '@/contexts/theme-context';
11
11
import { Colors } from '@/constants/theme';
12
12
import { useColorScheme } from '@/hooks/use-color-scheme';
13
-import { DEFAULT_REMINDER_TIME, ReminderTime, formatReminderTime } from '@/utils/reminder';
13
+import {
14
+ DEFAULT_REMINDER_SCHEDULE,
15
+ ReminderSchedule,
16
+ ReminderTime,
17
+ WEEKDAY_DISPLAY_ORDER,
18
+ WEEKDAY_LABELS,
19
+ describeReminderSchedule,
20
+ formatReminderTime,
21
+} from '@/utils/reminder';
14
22
15
23
export default function SettingsScreen() {
16
24
const [syncing, setSyncing] = useState(false);
17
25
const [autoArchive, setAutoArchive] = useState(false);
18
26
const [dailyNotifications, setDailyNotifications] = useState(false);
19
- const [reminderTime, setReminderTime] = useState<ReminderTime>(DEFAULT_REMINDER_TIME);
20
- const [showTimePicker, setShowTimePicker] = useState(false);
27
+ const [reminderSchedule, setReminderSchedule] = useState<ReminderSchedule>(DEFAULT_REMINDER_SCHEDULE);
28
+ const [pickerWeekday, setPickerWeekday] = useState<number | null>(null);
21
29
const [webdavConfigured, setWebdavConfigured] = useState(false);
22
30
const [webdavInfo, setWebdavInfo] = useState<{ url: string; username: string } | null>(null);
23
31
const { themeMode, setThemeMode } = useTheme();
@@ -42,7 +50,7 @@
42
50
43
51
const checkNotificationStatus = async () => {
44
52
setDailyNotifications(await notificationService.isDailyReminderEnabled());
45
- setReminderTime(await notificationService.getReminderTime());
53
+ setReminderSchedule(await notificationService.getReminderSchedule());
46
54
};
47
55
48
56
const handleExport = async () => {
@@ -215,7 +223,7 @@
215
223
setDailyNotifications(true);
216
224
Alert.alert(
217
225
'Notifications Enabled',
218
- `You will receive a daily reminder at ${formatReminderTime(reminderTime)}`
226
+ `You will receive a daily reminder ${describeReminderSchedule(reminderSchedule)}`
219
227
);
220
228
}
221
229
} else {
@@ -226,10 +234,11 @@
226
234
};
227
235
228
236
const handleReminderTimeChange = async (
237
+ weekday: number,
229
238
event: { type: string },
230
239
selectedDate?: Date
231
240
) => {
232
- setShowTimePicker(Platform.OS === 'ios');
241
+ setPickerWeekday(Platform.OS === 'ios' ? weekday : null);
233
242
if (event.type === 'dismissed' || !selectedDate) {
234
243
return;
235
244
}
@@ -237,17 +246,17 @@
237
246
hour: selectedDate.getHours(),
238
247
minute: selectedDate.getMinutes(),
239
248
};
240
- setReminderTime(newTime);
241
249
try {
242
- await notificationService.setReminderTime(newTime);
250
+ setReminderSchedule(await notificationService.setReminderTimeForWeekday(weekday, newTime));
243
251
} catch {
244
252
Alert.alert('Error', 'Failed to update reminder time');
245
253
}
246
254
};
247
255
248
- const reminderDate = (() => {
256
+ const pickerDate = (() => {
257
+ const { hour, minute } = reminderSchedule[pickerWeekday ?? 0];
249
258
const date = new Date();
250
- date.setHours(reminderTime.hour, reminderTime.minute, 0, 0);
259
+ date.setHours(hour, minute, 0, 0);
251
260
return date;
252
261
})();
253
262
@@ -330,7 +339,7 @@
330
339
<ThemedView style={styles.optionTextContainer}>
331
340
<ThemedText style={styles.optionText}>Daily Reminder</ThemedText>
332
341
<ThemedText style={styles.optionDescription}>
333
- Get notified daily at {formatReminderTime(reminderTime)} with your task count
342
+ Get notified with your task count, {describeReminderSchedule(reminderSchedule)}
334
343
</ThemedText>
335
344
</ThemedView>
336
345
<Switch
@@ -343,31 +352,40 @@
343
352
{dailyNotifications && (
344
353
<>
345
354
<ThemedView style={[styles.option, { borderBottomColor: colors.border }]}>
346
- <ThemedView style={styles.optionWithSwitch}>
347
- <ThemedView style={styles.optionTextContainer}>
348
- <ThemedText style={styles.optionText}>Reminder Time</ThemedText>
349
- <ThemedText style={styles.optionDescription}>
350
- When the daily reminder is sent
351
- </ThemedText>
352
- </ThemedView>
353
- <TouchableOpacity
354
- style={[styles.timeButton, { borderColor: colors.border }]}
355
- onPress={() => setShowTimePicker(true)}
356
- >
357
- <ThemedText style={[styles.timeButtonText, { color: colors.tint }]}>
358
- {formatReminderTime(reminderTime)}
359
- </ThemedText>
360
- </TouchableOpacity>
361
- </ThemedView>
355
+ <ThemedText style={styles.optionText}>Reminder Time</ThemedText>
356
+ <ThemedText style={styles.optionDescription}>
357
+ When the reminder is sent on each weekday
358
+ </ThemedText>
362
359
</ThemedView>
363
360
364
- {showTimePicker && (
361
+ {WEEKDAY_DISPLAY_ORDER.map(weekday => (
362
+ <ThemedView
363
+ key={weekday}
364
+ style={[styles.option, { borderBottomColor: colors.border }]}
365
+ >
366
+ <ThemedView style={styles.optionWithSwitch}>
367
+ <ThemedView style={styles.optionTextContainer}>
368
+ <ThemedText style={styles.optionText}>{WEEKDAY_LABELS[weekday]}</ThemedText>
369
+ </ThemedView>
370
+ <TouchableOpacity
371
+ style={[styles.timeButton, { borderColor: colors.border }]}
372
+ onPress={() => setPickerWeekday(weekday)}
373
+ >
374
+ <ThemedText style={[styles.timeButtonText, { color: colors.tint }]}>
375
+ {formatReminderTime(reminderSchedule[weekday])}
376
+ </ThemedText>
377
+ </TouchableOpacity>
378
+ </ThemedView>
379
+ </ThemedView>
380
+ ))}
381
+
382
+ {pickerWeekday !== null && (
365
383
<DateTimePicker
366
- value={reminderDate}
384
+ value={pickerDate}
367
385
mode="time"
368
386
is24Hour
369
387
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
370
- onChange={handleReminderTimeChange}
388
+ onChange={(event, date) => handleReminderTimeChange(pickerWeekday, event, date)}
371
389
/>
372
390
)}
373
391
MODIFY
services/notification.service.ts
+43 -17
@@ -8,15 +8,22 @@
8
8
import type { NotificationRequest } from 'expo-notifications';
9
9
import taskService from './task.service';
10
10
import {
11
- DEFAULT_REMINDER_TIME,
11
+ DEFAULT_REMINDER_SCHEDULE,
12
12
REMINDER_WINDOW_DAYS,
13
+ ReminderSchedule,
13
14
ReminderTime,
14
15
buildReminderBody,
15
16
buildReminderOccurrences,
17
+ createUniformSchedule,
18
+ isValidReminderSchedule,
16
19
isValidReminderTime,
20
+ withReminderTimeForWeekday,
17
21
} from '../utils/reminder';
18
22
19
-const REMINDER_TIME_KEY = '@taskflow_reminder_time';
23
+const REMINDER_SCHEDULE_KEY = '@taskflow_reminder_schedule';
24
+
25
+/** Pre-per-weekday key, holding a single time for all days. */
26
+const LEGACY_REMINDER_TIME_KEY = '@taskflow_reminder_time';
20
27
const REMINDER_ENABLED_KEY = '@taskflow_reminder_enabled';
21
28
22
29
/** Background task that recounts today's tasks and rewrites pending reminders. */
@@ -86,36 +93,55 @@
86
93
}
87
94
88
95
/**
89
- * Get the configured daily reminder time (falls back to the default).
96
+ * Get the reminder time of every weekday. A schedule stored before reminder
97
+ * times were per-weekday is carried over to all seven days.
90
98
*/
91
- async getReminderTime(): Promise<ReminderTime> {
99
+ async getReminderSchedule(): Promise<ReminderSchedule> {
92
100
try {
93
- const json = await AsyncStorage.getItem(REMINDER_TIME_KEY);
101
+ const json = await AsyncStorage.getItem(REMINDER_SCHEDULE_KEY);
94
102
if (json) {
95
- const parsed = JSON.parse(json) as ReminderTime;
96
- if (isValidReminderTime(parsed)) {
103
+ const parsed = JSON.parse(json) as ReminderSchedule;
104
+ if (isValidReminderSchedule(parsed)) {
97
105
return parsed;
98
106
}
99
107
}
108
+
109
+ const legacyJson = await AsyncStorage.getItem(LEGACY_REMINDER_TIME_KEY);
110
+ if (legacyJson) {
111
+ const legacy = JSON.parse(legacyJson) as ReminderTime;
112
+ if (isValidReminderTime(legacy)) {
113
+ return createUniformSchedule(legacy);
114
+ }
115
+ }
100
116
} catch (error) {
101
- console.error('Failed to load reminder time:', error);
117
+ console.error('Failed to load reminder schedule:', error);
102
118
}
103
- return DEFAULT_REMINDER_TIME;
119
+ return DEFAULT_REMINDER_SCHEDULE;
104
120
}
105
121
106
122
/**
107
- * Persist a new reminder time. If the daily reminder is enabled, the pending
108
- * reminders are rewritten to the new time.
123
+ * Persist a full reminder schedule. If the daily reminder is enabled, the
124
+ * pending reminders are rewritten to the new times.
109
125
*/
110
- async setReminderTime(time: ReminderTime): Promise<void> {
111
- if (!isValidReminderTime(time)) {
112
- throw new Error('Invalid reminder time');
126
+ async setReminderSchedule(schedule: ReminderSchedule): Promise<void> {
127
+ if (!isValidReminderSchedule(schedule)) {
128
+ throw new Error('Invalid reminder schedule');
113
129
}
114
- await AsyncStorage.setItem(REMINDER_TIME_KEY, JSON.stringify(time));
130
+ await AsyncStorage.setItem(REMINDER_SCHEDULE_KEY, JSON.stringify(schedule));
115
131
await this.refreshDailyNotificationIfEnabled();
116
132
}
117
133
118
134
/**
135
+ * Persist the reminder time of a single weekday (0 = Sunday), leaving the
136
+ * other days as they are.
137
+ */
138
+ async setReminderTimeForWeekday(weekday: number, time: ReminderTime): Promise<ReminderSchedule> {
139
+ const schedule = withReminderTimeForWeekday(await this.getReminderSchedule(), weekday, time);
140
+ await this.setReminderSchedule(schedule);
141
+ return schedule;
142
+ }
143
+
144
+ /**
119
145
* Whether the daily reminder is switched on. Installs from before the flag
120
146
* existed are recognised by their already-scheduled notifications.
121
147
*/
@@ -215,8 +241,8 @@
215
241
await Notifications.cancelAllScheduledNotificationsAsync();
216
242
217
243
const tasks = await taskService.getAllTasks();
218
- const time = await this.getReminderTime();
219
- const occurrences = buildReminderOccurrences(tasks, time, {
244
+ const schedule = await this.getReminderSchedule();
245
+ const occurrences = buildReminderOccurrences(tasks, schedule, {
220
246
now: new Date(),
221
247
days: REMINDER_WINDOW_DAYS,
222
248
});
MODIFY
utils/__tests__/reminder.test.ts
+126 -4
@@ -1,10 +1,17 @@
1
1
import {
2
2
DEFAULT_REMINDER_TIME,
3
+ WEEKDAY_LABELS,
4
+ WEEKDAY_DISPLAY_ORDER,
3
5
buildReminderBody,
4
6
buildReminderOccurrences,
5
7
countTasksDueOn,
8
+ createUniformSchedule,
9
+ describeReminderSchedule,
6
10
formatReminderTime,
11
+ getReminderTimeForDate,
12
+ isValidReminderSchedule,
7
13
isValidReminderTime,
14
+ withReminderTimeForWeekday,
8
15
} from '../reminder';
9
16
10
17
describe('buildReminderBody', () => {
@@ -62,12 +69,100 @@
62
69
});
63
70
});
64
71
72
+describe('createUniformSchedule', () => {
73
+ it('gives every weekday the same time', () => {
74
+ const schedule = createUniformSchedule({ hour: 7, minute: 30 });
75
+
76
+ expect(schedule).toHaveLength(7);
77
+ expect(schedule.every(time => formatReminderTime(time) === '07:30')).toBe(true);
78
+ });
79
+
80
+ it('does not share one time object between weekdays', () => {
81
+ const schedule = createUniformSchedule({ hour: 7, minute: 30 });
82
+ expect(schedule[0]).not.toBe(schedule[1]);
83
+ });
84
+});
85
+
86
+describe('withReminderTimeForWeekday', () => {
87
+ it('changes only the given weekday', () => {
88
+ const schedule = createUniformSchedule({ hour: 12, minute: 0 });
89
+ const updated = withReminderTimeForWeekday(schedule, 1, { hour: 6, minute: 45 });
90
+
91
+ expect(formatReminderTime(updated[1])).toBe('06:45');
92
+ expect(updated.filter(time => formatReminderTime(time) === '12:00')).toHaveLength(6);
93
+ });
94
+
95
+ it('leaves the original schedule untouched', () => {
96
+ const schedule = createUniformSchedule({ hour: 12, minute: 0 });
97
+ withReminderTimeForWeekday(schedule, 1, { hour: 6, minute: 45 });
98
+
99
+ expect(formatReminderTime(schedule[1])).toBe('12:00');
100
+ });
101
+});
102
+
103
+describe('getReminderTimeForDate', () => {
104
+ it('picks the time configured for that weekday', () => {
105
+ const schedule = createUniformSchedule({ hour: 12, minute: 0 });
106
+ schedule[1] = { hour: 6, minute: 45 };
107
+
108
+ // 2026-08-17 is a Monday, 2026-08-18 a Tuesday.
109
+ expect(formatReminderTime(getReminderTimeForDate(schedule, new Date(2026, 7, 17)))).toBe('06:45');
110
+ expect(formatReminderTime(getReminderTimeForDate(schedule, new Date(2026, 7, 18)))).toBe('12:00');
111
+ });
112
+});
113
+
114
+describe('isValidReminderSchedule', () => {
115
+ it('accepts a schedule of seven valid times', () => {
116
+ expect(isValidReminderSchedule(createUniformSchedule(DEFAULT_REMINDER_TIME))).toBe(true);
117
+ });
118
+
119
+ it('rejects a schedule that does not cover every weekday', () => {
120
+ expect(isValidReminderSchedule([{ hour: 8, minute: 0 }])).toBe(false);
121
+ });
122
+
123
+ it('rejects a schedule holding an invalid time', () => {
124
+ const schedule = createUniformSchedule(DEFAULT_REMINDER_TIME);
125
+ schedule[3] = { hour: 24, minute: 0 };
126
+ expect(isValidReminderSchedule(schedule)).toBe(false);
127
+ });
128
+
129
+ it('rejects non-arrays', () => {
130
+ expect(isValidReminderSchedule({ hour: 8, minute: 0 } as never)).toBe(false);
131
+ });
132
+});
133
+
134
+describe('describeReminderSchedule', () => {
135
+ it('names the time when every day shares it', () => {
136
+ expect(describeReminderSchedule(createUniformSchedule({ hour: 9, minute: 0 }))).toBe('09:00 every day');
137
+ });
138
+
139
+ it('says so when the days differ', () => {
140
+ const schedule = createUniformSchedule({ hour: 9, minute: 0 });
141
+ schedule[6] = { hour: 11, minute: 0 };
142
+ expect(describeReminderSchedule(schedule)).toBe('at a time set per weekday');
143
+ });
144
+});
145
+
146
+describe('weekday display order', () => {
147
+ it('starts the week on Monday and ends on Sunday', () => {
148
+ expect(WEEKDAY_DISPLAY_ORDER.map(day => WEEKDAY_LABELS[day])).toEqual([
149
+ 'Monday',
150
+ 'Tuesday',
151
+ 'Wednesday',
152
+ 'Thursday',
153
+ 'Friday',
154
+ 'Saturday',
155
+ 'Sunday',
156
+ ]);
157
+ });
158
+});
159
+
65
160
describe('buildReminderOccurrences', () => {
66
- const time = { hour: 12, minute: 0 };
161
+ const schedule = createUniformSchedule({ hour: 12, minute: 0 });
67
162
68
163
it('starts today when the reminder time is still ahead', () => {
69
164
const now = new Date(2026, 7, 17, 9, 30);
70
- const occurrences = buildReminderOccurrences([], time, { now, days: 2 });
165
+ const occurrences = buildReminderOccurrences([], schedule, { now, days: 2 });
71
166
72
167
expect(occurrences.map(o => o.triggerAt)).toEqual([
73
168
new Date(2026, 7, 17, 12, 0),
@@ -77,11 +172,38 @@
77
172
78
173
it('skips today when the reminder time has already passed', () => {
79
174
const now = new Date(2026, 7, 17, 13, 0);
80
- const occurrences = buildReminderOccurrences([], time, { now, days: 2 });
175
+ const occurrences = buildReminderOccurrences([], schedule, { now, days: 2 });
81
176
82
177
expect(occurrences.map(o => o.triggerAt)).toEqual([new Date(2026, 7, 18, 12, 0)]);
83
178
});
84
179
180
+ it('fires each day at the time configured for its weekday', () => {
181
+ const perDay = createUniformSchedule({ hour: 12, minute: 0 });
182
+ perDay[1] = { hour: 6, minute: 30 }; // Monday
183
+ perDay[2] = { hour: 20, minute: 15 }; // Tuesday
184
+ perDay[3] = { hour: 9, minute: 0 }; // Wednesday
185
+
186
+ const now = new Date(2026, 7, 17, 5, 0);
187
+ const occurrences = buildReminderOccurrences([], perDay, { now, days: 3 });
188
+
189
+ expect(occurrences.map(o => o.triggerAt)).toEqual([
190
+ new Date(2026, 7, 17, 6, 30),
191
+ new Date(2026, 7, 18, 20, 15),
192
+ new Date(2026, 7, 19, 9, 0),
193
+ ]);
194
+ });
195
+
196
+ it('skips only today when today’s own time has passed', () => {
197
+ const perDay = createUniformSchedule({ hour: 12, minute: 0 });
198
+ perDay[1] = { hour: 6, minute: 30 }; // Monday, already over
199
+ perDay[2] = { hour: 20, minute: 15 }; // Tuesday
200
+
201
+ const now = new Date(2026, 7, 17, 7, 0);
202
+ const occurrences = buildReminderOccurrences([], perDay, { now, days: 2 });
203
+
204
+ expect(occurrences.map(o => o.triggerAt)).toEqual([new Date(2026, 7, 18, 20, 15)]);
205
+ });
206
+
85
207
it('counts the tasks of each day separately', () => {
86
208
const now = new Date(2026, 7, 17, 9, 0);
87
209
const tasks = [
@@ -91,7 +213,7 @@
91
213
{ dueDate: new Date(2026, 7, 18, 20, 0).getTime(), completed: true },
92
214
];
93
215
94
- const occurrences = buildReminderOccurrences(tasks, time, { now, days: 3 });
216
+ const occurrences = buildReminderOccurrences(tasks, schedule, { now, days: 3 });
95
217
96
218
expect(occurrences.map(o => o.taskCount)).toEqual([1, 2, 0]);
97
219
expect(occurrences.map(o => o.body)).toEqual([
MODIFY
utils/reminder.ts
+72 -5
@@ -25,9 +25,55 @@
25
25
body: string;
26
26
}
27
27
28
+/**
29
+ * A reminder time for every weekday, indexed like `Date.getDay()`:
30
+ * 0 = Sunday ... 6 = Saturday.
31
+ */
32
+export type ReminderSchedule = ReminderTime[];
33
+
28
34
/** Default reminder time: 12:00 (noon). */
29
35
export const DEFAULT_REMINDER_TIME: ReminderTime = { hour: 12, minute: 0 };
30
36
37
+/** Weekday names, indexed like `Date.getDay()`. */
38
+export const WEEKDAY_LABELS = [
39
+ 'Sunday',
40
+ 'Monday',
41
+ 'Tuesday',
42
+ 'Wednesday',
43
+ 'Thursday',
44
+ 'Friday',
45
+ 'Saturday',
46
+];
47
+
48
+/** Weekday indices in the order they are shown, Monday first. */
49
+export const WEEKDAY_DISPLAY_ORDER = [1, 2, 3, 4, 5, 6, 0];
50
+
51
+/** Build a schedule that reminds at the same time on every weekday. */
52
+export function createUniformSchedule(time: ReminderTime): ReminderSchedule {
53
+ return WEEKDAY_LABELS.map(() => ({ ...time }));
54
+}
55
+
56
+/** Default schedule: the default time on every weekday. */
57
+export const DEFAULT_REMINDER_SCHEDULE: ReminderSchedule =
58
+ createUniformSchedule(DEFAULT_REMINDER_TIME);
59
+
60
+/** Copy of `schedule` with `weekday` set to `time`. */
61
+export function withReminderTimeForWeekday(
62
+ schedule: ReminderSchedule,
63
+ weekday: number,
64
+ time: ReminderTime
65
+): ReminderSchedule {
66
+ return schedule.map((existing, day) => (day === weekday ? { ...time } : { ...existing }));
67
+}
68
+
69
+/** The reminder time configured for the weekday `date` falls on. */
70
+export function getReminderTimeForDate(
71
+ schedule: ReminderSchedule,
72
+ date: Date
73
+): ReminderTime {
74
+ return schedule[date.getDay()];
75
+}
76
+
31
77
/** How many days of reminders are kept scheduled ahead. */
32
78
export const REMINDER_WINDOW_DAYS = 7;
33
79
@@ -58,13 +104,14 @@
58
104
}
59
105
60
106
/**
61
- * Build the upcoming reminders for the next `days` days, each carrying the
62
- * task count of its own day. Occurrences whose time has already passed are
63
- * left out, so the first entry is always in the future.
107
+ * Build the upcoming reminders for the next `days` days, each at the time its
108
+ * own weekday is configured for and carrying the task count of that day.
109
+ * Occurrences whose time has already passed are left out, so the first entry
110
+ * is always in the future.
64
111
*/
65
112
export function buildReminderOccurrences(
66
113
tasks: ReminderTask[],
67
- time: ReminderTime,
114
+ schedule: ReminderSchedule,
68
115
{ now, days }: { now: Date; days: number }
69
116
): ReminderOccurrence[] {
70
117
const occurrences: ReminderOccurrence[] = [];
@@ -72,7 +119,8 @@
72
119
for (let offset = 0; offset < days; offset++) {
73
120
const triggerAt = new Date(now);
74
121
triggerAt.setDate(triggerAt.getDate() + offset);
75
- triggerAt.setHours(time.hour, time.minute, 0, 0);
122
+ const { hour, minute } = getReminderTimeForDate(schedule, triggerAt);
123
+ triggerAt.setHours(hour, minute, 0, 0);
76
124
77
125
if (triggerAt.getTime() <= now.getTime()) {
78
126
continue;
@@ -92,6 +140,16 @@
92
140
return `${hh}:${mm}`;
93
141
}
94
142
143
+/**
144
+ * Describe a schedule in one phrase, naming the time when every weekday
145
+ * shares it.
146
+ */
147
+export function describeReminderSchedule(schedule: ReminderSchedule): string {
148
+ const first = formatReminderTime(schedule[0]);
149
+ const uniform = schedule.every(time => formatReminderTime(time) === first);
150
+ return uniform ? `${first} every day` : 'at a time set per weekday';
151
+}
152
+
95
153
/** Validate that a reminder time has in-range, integer hour and minute. */
96
154
export function isValidReminderTime(time: ReminderTime): boolean {
97
155
const { hour, minute } = time;
@@ -104,3 +162,12 @@
104
162
minute <= 59
105
163
);
106
164
}
165
+
166
+/** Validate that a schedule covers every weekday with a valid time. */
167
+export function isValidReminderSchedule(schedule: ReminderSchedule): boolean {
168
+ return (
169
+ Array.isArray(schedule) &&
170
+ schedule.length === WEEKDAY_LABELS.length &&
171
+ schedule.every(isValidReminderTime)
172
+ );
173
+}