Update quit smoking website

This commit is contained in:
Avery Felts 2026-01-25 12:30:09 -07:00
parent 380f1af1da
commit 7847b71c4d
9 changed files with 349 additions and 133 deletions

View File

@ -19,6 +19,9 @@ model UserPreferences {
dailyGoal Int?
userName String?
userAge Int?
religion String?
lastNicotineUsageTime String?
lastWeedUsageTime String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

View File

@ -33,6 +33,9 @@ export async function GET() {
quitPlan: preferences.quitPlanJson ? JSON.parse(preferences.quitPlanJson) : null,
userName: preferences.userName,
userAge: preferences.userAge,
religion: preferences.religion,
lastNicotineUsageTime: preferences.lastNicotineUsageTime,
lastWeedUsageTime: preferences.lastWeedUsageTime,
});
} catch (error) {
console.error('Error fetching preferences:', error);
@ -48,7 +51,18 @@ export async function POST(request: NextRequest) {
}
const body = await request.json();
const { substance, trackingStartDate, hasCompletedSetup, dailyGoal, quitPlan, userName, userAge } = body;
const {
substance,
trackingStartDate,
hasCompletedSetup,
dailyGoal,
quitPlan,
userName,
userAge,
religion,
lastNicotineUsageTime,
lastWeedUsageTime
} = body;
const preferences = await prisma.userPreferences.upsert({
where: { userId: session.user.id },
@ -60,6 +74,9 @@ export async function POST(request: NextRequest) {
quitPlanJson: quitPlan ? JSON.stringify(quitPlan) : null,
userName,
userAge,
religion,
lastNicotineUsageTime,
lastWeedUsageTime,
},
create: {
userId: session.user.id,
@ -70,6 +87,9 @@ export async function POST(request: NextRequest) {
quitPlanJson: quitPlan ? JSON.stringify(quitPlan) : null,
userName,
userAge,
religion,
lastNicotineUsageTime,
lastWeedUsageTime,
},
});
@ -81,6 +101,9 @@ export async function POST(request: NextRequest) {
quitPlan: preferences.quitPlanJson ? JSON.parse(preferences.quitPlanJson) : null,
userName: preferences.userName,
userAge: preferences.userAge,
religion: preferences.religion,
lastNicotineUsageTime: preferences.lastNicotineUsageTime,
lastWeedUsageTime: preferences.lastWeedUsageTime,
});
} catch (error) {
console.error('Error saving preferences:', error);

View File

@ -64,6 +64,7 @@ export function Dashboard({ user }: DashboardProps) {
setUsageData(usage);
setAchievements(achvs);
setSavingsConfig(savings);
console.log('[Dashboard] Loaded prefs:', prefs);
setRefreshKey(prev => prev + 1);
return { prefs, usage, achvs };
}, []);
@ -233,6 +234,11 @@ export function Dashboard({ user }: DashboardProps) {
setPreferences(updatedPrefs);
await savePreferencesAsync(updatedPrefs);
}}
preferences={preferences}
onPreferencesUpdate={async (updatedPrefs) => {
await savePreferencesAsync(updatedPrefs);
setPreferences(updatedPrefs);
}}
/>
</div>
<div className="opacity-0 animate-fade-in-up delay-200">

View File

