From 380f1af1da1d6f65ba8ee9ec91c1ea6452d800fc Mon Sep 17 00:00:00 2001 From: Avery Felts Date: Sun, 25 Jan 2026 11:16:32 -0700 Subject: [PATCH] fix: precise timestamp tracking for health timeline --- src/components/Dashboard.tsx | 12 +- src/components/HealthTimelineCard.tsx | 210 ++++++++++++++------------ src/lib/storage.ts | 53 ++++++- 3 files changed, 178 insertions(+), 97 deletions(-) diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index e5c1d55..7e23ac6 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -143,11 +143,21 @@ export function Dashboard({ user }: DashboardProps) { if (count > 0) { const today = new Date().toISOString().split('T')[0]; + const now = new Date().toISOString(); + await saveUsageEntryAsync({ date: today, count, substance, }); + + // Update preferences with last usage time + const updatedPrefs = { + ...preferences, + [substance === 'nicotine' ? 'lastNicotineUsageTime' : 'lastWeedUsageTime']: now, + }; + await savePreferencesAsync(updatedPrefs); + setPreferences(updatedPrefs); } setShowUsagePrompt(false); @@ -237,7 +247,7 @@ export function Dashboard({ user }: DashboardProps) { diff --git a/src/components/HealthTimelineCard.tsx b/src/components/HealthTimelineCard.tsx index 6b969fc..d80b9d0 100644 --- a/src/components/HealthTimelineCard.tsx +++ b/src/components/HealthTimelineCard.tsx @@ -1,8 +1,8 @@ 'use client'; -import { useMemo } from 'react'; +import { useMemo, useState, useEffect } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { HEALTH_MILESTONES, getMinutesSinceQuit, UsageEntry } from '@/lib/storage'; +import { HEALTH_MILESTONES, getMinutesSinceQuit, UsageEntry, UserPreferences } from '@/lib/storage'; import { useTheme } from '@/lib/theme-context'; import { Heart, @@ -15,11 +15,12 @@ import { HeartHandshake, CheckCircle2, Clock, + Cigarette, + Leaf } from 'lucide-react'; interface HealthTimelineCardProps { usageData: UsageEntry[]; - substance: 'nicotine' | 'weed'; } const iconMap: Record = { @@ -48,13 +49,13 @@ function formatTimeRemaining(currentMinutes: number, targetMinutes: number): str return `${formatDuration(remaining)} to go`; } -export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardProps) { - const { theme } = useTheme(); - - const minutesSinceQuit = useMemo(() => { - return getMinutesSinceQuit(usageData, substance); - }, [usageData, substance]); +interface TimelineColumnProps { + substance: 'nicotine' | 'weed'; + minutesSinceQuit: number; + theme: 'light' | 'dark'; +} +function TimelineColumn({ substance, minutesSinceQuit, theme }: TimelineColumnProps) { const currentMilestoneIndex = useMemo(() => { for (let i = HEALTH_MILESTONES.length - 1; i >= 0; i--) { if (minutesSinceQuit >= HEALTH_MILESTONES[i].timeMinutes) { @@ -83,110 +84,133 @@ export function HealthTimelineCard({ usageData, substance }: HealthTimelineCardP return Math.min(100, Math.max(0, (progress / range) * 100)); }, [minutesSinceQuit, nextMilestone, currentMilestoneIndex]); + const substanceLabel = substance === 'nicotine' ? 'Nicotine' : 'Marijuana'; + const SubstanceIcon = substance === 'nicotine' ? Cigarette : Leaf; + const accentColor = substance === 'nicotine' ? 'red' : 'green'; + const accentColorClass = substance === 'nicotine' ? 'text-red-500' : 'text-green-500'; + const bgAccentClass = substance === 'nicotine' ? 'bg-red-500' : 'bg-green-500'; + + return ( +
+ {/* Header */} +
+ + + {substanceLabel} + + + {formatDuration(Math.floor(minutesSinceQuit))} free + +
+ +
+ {/* Progress to next milestone */} + {nextMilestone && ( +
+
+ Next Up + + {formatTimeRemaining(Math.floor(minutesSinceQuit), nextMilestone.timeMinutes)} + +
+
+
+
+

+ {nextMilestone.title} +

+
+ )} + + {/* Timeline Items */} + {HEALTH_MILESTONES.map((milestone, index) => { + const isAchieved = minutesSinceQuit >= milestone.timeMinutes; + const isCurrent = index === currentMilestoneIndex; + const Icon = iconMap[milestone.icon] || Heart; + + return ( +
+ {/* Icon */} +
+ {isAchieved ? : } +
+ + {/* Content */} +
+
+

+ {milestone.title} +

+
+

+ {milestone.description} +

+
+
+ ); + })} +
+
+ ); +} + +export function HealthTimelineCard({ usageData, preferences }: HealthTimelineCardProps & { preferences?: UserPreferences | null }) { + const { theme } = useTheme(); + const [now, setNow] = useState(Date.now()); + + useEffect(() => { + const interval = setInterval(() => { + setNow(Date.now()); + }, 1000); // Update every second + + return () => clearInterval(interval); + }, []); + + const nicotineMinutes = useMemo(() => getMinutesSinceQuit(usageData, 'nicotine', true, preferences), [usageData, preferences, now]); + const weedMinutes = useMemo(() => getMinutesSinceQuit(usageData, 'weed', true, preferences), [usageData, preferences, now]); + const cardBackground = theme === 'light' ? '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'; - return (
- + Health Recovery

- {substanceLabel}-free for {formatDuration(minutesSinceQuit)} + Track your body's healing process for each substance independently.

- - {/* Progress to next milestone */} - {nextMilestone && ( -
-
- Next milestone - - {formatTimeRemaining(minutesSinceQuit, nextMilestone.timeMinutes)} - -
-
-
-
-

{nextMilestone.title}

-
- )} - - {/* Timeline */} -
- {HEALTH_MILESTONES.map((milestone, index) => { - const isAchieved = minutesSinceQuit >= milestone.timeMinutes; - const isCurrent = index === currentMilestoneIndex; - const Icon = iconMap[milestone.icon] || Heart; - - return ( -
- {/* Icon */} -
- {isAchieved ? ( - - ) : ( - - )} -
- - {/* Content */} -
-
-

- {milestone.title} -

- {isCurrent && ( - - Current - - )} -
-

- {milestone.description} -

-
- - - {formatDuration(milestone.timeMinutes)} - -
-
-
- ); - })} + +
+ +
- + ); } diff --git a/src/lib/storage.ts b/src/lib/storage.ts index 1db105a..9dfb58d 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -16,6 +16,8 @@ export interface UserPreferences { userName: string | null; userAge: number | null; religion: 'christian' | 'muslim' | 'jewish' | 'secular' | null; + lastNicotineUsageTime?: string | null; // ISO timestamp of last usage + lastWeedUsageTime?: string | null; // ISO timestamp of last usage } export interface QuitPlan { @@ -395,8 +397,51 @@ export function calculateTotalSaved( export function getMinutesSinceQuit( usageData: UsageEntry[], - substance: 'nicotine' | 'weed' + substance: 'nicotine' | 'weed', + precise: boolean = false, + preferences?: UserPreferences | null ): number { + // Try to use precise timestamp from preferences first + if (preferences) { + const lastUsageTimeStr = substance === 'nicotine' + ? preferences.lastNicotineUsageTime + : preferences.lastWeedUsageTime; + + if (lastUsageTimeStr) { + const now = new Date(); + const lastUsageTime = new Date(lastUsageTimeStr); + const diffMs = now.getTime() - lastUsageTime.getTime(); + const minutes = Math.max(0, diffMs / (1000 * 60)); + + // Sanity check: if the timestamp is OLDER than the last recorded date in usageData, + // it might mean the user manually added a later date in the calendar without a timestamp. + // In that case, we should fall back to the date-based logic. + + const substanceData = usageData + .filter((e) => e.substance === substance && e.count > 0) + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + + if (substanceData.length > 0) { + const lastDateStr = substanceData[0].date; + const lastDate = new Date(lastDateStr); + // Set lastDate to end of day to compare with timestamp + lastDate.setHours(23, 59, 59, 999); + + // If the timestamp is essentially on the same day or later than the last recorded date, rely on the timestamp + // (We allow the timestamp to be earlier in the same day, that's the whole point) + const lastDateStart = new Date(lastDateStr); + lastDateStart.setHours(0, 0, 0, 0); + + if (lastUsageTime >= lastDateStart) { + return precise ? minutes : Math.floor(minutes); + } + } else { + // No usage data but we have a timestamp? Trust the timestamp. + return precise ? minutes : Math.floor(minutes); + } + } + } + // Find the last usage date for this substance const substanceData = usageData .filter((e) => e.substance === substance && e.count > 0) @@ -411,7 +456,7 @@ export function getMinutesSinceQuit( const todayStr = now.toISOString().split('T')[0]; const lastUsageDateStr = substanceData[0].date; - // If the last usage was today, reset to 0 (just used) + // If the last usage was today, reset to 0 (just used, unknown time) if (lastUsageDateStr === todayStr) { return 0; } @@ -421,7 +466,9 @@ export function getMinutesSinceQuit( lastUsageDate.setHours(23, 59, 59, 999); const diffMs = now.getTime() - lastUsageDate.getTime(); - return Math.max(0, Math.floor(diffMs / (1000 * 60))); + const minutes = Math.max(0, diffMs / (1000 * 60)); + + return precise ? minutes : Math.floor(minutes); } export function checkBadgeEligibility(