feat: Add PWA auto-prompt and header edge blur effect

- Usage prompt auto-shows when app accessed as home screen shortcut (PWA mode)
- Added blur gradient overlay below header for smooth scroll fade effect
This commit is contained in:
Avery Felts 2026-01-25 18:27:02 -07:00
parent 7dd5e6359a
commit 29c11fcaa5
2 changed files with 214 additions and 190 deletions

View File

@ -106,7 +106,14 @@ export function Dashboard({ user }: DashboardProps) {
// Check for achievements
await checkAndUnlockAchievements(usage, prefs, achvs);
if (shouldShowUsagePrompt()) {
// Check if running as PWA (home screen shortcut)
const isStandalone = window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as Navigator & { standalone?: boolean }).standalone === true;
// Always show usage prompt when accessed as PWA shortcut
if (isStandalone) {
setShowUsagePrompt(true);
} else if (shouldShowUsagePrompt()) {
setShowUsagePrompt(true);
markPromptShown();
}

View File

@ -90,196 +90,213 @@ export function UserHeader({ user, preferences }: UserHeaderProps) {
};
return (
<header className="sticky top-0 z-50 border-b border-border/10 transition-colors duration-300" 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)',
}}>
<div className="container mx-auto px-4 py-3 sm:py-4 flex items-center justify-between">
<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"
onClick={() => handleNavigate('/')}
style={{
background: 'linear-gradient(135deg, #a78bfa 0%, #818cf8 50%, #6366f1 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
}}
>
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 at ${reminderSettings.reminderTime}` : '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>
</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">
<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" />
Daily Reminders
</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" />
)}
<span className="text-sm">
{reminderSettings.enabled ? 'Reminders On' : 'Reminders Off'}
</span>
</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>
{/* Time Picker */}
{reminderSettings.enabled && (
<div className="space-y-2">
<Label htmlFor="reminderTime" className="text-sm">
Reminder Time
</Label>
<Input
id="reminderTime"
type="time"
value={localTime}
onChange={(e) => handleTimeChange(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
You&apos;ll receive a reminder at this time each day
</p>
</div>
)}
{/* Request Permission Button */}
{isSupported && permission === 'default' && (
<Button
onClick={requestPermission}
variant="outline"
className="w-full"
>
<Bell className="mr-2 h-4 w-4" />
Enable Notifications
</Button>
)}
{/* 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>
<>
<header className="sticky top-0 z-50 border-b border-border/10 transition-colors duration-300" 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)',
}}>
{/* 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">
<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"
onClick={() => handleNavigate('/')}
style={{
background: 'linear-gradient(135deg, #a78bfa 0%, #818cf8 50%, #6366f1 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
}}
>
QuitTraq
</h1>
{userName && (
<p className="text-foreground/90 text-lg hidden sm:block ml-4">
Welcome {userName}, you got this!
</p>
)}
</div>
</DialogContent>
</Dialog>
</header>
<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 at ${reminderSettings.reminderTime}` : '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>
</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">
<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" />
Daily Reminders
</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" />
)}
<span className="text-sm">
{reminderSettings.enabled ? 'Reminders On' : 'Reminders Off'}
</span>
</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>
{/* Time Picker */}
{reminderSettings.enabled && (
<div className="space-y-2">
<Label htmlFor="reminderTime" className="text-sm">
Reminder Time
</Label>
<Input
id="reminderTime"
type="time"
value={localTime}
onChange={(e) => handleTimeChange(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
You&apos;ll receive a reminder at this time each day
</p>
</div>
)}
{/* Request Permission Button */}
{isSupported && permission === 'default' && (
<Button
onClick={requestPermission}
variant="outline"
className="w-full"
>
<Bell className="mr-2 h-4 w-4" />
Enable Notifications
</Button>
)}
{/* 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>
</>
);
}