From fac443c281d1e69f479400d114091b0cdbf13ffa Mon Sep 17 00:00:00 2001 From: Avery Felts Date: Sat, 24 Jan 2026 03:17:09 -0700 Subject: [PATCH] Add dark/light theme toggle with adaptive styling Implement theme context provider with localStorage persistence and add toggle button to header. Update Dashboard, StatsCard, QuitPlanCard, SubstanceTrackingPage, and UsageCalendar components with theme-aware gradients and colors. Also add daily inspirational quotes to calendar and fix usage prompt to only show once per day. --- src/app/globals.css | 5 + src/app/layout.tsx | 3 +- src/components/Dashboard.tsx | 10 +- src/components/Providers.tsx | 8 ++ src/components/QuitPlanCard.tsx | 17 +++- src/components/StatsCard.tsx | 19 +++- src/components/SubstanceTrackingPage.tsx | 18 ++-- src/components/UsageCalendar.tsx | 115 +++++++++++++++++++---- src/components/UserHeader.tsx | 25 +++-- src/lib/storage.ts | 16 +++- src/lib/theme-context.tsx | 43 +++++++++ 11 files changed, 239 insertions(+), 40 deletions(-) create mode 100644 src/components/Providers.tsx create mode 100644 src/lib/theme-context.tsx diff --git a/src/app/globals.css b/src/app/globals.css index e2828f4..104d828 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -197,4 +197,9 @@ pointer-events: none; z-index: -1; } + + /* Calendar styling - reduce overall size */ + .rdp { + --rdp-cell-size: 36px; + } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 0439016..630e5e4 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import "./globals.css"; +import { Providers } from "@/components/Providers"; export const metadata: Metadata = { title: "QuitTraq - Track Your Journey to Quit Smoking", @@ -19,7 +20,7 @@ export default function RootLayout({ - {children} + {children} ); diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index f93449c..4e492fc 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -8,6 +8,7 @@ import { savePreferencesAsync, saveUsageEntryAsync, shouldShowUsagePrompt, + markPromptShown, generateQuitPlan, UserPreferences, UsageEntry, @@ -20,6 +21,7 @@ import { StatsCard } from './StatsCard'; import { QuitPlanCard } from './QuitPlanCard'; import { Button } from '@/components/ui/button'; import { PlusCircle } from 'lucide-react'; +import { useTheme } from '@/lib/theme-context'; interface DashboardProps { user: User; @@ -32,6 +34,7 @@ export function Dashboard({ user }: DashboardProps) { const [showUsagePrompt, setShowUsagePrompt] = useState(false); const [isLoading, setIsLoading] = useState(true); const [refreshKey, setRefreshKey] = useState(0); + const { theme } = useTheme(); const loadData = useCallback(async () => { const [prefs, usage] = await Promise.all([ @@ -52,6 +55,7 @@ export function Dashboard({ user }: DashboardProps) { setShowSetup(true); } else if (shouldShowUsagePrompt()) { setShowUsagePrompt(true); + markPromptShown(); } setIsLoading(false); @@ -121,8 +125,12 @@ 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 ( -
+
diff --git a/src/components/Providers.tsx b/src/components/Providers.tsx new file mode 100644 index 0000000..7820894 --- /dev/null +++ b/src/components/Providers.tsx @@ -0,0 +1,8 @@ +'use client'; + +import { ThemeProvider } from '@/lib/theme-context'; +import { ReactNode } from 'react'; + +export function Providers({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/src/components/QuitPlanCard.tsx b/src/components/QuitPlanCard.tsx index 0cb31a6..79d234e 100644 --- a/src/components/QuitPlanCard.tsx +++ b/src/components/QuitPlanCard.tsx @@ -4,6 +4,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Button } from '@/components/ui/button'; import { QuitPlan, UsageEntry } from '@/lib/storage'; import { Target, TrendingDown } from 'lucide-react'; +import { useTheme } from '@/lib/theme-context'; interface QuitPlanCardProps { plan: QuitPlan | null; @@ -16,6 +17,8 @@ export function QuitPlanCard({ onGeneratePlan, usageData, }: QuitPlanCardProps) { + const { theme } = useTheme(); + // Count unique days with any logged data const uniqueDaysWithData = new Set(usageData.map(e => e.date)).size; const daysRemaining = Math.max(0, 7 - uniqueDaysWithData); @@ -25,10 +28,20 @@ export function QuitPlanCard({ const totalUsage = usageData.reduce((sum, e) => sum + e.count, 0); const currentAverage = uniqueDaysWithData > 0 ? Math.round(totalUsage / uniqueDaysWithData) : 0; + // Yellow gradient for tracking phase (darker in light mode) + const yellowBackground = theme === 'light' + ? 'linear-gradient(135deg, rgba(161, 98, 7, 0.85) 0%, rgba(133, 77, 14, 0.9) 100%)' + : 'linear-gradient(135deg, rgba(234, 179, 8, 0.2) 0%, rgba(202, 138, 4, 0.15) 100%)'; + + // Pink gradient for active plan (darker in light mode) + const pinkBackground = theme === 'light' + ? 'linear-gradient(135deg, rgba(157, 23, 77, 0.85) 0%, rgba(131, 24, 67, 0.9) 100%)' + : 'linear-gradient(135deg, rgba(236, 72, 153, 0.2) 0%, rgba(219, 39, 119, 0.15) 100%)'; + if (!plan) { return ( @@ -94,7 +107,7 @@ export function QuitPlanCard({ return ( diff --git a/src/components/StatsCard.tsx b/src/components/StatsCard.tsx index b3e5339..a1daf08 100644 --- a/src/components/StatsCard.tsx +++ b/src/components/StatsCard.tsx @@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { UsageEntry } from '@/lib/storage'; import { Cigarette, Leaf } from 'lucide-react'; +import { useTheme } from '@/lib/theme-context'; interface StatsCardProps { usageData: UsageEntry[]; @@ -10,6 +11,7 @@ interface StatsCardProps { } export function StatsCard({ usageData, substance }: StatsCardProps) { + const { theme } = useTheme(); const substanceData = usageData.filter((e) => e.substance === substance); // Calculate stats @@ -54,12 +56,21 @@ export function StatsCard({ usageData, substance }: StatsCardProps) { const unitLabel = substance === 'nicotine' ? 'puffs' : 'hits'; const iconColor = substance === 'nicotine' ? 'text-red-400' : 'text-green-400'; const borderColor = substance === 'nicotine' ? 'border-red-500/30' : 'border-green-500/30'; - const bgGradient = substance === 'nicotine' - ? 'from-red-500/20 to-red-900/10' - : 'from-green-500/20 to-green-900/10'; + + // Use darker gradients in light mode for better contrast + const cardBackground = theme === 'light' + ? substance === 'nicotine' + ? 'linear-gradient(135deg, rgba(185, 28, 28, 0.85) 0%, rgba(127, 29, 29, 0.9) 100%)' + : 'linear-gradient(135deg, rgba(22, 101, 52, 0.85) 0%, rgba(20, 83, 45, 0.9) 100%)' + : substance === 'nicotine' + ? 'linear-gradient(135deg, rgba(239, 68, 68, 0.2) 0%, rgba(127, 29, 29, 0.1) 100%)' + : 'linear-gradient(135deg, rgba(34, 197, 94, 0.2) 0%, rgba(20, 83, 45, 0.1) 100%)'; return ( - + diff --git a/src/components/SubstanceTrackingPage.tsx b/src/components/SubstanceTrackingPage.tsx index cfc9290..52d9b8d 100644 --- a/src/components/SubstanceTrackingPage.tsx +++ b/src/components/SubstanceTrackingPage.tsx @@ -7,6 +7,7 @@ import { UserHeader } from './UserHeader'; import { StatsCard } from './StatsCard'; import { UsageTrendGraph } from './UsageTrendGraph'; import { Cigarette, Leaf } from 'lucide-react'; +import { useTheme } from '@/lib/theme-context'; interface SubstanceTrackingPageProps { user: User; @@ -16,6 +17,7 @@ interface SubstanceTrackingPageProps { export function SubstanceTrackingPage({ user, substance }: SubstanceTrackingPageProps) { const [usageData, setUsageData] = useState([]); const [isLoading, setIsLoading] = useState(true); + const { theme } = useTheme(); const loadData = useCallback(async () => { const usage = await fetchUsageData(); @@ -50,8 +52,12 @@ export function SubstanceTrackingPage({ user, substance }: SubstanceTrackingPage ); } + const pageBackground = theme === 'dark' + ? 'linear-gradient(135deg, #0a0a14 0%, #141e3c 50%, #0f1932 100%)' + : 'linear-gradient(135deg, #ffffff 0%, #f0f4f8 50%, #e8ecf0 100%)'; + return ( -
+
@@ -62,8 +68,8 @@ export function SubstanceTrackingPage({ user, substance }: SubstanceTrackingPage
-

{substanceLabel} Tracking

-

Monitor your {substanceLabel.toLowerCase()} usage and progress

+

{substanceLabel} Tracking

+

Monitor your {substanceLabel.toLowerCase()} usage and progress

@@ -71,11 +77,11 @@ export function SubstanceTrackingPage({ user, substance }: SubstanceTrackingPage {/* Today's Status Message */}
{todayCount === 0 ? ( -

+

Great job, nothing yet!

) : ( -

+

{todayCount} {todayCount === 1 ? (substance === 'nicotine' ? 'puff' : 'hit') : unitLabel} recorded, you got this!

)} @@ -83,7 +89,7 @@ export function SubstanceTrackingPage({ user, substance }: SubstanceTrackingPage {/* Inspirational Message */}
-

+

"One day at a time..."

diff --git a/src/components/UsageCalendar.tsx b/src/components/UsageCalendar.tsx index 4c5bcba..3a4b8e1 100644 --- a/src/components/UsageCalendar.tsx +++ b/src/components/UsageCalendar.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useCallback } from 'react'; +import { useState, useCallback, useMemo } from 'react'; import { DayPicker, DayButtonProps } from 'react-day-picker'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { @@ -13,7 +13,42 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { UsageEntry, setUsageForDateAsync, clearDayDataAsync } from '@/lib/storage'; -import { ChevronLeftIcon, ChevronRightIcon, Cigarette, Leaf } from 'lucide-react'; +import { ChevronLeftIcon, ChevronRightIcon, Cigarette, Leaf, Sparkles } from 'lucide-react'; +import { useTheme } from '@/lib/theme-context'; + +const quotes = [ + { text: "The secret of getting ahead is getting started.", author: "Mark Twain" }, + { text: "It does not matter how slowly you go as long as you do not stop.", author: "Confucius" }, + { text: "Your future self will thank you for the choices you make today.", author: "Unknown" }, + { text: "Every moment is a fresh beginning.", author: "T.S. Eliot" }, + { text: "Believe you can and you're halfway there.", author: "Theodore Roosevelt" }, + { text: "Small steps every day lead to big changes over time.", author: "Unknown" }, + { text: "You are stronger than your cravings.", author: "Unknown" }, + { text: "The best time to plant a tree was 20 years ago. The second best time is now.", author: "Chinese Proverb" }, + { text: "Progress, not perfection, is what we should be asking of ourselves.", author: "Julia Cameron" }, + { text: "The greatest glory in living lies not in never falling, but in rising every time we fall.", author: "Nelson Mandela" }, + { text: "Your life does not get better by chance, it gets better by change.", author: "Jim Rohn" }, + { text: "You don't have to be great to start, but you have to start to be great.", author: "Zig Ziglar" }, + { text: "The pain of discipline is far less than the pain of regret.", author: "Sarah Bombell" }, + { text: "One day or day one. You decide.", author: "Unknown" }, + { text: "Your health is an investment, not an expense.", author: "Unknown" }, + { text: "The comeback is always stronger than the setback.", author: "Unknown" }, + { text: "Difficult roads often lead to beautiful destinations.", author: "Zig Ziglar" }, + { text: "Success is the sum of small efforts repeated day in and day out.", author: "Robert Collier" }, + { text: "Fall seven times, stand up eight.", author: "Japanese Proverb" }, + { text: "Great things never come from comfort zones.", author: "Unknown" }, + { text: "Every champion was once a contender that refused to give up.", author: "Rocky Balboa" }, + { text: "Don't stop when you're tired. Stop when you're done.", author: "Unknown" }, + { text: "Break free from the chains of habit and unlock your true potential.", author: "Unknown" }, + { text: "Dream it. Wish it. Do it.", author: "Unknown" }, + { text: "Your limitation—it's only your imagination.", author: "Unknown" }, + { text: "You are not defined by your past. You are prepared by it.", author: "Unknown" }, + { text: "Don't let yesterday take up too much of today.", author: "Will Rogers" }, + { text: "What lies behind us and what lies before us are tiny matters compared to what lies within us.", author: "Ralph Waldo Emerson" }, + { text: "The only person you are destined to become is the person you decide to be.", author: "Ralph Waldo Emerson" }, + { text: "The only way to do great work is to love what you do.", author: "Steve Jobs" }, + { text: "The harder you work for something, the greater you'll feel when you achieve it.", author: "Unknown" }, +]; interface UsageCalendarProps { usageData: UsageEntry[]; @@ -26,6 +61,17 @@ export function UsageCalendar({ usageData, onDataUpdate }: UsageCalendarProps) { const [editNicotineCount, setEditNicotineCount] = useState(''); const [editWeedCount, setEditWeedCount] = useState(''); const [isEditing, setIsEditing] = useState(false); + const { theme } = useTheme(); + + // Get a quote based on the day of the year + const dailyQuote = useMemo(() => { + const now = new Date(); + const start = new Date(now.getFullYear(), 0, 0); + const diff = now.getTime() - start.getTime(); + const oneDay = 1000 * 60 * 60 * 24; + const dayOfYear = Math.floor(diff / oneDay); + return quotes[dayOfYear % quotes.length]; + }, []); const getUsageForDate = (date: Date, substance: 'nicotine' | 'weed'): number => { const dateStr = date.toISOString().split('T')[0]; @@ -102,9 +148,12 @@ export function UsageCalendar({ usageData, onDataUpdate }: UsageCalendarProps) { } if (!hasNicotine && !hasWeed) { - // No usage - light blue hue + // No usage - lighter blue in light mode, standard blue in dark mode + const blueGradient = theme === 'light' + ? 'linear-gradient(135deg, rgba(147, 197, 253, 0.7) 0%, rgba(96, 165, 250, 0.8) 100%)' + : 'linear-gradient(135deg, rgba(96, 165, 250, 0.5) 0%, rgba(59, 130, 246, 0.6) 100%)'; return { - background: 'linear-gradient(135deg, rgba(96, 165, 250, 0.5) 0%, rgba(59, 130, 246, 0.6) 100%)', + background: blueGradient, color: 'white', }; } @@ -132,7 +181,7 @@ export function UsageCalendar({ usageData, onDataUpdate }: UsageCalendarProps) { background: `linear-gradient(135deg, rgba(239, 68, 68, ${0.5 + intensity * 0.4}) 0%, rgba(185, 28, 28, ${0.6 + intensity * 0.4}) 100%)`, color: 'white', }; - }, []); + }, [theme]); const CustomDayButton = useCallback(({ day, modifiers, ...props }: DayButtonProps) => { const date = day.date; @@ -176,6 +225,10 @@ export function UsageCalendar({ usageData, onDataUpdate }: UsageCalendarProps) { ); }, [usageData, getColorStyle]); + const calendarBackground = theme === 'light' + ? 'linear-gradient(135deg, rgba(20, 20, 30, 0.95) 0%, rgba(30, 30, 45, 0.9) 100%)' + : undefined; + return ( <> @@ -183,20 +236,44 @@ export function UsageCalendar({ usageData, onDataUpdate }: UsageCalendarProps) { Usage Calendar - - orientation === 'left' - ? - : , - }} - /> +
+ {/* Calendar */} +
+ + orientation === 'left' + ? + : , + }} + /> +
+ + {/* Daily Quote */} +
+
+ + Daily Inspiration +
+

+ “{dailyQuote.text}” +

+

+ — {dailyQuote.author} +

+
+
+
diff --git a/src/components/UserHeader.tsx b/src/components/UserHeader.tsx index 8927229..8cf17d9 100644 --- a/src/components/UserHeader.tsx +++ b/src/components/UserHeader.tsx @@ -12,7 +12,8 @@ import { User } from '@/lib/session'; import { fetchPreferences } from '@/lib/storage'; import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; -import { Cigarette, Leaf, LogOut, Home, ChevronDown } from 'lucide-react'; +import { Cigarette, Leaf, LogOut, Home, ChevronDown, Sun, Moon } from 'lucide-react'; +import { useTheme } from '@/lib/theme-context'; interface UserHeaderProps { user: User; @@ -21,6 +22,7 @@ interface UserHeaderProps { export function UserHeader({ user }: UserHeaderProps) { const [userName, setUserName] = useState(null); const router = useRouter(); + const { theme, toggleTheme } = useTheme(); useEffect(() => { const loadUserName = async () => { @@ -44,12 +46,12 @@ export function UserHeader({ user }: UserHeaderProps) { }; return ( -
-
+

handleNavigate('/')} @@ -57,13 +59,24 @@ export function UserHeader({ user }: UserHeaderProps) { QuitTraq

{userName && ( -

+

Welcome {userName}, you got this!

)}
-
+
+