Optimize performance, fix theme consistency, and improve notification UX
This commit is contained in:
parent
359908caf5
commit
edfc978217
14
prisma.config.ts
Normal file
14
prisma.config.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// This file was generated by Prisma, and assumes you have installed the following:
|
||||||
|
// npm install --save-dev prisma dotenv
|
||||||
|
import "dotenv/config";
|
||||||
|
import { defineConfig } from "prisma/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "prisma/schema.prisma",
|
||||||
|
migrations: {
|
||||||
|
path: "prisma/migrations",
|
||||||
|
},
|
||||||
|
datasource: {
|
||||||
|
url: process.env["DATABASE_URL"],
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -163,25 +163,33 @@
|
|||||||
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
||||||
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
||||||
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
||||||
|
|
||||||
|
/* Background gradients */
|
||||||
|
--bg-main: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 20%, #bbf7d0 40%, #dcfce7 60%, #f0fdf4 80%, #dcfce7 100%);
|
||||||
|
--bg-orbs:
|
||||||
|
radial-gradient(ellipse at 15% 10%, rgba(99, 102, 241, 0.1) 0%, transparent 40%),
|
||||||
|
radial-gradient(ellipse at 85% 20%, rgba(168, 85, 247, 0.08) 0%, transparent 35%),
|
||||||
|
radial-gradient(ellipse at 50% 50%, rgba(59, 130, 246, 0.06) 0%, transparent 50%),
|
||||||
|
radial-gradient(ellipse at 20% 80%, rgba(34, 197, 94, 0.05) 0%, transparent 40%),
|
||||||
|
radial-gradient(ellipse at 80% 85%, rgba(239, 68, 68, 0.05) 0%, transparent 35%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
letter-spacing: var(--tracking-normal);
|
letter-spacing: var(--tracking-normal);
|
||||||
background: linear-gradient(135deg,
|
background-color: transparent;
|
||||||
#0f0f1a 0%,
|
|
||||||
#1a1a2e 20%,
|
|
||||||
#16213e 40%,
|
|
||||||
#1a1a2e 60%,
|
|
||||||
#0f0f1a 80%,
|
|
||||||
#1a1a2e 100%);
|
|
||||||
background-attachment: fixed;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body::before {
|
body::before {
|
||||||
@ -191,14 +199,21 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background:
|
background: var(--bg-orbs), var(--bg-main);
|
||||||
|
background-size: cover;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode overrides */
|
||||||
|
.dark {
|
||||||
|
--bg-main: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 20%, #16213e 40%, #1a1a2e 60%, #0f0f1a 80%, #1a1a2e 100%);
|
||||||
|
--bg-orbs:
|
||||||
radial-gradient(ellipse at 15% 10%, rgba(99, 102, 241, 0.15) 0%, transparent 40%),
|
radial-gradient(ellipse at 15% 10%, rgba(99, 102, 241, 0.15) 0%, transparent 40%),
|
||||||
radial-gradient(ellipse at 85% 20%, rgba(168, 85, 247, 0.12) 0%, transparent 35%),
|
radial-gradient(ellipse at 85% 20%, rgba(168, 85, 247, 0.12) 0%, transparent 35%),
|
||||||
radial-gradient(ellipse at 50% 50%, rgba(45, 55, 72, 0.1) 0%, transparent 50%),
|
radial-gradient(ellipse at 50% 50%, rgba(45, 55, 72, 0.1) 0%, transparent 50%),
|
||||||
radial-gradient(ellipse at 20% 80%, rgba(34, 197, 94, 0.08) 0%, transparent 40%),
|
radial-gradient(ellipse at 20% 80%, rgba(34, 197, 94, 0.08) 0%, transparent 40%),
|
||||||
radial-gradient(ellipse at 80% 85%, rgba(239, 68, 68, 0.08) 0%, transparent 35%);
|
radial-gradient(ellipse at 80% 85%, rgba(239, 68, 68, 0.08) 0%, transparent 35%);
|
||||||
pointer-events: none;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Calendar styling - reduce overall size */
|
/* Calendar styling - reduce overall size */
|
||||||
@ -217,8 +232,13 @@
|
|||||||
|
|
||||||
/* Animation keyframes */
|
/* Animation keyframes */
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
from { opacity: 0; }
|
from {
|
||||||
to { opacity: 1; }
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-in-up {
|
@keyframes fade-in-up {
|
||||||
@ -226,6 +246,7 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(20px);
|
transform: translateY(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
@ -237,6 +258,7 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-20px);
|
transform: translateY(-20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
@ -248,6 +270,7 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
@ -259,6 +282,7 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(20px);
|
transform: translateX(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
@ -270,6 +294,7 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-20px);
|
transform: translateX(-20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
@ -277,23 +302,49 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse-subtle {
|
@keyframes pulse-subtle {
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.7; }
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
0%, 100% { transform: translateY(0); }
|
|
||||||
50% { transform: translateY(-5px); }
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
0% { background-position: -200% 0; }
|
0% {
|
||||||
100% { background-position: 200% 0; }
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes glow {
|
@keyframes glow {
|
||||||
0%, 100% { box-shadow: 0 0 5px rgba(99, 102, 241, 0.5); }
|
|
||||||
50% { box-shadow: 0 0 20px rgba(99, 102, 241, 0.8); }
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 5px rgba(99, 102, 241, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 20px rgba(99, 102, 241, 0.8);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes confetti {
|
@keyframes confetti {
|
||||||
@ -301,6 +352,7 @@
|
|||||||
transform: translateY(0) rotate(0deg);
|
transform: translateY(0) rotate(0deg);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
transform: translateY(100vh) rotate(720deg);
|
transform: translateY(100vh) rotate(720deg);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@ -349,15 +401,34 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Stagger delay utilities */
|
/* Stagger delay utilities */
|
||||||
.delay-100 { animation-delay: 100ms; }
|
.delay-100 {
|
||||||
.delay-200 { animation-delay: 200ms; }
|
animation-delay: 100ms;
|
||||||
.delay-300 { animation-delay: 300ms; }
|
}
|
||||||
.delay-400 { animation-delay: 400ms; }
|
|
||||||
.delay-500 { animation-delay: 500ms; }
|
.delay-200 {
|
||||||
.delay-600 { animation-delay: 600ms; }
|
animation-delay: 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delay-300 {
|
||||||
|
animation-delay: 300ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delay-400 {
|
||||||
|
animation-delay: 400ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delay-500 {
|
||||||
|
animation-delay: 500ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delay-600 {
|
||||||
|
animation-delay: 600ms;
|
||||||
|
}
|
||||||
|
|
||||||
/* Start hidden for animations */
|
/* Start hidden for animations */
|
||||||
.opacity-0 { opacity: 0; }
|
.opacity-0 {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Smooth transitions */
|
/* Smooth transitions */
|
||||||
.transition-smooth {
|
.transition-smooth {
|
||||||
@ -474,4 +545,4 @@
|
|||||||
opacity: 0.03;
|
opacity: 0.03;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
}
|
}
|
||||||
@ -187,13 +187,11 @@ export function Dashboard({ user }: DashboardProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageBackground = theme === 'dark'
|
|
||||||
? 'linear-gradient(135deg, #0a0a14 0%, #141e3c 50%, #0f1932 100%)'
|
|
||||||
: 'linear-gradient(135deg, #ffffff 0%, #f0f4f8 50%, #e8ecf0 100%)';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen" style={{ background: pageBackground }}>
|
<div className="min-h-screen">
|
||||||
<UserHeader user={user} />
|
<UserHeader user={user} preferences={preferences} />
|
||||||
|
|
||||||
<main className="container mx-auto px-4 py-8">
|
<main className="container mx-auto px-4 py-8">
|
||||||
{preferences && (
|
{preferences && (
|
||||||
|
|||||||
@ -85,7 +85,7 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP
|
|||||||
|
|
||||||
const cardBackground =
|
const cardBackground =
|
||||||
theme === 'light'
|
theme === 'light'
|
||||||
? 'linear-gradient(135deg, rgba(6, 95, 70, 0.85) 0%, rgba(4, 120, 87, 0.9) 100%)'
|
? 'linear-gradient(135deg, rgba(236, 253, 245, 0.9) 0%, rgba(209, 250, 229, 0.8) 100%)'
|
||||||
: 'linear-gradient(135deg, rgba(20, 184, 166, 0.2) 0%, rgba(6, 182, 212, 0.15) 100%)';
|
: 'linear-gradient(135deg, rgba(20, 184, 166, 0.2) 0%, rgba(6, 182, 212, 0.15) 100%)';
|
||||||
|
|
||||||
const substanceLabel = substance === 'nicotine' ? 'Nicotine' : 'Marijuana';
|
const substanceLabel = substance === 'nicotine' ? 'Nicotine' : 'Marijuana';
|
||||||
@ -98,11 +98,11 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP
|
|||||||
<div className="absolute top-0 right-0 w-40 h-40 bg-gradient-to-br from-teal-500/10 to-transparent rounded-full -translate-y-1/2 translate-x-1/2 pointer-events-none" />
|
<div className="absolute top-0 right-0 w-40 h-40 bg-gradient-to-br from-teal-500/10 to-transparent rounded-full -translate-y-1/2 translate-x-1/2 pointer-events-none" />
|
||||||
|
|
||||||
<CardHeader className="relative z-10 pb-2">
|
<CardHeader className="relative z-10 pb-2">
|
||||||
<CardTitle className="flex items-center gap-2 text-white text-shadow-sm">
|
<CardTitle className={`flex items-center gap-2 ${theme === 'light' ? 'text-teal-900' : 'text-white'} text-shadow-sm`}>
|
||||||
<Heart className="h-5 w-5 text-teal-400" />
|
<Heart className="h-5 w-5 text-teal-500" />
|
||||||
<span>Health Recovery</span>
|
<span>Health Recovery</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<p className="text-sm text-white/70">
|
<p className={`text-sm ${theme === 'light' ? 'text-teal-700' : 'text-white/70'}`}>
|
||||||
{substanceLabel}-free for {formatDuration(minutesSinceQuit)}
|
{substanceLabel}-free for {formatDuration(minutesSinceQuit)}
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@ -110,10 +110,10 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP
|
|||||||
<CardContent className="relative z-10">
|
<CardContent className="relative z-10">
|
||||||
{/* Progress to next milestone */}
|
{/* Progress to next milestone */}
|
||||||
{nextMilestone && (
|
{nextMilestone && (
|
||||||
<div className="mb-4 p-3 bg-teal-500/20 rounded-xl border border-teal-500/30">
|
<div className="mb-4 p-3 bg-teal-500/10 rounded-xl border border-teal-500/20">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-sm text-white/80">Next milestone</span>
|
<span className={`text-sm ${theme === 'light' ? 'text-teal-900' : 'text-white/80'}`}>Next milestone</span>
|
||||||
<span className="text-sm text-teal-300 font-medium">
|
<span className="text-sm text-teal-600 font-medium">
|
||||||
{formatTimeRemaining(minutesSinceQuit, nextMilestone.timeMinutes)}
|
{formatTimeRemaining(minutesSinceQuit, nextMilestone.timeMinutes)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -123,7 +123,7 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP
|
|||||||
style={{ width: `${progressToNext}%` }}
|
style={{ width: `${progressToNext}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-white/60 mt-2">{nextMilestone.title}</p>
|
<p className={`text-xs ${theme === 'light' ? 'text-teal-700' : 'text-white/60'} mt-2`}>{nextMilestone.title}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -137,19 +137,17 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={milestone.id}
|
key={milestone.id}
|
||||||
className={`flex items-start gap-3 p-2 rounded-lg transition-all ${
|
className={`flex items-start gap-3 p-2 rounded-lg transition-all ${isAchieved
|
||||||
isAchieved
|
? 'bg-teal-500/20'
|
||||||
? 'bg-teal-500/20'
|
: 'bg-black/5 opacity-60 dark:bg-white/5'
|
||||||
: 'bg-white/5 opacity-60'
|
} ${isCurrent ? 'ring-2 ring-teal-500/50' : ''}`}
|
||||||
} ${isCurrent ? 'ring-2 ring-teal-400/50' : ''}`}
|
|
||||||
>
|
>
|
||||||
{/* Icon */}
|
{/* Icon */}
|
||||||
<div
|
<div
|
||||||
className={`p-2 rounded-full shrink-0 ${
|
className={`p-2 rounded-full shrink-0 ${isAchieved
|
||||||
isAchieved
|
? 'bg-teal-500/20 text-teal-600 dark:text-teal-300'
|
||||||
? 'bg-teal-500/30 text-teal-300'
|
: 'bg-black/5 text-black/40 dark:bg-white/10 dark:text-white/40'
|
||||||
: 'bg-white/10 text-white/40'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{isAchieved ? (
|
{isAchieved ? (
|
||||||
<CheckCircle2 className="h-4 w-4" />
|
<CheckCircle2 className="h-4 w-4" />
|
||||||
@ -161,9 +159,9 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP
|
|||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<p
|
<p className={`text-sm font-medium ${isAchieved
|
||||||
className={`text-sm font-medium ${
|
? (theme === 'light' ? 'text-teal-900' : 'text-white')
|
||||||
isAchieved ? 'text-white' : 'text-white/60'
|
: (theme === 'light' ? 'text-teal-900/60' : 'text-white/60')
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{milestone.title}
|
{milestone.title}
|
||||||
@ -178,8 +176,8 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP
|
|||||||
{milestone.description}
|
{milestone.description}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-1 mt-1">
|
<div className="flex items-center gap-1 mt-1">
|
||||||
<Clock className="h-3 w-3 text-white/40" />
|
<Clock className={`h-3 w-3 ${theme === 'light' ? 'text-teal-700/40' : 'text-white/40'}`} />
|
||||||
<span className="text-[10px] text-white/40">
|
<span className={`text-[10px] ${theme === 'light' ? 'text-teal-700/40' : 'text-white/40'}`}>
|
||||||
{formatDuration(milestone.timeMinutes)}
|
{formatDuration(milestone.timeMinutes)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -189,6 +187,6 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export function SavingsSetupDialog({
|
|||||||
costPerUnit: cost,
|
costPerUnit: cost,
|
||||||
unitsPerDay: units,
|
unitsPerDay: units,
|
||||||
currency,
|
currency,
|
||||||
substance,
|
substance: substance as 'nicotine' | 'weed',
|
||||||
savingsGoal: savingsGoal ? parseFloat(savingsGoal) : null,
|
savingsGoal: savingsGoal ? parseFloat(savingsGoal) : null,
|
||||||
goalName: goalName.trim() || null,
|
goalName: goalName.trim() || null,
|
||||||
};
|
};
|
||||||
@ -97,13 +97,13 @@ export function SavingsSetupDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
||||||
<DialogContent className="sm:max-w-md bg-emerald-950 border-emerald-800 text-white">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2 text-white">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<DollarSign className="h-5 w-5 text-emerald-400" />
|
<DollarSign className="h-5 w-5 text-emerald-500" />
|
||||||
{existingConfig ? 'Edit Savings Tracker' : 'Set Up Savings Tracker'}
|
{existingConfig ? 'Edit Savings Tracker' : 'Set Up Savings Tracker'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-emerald-200">
|
<DialogDescription>
|
||||||
Enter your usage costs to track how much you're saving
|
Enter your usage costs to track how much you're saving
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@ -111,9 +111,9 @@ export function SavingsSetupDialog({
|
|||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
{/* Substance Selection */}
|
{/* Substance Selection */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-white">What are you tracking?</Label>
|
<Label>What are you tracking?</Label>
|
||||||
<Select value={substance} onValueChange={(v) => setSubstance(v as 'nicotine' | 'weed')}>
|
<Select value={substance} onValueChange={(v) => setSubstance(v as 'nicotine' | 'weed')}>
|
||||||
<SelectTrigger className="border-emerald-700 bg-emerald-900/50 text-white">
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select substance" />
|
<SelectValue placeholder="Select substance" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -125,9 +125,9 @@ export function SavingsSetupDialog({
|
|||||||
|
|
||||||
{/* Currency Selection */}
|
{/* Currency Selection */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-white">Currency</Label>
|
<Label>Currency</Label>
|
||||||
<Select value={currency} onValueChange={setCurrency}>
|
<Select value={currency} onValueChange={setCurrency}>
|
||||||
<SelectTrigger className="border-emerald-700 bg-emerald-900/50 text-white">
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select currency" />
|
<SelectValue placeholder="Select currency" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -142,11 +142,11 @@ export function SavingsSetupDialog({
|
|||||||
|
|
||||||
{/* Cost Per Unit */}
|
{/* Cost Per Unit */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="costPerUnit" className="text-white">
|
<Label htmlFor="costPerUnit">
|
||||||
Cost per pack/cartridge/unit
|
Cost per pack/cartridge/unit
|
||||||
</Label>
|
</Label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-emerald-300">
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
|
||||||
{CURRENCIES.find((c) => c.code === currency)?.symbol || '$'}
|
{CURRENCIES.find((c) => c.code === currency)?.symbol || '$'}
|
||||||
</span>
|
</span>
|
||||||
<Input
|
<Input
|
||||||
@ -156,18 +156,18 @@ export function SavingsSetupDialog({
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
value={costPerUnit}
|
value={costPerUnit}
|
||||||
onChange={(e) => setCostPerUnit(e.target.value)}
|
onChange={(e) => setCostPerUnit(e.target.value)}
|
||||||
className="pl-8 border-emerald-700 bg-emerald-900/50 text-white placeholder:text-emerald-400"
|
className="pl-8"
|
||||||
placeholder="10.00"
|
placeholder="10.00"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-emerald-300">
|
<p className="text-xs text-muted-foreground">
|
||||||
How much does one pack or cartridge cost?
|
How much does one pack or cartridge cost?
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Units Per Week */}
|
{/* Units Per Week */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="unitsPerDay" className="text-white">
|
<Label htmlFor="unitsPerDay">
|
||||||
Packs/vapes per week (before quitting)
|
Packs/vapes per week (before quitting)
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -178,24 +178,23 @@ export function SavingsSetupDialog({
|
|||||||
value={unitsPerDay}
|
value={unitsPerDay}
|
||||||
onChange={(e) => setUnitsPerDay(e.target.value)}
|
onChange={(e) => setUnitsPerDay(e.target.value)}
|
||||||
placeholder="1"
|
placeholder="1"
|
||||||
className="border-emerald-700 bg-emerald-900/50 text-white placeholder:text-emerald-400"
|
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-emerald-300">
|
<p className="text-xs text-muted-foreground">
|
||||||
How many packs/vapes did you typically use per week?
|
How many packs/vapes did you typically use per week?
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Optional: Savings Goal */}
|
{/* Optional: Savings Goal */}
|
||||||
<div className="pt-4 border-t border-emerald-800 space-y-4">
|
<div className="pt-4 border-t space-y-4">
|
||||||
<div className="flex items-center gap-2 text-sm text-emerald-300">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<Target className="h-4 w-4" />
|
<Target className="h-4 w-4" />
|
||||||
<span>See your real time savings:</span>
|
<span>See your real time savings:</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="savingsGoal" className="text-white">Target amount</Label>
|
<Label htmlFor="savingsGoal">Target amount</Label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-emerald-300">
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
|
||||||
{CURRENCIES.find((c) => c.code === currency)?.symbol || '$'}
|
{CURRENCIES.find((c) => c.code === currency)?.symbol || '$'}
|
||||||
</span>
|
</span>
|
||||||
<Input
|
<Input
|
||||||
@ -205,34 +204,33 @@ export function SavingsSetupDialog({
|
|||||||
step="1"
|
step="1"
|
||||||
value={savingsGoal}
|
value={savingsGoal}
|
||||||
onChange={(e) => setSavingsGoal(e.target.value)}
|
onChange={(e) => setSavingsGoal(e.target.value)}
|
||||||
className="pl-8 border-emerald-700 bg-emerald-900/50 text-white placeholder:text-emerald-400"
|
className="pl-8"
|
||||||
placeholder="500"
|
placeholder="500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="goalName" className="text-white">What are you saving for?</Label>
|
<Label htmlFor="goalName">What are you saving for?</Label>
|
||||||
<Input
|
<Input
|
||||||
id="goalName"
|
id="goalName"
|
||||||
type="text"
|
type="text"
|
||||||
value={goalName}
|
value={goalName}
|
||||||
onChange={(e) => setGoalName(e.target.value)}
|
onChange={(e) => setGoalName(e.target.value)}
|
||||||
placeholder="e.g., New Phone, Vacation"
|
placeholder="e.g., New Phone, Vacation"
|
||||||
className="border-emerald-700 bg-emerald-900/50 text-white placeholder:text-emerald-400"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-2 pt-4">
|
<div className="flex gap-2 pt-4">
|
||||||
<Button onClick={onClose} className="flex-1 bg-emerald-600 hover:bg-emerald-500 text-white">
|
<Button onClick={onClose} variant="outline" className="flex-1">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
className="flex-1 bg-emerald-600 hover:bg-emerald-500 text-white"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
{existingConfig ? 'Update' : 'Save'}
|
{existingConfig ? 'Update' : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -232,7 +232,7 @@ export function UsageCalendar({ usageData, onDataUpdate }: UsageCalendarProps) {
|
|||||||
|
|
||||||
const calendarBackground = theme === 'light'
|
const calendarBackground = theme === 'light'
|
||||||
? 'linear-gradient(135deg, rgba(20, 20, 30, 0.95) 0%, rgba(30, 30, 45, 0.9) 100%)'
|
? 'linear-gradient(135deg, rgba(20, 20, 30, 0.95) 0%, rgba(30, 30, 45, 0.9) 100%)'
|
||||||
: undefined;
|
: 'linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(240, 240, 245, 0.9) 100%)';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -248,8 +248,8 @@ export function UsageCalendar({ usageData, onDataUpdate }: UsageCalendarProps) {
|
|||||||
mode="single"
|
mode="single"
|
||||||
selected={selectedDate}
|
selected={selectedDate}
|
||||||
onSelect={handleDateSelect}
|
onSelect={handleDateSelect}
|
||||||
className={`rounded-md border p-3 ${theme === 'light' ? 'text-white' : 'bg-background/50'}`}
|
className={`rounded-md border p-3 ${theme === 'light' ? 'text-white' : 'text-slate-900 bg-background/50'}`}
|
||||||
style={theme === 'light' ? { background: calendarBackground } : undefined}
|
style={{ background: calendarBackground }}
|
||||||
showOutsideDays={false}
|
showOutsideDays={false}
|
||||||
components={{
|
components={{
|
||||||
DayButton: CustomDayButton,
|
DayButton: CustomDayButton,
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { User } from '@/lib/session';
|
import { User } from '@/lib/session';
|
||||||
import { fetchPreferences, fetchReminderSettings, saveReminderSettings, ReminderSettings } from '@/lib/storage';
|
import { fetchPreferences, fetchReminderSettings, saveReminderSettings, ReminderSettings, UserPreferences } from '@/lib/storage';
|
||||||
import { useNotifications } from '@/hooks/useNotifications';
|
import { useNotifications } from '@/hooks/useNotifications';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@ -27,9 +27,10 @@ import { useTheme } from '@/lib/theme-context';
|
|||||||
|
|
||||||
interface UserHeaderProps {
|
interface UserHeaderProps {
|
||||||
user: User;
|
user: User;
|
||||||
|
preferences?: UserPreferences | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserHeader({ user }: 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' });
|
const [reminderSettings, setReminderSettings] = useState<ReminderSettings>({ enabled: false, reminderTime: '09:00' });
|
||||||
const [showReminderDialog, setShowReminderDialog] = useState(false);
|
const [showReminderDialog, setShowReminderDialog] = useState(false);
|
||||||
@ -40,16 +41,22 @@ export function UserHeader({ user }: UserHeaderProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
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([
|
const [prefs, reminders] = await Promise.all([
|
||||||
fetchPreferences(),
|
preferences ? Promise.resolve(preferences) : fetchPreferences(),
|
||||||
fetchReminderSettings(),
|
fetchReminderSettings(),
|
||||||
]);
|
]);
|
||||||
setUserName(prefs.userName);
|
|
||||||
|
if (prefs) {
|
||||||
|
setUserName(prefs.userName);
|
||||||
|
}
|
||||||
|
|
||||||
setReminderSettings(reminders);
|
setReminderSettings(reminders);
|
||||||
setLocalTime(reminders.reminderTime);
|
setLocalTime(reminders.reminderTime);
|
||||||
};
|
};
|
||||||
loadData();
|
loadData();
|
||||||
}, []);
|
}, [preferences]);
|
||||||
|
|
||||||
const handleToggleReminders = async () => {
|
const handleToggleReminders = async () => {
|
||||||
if (!reminderSettings.enabled && permission !== 'granted') {
|
if (!reminderSettings.enabled && permission !== 'granted') {
|
||||||
@ -82,8 +89,10 @@ export function UserHeader({ user }: UserHeaderProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 border-b border-white/10" style={{
|
<header className="sticky top-0 z-50 border-b border-border/10 transition-colors duration-300" style={{
|
||||||
background: 'linear-gradient(135deg, rgba(10, 10, 20, 0.98) 0%, rgba(20, 30, 60, 0.95) 50%, rgba(15, 25, 50, 0.98) 100%)',
|
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)',
|
backdropFilter: 'blur(10px)',
|
||||||
}}>
|
}}>
|
||||||
<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">
|
||||||
@ -101,7 +110,7 @@ export function UserHeader({ user }: UserHeaderProps) {
|
|||||||
QuitTraq
|
QuitTraq
|
||||||
</h1>
|
</h1>
|
||||||
{userName && (
|
{userName && (
|
||||||
<p className="text-white/90 text-lg hidden sm:block ml-4">
|
<p className="text-foreground/90 text-lg hidden sm:block ml-4">
|
||||||
Welcome {userName}, you got this!
|
Welcome {userName}, you got this!
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -110,23 +119,22 @@ export function UserHeader({ user }: UserHeaderProps) {
|
|||||||
<div className="flex items-center gap-2 sm:gap-3">
|
<div className="flex items-center gap-2 sm:gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowReminderDialog(true)}
|
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 ${
|
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
|
||||||
reminderSettings.enabled
|
? 'bg-indigo-500/30 hover:bg-indigo-500/40'
|
||||||
? 'bg-indigo-500/30 hover:bg-indigo-500/40'
|
: 'bg-muted hover:bg-muted/80'
|
||||||
: 'bg-white/10 hover:bg-white/20'
|
}`}
|
||||||
}`}
|
|
||||||
aria-label="Reminder settings"
|
aria-label="Reminder settings"
|
||||||
title={reminderSettings.enabled ? `Reminders on at ${reminderSettings.reminderTime}` : 'Reminders off'}
|
title={reminderSettings.enabled ? `Reminders on at ${reminderSettings.reminderTime}` : 'Reminders off'}
|
||||||
>
|
>
|
||||||
{reminderSettings.enabled ? (
|
{reminderSettings.enabled ? (
|
||||||
<BellRing className="h-5 w-5 text-indigo-300 transition-transform duration-300" />
|
<BellRing className="h-5 w-5 text-indigo-300 transition-transform duration-300" />
|
||||||
) : (
|
) : (
|
||||||
<Bell className="h-5 w-5 text-white/70 transition-transform duration-300" />
|
<Bell className="h-5 w-5 text-muted-foreground transition-transform duration-300" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
className="p-2.5 sm:p-2 rounded-full bg-white/10 hover:bg-white/20 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-white/30 hover:scale-110 active:scale-95"
|
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"
|
aria-label="Toggle theme"
|
||||||
>
|
>
|
||||||
{theme === 'dark' ? (
|
{theme === 'dark' ? (
|
||||||
@ -137,17 +145,17 @@ export function UserHeader({ user }: UserHeaderProps) {
|
|||||||
</button>
|
</button>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button className="flex items-center gap-2 px-3 py-2 rounded-full bg-white/10 hover:bg-white/20 transition-all focus:outline-none focus:ring-2 focus:ring-white/30">
|
<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-white/30">
|
<Avatar className="h-8 w-8 ring-2 ring-primary/30">
|
||||||
<AvatarImage src={user.profilePictureUrl ?? undefined} alt={userName || 'User'} />
|
<AvatarImage src={user.profilePictureUrl ?? undefined} alt={userName || 'User'} />
|
||||||
<AvatarFallback className="bg-white/20 text-white text-sm">{initials}</AvatarFallback>
|
<AvatarFallback className="bg-primary/20 text-primary text-sm">{initials}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<ChevronDown className="h-4 w-4 text-white" />
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" sideOffset={8}>
|
<DropdownMenuContent align="end" sideOffset={8}>
|
||||||
<DropdownMenuItem onClick={() => handleNavigate('/')}>
|
<DropdownMenuItem onClick={() => handleNavigate('/')}>
|
||||||
<Home className="mr-3 h-4 w-4 text-white/70" />
|
<Home className="mr-3 h-4 w-4 text-muted-foreground" />
|
||||||
<span>Dashboard</span>
|
<span>Dashboard</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
@ -170,7 +178,7 @@ export function UserHeader({ user }: UserHeaderProps) {
|
|||||||
</div>
|
</div>
|
||||||
{userName && (
|
{userName && (
|
||||||
<div className="sm:hidden container mx-auto px-4 pb-2">
|
<div className="sm:hidden container mx-auto px-4 pb-2">
|
||||||
<p className="text-white/90 text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
Welcome {userName}, you got this!
|
Welcome {userName}, you got this!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -187,19 +195,7 @@ export function UserHeader({ user }: UserHeaderProps) {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
{/* Permission Status */}
|
|
||||||
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
|
||||||
<span className="text-sm text-muted-foreground">Notifications</span>
|
|
||||||
<span className={`text-sm font-medium ${
|
|
||||||
!isSupported ? 'text-red-400' :
|
|
||||||
permission === 'granted' ? 'text-green-400' :
|
|
||||||
permission === 'denied' ? 'text-red-400' : 'text-yellow-400'
|
|
||||||
}`}>
|
|
||||||
{!isSupported ? 'Not supported' :
|
|
||||||
permission === 'granted' ? 'Enabled' :
|
|
||||||
permission === 'denied' ? 'Blocked' : 'Not set'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Enable/Disable Toggle */}
|
{/* Enable/Disable Toggle */}
|
||||||
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
||||||
@ -215,15 +211,13 @@ export function UserHeader({ user }: UserHeaderProps) {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleToggleReminders}
|
onClick={handleToggleReminders}
|
||||||
disabled={!isSupported || permission === 'denied'}
|
disabled={!isSupported || (permission === 'denied' && !reminderSettings.enabled)}
|
||||||
className={`relative w-12 h-6 rounded-full transition-all duration-300 ${
|
className={`relative w-12 h-6 rounded-full transition-all duration-300 ${reminderSettings.enabled ? 'bg-indigo-500' : 'bg-muted-foreground/30'
|
||||||
reminderSettings.enabled ? 'bg-indigo-500' : 'bg-muted-foreground/30'
|
} ${!isSupported || (permission === 'denied' && !reminderSettings.enabled) ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||||
} ${!isSupported || permission === 'denied' ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-all duration-300 ${
|
className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-all duration-300 ${reminderSettings.enabled ? 'left-7' : 'left-1'
|
||||||
reminderSettings.enabled ? 'left-7' : 'left-1'
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -123,6 +123,7 @@ export function getCurrentUserId(): string | null {
|
|||||||
|
|
||||||
// Async API functions
|
// Async API functions
|
||||||
export async function fetchPreferences(): Promise<UserPreferences> {
|
export async function fetchPreferences(): Promise<UserPreferences> {
|
||||||
|
if (preferencesCache) return preferencesCache;
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/preferences');
|
const response = await fetch('/api/preferences');
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -154,6 +155,7 @@ export async function savePreferencesAsync(preferences: UserPreferences): Promis
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchUsageData(): Promise<UsageEntry[]> {
|
export async function fetchUsageData(): Promise<UsageEntry[]> {
|
||||||
|
if (usageDataCache) return usageDataCache;
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/usage');
|
const response = await fetch('/api/usage');
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -216,6 +218,7 @@ export async function clearDayDataAsync(
|
|||||||
// ============ ACHIEVEMENTS FUNCTIONS ============
|
// ============ ACHIEVEMENTS FUNCTIONS ============
|
||||||
|
|
||||||
export async function fetchAchievements(): Promise<Achievement[]> {
|
export async function fetchAchievements(): Promise<Achievement[]> {
|
||||||
|
if (achievementsCache) return achievementsCache;
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/achievements');
|
const response = await fetch('/api/achievements');
|
||||||
if (!response.ok) return [];
|
if (!response.ok) return [];
|
||||||
@ -264,6 +267,7 @@ export function getAchievements(): Achievement[] {
|
|||||||
// ============ REMINDERS FUNCTIONS ============
|
// ============ REMINDERS FUNCTIONS ============
|
||||||
|
|
||||||
export async function fetchReminderSettings(): Promise<ReminderSettings> {
|
export async function fetchReminderSettings(): Promise<ReminderSettings> {
|
||||||
|
if (reminderSettingsCache) return reminderSettingsCache;
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/reminders');
|
const response = await fetch('/api/reminders');
|
||||||
if (!response.ok) return { enabled: false, reminderTime: '09:00' };
|
if (!response.ok) return { enabled: false, reminderTime: '09:00' };
|
||||||
@ -298,6 +302,7 @@ export function getReminderSettings(): ReminderSettings {
|
|||||||
// ============ SAVINGS FUNCTIONS ============
|
// ============ SAVINGS FUNCTIONS ============
|
||||||
|
|
||||||
export async function fetchSavingsConfig(): Promise<SavingsConfig | null> {
|
export async function fetchSavingsConfig(): Promise<SavingsConfig | null> {
|
||||||
|
if (savingsConfigCache) return savingsConfigCache;
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/savings');
|
const response = await fetch('/api/savings');
|
||||||
if (!response.ok) return null;
|
if (!response.ok) return null;
|
||||||
@ -403,7 +408,7 @@ export function getMinutesSinceQuit(
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
const todayStr = now.toISOString().split('T')[0];
|
const todayStr = now.toISOString().split('T')[0];
|
||||||
const lastUsageDateStr = substanceData[0].date;
|
const lastUsageDateStr = substanceData[0].date;
|
||||||
|
|
||||||
// If the last usage was today, reset to 0 (just used)
|
// If the last usage was today, reset to 0 (just used)
|
||||||
if (lastUsageDateStr === todayStr) {
|
if (lastUsageDateStr === todayStr) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@ -21,6 +21,15 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
if (theme === 'dark') {
|
||||||
|
root.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
root.classList.remove('dark');
|
||||||
|
}
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
const newTheme = theme === 'dark' ? 'light' : 'dark';
|
const newTheme = theme === 'dark' ? 'light' : 'dark';
|
||||||
setTheme(newTheme);
|
setTheme(newTheme);
|
||||||
|
|||||||
9
src/proxy 2.ts
Normal file
9
src/proxy 2.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { authkitMiddleware } from "@workos-inc/authkit-nextjs";
|
||||||
|
|
||||||
|
export const proxy = authkitMiddleware();
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
|
||||||
|
],
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user