From edfc97821735e9f216d8d106ee3d7441d135ee12 Mon Sep 17 00:00:00 2001 From: Avery Felts Date: Sat, 24 Jan 2026 13:11:43 -0700 Subject: [PATCH] Optimize performance, fix theme consistency, and improve notification UX --- prisma.config.ts | 14 +++ src/app/globals.css | 129 ++++++++++++++++++++------ src/components/Dashboard.tsx | 8 +- src/components/HealthTimelineCard.tsx | 46 +++++---- src/components/SavingsSetupDialog.tsx | 48 +++++----- src/components/UsageCalendar.tsx | 6 +- src/components/UserHeader.tsx | 76 +++++++-------- src/lib/storage.ts | 7 +- src/lib/theme-context.tsx | 9 ++ src/proxy 2.ts | 9 ++ 10 files changed, 224 insertions(+), 128 deletions(-) create mode 100644 prisma.config.ts create mode 100644 src/proxy 2.ts diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..831a20f --- /dev/null +++ b/prisma.config.ts @@ -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"], + }, +}); diff --git a/src/app/globals.css b/src/app/globals.css index f5576e2..74c8c00 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -163,25 +163,33 @@ --tracking-wide: calc(var(--tracking-normal) + 0.025em); --tracking-wider: calc(var(--tracking-normal) + 0.05em); --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 { + html { + scroll-behavior: smooth; + } + * { @apply border-border outline-ring/50; } + body { @apply text-foreground; font-family: var(--font-sans); letter-spacing: var(--tracking-normal); - background: linear-gradient(135deg, - #0f0f1a 0%, - #1a1a2e 20%, - #16213e 40%, - #1a1a2e 60%, - #0f0f1a 80%, - #1a1a2e 100%); - background-attachment: fixed; + background-color: transparent; min-height: 100vh; + overflow-x: hidden; } body::before { @@ -191,14 +199,21 @@ left: 0; right: 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 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 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%); - pointer-events: none; - z-index: -1; } /* Calendar styling - reduce overall size */ @@ -217,8 +232,13 @@ /* Animation keyframes */ @keyframes fade-in { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + + to { + opacity: 1; + } } @keyframes fade-in-up { @@ -226,6 +246,7 @@ opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); @@ -237,6 +258,7 @@ opacity: 0; transform: translateY(-20px); } + to { opacity: 1; transform: translateY(0); @@ -248,6 +270,7 @@ opacity: 0; transform: scale(0.95); } + to { opacity: 1; transform: scale(1); @@ -259,6 +282,7 @@ opacity: 0; transform: translateX(20px); } + to { opacity: 1; transform: translateX(0); @@ -270,6 +294,7 @@ opacity: 0; transform: translateX(-20px); } + to { opacity: 1; transform: translateX(0); @@ -277,23 +302,49 @@ } @keyframes pulse-subtle { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.7; } + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.7; + } } @keyframes float { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-5px); } + + 0%, + 100% { + transform: translateY(0); + } + + 50% { + transform: translateY(-5px); + } } @keyframes shimmer { - 0% { background-position: -200% 0; } - 100% { background-position: 200% 0; } + 0% { + background-position: -200% 0; + } + + 100% { + background-position: 200% 0; + } } @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 { @@ -301,6 +352,7 @@ transform: translateY(0) rotate(0deg); opacity: 1; } + 100% { transform: translateY(100vh) rotate(720deg); opacity: 0; @@ -349,15 +401,34 @@ } /* Stagger delay utilities */ -.delay-100 { animation-delay: 100ms; } -.delay-200 { animation-delay: 200ms; } -.delay-300 { animation-delay: 300ms; } -.delay-400 { animation-delay: 400ms; } -.delay-500 { animation-delay: 500ms; } -.delay-600 { animation-delay: 600ms; } +.delay-100 { + animation-delay: 100ms; +} + +.delay-200 { + 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 */ -.opacity-0 { opacity: 0; } +.opacity-0 { + opacity: 0; +} /* Smooth transitions */ .transition-smooth { @@ -474,4 +545,4 @@ opacity: 0.03; pointer-events: none; border-radius: inherit; -} +} \ No newline at end of file diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index f385d5c..7182c55 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -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 ( -
- +
+
{preferences && ( diff --git a/src/components/HealthTimelineCard.tsx b/src/components/HealthTimelineCard.tsx index f9f1af3..6b969fc 100644 --- a/src/components/HealthTimelineCard.tsx +++ b/src/components/HealthTimelineCard.tsx @@ -85,7 +85,7 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP const cardBackground = 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%)'; const substanceLabel = substance === 'nicotine' ? 'Nicotine' : 'Marijuana'; @@ -98,11 +98,11 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP
- - + + Health Recovery -

+

{substanceLabel}-free for {formatDuration(minutesSinceQuit)}

@@ -110,10 +110,10 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP {/* Progress to next milestone */} {nextMilestone && ( -
+
- Next milestone - + Next milestone + {formatTimeRemaining(minutesSinceQuit, nextMilestone.timeMinutes)}
@@ -123,7 +123,7 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP style={{ width: `${progressToNext}%` }} />
-

{nextMilestone.title}

+

{nextMilestone.title}

)} @@ -137,19 +137,17 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP return (
{/* Icon */}
{isAchieved ? ( @@ -161,9 +159,9 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP {/* Content */}
-

{milestone.title} @@ -178,8 +176,8 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP {milestone.description}

- - + + {formatDuration(milestone.timeMinutes)}
@@ -189,6 +187,6 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP })}
- + ); } diff --git a/src/components/SavingsSetupDialog.tsx b/src/components/SavingsSetupDialog.tsx index 2c1a7cb..7e200c1 100644 --- a/src/components/SavingsSetupDialog.tsx +++ b/src/components/SavingsSetupDialog.tsx @@ -79,7 +79,7 @@ export function SavingsSetupDialog({ costPerUnit: cost, unitsPerDay: units, currency, - substance, + substance: substance as 'nicotine' | 'weed', savingsGoal: savingsGoal ? parseFloat(savingsGoal) : null, goalName: goalName.trim() || null, }; @@ -97,13 +97,13 @@ export function SavingsSetupDialog({ return ( !isOpen && onClose()}> - + - - + + {existingConfig ? 'Edit Savings Tracker' : 'Set Up Savings Tracker'} - + Enter your usage costs to track how much you're saving @@ -111,9 +111,9 @@ export function SavingsSetupDialog({
{/* Substance Selection */}
- + - + @@ -142,11 +142,11 @@ export function SavingsSetupDialog({ {/* Cost Per Unit */}
-
{/* Units Per Week */}
-
{/* Optional: Savings Goal */} -
-
+
+
See your real time savings:
- +
- + {CURRENCIES.find((c) => c.code === currency)?.symbol || '$'} 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" />
- + setGoalName(e.target.value)} placeholder="e.g., New Phone, Vacation" - className="border-emerald-700 bg-emerald-900/50 text-white placeholder:text-emerald-400" />
{/* Actions */}
- diff --git a/src/components/UsageCalendar.tsx b/src/components/UsageCalendar.tsx index f138780..dc18da8 100644 --- a/src/components/UsageCalendar.tsx +++ b/src/components/UsageCalendar.tsx @@ -232,7 +232,7 @@ export function UsageCalendar({ usageData, onDataUpdate }: UsageCalendarProps) { const calendarBackground = theme === 'light' ? '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 ( <> @@ -248,8 +248,8 @@ export function UsageCalendar({ usageData, onDataUpdate }: UsageCalendarProps) { mode="single" selected={selectedDate} onSelect={handleDateSelect} - className={`rounded-md border p-3 ${theme === 'light' ? 'text-white' : 'bg-background/50'}`} - style={theme === 'light' ? { background: calendarBackground } : undefined} + className={`rounded-md border p-3 ${theme === 'light' ? 'text-white' : 'text-slate-900 bg-background/50'}`} + style={{ background: calendarBackground }} showOutsideDays={false} components={{ DayButton: CustomDayButton, diff --git a/src/components/UserHeader.tsx b/src/components/UserHeader.tsx index e92f217..b34e80e 100644 --- a/src/components/UserHeader.tsx +++ b/src/components/UserHeader.tsx @@ -18,7 +18,7 @@ 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 } from '@/lib/storage'; +import { fetchPreferences, fetchReminderSettings, saveReminderSettings, ReminderSettings, UserPreferences } from '@/lib/storage'; import { useNotifications } from '@/hooks/useNotifications'; import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; @@ -27,9 +27,10 @@ import { useTheme } from '@/lib/theme-context'; interface UserHeaderProps { user: User; + preferences?: UserPreferences | null; } -export function UserHeader({ user }: UserHeaderProps) { +export function UserHeader({ user, preferences }: UserHeaderProps) { const [userName, setUserName] = useState(null); const [reminderSettings, setReminderSettings] = useState({ enabled: false, reminderTime: '09:00' }); const [showReminderDialog, setShowReminderDialog] = useState(false); @@ -40,16 +41,22 @@ export function UserHeader({ user }: UserHeaderProps) { 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([ - fetchPreferences(), + preferences ? Promise.resolve(preferences) : fetchPreferences(), fetchReminderSettings(), ]); - setUserName(prefs.userName); + + if (prefs) { + setUserName(prefs.userName); + } + setReminderSettings(reminders); setLocalTime(reminders.reminderTime); }; loadData(); - }, []); + }, [preferences]); const handleToggleReminders = async () => { if (!reminderSettings.enabled && permission !== 'granted') { @@ -82,8 +89,10 @@ export function UserHeader({ user }: UserHeaderProps) { }; return ( -
@@ -101,7 +110,7 @@ export function UserHeader({ user }: UserHeaderProps) { QuitTraq {userName && ( -

+

Welcome {userName}, you got this!

)} @@ -110,23 +119,22 @@ export function UserHeader({ user }: UserHeaderProps) {
- handleNavigate('/')}> - + Dashboard @@ -170,7 +178,7 @@ export function UserHeader({ user }: UserHeaderProps) {
{userName && (
-

+

Welcome {userName}, you got this!

@@ -187,19 +195,7 @@ export function UserHeader({ user }: UserHeaderProps) {
- {/* Permission Status */} -
- Notifications - - {!isSupported ? 'Not supported' : - permission === 'granted' ? 'Enabled' : - permission === 'denied' ? 'Blocked' : 'Not set'} - -
+ {/* Enable/Disable Toggle */}
@@ -215,15 +211,13 @@ export function UserHeader({ user }: UserHeaderProps) {
diff --git a/src/lib/storage.ts b/src/lib/storage.ts index 26d43be..9c16529 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -123,6 +123,7 @@ export function getCurrentUserId(): string | null { // Async API functions export async function fetchPreferences(): Promise { + if (preferencesCache) return preferencesCache; try { const response = await fetch('/api/preferences'); if (!response.ok) { @@ -154,6 +155,7 @@ export async function savePreferencesAsync(preferences: UserPreferences): Promis } export async function fetchUsageData(): Promise { + if (usageDataCache) return usageDataCache; try { const response = await fetch('/api/usage'); if (!response.ok) { @@ -216,6 +218,7 @@ export async function clearDayDataAsync( // ============ ACHIEVEMENTS FUNCTIONS ============ export async function fetchAchievements(): Promise { + if (achievementsCache) return achievementsCache; try { const response = await fetch('/api/achievements'); if (!response.ok) return []; @@ -264,6 +267,7 @@ export function getAchievements(): Achievement[] { // ============ REMINDERS FUNCTIONS ============ export async function fetchReminderSettings(): Promise { + if (reminderSettingsCache) return reminderSettingsCache; try { const response = await fetch('/api/reminders'); if (!response.ok) return { enabled: false, reminderTime: '09:00' }; @@ -298,6 +302,7 @@ export function getReminderSettings(): ReminderSettings { // ============ SAVINGS FUNCTIONS ============ export async function fetchSavingsConfig(): Promise { + if (savingsConfigCache) return savingsConfigCache; try { const response = await fetch('/api/savings'); if (!response.ok) return null; @@ -403,7 +408,7 @@ export function getMinutesSinceQuit( const now = new Date(); const todayStr = now.toISOString().split('T')[0]; const lastUsageDateStr = substanceData[0].date; - + // If the last usage was today, reset to 0 (just used) if (lastUsageDateStr === todayStr) { return 0; diff --git a/src/lib/theme-context.tsx b/src/lib/theme-context.tsx index 81ce909..d8a450d 100644 --- a/src/lib/theme-context.tsx +++ b/src/lib/theme-context.tsx @@ -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 newTheme = theme === 'dark' ? 'light' : 'dark'; setTheme(newTheme); diff --git a/src/proxy 2.ts b/src/proxy 2.ts new file mode 100644 index 0000000..5c6e3a3 --- /dev/null +++ b/src/proxy 2.ts @@ -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)$).*)", + ], +}; \ No newline at end of file