From 3a31c8a956e7654a522647ea6cbb2b961e7bb25e Mon Sep 17 00:00:00 2001 From: Avery Felts Date: Sat, 31 Jan 2026 17:37:25 -0700 Subject: [PATCH] Refactor: Unified independent quit plans for Nicotine and Weed, fixed persistence, and upgraded UI with expandable sections --- src/app/api/preferences/route.ts | 11 +- src/components/Dashboard.tsx | 44 +--- src/components/QuitPlanCard.tsx | 229 ------------------- src/components/UnifiedQuitPlanCard.tsx | 294 +++++++++++++++++++++++++ 4 files changed, 312 insertions(+), 266 deletions(-) delete mode 100644 src/components/QuitPlanCard.tsx create mode 100644 src/components/UnifiedQuitPlanCard.tsx diff --git a/src/app/api/preferences/route.ts b/src/app/api/preferences/route.ts index 5108ec5..6862183 100644 --- a/src/app/api/preferences/route.ts +++ b/src/app/api/preferences/route.ts @@ -23,12 +23,21 @@ export async function GET() { }); } + // Parse JSON to construct quitState + const rawJson = preferences.quitPlanJson ? JSON.parse(preferences.quitPlanJson) : null; + const isNewFormat = rawJson && 'nicotine' in rawJson; + const quitState = isNewFormat ? rawJson : { + nicotine: preferences.substance === 'nicotine' ? { plan: rawJson, startDate: preferences.trackingStartDate } : { plan: null, startDate: null }, + weed: preferences.substance === 'weed' ? { plan: rawJson, startDate: preferences.trackingStartDate } : { plan: null, startDate: null } + }; + return NextResponse.json({ substance: preferences.substance, trackingStartDate: preferences.trackingStartDate, hasCompletedSetup: !!preferences.hasCompletedSetup, dailyGoal: preferences.dailyGoal, - quitPlan: preferences.quitPlanJson ? JSON.parse(preferences.quitPlanJson) : null, + quitPlan: null, + quitState, userName: preferences.userName, userAge: preferences.userAge, religion: preferences.religion, diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 2102c1e..2c76154 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -28,7 +28,7 @@ import { SetupWizard } from './SetupWizard'; import { UsagePromptDialog } from './UsagePromptDialog'; import { UsageCalendar } from './UsageCalendar'; import { StatsCard } from './StatsCard'; -import { QuitPlanCard } from './QuitPlanCard'; +import { UnifiedQuitPlanCard } from './UnifiedQuitPlanCard'; import { AchievementsCard } from './AchievementsCard'; import { CelebrationAnimation } from './CelebrationAnimation'; import { HealthTimelineCard } from './HealthTimelineCard'; @@ -358,41 +358,13 @@ export function Dashboard({ user }: DashboardProps) {
- {/* Nicotine Plan */} - {(preferences.substance === 'nicotine' || usageData.some(e => e.substance === 'nicotine')) && ( - handleGeneratePlan('nicotine')} - usageData={usageData} - trackingStartDate={ - preferences.quitState?.nicotine.startDate || - (preferences.substance === 'nicotine' ? preferences.trackingStartDate : null) || - // Fallback: Find earliest usage date - usageData.filter(e => e.substance === 'nicotine').sort((a, b) => a.date.localeCompare(b.date))[0]?.date || - null - } - substance="nicotine" - /> - )} - - {/* Weed Plan */} - {(preferences.substance === 'weed' || usageData.some(e => e.substance === 'weed')) && ( - handleGeneratePlan('weed')} - usageData={usageData} - trackingStartDate={ - preferences.quitState?.weed.startDate || - (preferences.substance === 'weed' ? preferences.trackingStartDate : null) || - // Fallback: Find earliest usage date - usageData.filter(e => e.substance === 'weed').sort((a, b) => a.date.localeCompare(b.date))[0]?.date || - null - } - substance="weed" - /> - )} + {/* Unified Quit Plan Placard */} +
diff --git a/src/components/QuitPlanCard.tsx b/src/components/QuitPlanCard.tsx deleted file mode 100644 index 6b639e2..0000000 --- a/src/components/QuitPlanCard.tsx +++ /dev/null @@ -1,229 +0,0 @@ -'use client'; - -import React from 'react'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { QuitPlan, UsageEntry } from '@/lib/storage'; -import { Target, TrendingDown } from 'lucide-react'; -import { useTheme } from '@/lib/theme-context'; -import { getTodayString } from '@/lib/date-utils'; - -interface QuitPlanCardProps { - plan: QuitPlan | null; - onGeneratePlan: () => void; - usageData: UsageEntry[]; - trackingStartDate: string | null; - substance: 'nicotine' | 'weed'; -} - -function QuitPlanCardComponent({ - plan, - onGeneratePlan, - usageData, - trackingStartDate, - substance, -}: QuitPlanCardProps) { - const { theme } = useTheme(); - - // Count unique days with any logged data - const uniqueDaysWithData = new Set(usageData.filter(e => e.substance === substance).map(e => e.date)).size; - const daysRemaining = Math.max(0, 7 - uniqueDaysWithData); - - // Logic: Unlocked if 7+ days tracked AND (It's Day 8+ OR usage exists for Day 8+) - // This effectively locks it until 12:01 AM next day after Day 7 is done - const isUnlocked = React.useMemo(() => { - // Determine the local start date cleanly (ignoring time) - if (!trackingStartDate || uniqueDaysWithData < 7) return false; - - // Parse YYYY-MM-DD - const [y, m, d] = trackingStartDate.split('-').map(Number); - const startObj = new Date(y, m - 1, d); // Local midnight - - const now = new Date(); - // Get today's local midnight - const todayObj = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - - // Calculate difference in full days - // Jan 1 to Jan 8: difference of 7 days. - const diffTime = todayObj.getTime() - startObj.getTime(); - const daysPassed = Math.floor(diffTime / (1000 * 60 * 60 * 24)); - - // If 7 days have passed (meaning we are on Day 8 or later), unlock. - if (daysPassed >= 7) return true; - - // Also check if usage count is > 7, implying usage beyond the first week - if (uniqueDaysWithData > 7) return true; - - return false; - }, [uniqueDaysWithData, trackingStartDate]); - - // Calculate current average - const totalUsage = usageData.filter(e => e.substance === substance).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 ( - -
- - - - Your {substance === 'nicotine' ? 'Nicotine' : 'Weed'} Quit Plan - - - We're tracking your usage to build your custom quit plan - - - -
-
- Tracking Progress - - {daysRemaining > 0 ? `${daysRemaining} days left` : 'Ready!'} - -
-
-
-
-

- {uniqueDaysWithData} of 7 days tracked -

-
- - {isUnlocked ? ( -
-

- Great work! Your average daily usage is{' '} - {currentAverage} per day. -

- -
- ) : ( -
-

- Log your usage each day. After 7 days, we'll create a personalized plan to help you reduce by 25% each week. -

-

- Your plan will be tailored to your habits -

-
- )} - - - ); - } - - const startDate = new Date(plan.startDate); - const today = new Date(); - const weekNumber = Math.floor( - (today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24 * 7) - ) + 1; - const totalWeeks = plan.weeklyTargets.length; - const currentTarget = weekNumber <= totalWeeks ? plan.weeklyTargets[weekNumber - 1] : 0; - - // Calculate today's usage for progress bar - const todayStr = getTodayString(); - const todayUsage = usageData - .filter(e => e.date === todayStr && e.substance === substance) - .reduce((sum, e) => sum + e.count, 0); - - const usagePercent = currentTarget > 0 ? (todayUsage / currentTarget) * 100 : 0; - - // Progress bar color based on usage - let progressColor = 'bg-emerald-400'; // Good - if (usagePercent >= 100) progressColor = 'bg-red-500'; // Over limit - else if (usagePercent >= 80) progressColor = 'bg-yellow-400'; // Warning - - return ( - -
- - - - Your {substance === 'nicotine' ? 'Nicotine' : 'Weed'} Plan - - - Week {Math.min(weekNumber, totalWeeks)} of {totalWeeks} - 25% weekly reduction - - - -
-

{substance === 'nicotine' ? 'Nicotine' : 'Weed'} Max Puffs Target

-

- {currentTarget !== null && currentTarget > 0 ? currentTarget : '0'} -

-

per day

- - {/* Daily Progress Bar */} -
-
-
-

- {todayUsage} used / {currentTarget} allowed -

-
- -
-

Weekly targets:

-
- {plan.weeklyTargets.map((target, index) => { - const weekNum = index + 1; - const isFuture = weekNum > weekNumber; - const isCurrent = weekNum === weekNumber; - - return ( -
-

Week {weekNum}

-

- {isFuture ? '?' : target} -

-
- ) - })} -
-
- -
-

- Started at: {plan.baselineAverage}/day -

-

- Goal: Quit by {new Date(plan.endDate).toLocaleDateString()} -

-
- - - ); -} -export const QuitPlanCard = React.memo(QuitPlanCardComponent); diff --git a/src/components/UnifiedQuitPlanCard.tsx b/src/components/UnifiedQuitPlanCard.tsx new file mode 100644 index 0000000..111d345 --- /dev/null +++ b/src/components/UnifiedQuitPlanCard.tsx @@ -0,0 +1,294 @@ +'use client'; + +import React, { useState, useMemo } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { QuitPlan, UsageEntry, UserPreferences } from '@/lib/storage'; +import { Target, TrendingDown, ChevronDown, ChevronUp, Cigarette, Leaf } from 'lucide-react'; +import { useTheme } from '@/lib/theme-context'; +import { getTodayString } from '@/lib/date-utils'; +import { cn } from '@/lib/utils'; + +interface SubstancePlanSectionProps { + substance: 'nicotine' | 'weed'; + plan: QuitPlan | null; + usageData: UsageEntry[]; + trackingStartDate: string | null; + onGeneratePlan: () => void; + isExpanded: boolean; + onToggle: () => void; +} + +function SubstancePlanSection({ + substance, + plan, + usageData, + trackingStartDate, + onGeneratePlan, + isExpanded, + onToggle +}: SubstancePlanSectionProps) { + const { theme } = useTheme(); + + // 1. Data Processing + const substanceUsage = useMemo(() => usageData.filter(e => e.substance === substance), [usageData, substance]); + const uniqueDaysWithData = useMemo(() => new Set(substanceUsage.map(e => e.date)).size, [substanceUsage]); + const daysRemaining = Math.max(0, 7 - uniqueDaysWithData); + + const totalUsage = substanceUsage.reduce((sum, e) => sum + e.count, 0); + const currentAverage = uniqueDaysWithData > 0 ? Math.round(totalUsage / uniqueDaysWithData) : 0; + + // 2. Unlock Logic + const isUnlocked = useMemo(() => { + if (!trackingStartDate || uniqueDaysWithData < 7) return false; + const [y, m, d] = trackingStartDate.split('-').map(Number); + const startObj = new Date(y, m - 1, d); + const now = new Date(); + const todayObj = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const daysPassed = Math.floor((todayObj.getTime() - startObj.getTime()) / (1000 * 60 * 60 * 24)); + return daysPassed >= 7 || uniqueDaysWithData > 7; + }, [uniqueDaysWithData, trackingStartDate]); + + // 3. Plan Validation & Calculations + const isValidPlan = plan && plan.startDate && plan.weeklyTargets && Array.isArray(plan.weeklyTargets); + const activePlan = isValidPlan ? plan : null; + + const todayStr = getTodayString(); + const todayUsage = substanceUsage + .filter(e => e.date === todayStr) + .reduce((sum, e) => sum + e.count, 0); + + let weekNumber = 0; + let currentTarget = 0; + let totalWeeks = 0; + let usagePercent = 0; + + if (activePlan) { + const startDate = new Date(activePlan.startDate); + const today = new Date(); + weekNumber = Math.floor((today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24 * 7)) + 1; + totalWeeks = activePlan.weeklyTargets.length; + currentTarget = weekNumber <= totalWeeks ? activePlan.weeklyTargets[weekNumber - 1] : 0; + usagePercent = currentTarget > 0 ? (todayUsage / currentTarget) * 100 : 0; + } + + // 4. Styling + const isNicotine = substance === 'nicotine'; + const Icon = isNicotine ? Cigarette : Leaf; + const label = isNicotine ? 'Nicotine' : 'Weed'; + + // Base Colors + const bgColor = isNicotine + ? (theme === 'light' ? 'bg-yellow-500/10' : 'bg-yellow-500/5') + : (theme === 'light' ? 'bg-emerald-500/10' : 'bg-emerald-500/5'); + + const borderColor = isNicotine + ? (theme === 'light' ? 'border-yellow-500/20' : 'border-yellow-500/10') + : (theme === 'light' ? 'border-emerald-500/20' : 'border-emerald-500/10'); + + const accentColor = isNicotine ? 'text-yellow-500' : 'text-emerald-500'; + const progressFill = isNicotine ? 'bg-yellow-500' : 'bg-emerald-500'; + + // Specific plan color for the progress bar alert states + let progressColor = progressFill; + if (activePlan) { + if (usagePercent >= 100) progressColor = 'bg-red-500'; + else if (usagePercent >= 80) progressColor = isNicotine ? 'bg-orange-400' : 'bg-yellow-400'; + } + + return ( +
+ {/* HEADER / SUMMARY ROW */} +
+
+
+ +
+
+

{label} Plan

+

+ {activePlan ? `Week ${Math.min(weekNumber, totalWeeks)} of ${totalWeeks}` : `Tracking: Day ${uniqueDaysWithData}/7`} +

+
+
+ +
+
+ Today + = 100 ? "text-red-500" : accentColor)}> + {todayUsage}{activePlan ? ` / ${currentTarget}` : ''} + +
+ {isExpanded ? : } +
+
+ + {/* EXPANDED CONTENT */} + {isExpanded && ( +
+
+ + {!activePlan ? ( +
+
+
+ Weekly Baseline Progress + + {daysRemaining > 0 ? `${daysRemaining} days left` : 'Ready!'} + +
+
+
+
+
+ + {isUnlocked ? ( +
+

+ Baseline established: {currentAverage} puffs/day +

+ +
+ ) : ( +

+ Keep logging for {daysRemaining} more days to calculate your personalized reduction plan. +

+ )} +
+ ) : ( +
+ {/* Active Plan Detail */} +
+

Current Daily Limit

+

{currentTarget}

+

puffs allowed today

+
+ + {/* Progress Bar Detail */} +
+
+ Usage Progress + {Math.round(usagePercent)}% +
+
+
+
+
+ + {/* Weekly Matrix */} +
+ {activePlan.weeklyTargets.map((target, idx) => { + const wNum = idx + 1; + const isFuture = wNum > weekNumber; + const isCurrent = wNum === weekNumber; + return ( +
+

Wk {wNum}

+

{isFuture ? '?' : target}

+
+ ); + })} +
+ +
+ Start: {activePlan.baselineAverage}/day + End: {new Date(activePlan.endDate).toLocaleDateString()} +
+
+ )} +
+ )} +
+ ); +} + +interface UnifiedQuitPlanCardProps { + preferences: UserPreferences | null; + usageData: UsageEntry[]; + onGeneratePlan: (substance: 'nicotine' | 'weed') => void; + refreshKey: number; +} + +export function UnifiedQuitPlanCard({ + preferences, + usageData, + onGeneratePlan, + refreshKey +}: UnifiedQuitPlanCardProps) { + const [expandedSubstance, setExpandedSubstance] = useState<'nicotine' | 'weed' | 'none'>('nicotine'); + + if (!preferences) return null; + + // Determine which substances to show + const showNicotine = preferences.substance === 'nicotine' || usageData.some(e => e.substance === 'nicotine'); + const showWeed = preferences.substance === 'weed' || usageData.some(e => e.substance === 'weed'); + + if (!showNicotine && !showWeed) return null; + + return ( + + + + + Quit Journey Plan + + + + {showNicotine && ( + setExpandedSubstance(expandedSubstance === 'nicotine' ? 'none' : 'nicotine')} + plan={preferences.quitState?.nicotine?.plan || (preferences.substance === 'nicotine' ? preferences.quitPlan : null)} + usageData={usageData} + trackingStartDate={ + preferences.quitState?.nicotine?.startDate || + (preferences.substance === 'nicotine' ? preferences.trackingStartDate : null) || + usageData.filter(e => e.substance === 'nicotine').sort((a, b) => a.date.localeCompare(b.date))[0]?.date || + null + } + onGeneratePlan={() => onGeneratePlan('nicotine')} + /> + )} + + {showWeed && ( + setExpandedSubstance(expandedSubstance === 'weed' ? 'none' : 'weed')} + plan={preferences.quitState?.weed?.plan || (preferences.substance === 'weed' ? preferences.quitPlan : null)} + usageData={usageData} + trackingStartDate={ + preferences.quitState?.weed?.startDate || + (preferences.substance === 'weed' ? preferences.trackingStartDate : null) || + usageData.filter(e => e.substance === 'weed').sort((a, b) => a.date.localeCompare(b.date))[0]?.date || + null + } + onGeneratePlan={() => onGeneratePlan('weed')} + /> + )} + + + ); +}