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.
This commit is contained in:
parent
ec0d83586d
commit
fac443c281
@ -197,4 +197,9 @@
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Calendar styling - reduce overall size */
|
||||
.rdp {
|
||||
--rdp-cell-size: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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({
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body className="antialiased">
|
||||
{children}
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@ -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 (
|
||||
<div className="min-h-screen">
|
||||
<div className="min-h-screen" style={{ background: pageBackground }}>
|
||||
<UserHeader user={user} />
|
||||
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
|
||||
8
src/components/Providers.tsx
Normal file
8
src/components/Providers.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { ThemeProvider } from '@/lib/theme-context';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export function Providers({ children }: { children: ReactNode }) {
|
||||
return <ThemeProvider>{children}</ThemeProvider>;
|
||||
}
|
||||
@ -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 (
|
||||
<Card className="backdrop-blur-sm shadow-lg drop-shadow-md border-yellow-500/30" style={{
|
||||
background: 'linear-gradient(135deg, rgba(234, 179, 8, 0.2) 0%, rgba(202, 138, 4, 0.15) 100%)'
|
||||
background: yellowBackground
|
||||
}}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-white">
|
||||
@ -94,7 +107,7 @@ export function QuitPlanCard({
|
||||
|
||||
return (
|
||||
<Card className="backdrop-blur-sm shadow-lg drop-shadow-md border-pink-500/30" style={{
|
||||
background: 'linear-gradient(135deg, rgba(236, 72, 153, 0.2) 0%, rgba(219, 39, 119, 0.15) 100%)'
|
||||
background: pinkBackground
|
||||
}}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-white">
|
||||
|
||||
@ -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 (
|
||||
<Card className={`bg-card/80 backdrop-blur-sm border ${borderColor} bg-gradient-to-br ${bgGradient} shadow-lg drop-shadow-md`}>
|
||||
<Card
|
||||
className={`backdrop-blur-sm border ${borderColor} shadow-lg drop-shadow-md`}
|
||||
style={{ background: cardBackground }}
|
||||
>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-white">
|
||||
<SubstanceIcon className={`h-5 w-5 ${iconColor}`} />
|
||||
|
||||
@ -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<UsageEntry[]>([]);
|
||||
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 (
|
||||
<div className="min-h-screen">
|
||||
<div className="min-h-screen" style={{ background: pageBackground }}>
|
||||
<UserHeader user={user} />
|
||||
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
@ -62,8 +68,8 @@ export function SubstanceTrackingPage({ user, substance }: SubstanceTrackingPage
|
||||
<SubstanceIcon className="h-8 w-8" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">{substanceLabel} Tracking</h1>
|
||||
<p className="text-white/70 mt-1">Monitor your {substanceLabel.toLowerCase()} usage and progress</p>
|
||||
<h1 className={`text-3xl font-bold ${theme === 'light' ? 'text-gray-900' : 'text-white'}`}>{substanceLabel} Tracking</h1>
|
||||
<p className={`mt-1 ${theme === 'light' ? 'text-gray-700' : 'text-white/70'}`}>Monitor your {substanceLabel.toLowerCase()} usage and progress</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -71,11 +77,11 @@ export function SubstanceTrackingPage({ user, substance }: SubstanceTrackingPage
|
||||
{/* Today's Status Message */}
|
||||
<div className="mb-8 text-center">
|
||||
{todayCount === 0 ? (
|
||||
<p className="text-2xl font-medium text-green-400">
|
||||
<p className={`text-2xl font-medium ${theme === 'light' ? 'text-green-600' : 'text-green-400'}`}>
|
||||
Great job, nothing yet!
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-2xl font-medium text-white">
|
||||
<p className={`text-2xl font-medium ${theme === 'light' ? 'text-gray-900' : 'text-white'}`}>
|
||||
{todayCount} {todayCount === 1 ? (substance === 'nicotine' ? 'puff' : 'hit') : unitLabel} recorded, you got this!
|
||||
</p>
|
||||
)}
|
||||
@ -83,7 +89,7 @@ export function SubstanceTrackingPage({ user, substance }: SubstanceTrackingPage
|
||||
|
||||
{/* Inspirational Message */}
|
||||
<div className="mb-8 text-center">
|
||||
<p className="text-xl font-light text-white/60 italic">
|
||||
<p className={`text-xl font-light italic ${theme === 'light' ? 'text-gray-500' : 'text-white/60'}`}>
|
||||
"One day at a time..."
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<Card className="bg-card/80 backdrop-blur-sm shadow-lg drop-shadow-md">
|
||||
@ -183,20 +236,44 @@ export function UsageCalendar({ usageData, onDataUpdate }: UsageCalendarProps) {
|
||||
<CardTitle>Usage Calendar</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DayPicker
|
||||
mode="single"
|
||||
selected={selectedDate}
|
||||
onSelect={handleDateSelect}
|
||||
className="rounded-md border p-3 bg-background/50"
|
||||
showOutsideDays={false}
|
||||
components={{
|
||||
DayButton: CustomDayButton,
|
||||
Chevron: ({ orientation }) =>
|
||||
orientation === 'left'
|
||||
? <ChevronLeftIcon className="h-4 w-4" />
|
||||
: <ChevronRightIcon className="h-4 w-4" />,
|
||||
}}
|
||||
/>
|
||||
<div className="flex gap-4">
|
||||
{/* Calendar */}
|
||||
<div className="shrink-0">
|
||||
<DayPicker
|
||||
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}
|
||||
showOutsideDays={false}
|
||||
components={{
|
||||
DayButton: CustomDayButton,
|
||||
Chevron: ({ orientation }) =>
|
||||
orientation === 'left'
|
||||
? <ChevronLeftIcon className="h-4 w-4" />
|
||||
: <ChevronRightIcon className="h-4 w-4" />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Daily Quote */}
|
||||
<div
|
||||
className="flex-1 flex flex-col justify-center p-4 rounded-lg border border-indigo-500/30"
|
||||
style={{ background: 'linear-gradient(135deg, rgba(67, 56, 202, 0.4) 0%, rgba(109, 40, 217, 0.35) 50%, rgba(76, 29, 149, 0.45) 100%)' }}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Sparkles className="h-4 w-4 text-yellow-300" />
|
||||
<span className="text-xs font-medium text-white/70 uppercase tracking-wide">Daily Inspiration</span>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-white leading-relaxed mb-3">
|
||||
“{dailyQuote.text}”
|
||||
</p>
|
||||
<p className="text-xs text-white/60">
|
||||
— {dailyQuote.author}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-4 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded" style={{ background: 'linear-gradient(135deg, rgba(96,165,250,0.5), rgba(59,130,246,0.6))' }} />
|
||||
|
||||
@ -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<string | null>(null);
|
||||
const router = useRouter();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const loadUserName = async () => {
|
||||
@ -44,12 +46,12 @@ export function UserHeader({ user }: UserHeaderProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="border-b border-white/10" style={{
|
||||
background: 'linear-gradient(135deg, rgba(10, 10, 20, 0.95) 0%, rgba(20, 30, 60, 0.9) 50%, rgba(15, 25, 50, 0.95) 100%)',
|
||||
<header className="sticky top-0 z-50 border-b border-white/10" 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%)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
}}>
|
||||
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-8">
|
||||
<h1
|
||||
className="text-2xl font-bold text-white cursor-pointer hover:opacity-80 transition-opacity"
|
||||
onClick={() => handleNavigate('/')}
|
||||
@ -57,13 +59,24 @@ export function UserHeader({ user }: UserHeaderProps) {
|
||||
QuitTraq
|
||||
</h1>
|
||||
{userName && (
|
||||
<p className="text-white/90 text-lg hidden sm:block">
|
||||
<p className="text-white/90 text-lg hidden sm:block ml-4">
|
||||
Welcome {userName}, you got this!
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-2 rounded-full bg-white/10 hover:bg-white/20 transition-all focus:outline-none focus:ring-2 focus:ring-white/30"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
<Moon className="h-5 w-5 text-blue-300" />
|
||||
) : (
|
||||
<Sun className="h-5 w-5 text-yellow-400" />
|
||||
)}
|
||||
</button>
|
||||
<DropdownMenu>
|
||||
<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">
|
||||
|
||||
@ -191,8 +191,22 @@ export function clearDayData(
|
||||
clearDayDataAsync(date, substance);
|
||||
}
|
||||
|
||||
const LAST_PROMPT_KEY = 'quittraq_last_prompt_date';
|
||||
|
||||
export function shouldShowUsagePrompt(): boolean {
|
||||
return true;
|
||||
if (typeof window === 'undefined') return false;
|
||||
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const lastPromptDate = localStorage.getItem(LAST_PROMPT_KEY);
|
||||
|
||||
return lastPromptDate !== today;
|
||||
}
|
||||
|
||||
export function markPromptShown(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
localStorage.setItem(LAST_PROMPT_KEY, today);
|
||||
}
|
||||
|
||||
export function getWeeklyData(substance: 'nicotine' | 'weed', _userId?: string): UsageEntry[] {
|
||||
|
||||
43
src/lib/theme-context.tsx
Normal file
43
src/lib/theme-context.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
|
||||
type Theme = 'dark' | 'light';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setTheme] = useState<Theme>('dark');
|
||||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem('theme') as Theme | null;
|
||||
if (saved) {
|
||||
setTheme(saved);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newTheme = theme === 'dark' ? 'light' : 'dark';
|
||||
setTheme(newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user