feat: add daily/hourly notification frequency and cloudy header effect
This commit is contained in:
parent
630d88e4bd
commit
431966a634
1
migrations/0002_add_reminder_frequency.sql
Normal file
1
migrations/0002_add_reminder_frequency.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE ReminderSettings ADD COLUMN frequency TEXT DEFAULT 'daily';
|
||||
@ -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);
|
||||
|
||||
@ -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'll receive reminders every hour during active hours (9 AM - 9 PM).
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Request Permission Button */}
|
||||
{isSupported && permission === 'default' && (
|
||||
<Button
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 ============
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user