636 lines
27 KiB
TypeScript
636 lines
27 KiB
TypeScript
'use client';
|
|
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Button } from '@/components/ui/button';
|
|
import { User } from '@/lib/session';
|
|
import { fetchPreferences, fetchReminderSettings, saveReminderSettings, ReminderSettings, UserPreferences } from '@/lib/storage';
|
|
import { useNotifications } from '@/hooks/useNotifications';
|
|
import { useEffect, useState } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { Cigarette, Leaf, LogOut, Home, ChevronDown, Sun, Moon, Bell, BellOff, BellRing, Menu, Sparkles, Link as LinkIcon } from 'lucide-react';
|
|
import { useTheme } from '@/lib/theme-context';
|
|
import { InstallAppButton } from './InstallAppButton';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface UserHeaderProps {
|
|
user: User;
|
|
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: 60 }, (_, i) => i.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) {
|
|
const [userName, setUserName] = useState<string | null>(null);
|
|
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);
|
|
|
|
// Helper to parse time string
|
|
const [parsedHours, parsedMinutes] = reminderSettings.reminderTime.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 = async (newHourStr: string, newMinuteStr: string, newAmpmStr: string) => {
|
|
let h = parseInt(newHourStr);
|
|
if (newAmpmStr === 'PM' && h !== 12) h += 12;
|
|
if (newAmpmStr === 'AM' && h === 12) h = 0;
|
|
|
|
const timeString = `${h.toString().padStart(2, '0')}:${newMinuteStr}`;
|
|
setLocalTime(timeString);
|
|
|
|
const newSettings = { ...reminderSettings, reminderTime: timeString };
|
|
setReminderSettings(newSettings);
|
|
await saveReminderSettings(newSettings);
|
|
};
|
|
|
|
// Generate options
|
|
const hoursOptions = Array.from({ length: 12 }, (_, i) => (i + 1).toString().padStart(2, '0'));
|
|
const minutesOptions = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0'));
|
|
|
|
useEffect(() => {
|
|
const loadData = async () => {
|
|
// If preferences passed from parent, use them. Otherwise fetch.
|
|
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
|
const [prefs, reminders] = await Promise.all([
|
|
preferences ? Promise.resolve(preferences) : fetchPreferences(),
|
|
fetchReminderSettings(),
|
|
]);
|
|
|
|
if (prefs) {
|
|
setUserName(prefs.userName);
|
|
}
|
|
|
|
const detectedTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
let settingsToUse = reminders;
|
|
|
|
// If timezone is missing or different, update it
|
|
if (reminders.timezone !== detectedTimezone) {
|
|
settingsToUse = { ...reminders, timezone: detectedTimezone };
|
|
await saveReminderSettings(settingsToUse);
|
|
}
|
|
|
|
setReminderSettings(settingsToUse);
|
|
setLocalTime(settingsToUse.reminderTime);
|
|
setLocalFrequency(settingsToUse.frequency || 'daily');
|
|
};
|
|
loadData();
|
|
}, [preferences]);
|
|
|
|
const handleToggleReminders = async () => {
|
|
if (!reminderSettings.enabled) {
|
|
const result = await requestPermission();
|
|
if (result !== 'granted') return;
|
|
}
|
|
const newSettings = { ...reminderSettings, enabled: !reminderSettings.enabled };
|
|
setReminderSettings(newSettings);
|
|
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('')
|
|
.toUpperCase() || user.email[0].toUpperCase();
|
|
|
|
const handleLogout = () => {
|
|
window.location.href = '/api/auth/logout';
|
|
};
|
|
|
|
const handleNavigate = (path: string) => {
|
|
router.push(path);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<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 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-indigo-500/10 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/20 rounded-full blur-3xl animate-float" style={{ animationDuration: '18s', animationDelay: '-2s' }} />
|
|
|
|
{/* Subtle moving fog layers - CSS procedural animation */}
|
|
<div
|
|
className={cn(
|
|
"absolute inset-0 z-10 opacity-30 pointer-events-none transition-all duration-1000",
|
|
theme === 'dark' ? "mix-blend-screen" : "mix-blend-multiply"
|
|
)}
|
|
style={{ filter: theme === 'dark' ? 'invert(1)' : 'none' }}
|
|
>
|
|
<div className="absolute inset-0 fog-layer-1" />
|
|
<div className="absolute inset-0 fog-layer-2" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Edge blur overlay - fades content into header */}
|
|
<div
|
|
className="absolute left-0 right-0 pointer-events-none z-40"
|
|
style={{
|
|
bottom: '-40px',
|
|
height: '40px',
|
|
background: theme === 'light'
|
|
? 'linear-gradient(to bottom, rgba(255, 255, 255, 0.9) 0%, rgba(255, 255, 255, 0.5) 50%, transparent 100%)'
|
|
: 'linear-gradient(to bottom, rgba(10, 10, 20, 0.95) 0%, rgba(10, 10, 20, 0.5) 50%, transparent 100%)',
|
|
backdropFilter: 'blur(4px)',
|
|
WebkitBackdropFilter: 'blur(4px)',
|
|
maskImage: 'linear-gradient(to bottom, black, transparent)',
|
|
WebkitMaskImage: 'linear-gradient(to bottom, black, transparent)',
|
|
}}
|
|
/>
|
|
<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-2xl sm:text-3xl font-extrabold cursor-pointer transition-all duration-300 hover:scale-105 tracking-tighter"
|
|
onClick={() => handleNavigate('/')}
|
|
style={{
|
|
background: theme === 'light'
|
|
? 'linear-gradient(135deg, #6366f1 0%, #4338ca 100%)'
|
|
: 'linear-gradient(135deg, #c084fc 0%, #818cf8 50%, #6366f1 100%)',
|
|
WebkitBackgroundClip: 'text',
|
|
WebkitTextFillColor: 'transparent',
|
|
backgroundClip: 'text',
|
|
filter: theme === 'light'
|
|
? 'drop-shadow(0 2px 4px rgba(99, 102, 241, 0.2))'
|
|
: 'drop-shadow(0 0 12px rgba(167, 139, 250, 0.4))'
|
|
}}
|
|
>
|
|
QuitTraq
|
|
</h1>
|
|
{userName && (
|
|
<p className="text-foreground/90 text-lg hidden sm:block ml-4">
|
|
Welcome {userName}, you got this!
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 sm:gap-3">
|
|
<button
|
|
onClick={() => setShowReminderDialog(true)}
|
|
className={`p-2.5 sm:p-2 rounded-full transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-white/30 hover:scale-110 active:scale-95 ${reminderSettings.enabled
|
|
? 'bg-indigo-500/30 hover:bg-indigo-500/40'
|
|
: 'bg-muted hover:bg-muted/80'
|
|
}`}
|
|
aria-label="Reminder settings"
|
|
title={reminderSettings.enabled ? `Reminders on (${reminderSettings.frequency})` : 'Reminders off'}
|
|
>
|
|
{reminderSettings.enabled ? (
|
|
<BellRing className="h-5 w-5 text-indigo-300 transition-transform duration-300" />
|
|
) : (
|
|
<Bell className="h-5 w-5 text-muted-foreground transition-transform duration-300" />
|
|
)}
|
|
</button>
|
|
<InstallAppButton />
|
|
<button
|
|
onClick={toggleTheme}
|
|
className="p-2.5 sm:p-2 rounded-full bg-muted hover:bg-muted/80 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-primary/30 hover:scale-110 active:scale-95"
|
|
aria-label="Toggle theme"
|
|
>
|
|
{theme === 'dark' ? (
|
|
<Moon className="h-5 w-5 text-blue-300 transition-transform duration-300" />
|
|
) : (
|
|
<Sun className="h-5 w-5 text-yellow-400 transition-transform duration-300" />
|
|
)}
|
|
</button>
|
|
|
|
{/* Main Navigation Menu */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button
|
|
className="p-2.5 sm:p-2 rounded-full bg-muted hover:bg-muted/80 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-primary/30 hover:scale-110 active:scale-95"
|
|
aria-label="Open menu"
|
|
>
|
|
<Menu className="h-5 w-5 text-muted-foreground" />
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" sideOffset={8}>
|
|
<DropdownMenuItem onClick={() => handleNavigate('/')}>
|
|
<Home className="mr-3 h-4 w-4 text-muted-foreground" />
|
|
<span>Dashboard</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={() => handleNavigate('/track/nicotine')}>
|
|
<Cigarette className="mr-3 h-4 w-4 text-red-400" />
|
|
<span>Track Nicotine Usage</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleNavigate('/track/marijuana')}>
|
|
<Leaf className="mr-3 h-4 w-4 text-green-400" />
|
|
<span>Track Marijuana Usage</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={() => handleNavigate('/smoking-aids')}>
|
|
<Sparkles className="mr-3 h-4 w-4 text-purple-400" />
|
|
<span>Smoking Aids</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button className="flex items-center gap-2 px-3 py-2 rounded-full bg-muted hover:bg-muted/80 transition-all focus:outline-none focus:ring-2 focus:ring-primary/30">
|
|
<Avatar className="h-8 w-8 ring-2 ring-primary/30">
|
|
<AvatarImage src={user.profilePictureUrl ?? undefined} alt={userName || 'User'} />
|
|
<AvatarFallback className="bg-primary/20 text-primary text-sm">{initials}</AvatarFallback>
|
|
</Avatar>
|
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" sideOffset={8}>
|
|
|
|
<DropdownMenuItem onClick={handleLogout} className="text-red-400 hover:text-red-300">
|
|
<LogOut className="mr-3 h-4 w-4" />
|
|
<span>Sign out</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
{userName && (
|
|
<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>
|
|
</div>
|
|
)}
|
|
|
|
{/* Reminder Settings Dialog */}
|
|
<Dialog open={showReminderDialog} onOpenChange={setShowReminderDialog}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Bell className="h-5 w-5 text-indigo-400" />
|
|
Notification Settings
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 py-4">
|
|
|
|
|
|
{/* Enable/Disable Toggle */}
|
|
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
|
<div className="flex items-center gap-2">
|
|
{reminderSettings.enabled ? (
|
|
<BellRing className="h-4 w-4 text-indigo-400" />
|
|
) : (
|
|
<BellOff className="h-4 w-4 text-muted-foreground" />
|
|
)}
|
|
<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}
|
|
disabled={!isSupported || (permission === 'denied' && !reminderSettings.enabled)}
|
|
className={`relative w-12 h-6 rounded-full transition-all duration-300 ${reminderSettings.enabled ? 'bg-indigo-500' : 'bg-muted-foreground/30'
|
|
} ${!isSupported || (permission === 'denied' && !reminderSettings.enabled) ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
|
>
|
|
<div
|
|
className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-all duration-300 ${reminderSettings.enabled ? 'left-7' : 'left-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{/* 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 className="text-sm">
|
|
Reminder Time
|
|
</Label>
|
|
<div className="flex gap-2">
|
|
{/* 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>
|
|
<p className="text-xs text-muted-foreground">
|
|
You'll receive a reminder at this time each day
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Hourly Time Pickers */}
|
|
{reminderSettings.enabled && localFrequency === 'hourly' && (
|
|
<div className="space-y-4">
|
|
|
|
{/* Start Time */}
|
|
<div className="space-y-2">
|
|
<Label className="text-sm flex items-center justify-between">
|
|
Start Time
|
|
<span className="flex items-center gap-1 text-[10px] text-indigo-400 font-normal">
|
|
<LinkIcon className="w-3 h-3" />
|
|
Minute Link
|
|
</span>
|
|
</Label>
|
|
<div className="flex gap-2">
|
|
<HourlyTimePicker
|
|
value={reminderSettings.hourlyStart || '09:00'}
|
|
onChange={async (newTime) => {
|
|
const [h, m] = newTime.split(':');
|
|
const end = (reminderSettings.hourlyEnd || '21:00').split(':');
|
|
const newEnd = `${end[0]}:${m}`;
|
|
|
|
const newSettings = {
|
|
...reminderSettings,
|
|
hourlyStart: newTime,
|
|
hourlyEnd: newEnd
|
|
};
|
|
setReminderSettings(newSettings);
|
|
await saveReminderSettings(newSettings);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* End Time */}
|
|
<div className="space-y-2">
|
|
<Label className="text-sm flex items-center justify-between">
|
|
End Time
|
|
<span className="text-[10px] text-indigo-400/70 font-normal">Minutes synced with Start</span>
|
|
</Label>
|
|
<div className="flex gap-2">
|
|
<HourlyTimePicker
|
|
value={reminderSettings.hourlyEnd || '21:00'}
|
|
onChange={async (newTime) => {
|
|
const [h, m] = newTime.split(':');
|
|
const start = (reminderSettings.hourlyStart || '09:00').split(':');
|
|
const newStart = `${start[0]}:${m}`;
|
|
|
|
const newSettings = {
|
|
...reminderSettings,
|
|
hourlyEnd: newTime,
|
|
hourlyStart: newStart
|
|
};
|
|
setReminderSettings(newSettings);
|
|
await saveReminderSettings(newSettings);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<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 between these times.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Push Permission / Re-Sync Button */}
|
|
{reminderSettings.enabled && isSupported && (
|
|
<div className="pt-2 border-t border-border/50 space-y-2">
|
|
<button
|
|
onClick={async () => {
|
|
// 1. Request/Refresh Permission & Subscription
|
|
const result = await requestPermission();
|
|
|
|
// 2. If granted, try to send a test notification immediately
|
|
if (result === 'granted') {
|
|
try {
|
|
const res = await fetch('/api/notifications/test', { method: 'POST' });
|
|
if (!res.ok) {
|
|
const errData = await res.json() as { error?: string };
|
|
throw new Error(errData.error || `Server error ${res.status}`);
|
|
}
|
|
alert("Success! Push notifications are now active.");
|
|
} catch (err) {
|
|
console.error(err);
|
|
// @ts-expect-error - err is unknown
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
} else {
|
|
alert("Please enable notifications in your browser settings.");
|
|
}
|
|
}}
|
|
className={`w-full py-3 text-sm font-semibold rounded-lg transition-colors flex items-center justify-center gap-2 shadow-sm ${permission === 'granted'
|
|
? 'text-emerald-600 bg-emerald-50 hover:bg-emerald-100 border border-emerald-200'
|
|
: 'text-white bg-emerald-600 hover:bg-emerald-500'
|
|
}`}
|
|
>
|
|
<Bell className="w-4 h-4" />
|
|
Enable Push
|
|
</button>
|
|
<p className="text-[10px] text-muted-foreground text-center">
|
|
{permission === 'granted'
|
|
? 'Tap if you are not receiving alerts'
|
|
: 'Required for background alerts'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Denied Message */}
|
|
{permission === 'denied' && (
|
|
<div className="p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
|
<p className="text-xs text-red-400">
|
|
Notifications are blocked. Please enable them in your browser settings to receive reminders.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</header>
|
|
</>
|
|
);
|
|
}
|