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')}
+ />
+ )}
+
+
+ );
+}