@ -1,8 +1,8 @@
'use client';
import { useMemo, useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { HEALTH_MILESTONES, getMinutesSinceQuit, UsageEntry, UserPreferences } from '@/lib/storage';
import { HEALTH_MILESTONES, UsageEntry, UserPreferences } from '@/lib/storage';
import { useTheme } from '@/lib/theme-context';
import {
Heart,
@ -14,13 +14,13 @@ import {
Sparkles,
HeartHandshake,
CheckCircle2,
Clock,
Cigarette,
Leaf
} from 'lucide-react';
interface HealthTimelineCardProps {
usageData: UsageEntry[];
preferences?: UserPreferences | null;
}
const iconMap: Record<string, React.ElementType> = {
@ -34,8 +34,60 @@ const iconMap: Record<string, React.ElementType> = {
HeartHandshake,
};
// Simple, direct calculation of minutes since last usage
function calculateMinutesFree(
substance: 'nicotine' | 'weed',
usageData: UsageEntry[],
preferences: UserPreferences | null
): number {
const now = new Date();
// 1. Check for stored timestamp first (most accurate)
const lastUsageTime = substance === 'nicotine'
? preferences?.lastNicotineUsageTime
: preferences?.lastWeedUsageTime;
if (lastUsageTime) {
const lastTime = new Date(lastUsageTime);
const diffMs = now.getTime() - lastTime.getTime();
return Math.max(0, diffMs / (1000 * 60));
}
// 2. Find last recorded usage from usage data
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 todayStr = now.toISOString().split('T')[0];
// If last usage was today but no timestamp, count from now (0 minutes)
if (lastDateStr === todayStr) {
return 0;
}
// For past days, count from end of that day (23:59:59)
const lastDate = new Date(lastDateStr);
lastDate.setHours(23, 59, 59, 999);
const diffMs = now.getTime() - lastDate.getTime();
return Math.max(0, diffMs / (1000 * 60));
}
// 3. No usage ever - count from tracking start date
if (preferences?.trackingStartDate) {
const startDate = new Date(preferences.trackingStartDate);
startDate.setHours(0, 0, 0, 0);
const diffMs = now.getTime() - startDate.getTime();
return Math.max(0, diffMs / (1000 * 60));
}
return 0;
}
function formatDuration(minutes: number): string {
if (minutes < 60) return `${minutes} min`;
if (minutes < 1) return '< 1 min';
if (minutes < 60) return `${Math.floor(minutes)} min`;
if (minutes < 1440) return `${Math.floor(minutes / 60)} hrs`;
if (minutes < 10080) return `${Math.floor(minutes / 1440)} days`;
if (minutes < 43200) return `${Math.floor(minutes / 10080)} weeks`;
@ -51,55 +103,52 @@ function formatTimeRemaining(currentMinutes: number, targetMinutes: number): str
interface TimelineColumnProps {
substance: 'nicotine' | 'weed';
minutesSinceQuit: number;
minutesFree: 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) {
return i;
}
function TimelineColumn({ substance, minutesFree, theme }: TimelineColumnProps) {
// Find current milestone
let currentMilestoneIndex = -1;
for (let i = HEALTH_MILESTONES.length - 1; i >= 0; i--) {
if (minutesFree >= HEALTH_MILESTONES[i].timeMinutes) {
currentMilestoneIndex = i;
break;
}
return -1;
}, [minutesSinceQuit]);
}
const nextMilestone = useMemo(() => {
const nextIndex = currentMilestoneIndex + 1;
if (nextIndex < HEALTH_MILESTONES.length) {
return HEALTH_MILESTONES[nextIndex];
}
return null;
}, [currentMilestoneIndex]);
// Find next milestone
const nextMilestoneIndex = currentMilestoneIndex + 1;
const nextMilestone = nextMilestoneIndex < HEALTH_MILESTONES.length
? HEALTH_MILESTONES[nextMilestoneIndex]
: null;
const progressToNext = useMemo(() => {
if (!nextMilestone) return 100;
const prevMinutes =
currentMilestoneIndex >= 0
? HEALTH_MILESTONES[currentMilestoneIndex].timeMinutes
: 0;
// Calculate progress to next milestone
let progressToNext = 100;
if (nextMilestone) {
const prevMinutes = currentMilestoneIndex >= 0
? HEALTH_MILESTONES[currentMilestoneIndex].timeMinutes
: 0;
const range = nextMilestone.timeMinutes - prevMinutes;
const progress = minutesSinceQuit - prevMinutes;
return Math.min(100, Math.max(0, (progress / range) * 100));
}, [minutesSinceQuit, nextMilestone, currentMilestoneIndex]);
const progress = minutesFree - prevMinutes;
progressToNext = Math.min(100, Math.max(0, (progress / range) * 100));
}
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 (
<div className="flex flex-col h-full bg-black/5 dark:bg-white/5 rounded-xl border border-white/5 overflow-hidden">
{/* Header */}
{/* Header with live timer */}
<div className={`p-3 border-b border-white/5 flex items-center gap-2 ${theme === 'light' ? 'bg-white/50' : 'bg-black/20'}`}>
<SubstanceIcon className={`h-4 w-4 ${accentColorClass}`} />
<span className={`text-sm font-semibold ${theme === 'light' ? 'text-slate-900' : 'text-white'}`}>
{substanceLabel}
</span>
<span className="ml-auto text-xs opacity-70 font-medium">
{formatDuration(Math.floor(minutesSinceQuit))} free
<span className="ml-auto text-xs opacity-70 font-medium tabular-nums">
{formatDuration(minutesFree)} free
</span>
</div>
@ -110,7 +159,7 @@ function TimelineColumn({ substance, minutesSinceQuit, theme }: TimelineColumnPr
<div className="flex items-center justify-between mb-2">
<span className={`text-xs font-medium opacity-80 ${theme === 'light' ? 'text-slate-700' : 'text-white'}`}>Next Up</span>
<span className={`text-xs font-bold ${accentColorClass}`}>
{formatTimeRemaining(Math.floor(minutesSinceQuit), nextMilestone.timeMinutes)}
{formatTimeRemaining(minutesFree, nextMilestone.timeMinutes)}
</span>
</div>
<div className="w-full bg-slate-200 dark:bg-white/10 rounded-full h-1.5 overflow-hidden">
@ -127,7 +176,7 @@ function TimelineColumn({ substance, minutesSinceQuit, theme }: TimelineColumnPr
{/* Timeline Items */}
{HEALTH_MILESTONES.map((milestone, index) => {
const isAchieved = minutesSinceQuit >= milestone.timeMinutes;
const isAchieved = minutesFree >= milestone.timeMinutes;
const isCurrent = index === currentMilestoneIndex;
const Icon = iconMap[milestone.icon] || Heart;
@ -135,15 +184,15 @@ function TimelineColumn({ substance, minutesSinceQuit, theme }: TimelineColumnPr
<div
key={milestone.id}
className={`flex items-start gap-2.5 p-2 rounded-lg transition-all ${isAchieved
? (theme === 'light' ? 'bg-slate-100/50' : 'bg-white/5')
: 'opacity-50 grayscale'
} ${isCurrent ? `ring-1 ring-${accentColor}-500/50 bg-${accentColor}-500/5` : ''}`}
? (theme === 'light' ? 'bg-slate-100/50' : 'bg-white/5')
: 'opacity-50 grayscale'
} ${isCurrent ? 'ring-1 ring-offset-1 ring-offset-transparent ' + (substance === 'nicotine' ? 'ring-red-500/50' : 'ring-green-500/50') : ''}`}
>
{/* Icon */}
<div
className={`p-1.5 rounded-full shrink-0 mt-0.5 ${isAchieved
? (theme === 'light' ? 'bg-white text-slate-700 shadow-sm' : 'bg-white/10 text-white')
: 'bg-black/5 text-black/30 dark:bg-white/5 dark:text-white/30'
? (theme === 'light' ? 'bg-white text-slate-700 shadow-sm' : 'bg-white/10 text-white')
: 'bg-black/5 text-black/30 dark:bg-white/5 dark:text-white/30'
}`}
>
{isAchieved ? <CheckCircle2 className="h-3 w-3" /> : <Icon className="h-3 w-3" />}
@ -151,11 +200,9 @@ function TimelineColumn({ substance, minutesSinceQuit, theme }: TimelineColumnPr
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-1">
<p className={`text-xs font-semibold ${theme === 'light' ? 'text-slate-900' : 'text-white'}`}>
{milestone.title}
</p>
</div>
<p className={`text-xs font-semibold ${theme === 'light' ? 'text-slate-900' : 'text-white'}`}>
{milestone.title}
</p>
<p className={`text-[10px] mt-0.5 leading-tight ${theme === 'light' ? 'text-slate-600' : 'text-white/50'}`}>
{milestone.description}
</p>
@ -168,20 +215,30 @@ function TimelineColumn({ substance, minutesSinceQuit, theme }: TimelineColumnPr
);
}
export function HealthTimelineCard({ usageData, preferences }: HealthTimelineCardProps & { preferences?: UserPreferences | null }) {
export function HealthTimelineCard({ usageData, preferences }: HealthTimelineCardProps) {
const { theme } = useTheme();
const [now, setNow] = useState(Date.now());
// State for live timer values
const [nicotineMinutes, setNicotineMinutes] = useState(0);
const [weedMinutes, setWeedMinutes] = useState(0);
// Function to recalculate both timers
const updateTimers = useCallback(() => {
const prefs = preferences || null;
setNicotineMinutes(calculateMinutesFree('nicotine', usageData, prefs));
setWeedMinutes(calculateMinutesFree('weed', usageData, prefs));
}, [usageData, preferences]);
// Initial calculation and start interval
useEffect(() => {
const interval = setInterval(() => {
setNow(Date.now());
}, 1000); // Update every second
// Calculate immediately
updateTimers();
// Update every second
const interval = setInterval(updateTimers, 1000);
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]);
}, [updateTimers]);
const cardBackground =
theme === 'light'
@ -207,8 +264,8 @@ export function HealthTimelineCard({ usageData, preferences }: HealthTimelineCar
<CardContent className="relative z-10 flex-1 min-h-0 pb-6 pt-0">
<div className="grid grid-cols-2 gap-4 h-full">
<TimelineColumn substance="nicotine" minutesSinceQuit={nicotineMinutes} theme={theme} />
<TimelineColumn substance="weed" minutesSinceQuit={weedMinutes} theme={theme} />
<TimelineColumn substance="nicotine" minutesFree={nicotineMinutes} theme={theme} />
<TimelineColumn substance="weed" minutesFree={weedMinutes} theme={theme} />
</div>
</CardContent>
</Card>

View File

@ -12,7 +12,7 @@ import {
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 { UsageEntry, UserPreferences, setUsageForDateAsync, clearDayDataAsync } from '@/lib/storage';
import { ChevronLeftIcon, ChevronRightIcon, Cigarette, Leaf, Sparkles } from 'lucide-react';
import { useTheme } from '@/lib/theme-context';
import { DailyInspirationCard } from './DailyInspirationCard';
@ -25,9 +25,11 @@ interface UsageCalendarProps {
userId: string;
religion?: 'christian' | 'muslim' | 'jewish' | 'secular' | null;
onReligionUpdate?: (religion: 'christian' | 'muslim' | 'jewish' | 'secular') => void;
preferences?: UserPreferences | null;
onPreferencesUpdate?: (prefs: UserPreferences) => Promise<void>;
}
export function UsageCalendar({ usageData, onDataUpdate, religion, onReligionUpdate }: UsageCalendarProps) {
export function UsageCalendar({ usageData, onDataUpdate, religion, onReligionUpdate, preferences, onPreferencesUpdate }: UsageCalendarProps) {
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
const [editNicotineCount, setEditNicotineCount] = useState('');
const [editWeedCount, setEditWeedCount] = useState('');
@ -59,6 +61,7 @@ export function UsageCalendar({ usageData, onDataUpdate, religion, onReligionUpd
const handleSave = async () => {
if (selectedDate) {
const dateStr = selectedDate.toISOString().split('T')[0];
const todayStr = new Date().toISOString().split('T')[0];
const newNicotineCount = parseInt(editNicotineCount, 10) || 0;
const newWeedCount = parseInt(editWeedCount, 10) || 0;
@ -66,6 +69,25 @@ export function UsageCalendar({ usageData, onDataUpdate, religion, onReligionUpd
setUsageForDateAsync(dateStr, newNicotineCount, 'nicotine'),
setUsageForDateAsync(dateStr, newWeedCount, 'weed'),
]);
// Update last usage time preferences if editing today's usage and count > 0
if (dateStr === todayStr && preferences && onPreferencesUpdate) {
const now = new Date().toISOString();
const updatedPrefs = { ...preferences };
if (newNicotineCount > 0) {
updatedPrefs.lastNicotineUsageTime = now;
}
if (newWeedCount > 0) {
updatedPrefs.lastWeedUsageTime = now;
}
// Only update if we changed something
if (newNicotineCount > 0 || newWeedCount > 0) {
await onPreferencesUpdate(updatedPrefs);
}
}
onDataUpdate();
}
setIsEditing(false);

View File

@ -395,81 +395,7 @@ export function calculateTotalSaved(
return Math.max(0, expectedSpend - actualSpend);
}
export function getMinutesSinceQuit(
usageData: UsageEntry[],
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)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
if (substanceData.length === 0) {
// No usage recorded, assume they just started
return 0;
}
const now = new Date();
const todayStr = now.toISOString().split('T')[0];
const lastUsageDateStr = substanceData[0].date;
// If the last usage was today, reset to 0 (just used, unknown time)
if (lastUsageDateStr === todayStr) {
return 0;
}
// For past days, count from the end of that day
const lastUsageDate = new Date(lastUsageDateStr);
lastUsageDate.setHours(23, 59, 59, 999);
const diffMs = now.getTime() - lastUsageDate.getTime();
const minutes = Math.max(0, diffMs / (1000 * 60));
return precise ? minutes : Math.floor(minutes);
}
export function checkBadgeEligibility(
badgeId: string,

View File

@ -0,0 +1,77 @@
import { RecoveryTracker } from './RecoveryTracker';
export class NicotineTracker extends RecoveryTracker {
calculateMinutesFree(precise: boolean = false): number {
// 1. Try to use precise timestamp from preferences first
if (this.preferences?.lastNicotineUsageTime) {
const now = new Date();
const lastUsageTime = new Date(this.preferences.lastNicotineUsageTime);
console.log('[NicotineTracker] Found timestamp:', this.preferences.lastNicotineUsageTime);
const diffMs = now.getTime() - lastUsageTime.getTime();
const minutes = this.msToMinutes(diffMs, precise);
// Validation: Ensure the timestamp aligns with the last recorded usage date.
// If the user manually edited usage for a LATER date, the timestamp might be stale.
const lastRecordedDateStr = this.getLastRecordedDate();
console.log('[NicotineTracker] Last recorded date:', lastRecordedDateStr);
if (lastRecordedDateStr) {
const lastRecordedDate = new Date(lastRecordedDateStr);
lastRecordedDate.setHours(0, 0, 0, 0);
// If the timestamp is older than the start of the last recorded day,
// it means we have a newer manual entry without a timestamp.
// In this case, fall back to date-based logic.
if (lastUsageTime < lastRecordedDate) {
console.log('[NicotineTracker] Timestamp is stale, falling back to date logic');
return this.calculateDateBasedMinutes(lastRecordedDateStr, precise);
}
}
return minutes;
}
// 2. Fallback to date-based logic if no timestamp exists
const lastDateStr = this.getLastRecordedDate();
// 3. If no nicotine usage ever recorded, use tracking start date
if (!lastDateStr) {
if (this.preferences?.trackingStartDate) {
const startDate = new Date(this.preferences.trackingStartDate);
startDate.setHours(0, 0, 0, 0); // Count from start of tracking day
const now = new Date();
const diffMs = now.getTime() - startDate.getTime();
return this.msToMinutes(diffMs, precise);
}
return 0; // No usage and no tracking start date
}
return this.calculateDateBasedMinutes(lastDateStr, precise);
}
private getLastRecordedDate(): string | null {
const nicotineData = this.usageData
.filter((e) => e.substance === 'nicotine' && e.count > 0)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
return nicotineData.length > 0 ? nicotineData[0].date : null;
}
private calculateDateBasedMinutes(dateStr: string, precise: boolean): number {
const now = new Date();
const todayStr = now.toISOString().split('T')[0];
// If the last usage was today but we have no timestamp, reset to 0
if (dateStr === todayStr) {
return 0;
}
// For past days, count from the END of that day (23:59:59)
const lastUsageDate = new Date(dateStr);
lastUsageDate.setHours(23, 59, 59, 999);
const diffMs = now.getTime() - lastUsageDate.getTime();
return this.msToMinutes(diffMs, precise);
}
}

View File

@ -0,0 +1,33 @@
import { UsageEntry, UserPreferences } from '../storage';
export abstract class RecoveryTracker {
protected usageData: UsageEntry[];
protected preferences: UserPreferences | null;
constructor(usageData: UsageEntry[], preferences: UserPreferences | null) {
this.usageData = usageData;
this.preferences = preferences;
}
/**
* Calculates the number of minutes elapsed since the last usage.
* This is the core logic that subclasses must support, but the implementation
* heavily depends on the specific substance's data source (preferences timestamp vs usage logs).
*/
abstract calculateMinutesFree(precise?: boolean): number;
/**
* Helper to convert milliseconds to minutes with optional precision.
*/
protected msToMinutes(ms: number, precise: boolean = false): number {
const minutes = Math.max(0, ms / (1000 * 60));
return precise ? minutes : Math.floor(minutes);
}
/**
* Helper to check if a timestamp is valid and recent enough to rely on.
*/
protected isValidTimestamp(timestamp: string | null | undefined): boolean {
return !!timestamp && !isNaN(new Date(timestamp).getTime());
}
}

View File

@ -0,0 +1,69 @@
import { RecoveryTracker } from './RecoveryTracker';
export class WeedTracker extends RecoveryTracker {
calculateMinutesFree(precise: boolean = false): number {
// 1. Try to use precise timestamp from preferences first
if (this.preferences?.lastWeedUsageTime) {
const now = new Date();
const lastUsageTime = new Date(this.preferences.lastWeedUsageTime);
const diffMs = now.getTime() - lastUsageTime.getTime();
const minutes = this.msToMinutes(diffMs, precise);
// Validation: Ensure the timestamp aligns with the last recorded usage date.
const lastRecordedDateStr = this.getLastRecordedDate();
if (lastRecordedDateStr) {
const lastRecordedDate = new Date(lastRecordedDateStr);
lastRecordedDate.setHours(0, 0, 0, 0);
if (lastUsageTime < lastRecordedDate) {
return this.calculateDateBasedMinutes(lastRecordedDateStr, precise);
}
}
return minutes;
}
// 2. Fallback to date-based logic if no timestamp exists
const lastDateStr = this.getLastRecordedDate();
// 3. If no weed usage ever recorded, use tracking start date
if (!lastDateStr) {
if (this.preferences?.trackingStartDate) {
const startDate = new Date(this.preferences.trackingStartDate);
startDate.setHours(0, 0, 0, 0); // Count from start of tracking day
const now = new Date();
const diffMs = now.getTime() - startDate.getTime();
return this.msToMinutes(diffMs, precise);
}
return 0; // No usage and no tracking start date
}
return this.calculateDateBasedMinutes(lastDateStr, precise);
}
private getLastRecordedDate(): string | null {
const weedData = this.usageData
.filter((e) => e.substance === 'weed' && e.count > 0)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
return weedData.length > 0 ? weedData[0].date : null;
}
private calculateDateBasedMinutes(dateStr: string, precise: boolean): number {
const now = new Date();
const todayStr = now.toISOString().split('T')[0];
// If the last usage was today but we have no timestamp, reset to 0
if (dateStr === todayStr) {
return 0;
}
// For past days, count from the END of that day (23:59:59)
const lastUsageDate = new Date(dateStr);
lastUsageDate.setHours(23, 59, 59, 999);
const diffMs = now.getTime() - lastUsageDate.getTime();
return this.msToMinutes(diffMs, precise);
}
}