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 {