Add hourly time picker range and update notification logic
This commit is contained in:
parent
9967399145
commit
9f0eb9a5bd
2
migrations/0003_add_hourly_times.sql
Normal file
2
migrations/0003_add_hourly_times.sql
Normal 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';
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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'll receive reminders every hour during active hours (9 AM - 9 PM).
|
You'll receive reminders every hour between these times.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -88,16 +88,60 @@ 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 now = new Date();
|
||||||
const lastNotified = localStorage.getItem(LAST_NOTIFICATION_KEY);
|
const today = now.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
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
|
if (lastNotified === today) return; // Already notified today
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
const [hours, minutes] = reminderSettings.reminderTime.split(':').map(Number);
|
const [hours, minutes] = reminderSettings.reminderTime.split(':').map(Number);
|
||||||
const reminderTime = new Date();
|
const reminderTime = new Date();
|
||||||
reminderTime.setHours(hours, minutes, 0, 0);
|
reminderTime.setHours(hours, minutes, 0, 0);
|
||||||
@ -112,6 +156,7 @@ export function useNotifications(reminderSettings: ReminderSettings) {
|
|||||||
});
|
});
|
||||||
localStorage.setItem(LAST_NOTIFICATION_KEY, today);
|
localStorage.setItem(LAST_NOTIFICATION_KEY, today);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [reminderSettings, permission, sendNotification, playNotificationSound]);
|
}, [reminderSettings, permission, sendNotification, playNotificationSound]);
|
||||||
|
|
||||||
// Set up interval to check for reminder time
|
// Set up interval to check for reminder time
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user