Add hourly time picker range and update notification logic

This commit is contained in:
Avery Felts 2026-01-27 14:26:24 -07:00
parent 9967399145
commit 9f0eb9a5bd
6 changed files with 213 additions and 30 deletions

View File

@ -0,0 +1,2 @@
ALTER TABLE ReminderSettings ADD COLUMN hourlyStart TEXT DEFAULT '09:00';
ALTER TABLE ReminderSettings ADD COLUMN hourlyEnd TEXT DEFAULT '21:00';

View File

@ -23,6 +23,8 @@ export async function GET() {
enabled: !!settings.enabled, enabled: !!settings.enabled,
reminderTime: settings.reminderTime, reminderTime: settings.reminderTime,
frequency: settings.frequency || 'daily', frequency: settings.frequency || 'daily',
hourlyStart: settings.hourlyStart || '09:00',
hourlyEnd: settings.hourlyEnd || '21:00',
}); });
} catch (error) { } catch (error) {
console.error('Error fetching reminder settings:', 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 }); return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
} }
const body = await request.json() as { enabled?: boolean; reminderTime?: string; frequency?: string }; const body = await request.json() as {
const { enabled, reminderTime, frequency } = body; enabled?: boolean;
reminderTime?: string;
frequency?: string;
hourlyStart?: string;
hourlyEnd?: string;
};
const { enabled, reminderTime, frequency, hourlyStart, hourlyEnd } = body;
const settings = await upsertReminderSettingsD1( const settings = await upsertReminderSettingsD1(
session.user.id, session.user.id,
enabled ?? false, enabled ?? false,
reminderTime ?? '09:00', reminderTime ?? '09:00',
frequency ?? 'daily' frequency ?? 'daily',
hourlyStart ?? '09:00',
hourlyEnd ?? '21:00'
); );
if (!settings) { if (!settings) {
@ -55,6 +65,8 @@ export async function POST(request: NextRequest) {
enabled: !!settings.enabled, enabled: !!settings.enabled,
reminderTime: settings.reminderTime, reminderTime: settings.reminderTime,
frequency: settings.frequency || 'daily', frequency: settings.frequency || 'daily',
hourlyStart: settings.hourlyStart || '09:00',
hourlyEnd: settings.hourlyEnd || '21:00',
}); });
} catch (error) { } catch (error) {
console.error('Error saving reminder settings:', error); console.error('Error saving reminder settings:', error);

View File

@ -38,6 +38,88 @@ interface UserHeaderProps {
preferences?: UserPreferences | null; 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 (
<div className="flex gap-2 w-full">
{/* Hour Select */}
<div className="flex-1">
<Select
value={hourString}
onValueChange={(val) => updateTime(val, minuteString, currentAmpm)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Hour" />
</SelectTrigger>
<SelectContent>
{hoursOptions.map((h) => (
<SelectItem key={h} value={h}>
{h}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Minute Select */}
<div className="flex-1">
<Select
value={minuteString}
onValueChange={(val) => updateTime(hourString, val, currentAmpm)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Min" />
</SelectTrigger>
<SelectContent>
{minutesOptions.map((m) => (
<SelectItem key={m} value={m}>
{m}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* AM/PM Select */}
<div className="w-24">
<Select
value={currentAmpm}
onValueChange={(val) => updateTime(hourString, minuteString, val)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="AM/PM" />
</SelectTrigger>
<SelectContent>
<SelectItem value="AM">AM</SelectItem>
<SelectItem value="PM">PM</SelectItem>
</SelectContent>
</Select>
</div>
</div>
);
}
export function UserHeader({ user, preferences }: UserHeaderProps) { export function UserHeader({ user, preferences }: UserHeaderProps) {
const [userName, setUserName] = useState<string | null>(null); const [userName, setUserName] = useState<string | null>(null);
const [reminderSettings, setReminderSettings] = useState<ReminderSettings>({ enabled: false, reminderTime: '09:00', frequency: 'daily' }); const [reminderSettings, setReminderSettings] = useState<ReminderSettings>({ enabled: false, reminderTime: '09:00', frequency: 'daily' });
@ -403,12 +485,43 @@ export function UserHeader({ user, preferences }: UserHeaderProps) {
</div> </div>
)} )}
{/* Hourly Info */} {/* Hourly Time Pickers */}
{reminderSettings.enabled && localFrequency === 'hourly' && ( {reminderSettings.enabled && localFrequency === 'hourly' && (
<div className="p-3 bg-muted/50 rounded-lg border border-border/50"> <div className="space-y-4">
{/* Start Time */}
<div className="space-y-2">
<Label className="text-sm">Start Time</Label>
<div className="flex gap-2">
<HourlyTimePicker
value={reminderSettings.hourlyStart || '09:00'}
onChange={async (newTime) => {
const newSettings = { ...reminderSettings, hourlyStart: newTime };
setReminderSettings(newSettings);
await saveReminderSettings(newSettings);
}}
/>
</div>
</div>
{/* End Time */}
<div className="space-y-2">
<Label className="text-sm">End Time</Label>
<div className="flex gap-2">
<HourlyTimePicker
value={reminderSettings.hourlyEnd || '21:00'}
onChange={async (newTime) => {
const newSettings = { ...reminderSettings, hourlyEnd: newTime };
setReminderSettings(newSettings);
await saveReminderSettings(newSettings);
}}
/>
</div>
</div>
<p className="text-xs text-muted-foreground flex items-center gap-2"> <p className="text-xs text-muted-foreground flex items-center gap-2">
<Sparkles className="w-3 h-3 text-indigo-400" /> <Sparkles className="w-3 h-3 text-indigo-400" />
You&apos;ll receive reminders every hour during active hours (9 AM - 9 PM). You&apos;ll receive reminders every hour between these times.
</p> </p>
</div> </div>
)} )}

View File

@ -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(() => { const checkAndSendReminder = useCallback(() => {
if (!reminderSettings.enabled || permission !== 'granted') return; 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 now = new Date();
const [hours, minutes] = reminderSettings.reminderTime.split(':').map(Number); const today = now.toISOString().split('T')[0];
const reminderTime = new Date();
reminderTime.setHours(hours, minutes, 0, 0);
// If current time is past the reminder time, send it if (reminderSettings.frequency === 'hourly') {
if (now >= reminderTime) { const LAST_HOURLY_KEY = 'quittraq_last_hourly_notification';
playNotificationSound(); const lastNotifiedHour = localStorage.getItem(LAST_HOURLY_KEY);
sendNotification('QuitTraq Reminder', { const currentHourKey = `${today}-${now.getHours()}`;
body: "Time to log your daily usage! Every day counts on your journey.",
tag: 'daily-reminder', // Tag ensures we don't stack multiple notifications if (lastNotifiedHour === currentHourKey) return; // Already notified this hour
requireInteraction: true, // Keep it visible until user interacts
}); // Check active hours
localStorage.setItem(LAST_NOTIFICATION_KEY, today); 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]); }, [reminderSettings, permission, sendNotification, playNotificationSound]);

View File

@ -235,6 +235,8 @@ export interface ReminderSettingsRow {
enabled: number; // SQLite boolean enabled: number; // SQLite boolean
reminderTime: string; reminderTime: string;
frequency: string; frequency: string;
hourlyStart: string | null;
hourlyEnd: string | null;
lastNotifiedDate: string | null; lastNotifiedDate: string | null;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@ -251,7 +253,14 @@ export async function getReminderSettingsD1(userId: string): Promise<ReminderSet
return result; return result;
} }
export async function upsertReminderSettingsD1(userId: string, enabled: boolean, reminderTime: string, frequency: string = 'daily'): Promise<ReminderSettingsRow | null> { export async function upsertReminderSettingsD1(
userId: string,
enabled: boolean,
reminderTime: string,
frequency: string = 'daily',
hourlyStart: string = '09:00',
hourlyEnd: string = '21:00'
): Promise<ReminderSettingsRow | null> {
const db = getD1(); const db = getD1();
if (!db) return null; if (!db) return null;
@ -261,13 +270,13 @@ export async function upsertReminderSettingsD1(userId: string, enabled: boolean,
if (existing) { if (existing) {
await db.prepare( await db.prepare(
'UPDATE ReminderSettings SET enabled = ?, reminderTime = ?, frequency = ?, updatedAt = ? WHERE userId = ?' 'UPDATE ReminderSettings SET enabled = ?, reminderTime = ?, frequency = ?, hourlyStart = ?, hourlyEnd = ?, updatedAt = ? WHERE userId = ?'
).bind(enabled ? 1 : 0, reminderTime, frequency, now, userId).run(); ).bind(enabled ? 1 : 0, reminderTime, frequency, hourlyStart, hourlyEnd, now, userId).run();
} else { } else {
await db.prepare( await db.prepare(
`INSERT INTO ReminderSettings (id, userId, enabled, reminderTime, frequency, createdAt, updatedAt) `INSERT INTO ReminderSettings (id, userId, enabled, reminderTime, frequency, hourlyStart, hourlyEnd, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?)` VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
).bind(id, userId, enabled ? 1 : 0, reminderTime, frequency, now, now).run(); ).bind(id, userId, enabled ? 1 : 0, reminderTime, frequency, hourlyStart, hourlyEnd, now, now).run();
} }
return getReminderSettingsD1(userId); return getReminderSettingsD1(userId);

View File

@ -39,6 +39,8 @@ export interface ReminderSettings {
enabled: boolean; enabled: boolean;
reminderTime: string; // HH:MM format reminderTime: string; // HH:MM format
frequency: 'daily' | 'hourly'; frequency: 'daily' | 'hourly';
hourlyStart?: string; // HH:MM
hourlyEnd?: string; // HH:MM
} }
export interface SavingsConfig { export interface SavingsConfig {