From 9f0eb9a5bd49d167e746eb10aeea868ede1626d8 Mon Sep 17 00:00:00 2001 From: Avery Felts Date: Tue, 27 Jan 2026 14:26:24 -0700 Subject: [PATCH] Add hourly time picker range and update notification logic --- migrations/0003_add_hourly_times.sql | 2 + src/app/api/reminders/route.ts | 18 +++- src/components/UserHeader.tsx | 119 ++++++++++++++++++++++++++- src/hooks/useNotifications.ts | 81 ++++++++++++++---- src/lib/d1.ts | 21 +++-- src/lib/storage.ts | 2 + 6 files changed, 213 insertions(+), 30 deletions(-) create mode 100644 migrations/0003_add_hourly_times.sql diff --git a/migrations/0003_add_hourly_times.sql b/migrations/0003_add_hourly_times.sql new file mode 100644 index 0000000..55c83ce --- /dev/null +++ b/migrations/0003_add_hourly_times.sql @@ -0,0 +1,2 @@ +ALTER TABLE ReminderSettings ADD COLUMN hourlyStart TEXT DEFAULT '09:00'; +ALTER TABLE ReminderSettings ADD COLUMN hourlyEnd TEXT DEFAULT '21:00'; diff --git a/src/app/api/reminders/route.ts b/src/app/api/reminders/route.ts index e1bd4eb..4730b8a 100644 --- a/src/app/api/reminders/route.ts +++ b/src/app/api/reminders/route.ts @@ -23,6 +23,8 @@ export async function GET() { enabled: !!settings.enabled, reminderTime: settings.reminderTime, frequency: settings.frequency || 'daily', + hourlyStart: settings.hourlyStart || '09:00', + hourlyEnd: settings.hourlyEnd || '21:00', }); } catch (error) { console.error('Error fetching reminder settings:', error); @@ -37,14 +39,22 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const body = await request.json() as { enabled?: boolean; reminderTime?: string; frequency?: string }; - const { enabled, reminderTime, frequency } = body; + const body = await request.json() as { + enabled?: boolean; + reminderTime?: string; + frequency?: string; + hourlyStart?: string; + hourlyEnd?: string; + }; + const { enabled, reminderTime, frequency, hourlyStart, hourlyEnd } = body; const settings = await upsertReminderSettingsD1( session.user.id, enabled ?? false, reminderTime ?? '09:00', - frequency ?? 'daily' + frequency ?? 'daily', + hourlyStart ?? '09:00', + hourlyEnd ?? '21:00' ); if (!settings) { @@ -55,6 +65,8 @@ export async function POST(request: NextRequest) { enabled: !!settings.enabled, reminderTime: settings.reminderTime, frequency: settings.frequency || 'daily', + hourlyStart: settings.hourlyStart || '09:00', + hourlyEnd: settings.hourlyEnd || '21:00', }); } catch (error) { console.error('Error saving reminder settings:', error); diff --git a/src/components/UserHeader.tsx b/src/components/UserHeader.tsx index 5e50b69..483e1a5 100644 --- a/src/components/UserHeader.tsx +++ b/src/components/UserHeader.tsx @@ -38,6 +38,88 @@ interface UserHeaderProps { preferences?: UserPreferences | null; } +interface HourlyTimePickerProps { + value: string; + onChange: (time: string) => void; +} + +function HourlyTimePicker({ value, onChange }: HourlyTimePickerProps) { + const [parsedHours, parsedMinutes] = value.split(':').map(Number); + const currentAmpm = parsedHours >= 12 ? 'PM' : 'AM'; + const currentHour12 = parsedHours % 12 || 12; + const hourString = currentHour12.toString().padStart(2, '0'); + const minuteString = parsedMinutes.toString().padStart(2, '0'); + + const updateTime = (newHourStr: string, newMinuteStr: string, newAmpmStr: string) => { + let h = parseInt(newHourStr); + if (newAmpmStr === 'PM' && h !== 12) h += 12; + if (newAmpmStr === 'AM' && h === 12) h = 0; + + onChange(`${h.toString().padStart(2, '0')}:${newMinuteStr}`); + }; + + const hoursOptions = Array.from({ length: 12 }, (_, i) => (i + 1).toString().padStart(2, '0')); + const minutesOptions = Array.from({ length: 12 }, (_, i) => (i * 5).toString().padStart(2, '0')); + + return ( +
+ {/* Hour Select */} +
+ +
+ + {/* Minute Select */} +
+ +
+ + {/* AM/PM Select */} +
+ +
+
+ ); +} + export function UserHeader({ user, preferences }: UserHeaderProps) { const [userName, setUserName] = useState(null); const [reminderSettings, setReminderSettings] = useState({ enabled: false, reminderTime: '09:00', frequency: 'daily' }); @@ -403,12 +485,43 @@ export function UserHeader({ user, preferences }: UserHeaderProps) { )} - {/* Hourly Info */} + {/* Hourly Time Pickers */} {reminderSettings.enabled && localFrequency === 'hourly' && ( -
+
+ + {/* Start Time */} +
+ +
+ { + const newSettings = { ...reminderSettings, hourlyStart: newTime }; + setReminderSettings(newSettings); + await saveReminderSettings(newSettings); + }} + /> +
+
+ + {/* End Time */} +
+ +
+ { + const newSettings = { ...reminderSettings, hourlyEnd: newTime }; + setReminderSettings(newSettings); + await saveReminderSettings(newSettings); + }} + /> +
+
+

- You'll receive reminders every hour during active hours (9 AM - 9 PM). + You'll receive reminders every hour between these times.

)} diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts index 58b3e9d..8e61e46 100644 --- a/src/hooks/useNotifications.ts +++ b/src/hooks/useNotifications.ts @@ -88,29 +88,74 @@ export function useNotifications(reminderSettings: ReminderSettings) { } }, []); - // Check and send daily reminder + // Track previous settings to detect changes + const [prevSettings, setPrevSettings] = useState(reminderSettings); + + // If settings change, we might need to reset notification history to allow "update and resend" + useEffect(() => { + if (JSON.stringify(prevSettings) !== JSON.stringify(reminderSettings)) { + // Only reset if time changed significantly or if toggled. + // For simplicity, if the user updates settings, we clear the daily lock *if* the time is in the future relative to previous check? + // Actually, the user wants "sent out again" if edited. + // We can simply clear the 'last notified' date if the reminderTime changed. + if (prevSettings.reminderTime !== reminderSettings.reminderTime && reminderSettings.frequency === 'daily') { + localStorage.removeItem(LAST_NOTIFICATION_KEY); + } + setPrevSettings(reminderSettings); + } + }, [reminderSettings, prevSettings]); + + // Check and send reminder const checkAndSendReminder = useCallback(() => { if (!reminderSettings.enabled || permission !== 'granted') return; - const today = new Date().toISOString().split('T')[0]; - const lastNotified = localStorage.getItem(LAST_NOTIFICATION_KEY); - - if (lastNotified === today) return; // Already notified today - const now = new Date(); - const [hours, minutes] = reminderSettings.reminderTime.split(':').map(Number); - const reminderTime = new Date(); - reminderTime.setHours(hours, minutes, 0, 0); + const today = now.toISOString().split('T')[0]; - // If current time is past the reminder time, send it - if (now >= reminderTime) { - playNotificationSound(); - sendNotification('QuitTraq Reminder', { - body: "Time to log your daily usage! Every day counts on your journey.", - tag: 'daily-reminder', // Tag ensures we don't stack multiple notifications - requireInteraction: true, // Keep it visible until user interacts - }); - localStorage.setItem(LAST_NOTIFICATION_KEY, today); + if (reminderSettings.frequency === 'hourly') { + const LAST_HOURLY_KEY = 'quittraq_last_hourly_notification'; + const lastNotifiedHour = localStorage.getItem(LAST_HOURLY_KEY); + const currentHourKey = `${today}-${now.getHours()}`; + + if (lastNotifiedHour === currentHourKey) return; // Already notified this hour + + // Check active hours + const startParts = (reminderSettings.hourlyStart || '09:00').split(':').map(Number); + const endParts = (reminderSettings.hourlyEnd || '21:00').split(':').map(Number); + + const startMinutes = startParts[0] * 60 + startParts[1]; + const endMinutes = endParts[0] * 60 + endParts[1]; + const currentMinutes = now.getHours() * 60 + now.getMinutes(); + + if (currentMinutes >= startMinutes && currentMinutes <= endMinutes) { + playNotificationSound(); + sendNotification('QuitTraq Hourly Check-in', { + body: "How are you doing? Log your status to stay on track!", + tag: 'hourly-reminder', + requireInteraction: true, + }); + localStorage.setItem(LAST_HOURLY_KEY, currentHourKey); + } + + } else { + // Daily logic + const lastNotified = localStorage.getItem(LAST_NOTIFICATION_KEY); + if (lastNotified === today) return; // Already notified today + + const [hours, minutes] = reminderSettings.reminderTime.split(':').map(Number); + const reminderTime = new Date(); + reminderTime.setHours(hours, minutes, 0, 0); + + // If current time is past the reminder time, send it + if (now >= reminderTime) { + playNotificationSound(); + sendNotification('QuitTraq Reminder', { + body: "Time to log your daily usage! Every day counts on your journey.", + tag: 'daily-reminder', // Tag ensures we don't stack multiple notifications + requireInteraction: true, // Keep it visible until user interacts + }); + localStorage.setItem(LAST_NOTIFICATION_KEY, today); + } } }, [reminderSettings, permission, sendNotification, playNotificationSound]); diff --git a/src/lib/d1.ts b/src/lib/d1.ts index c744ef1..1fdcca7 100644 --- a/src/lib/d1.ts +++ b/src/lib/d1.ts @@ -235,6 +235,8 @@ export interface ReminderSettingsRow { enabled: number; // SQLite boolean reminderTime: string; frequency: string; + hourlyStart: string | null; + hourlyEnd: string | null; lastNotifiedDate: string | null; createdAt: string; updatedAt: string; @@ -251,7 +253,14 @@ export async function getReminderSettingsD1(userId: string): Promise { +export async function upsertReminderSettingsD1( + userId: string, + enabled: boolean, + reminderTime: string, + frequency: string = 'daily', + hourlyStart: string = '09:00', + hourlyEnd: string = '21:00' +): Promise { const db = getD1(); if (!db) return null; @@ -261,13 +270,13 @@ export async function upsertReminderSettingsD1(userId: string, enabled: boolean, if (existing) { await db.prepare( - 'UPDATE ReminderSettings SET enabled = ?, reminderTime = ?, frequency = ?, updatedAt = ? WHERE userId = ?' - ).bind(enabled ? 1 : 0, reminderTime, frequency, now, userId).run(); + 'UPDATE ReminderSettings SET enabled = ?, reminderTime = ?, frequency = ?, hourlyStart = ?, hourlyEnd = ?, updatedAt = ? WHERE userId = ?' + ).bind(enabled ? 1 : 0, reminderTime, frequency, hourlyStart, hourlyEnd, now, userId).run(); } else { await db.prepare( - `INSERT INTO ReminderSettings (id, userId, enabled, reminderTime, frequency, createdAt, updatedAt) - VALUES (?, ?, ?, ?, ?, ?, ?)` - ).bind(id, userId, enabled ? 1 : 0, reminderTime, frequency, now, now).run(); + `INSERT INTO ReminderSettings (id, userId, enabled, reminderTime, frequency, hourlyStart, hourlyEnd, createdAt, updatedAt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` + ).bind(id, userId, enabled ? 1 : 0, reminderTime, frequency, hourlyStart, hourlyEnd, now, now).run(); } return getReminderSettingsD1(userId); diff --git a/src/lib/storage.ts b/src/lib/storage.ts index 0d604ee..156011c 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -39,6 +39,8 @@ export interface ReminderSettings { enabled: boolean; reminderTime: string; // HH:MM format frequency: 'daily' | 'hourly'; + hourlyStart?: string; // HH:MM + hourlyEnd?: string; // HH:MM } export interface SavingsConfig {