feat: add daily/hourly notification frequency and cloudy header effect

This commit is contained in:
Avery Felts 2026-01-27 09:31:01 -07:00
parent 630d88e4bd
commit 431966a634
5 changed files with 89 additions and 22 deletions

View File

@ -0,0 +1 @@
ALTER TABLE ReminderSettings ADD COLUMN frequency TEXT DEFAULT 'daily';

View File

@ -15,12 +15,14 @@ export async function GET() {
return NextResponse.json({
enabled: false,
reminderTime: '09:00',
frequency: 'daily',
});
}
return NextResponse.json({
enabled: !!settings.enabled,
reminderTime: settings.reminderTime,
frequency: settings.frequency || 'daily',
});
} catch (error) {
console.error('Error fetching reminder settings:', error);
@ -35,13 +37,14 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json() as { enabled?: boolean; reminderTime?: string };
const { enabled, reminderTime } = body;
const body = await request.json() as { enabled?: boolean; reminderTime?: string; frequency?: string };
const { enabled, reminderTime, frequency } = body;
const settings = await upsertReminderSettingsD1(
session.user.id,
enabled ?? false,
reminderTime ?? '09:00'
reminderTime ?? '09:00',
frequency ?? 'daily'
);
if (!settings) {
@ -51,6 +54,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({
enabled: !!settings.enabled,
reminderTime: settings.reminderTime,
frequency: settings.frequency || 'daily',
});
} catch (error) {
console.error('Error saving reminder settings:', error);

View File

@ -33,9 +33,10 @@ interface UserHeaderProps {
export function UserHeader({ user, preferences }: UserHeaderProps) {
const [userName, setUserName] = useState<string | null>(null);
const [reminderSettings, setReminderSettings] = useState<ReminderSettings>({ enabled: false, reminderTime: '09:00' });
const [reminderSettings, setReminderSettings] = useState<ReminderSettings>({ enabled: false, reminderTime: '09:00', frequency: 'daily' });
const [showReminderDialog, setShowReminderDialog] = useState(false);
const [localTime, setLocalTime] = useState('09:00');
const [localFrequency, setLocalFrequency] = useState<'daily' | 'hourly'>('daily');
const router = useRouter();
const { theme, toggleTheme } = useTheme();
const { isSupported, permission, requestPermission } = useNotifications(reminderSettings);
@ -55,6 +56,7 @@ export function UserHeader({ user, preferences }: UserHeaderProps) {
setReminderSettings(reminders);
setLocalTime(reminders.reminderTime);
setLocalFrequency(reminders.frequency || 'daily');
};
loadData();
}, [preferences]);
@ -76,6 +78,13 @@ export function UserHeader({ user, preferences }: UserHeaderProps) {
await saveReminderSettings(newSettings);
};
const handleFrequencyChange = async (newFrequency: 'daily' | 'hourly') => {
setLocalFrequency(newFrequency);
const newSettings = { ...reminderSettings, frequency: newFrequency };
setReminderSettings(newSettings);
await saveReminderSettings(newSettings);
};
const initials = [user.firstName?.[0], user.lastName?.[0]]
.filter(Boolean)
.join('')
@ -91,12 +100,21 @@ export function UserHeader({ user, preferences }: UserHeaderProps) {
return (
<>
<header className="sticky top-0 z-50 border-b border-border/10 transition-colors duration-300" style={{
<header className="sticky top-0 z-50 border-b border-border/10 transition-colors duration-300 relative overflow-hidden" style={{
background: theme === 'light'
? 'rgba(255, 255, 255, 0.8)'
: 'linear-gradient(135deg, rgba(10, 10, 20, 0.98) 0%, rgba(20, 30, 60, 0.95) 50%, rgba(15, 25, 50, 0.98) 100%)',
backdropFilter: 'blur(10px)',
}}>
{/* Cloudy/Foggy effect overlay */}
<div className="absolute inset-0 pointer-events-none opacity-30 select-none">
<div className="absolute -top-10 -left-10 w-64 h-64 bg-neutral-200/40 rounded-full blur-3xl animate-float" style={{ animationDuration: '15s', animationDelay: '0s' }} />
<div className="absolute top-1/2 left-1/3 w-96 h-32 bg-slate-300/30 rounded-full blur-3xl animate-float" style={{ animationDuration: '20s', animationDelay: '-5s' }} />
<div className="absolute -bottom-10 right-0 w-80 h-80 bg-stone-200/30 rounded-full blur-3xl animate-float" style={{ animationDuration: '18s', animationDelay: '-2s' }} />
{/* Subtle moving fog layer */}
<div className="absolute inset-0 bg-[url('/fog-texture.png')] opacity-10 animate-slide-in-right" style={{ animationDuration: '60s', animationTimingFunction: 'linear', animationIterationCount: 'infinite' }} />
</div>
{/* Edge blur overlay - fades content into header */}
<div
className="absolute left-0 right-0 pointer-events-none z-40"
@ -112,7 +130,7 @@ export function UserHeader({ user, preferences }: UserHeaderProps) {
WebkitMaskImage: 'linear-gradient(to bottom, black, transparent)',
}}
/>
<div className="container mx-auto px-4 py-3 sm:py-4 flex items-center justify-between">
<div className="container mx-auto px-4 py-3 sm:py-4 flex items-center justify-between relative z-50">
<div className="flex items-center gap-4 sm:gap-8">
<h1
className="text-xl sm:text-2xl font-bold cursor-pointer hover:opacity-90 transition-all duration-300 hover:scale-105"
@ -141,7 +159,7 @@ export function UserHeader({ user, preferences }: UserHeaderProps) {
: 'bg-muted hover:bg-muted/80'
}`}
aria-label="Reminder settings"
title={reminderSettings.enabled ? `Reminders on at ${reminderSettings.reminderTime}` : 'Reminders off'}
title={reminderSettings.enabled ? `Reminders on (${reminderSettings.frequency})` : 'Reminders off'}
>
{reminderSettings.enabled ? (
<BellRing className="h-5 w-5 text-indigo-300 transition-transform duration-300" />
@ -215,7 +233,7 @@ export function UserHeader({ user, preferences }: UserHeaderProps) {
</div>
</div>
{userName && (
<div className="sm:hidden container mx-auto px-4 pb-2">
<div className="sm:hidden container mx-auto px-4 pb-2 relative z-50">
<p className="text-muted-foreground text-sm">
Welcome {userName}, you got this!
</p>
@ -228,7 +246,7 @@ export function UserHeader({ user, preferences }: UserHeaderProps) {
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Bell className="h-5 w-5 text-indigo-400" />
Daily Reminders
Notification Settings
</DialogTitle>
</DialogHeader>
@ -243,9 +261,14 @@ export function UserHeader({ user, preferences }: UserHeaderProps) {
) : (
<BellOff className="h-4 w-4 text-muted-foreground" />
)}
<span className="text-sm">
{reminderSettings.enabled ? 'Reminders On' : 'Reminders Off'}
</span>
<div className="flex flex-col">
<span className="text-sm font-medium">
{reminderSettings.enabled ? 'Notifications On' : 'Notifications Off'}
</span>
<span className="text-xs text-muted-foreground">
{reminderSettings.enabled ? 'You will be notified to log usage' : 'Turn on to get reminders'}
</span>
</div>
</div>
<button
onClick={handleToggleReminders}
@ -260,8 +283,35 @@ export function UserHeader({ user, preferences }: UserHeaderProps) {
</button>
</div>
{/* Time Picker */}
{/* Frequency Selection */}
{reminderSettings.enabled && (
<div className="space-y-3">
<Label className="text-sm font-medium">Frequency</Label>
<div className="grid grid-cols-2 gap-3">
<button
onClick={() => handleFrequencyChange('daily')}
className={`p-3 rounded-lg border text-sm font-medium transition-all ${localFrequency === 'daily'
? 'bg-indigo-500/10 border-indigo-500/50 text-indigo-400'
: 'bg-background border-border hover:border-border/80'
}`}
>
Daily
</button>
<button
onClick={() => handleFrequencyChange('hourly')}
className={`p-3 rounded-lg border text-sm font-medium transition-all ${localFrequency === 'hourly'
? 'bg-indigo-500/10 border-indigo-500/50 text-indigo-400'
: 'bg-background border-border hover:border-border/80'
}`}
>
Hourly
</button>
</div>
</div>
)}
{/* Time Picker (Only for Daily) */}
{reminderSettings.enabled && localFrequency === 'daily' && (
<div className="space-y-2">
<Label htmlFor="reminderTime" className="text-sm">
Reminder Time
@ -278,6 +328,16 @@ export function UserHeader({ user, preferences }: UserHeaderProps) {
</div>
)}
{/* Hourly Info */}
{reminderSettings.enabled && localFrequency === 'hourly' && (
<div className="p-3 bg-muted/50 rounded-lg border border-border/50">
<p className="text-xs text-muted-foreground flex items-center gap-2">
<Sparkles className="w-3 h-3 text-indigo-400" />
You&apos;ll receive reminders every hour during active hours (9 AM - 9 PM).
</p>
</div>
)}
{/* Request Permission Button */}
{isSupported && permission === 'default' && (
<Button

View File

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

View File

@ -38,6 +38,7 @@ export interface Achievement {
export interface ReminderSettings {
enabled: boolean;
reminderTime: string; // HH:MM format
frequency: 'daily' | 'hourly';
}
export interface SavingsConfig {
@ -274,13 +275,13 @@ export async function fetchReminderSettings(): Promise<ReminderSettings> {
if (reminderSettingsCache) return reminderSettingsCache;
try {
const response = await fetch('/api/reminders');
if (!response.ok) return { enabled: false, reminderTime: '09:00' };
if (!response.ok) return { enabled: false, reminderTime: '09:00', frequency: 'daily' };
const data = await response.json() as ReminderSettings;
reminderSettingsCache = data;
return data;
} catch (error) {
console.error('Error fetching reminder settings:', error);
return { enabled: false, reminderTime: '09:00' };
return { enabled: false, reminderTime: '09:00', frequency: 'daily' };
}
}
@ -300,7 +301,7 @@ export async function saveReminderSettings(settings: ReminderSettings): Promise<
}
export function getReminderSettings(): ReminderSettings {
return reminderSettingsCache || { enabled: false, reminderTime: '09:00' };
return reminderSettingsCache || { enabled: false, reminderTime: '09:00', frequency: 'daily' };
}
// ============ SAVINGS FUNCTIONS ============