feat: Implement independent nicotine/weed quit plans with refined UI and auto-unlock logic
This commit is contained in:
parent
7046febd00
commit
75a75fd499
@ -54,6 +54,7 @@ export async function POST(request: NextRequest) {
|
||||
hasCompletedSetup?: boolean;
|
||||
dailyGoal?: number;
|
||||
quitPlan?: unknown;
|
||||
quitState?: unknown;
|
||||
userName?: string;
|
||||
userAge?: number;
|
||||
religion?: string;
|
||||
@ -61,12 +62,17 @@ export async function POST(request: NextRequest) {
|
||||
lastWeedUsageTime?: string;
|
||||
};
|
||||
|
||||
// If quitState is provided in body, save it to quitPlanJson
|
||||
const quitPlanJson = body.quitState
|
||||
? JSON.stringify(body.quitState)
|
||||
: (body.quitPlan ? JSON.stringify(body.quitPlan) : undefined);
|
||||
|
||||
const preferences = await upsertPreferencesD1(session.user.id, {
|
||||
substance: body.substance,
|
||||
trackingStartDate: body.trackingStartDate,
|
||||
hasCompletedSetup: body.hasCompletedSetup ? 1 : 0,
|
||||
dailyGoal: body.dailyGoal,
|
||||
quitPlanJson: body.quitPlan ? JSON.stringify(body.quitPlan) : undefined,
|
||||
quitPlanJson: quitPlanJson,
|
||||
userName: body.userName,
|
||||
userAge: body.userAge,
|
||||
religion: body.religion,
|
||||
@ -78,12 +84,21 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Failed to save preferences' }, { status: 500 });
|
||||
}
|
||||
|
||||
// Parse returned JSON to construct state again
|
||||
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,
|
||||
|
||||
@ -106,7 +106,10 @@ export function Dashboard({ user }: DashboardProps) {
|
||||
prefs: UserPreferences,
|
||||
currentAchievements: Achievement[]
|
||||
) => {
|
||||
// Current unlocked set (local + server)
|
||||
const unlockedIds = new Set(currentAchievements.map(a => `${a.badgeId}-${a.substance}`));
|
||||
const newUnlocked: Achievement[] = [];
|
||||
let badgeToCelebrate: BadgeDefinition | null = null;
|
||||
|
||||
for (const badge of BADGE_DEFINITIONS) {
|
||||
for (const substance of ['nicotine', 'weed'] as const) {
|
||||
@ -115,16 +118,34 @@ export function Dashboard({ user }: DashboardProps) {
|
||||
|
||||
const isEligible = checkBadgeEligibility(badge.id, usage, prefs, substance);
|
||||
if (isEligible) {
|
||||
try {
|
||||
const result = await unlockAchievement(badge.id, substance);
|
||||
if (result.isNew && result.achievement) {
|
||||
setNewBadge(badge);
|
||||
newUnlocked.push(result.achievement);
|
||||
// Prioritize celebrating the first one found
|
||||
if (!badgeToCelebrate) {
|
||||
badgeToCelebrate = badge;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error unlocking achievement:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newUnlocked.length > 0) {
|
||||
// Update local state with ALL new achievements
|
||||
setAchievements(prev => [...prev, ...newUnlocked]);
|
||||
|
||||
// Show celebration for determining badge
|
||||
if (badgeToCelebrate) {
|
||||
setNewBadge(badgeToCelebrate);
|
||||
setShowCelebration(true);
|
||||
setAchievements(prev => [...prev, result.achievement!]);
|
||||
return; // Only show one celebration at a time
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newUnlocked.length > 0;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@ -208,18 +229,41 @@ export function Dashboard({ user }: DashboardProps) {
|
||||
setUsageData(usage);
|
||||
setRefreshKey(prev => prev + 1);
|
||||
|
||||
// Check for new achievements immediately
|
||||
// Check for new achievements metrics FIRST
|
||||
await checkAndUnlockAchievements(usage, latestPrefs, achievements);
|
||||
|
||||
// Force a fresh fetch of all data to ensure UI sync
|
||||
const freshAchievements = await fetchAchievements();
|
||||
setAchievements(freshAchievements);
|
||||
|
||||
// THEN refresh UI components
|
||||
setRefreshKey(prev => prev + 1);
|
||||
};
|
||||
|
||||
const handleGeneratePlan = async () => {
|
||||
const handleGeneratePlan = async (targetSubstance: 'nicotine' | 'weed') => {
|
||||
if (!preferences) return;
|
||||
|
||||
const plan = generateQuitPlan(preferences.substance);
|
||||
const plan = generateQuitPlan(targetSubstance);
|
||||
|
||||
// Construct new state
|
||||
const currentQuitState = preferences.quitState || {
|
||||
nicotine: { plan: null, startDate: null },
|
||||
weed: { plan: null, startDate: null }
|
||||
};
|
||||
|
||||
const updatedQuitState = {
|
||||
...currentQuitState,
|
||||
[targetSubstance]: {
|
||||
plan,
|
||||
startDate: currentQuitState[targetSubstance].startDate || (preferences.substance === targetSubstance ? preferences.trackingStartDate : null) || getTodayString()
|
||||
}
|
||||
};
|
||||
|
||||
const updatedPrefs = {
|
||||
...preferences,
|
||||
quitPlan: plan,
|
||||
quitState: updatedQuitState
|
||||
};
|
||||
|
||||
await savePreferencesAsync(updatedPrefs);
|
||||
setPreferences(updatedPrefs);
|
||||
setRefreshKey(prev => prev + 1);
|
||||
@ -314,12 +358,41 @@ export function Dashboard({ user }: DashboardProps) {
|
||||
</div>
|
||||
<div className="space-y-4 sm:grid sm:grid-cols-2 sm:gap-6 sm:space-y-0">
|
||||
<MoodTracker />
|
||||
{/* Nicotine Plan */}
|
||||
{(preferences.substance === 'nicotine' || usageData.some(e => e.substance === 'nicotine')) && (
|
||||
<QuitPlanCard
|
||||
key={`quit-plan-${refreshKey}`}
|
||||
plan={preferences.quitPlan}
|
||||
onGeneratePlan={handleGeneratePlan}
|
||||
key={`quit-plan-nicotine-${refreshKey}`}
|
||||
plan={preferences.quitState?.nicotine.plan || (preferences.substance === 'nicotine' ? preferences.quitPlan : null)}
|
||||
onGeneratePlan={() => 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')) && (
|
||||
<QuitPlanCard
|
||||
key={`quit-plan-weed-${refreshKey}`}
|
||||
plan={preferences.quitState?.weed.plan || (preferences.substance === 'weed' ? preferences.quitPlan : null)}
|
||||
onGeneratePlan={() => 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"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -6,27 +6,59 @@ 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.map(e => e.date)).size;
|
||||
const uniqueDaysWithData = new Set(usageData.filter(e => e.substance === substance).map(e => e.date)).size;
|
||||
const daysRemaining = Math.max(0, 7 - uniqueDaysWithData);
|
||||
const hasEnoughData = uniqueDaysWithData >= 7;
|
||||
|
||||
// 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.reduce((sum, e) => sum + e.count, 0);
|
||||
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)
|
||||
@ -48,7 +80,7 @@ function QuitPlanCardComponent({
|
||||
<CardHeader className="relative z-10">
|
||||
<CardTitle className="flex items-center gap-2 text-white text-shadow-sm">
|
||||
<Target className="h-5 w-5 text-yellow-400" />
|
||||
Your Personalized Plan
|
||||
Your {substance === 'nicotine' ? 'Nicotine' : 'Weed'} Quit Plan
|
||||
</CardTitle>
|
||||
<CardDescription className="text-white/70">
|
||||
We're tracking your usage to build your custom quit plan
|
||||
@ -73,7 +105,7 @@ function QuitPlanCardComponent({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{hasEnoughData ? (
|
||||
{isUnlocked ? (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-white text-center">
|
||||
Great work! Your average daily usage is{' '}
|
||||
@ -107,6 +139,19 @@ function QuitPlanCardComponent({
|
||||
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 (
|
||||
<Card className="backdrop-blur-xl shadow-xl drop-shadow-lg border-pink-500/40 hover-lift transition-all duration-300 overflow-hidden relative" style={{
|
||||
background: pinkBackground
|
||||
@ -115,7 +160,7 @@ function QuitPlanCardComponent({
|
||||
<CardHeader className="relative z-10">
|
||||
<CardTitle className="flex items-center gap-2 text-white text-shadow-sm">
|
||||
<TrendingDown className="h-5 w-5 text-pink-400" />
|
||||
Your Quit Plan
|
||||
Your {substance === 'nicotine' ? 'Nicotine' : 'Weed'} Plan
|
||||
</CardTitle>
|
||||
<CardDescription className="text-white/70">
|
||||
Week {Math.min(weekNumber, totalWeeks)} of {totalWeeks} - 25% weekly reduction
|
||||
@ -123,30 +168,49 @@ function QuitPlanCardComponent({
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 relative z-10">
|
||||
<div className="bg-gradient-to-br from-pink-500/25 to-pink-600/20 border border-pink-500/40 p-5 rounded-xl text-center backdrop-blur-sm">
|
||||
<p className="text-sm text-white/70 mb-1">This week's daily target</p>
|
||||
<p className="text-sm text-white/70 mb-1">{substance === 'nicotine' ? 'Nicotine' : 'Weed'} Max Puffs Target</p>
|
||||
<p className="text-5xl font-bold text-pink-300 text-shadow">
|
||||
{currentTarget !== null && currentTarget > 0 ? currentTarget : '0'}
|
||||
</p>
|
||||
<p className="text-sm text-white/60">per day</p>
|
||||
<p className="text-sm text-white/60 mb-3">per day</p>
|
||||
|
||||
{/* Daily Progress Bar */}
|
||||
<div className="w-full bg-black/20 rounded-full h-2 mb-1 overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-500 ${progressColor}`}
|
||||
style={{ width: `${Math.min(100, usagePercent)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-white/60">
|
||||
{todayUsage} used / {currentTarget} allowed
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 relative z-10">
|
||||
<p className="text-sm font-medium text-white">Weekly targets:</p>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{plan.weeklyTargets.map((target, index) => (
|
||||
{plan.weeklyTargets.map((target, index) => {
|
||||
const weekNum = index + 1;
|
||||
const isFuture = weekNum > weekNumber;
|
||||
const isCurrent = weekNum === weekNumber;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`text-center p-2 rounded-lg transition-all duration-200 hover:scale-105 ${index + 1 === weekNumber
|
||||
? 'bg-gradient-to-br from-pink-500 to-pink-600 text-white shadow-lg shadow-pink-500/30'
|
||||
: index + 1 < weekNumber
|
||||
? 'bg-pink-900/50 text-pink-200'
|
||||
: 'bg-white/10 text-white/60'
|
||||
className={`text-center p-2 rounded-lg transition-all duration-200 ${isCurrent
|
||||
? 'bg-gradient-to-br from-pink-500 to-pink-600 text-white shadow-lg shadow-pink-500/30 scale-105'
|
||||
: isFuture
|
||||
? 'bg-white/5 text-white/40'
|
||||
: 'bg-pink-900/50 text-pink-200'
|
||||
}`}
|
||||
>
|
||||
<p className="text-xs">Week {index + 1}</p>
|
||||
<p className="font-bold">{target}</p>
|
||||
<p className="text-xs">Week {weekNum}</p>
|
||||
<p className="font-bold">
|
||||
{isFuture ? '?' : target}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -7,17 +7,26 @@ export interface UsageEntry {
|
||||
substance: 'nicotine' | 'weed';
|
||||
}
|
||||
|
||||
export interface SubstanceState {
|
||||
plan: QuitPlan | null;
|
||||
startDate: string | null;
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
substance: 'nicotine' | 'weed';
|
||||
trackingStartDate: string | null;
|
||||
hasCompletedSetup: boolean;
|
||||
dailyGoal: number | null;
|
||||
quitPlan: QuitPlan | null;
|
||||
quitState?: { // NEW: Flexible container for dual state
|
||||
nicotine: SubstanceState;
|
||||
weed: SubstanceState;
|
||||
};
|
||||
userName: string | null;
|
||||
userAge: number | null;
|
||||
religion: 'christian' | 'secular' | null;
|
||||
lastNicotineUsageTime?: string | null; // ISO timestamp of last usage
|
||||
lastWeedUsageTime?: string | null; // ISO timestamp of last usage
|
||||
lastNicotineUsageTime?: string | null;
|
||||
lastWeedUsageTime?: string | null;
|
||||
}
|
||||
|
||||
export interface QuitPlan {
|
||||
@ -110,6 +119,10 @@ const defaultPreferences: UserPreferences = {
|
||||
hasCompletedSetup: false,
|
||||
dailyGoal: null,
|
||||
quitPlan: null,
|
||||
quitState: {
|
||||
nicotine: { plan: null, startDate: null },
|
||||
weed: { plan: null, startDate: null }
|
||||
},
|
||||
userName: null,
|
||||
userAge: null,
|
||||
religion: null,
|
||||
@ -145,7 +158,7 @@ export function getCurrentUserId(): string | null {
|
||||
export async function fetchPreferences(): Promise<UserPreferences> {
|
||||
if (preferencesCache) return preferencesCache;
|
||||
try {
|
||||
const response = await fetch('/api/preferences');
|
||||
const response = await fetch('/api/preferences', { cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch preferences');
|
||||
return defaultPreferences;
|
||||
@ -177,7 +190,7 @@ export async function savePreferencesAsync(preferences: UserPreferences): Promis
|
||||
export async function fetchUsageData(): Promise<UsageEntry[]> {
|
||||
if (usageDataCache) return usageDataCache;
|
||||
try {
|
||||
const response = await fetch('/api/usage');
|
||||
const response = await fetch('/api/usage', { cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch usage data');
|
||||
return [];
|
||||
@ -240,7 +253,7 @@ export async function clearDayDataAsync(
|
||||
export async function fetchAchievements(): Promise<Achievement[]> {
|
||||
if (achievementsCache) return achievementsCache;
|
||||
try {
|
||||
const response = await fetch('/api/achievements');
|
||||
const response = await fetch('/api/achievements', { cache: 'no-store' });
|
||||
if (!response.ok) return [];
|
||||
const data = await response.json() as Achievement[];
|
||||
achievementsCache = data;
|
||||
@ -324,7 +337,7 @@ export function getReminderSettings(): ReminderSettings {
|
||||
export async function fetchSavingsConfig(): Promise<SavingsConfig | null> {
|
||||
if (savingsConfigCache) return savingsConfigCache;
|
||||
try {
|
||||
const response = await fetch('/api/savings');
|
||||
const response = await fetch('/api/savings', { cache: 'no-store' });
|
||||
if (!response.ok) return null;
|
||||
const data = await response.json() as SavingsConfig | null;
|
||||
savingsConfigCache = data;
|
||||
@ -359,7 +372,7 @@ export function getSavingsConfig(): SavingsConfig | null {
|
||||
export async function fetchMoodEntries(): Promise<MoodEntry[]> {
|
||||
if (moodEntriesCache) return moodEntriesCache;
|
||||
try {
|
||||
const response = await fetch('/api/mood');
|
||||
const response = await fetch('/api/mood', { cache: 'no-store' });
|
||||
if (!response.ok) return [];
|
||||
const data = await response.json() as MoodEntry[];
|
||||
moodEntriesCache = data;
|
||||
@ -412,7 +425,10 @@ export function calculateStreak(
|
||||
for (let i = 0; i <= 365; i++) {
|
||||
const checkDate = new Date(today);
|
||||
checkDate.setDate(checkDate.getDate() - i);
|
||||
const dateStr = checkDate.toISOString().split('T')[0];
|
||||
// Use local date string to match storage format
|
||||
const offset = checkDate.getTimezoneOffset();
|
||||
const localDate = new Date(checkDate.getTime() - (offset * 60 * 1000));
|
||||
const dateStr = localDate.toISOString().split('T')[0];
|
||||
|
||||
// O(1) lookup
|
||||
const dayUsage = substanceMap.get(dateStr) ?? -1;
|
||||
@ -496,7 +512,10 @@ export function checkBadgeEligibility(
|
||||
for (let i = 0; i <= 365; i++) {
|
||||
const d = new Date(today);
|
||||
d.setDate(d.getDate() - i);
|
||||
const ds = d.toISOString().split('T')[0];
|
||||
// Use local date string to match storage format
|
||||
const offset = d.getTimezoneOffset();
|
||||
const localDate = new Date(d.getTime() - (offset * 60 * 1000));
|
||||
const ds = localDate.toISOString().split('T')[0];
|
||||
const val = map.get(ds) ?? -1;
|
||||
if (val === 0) streak++;
|
||||
else if (val > 0) break;
|
||||
@ -507,23 +526,35 @@ export function checkBadgeEligibility(
|
||||
const streak = getStreakFromMap(substance === 'nicotine' ? stats.nicotineMap : stats.weedMap);
|
||||
|
||||
const checkMonthlyReduction = (): boolean => {
|
||||
const checkDate = new Date();
|
||||
// Use local dates to avoid UTC offset issues
|
||||
const offset = checkDate.getTimezoneOffset();
|
||||
const todayLocal = new Date(checkDate.getTime() - (offset * 60 * 1000));
|
||||
|
||||
if (!preferences.trackingStartDate) return false;
|
||||
const start = new Date(preferences.trackingStartDate);
|
||||
const today = new Date();
|
||||
const daysSinceStart = Math.floor((today.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
// Parse start date as local
|
||||
const [y, m, d] = preferences.trackingStartDate.split('-').map(Number);
|
||||
const startLocal = new Date(y, m - 1, d); // Month is 0-indexed in Date constructor
|
||||
|
||||
const daysSinceStart = Math.floor((todayLocal.getTime() - startLocal.getTime()) / (1000 * 60 * 60 * 24));
|
||||
if (daysSinceStart < 30) return false;
|
||||
|
||||
// Use current Map for O(1) lookups in week buckets
|
||||
let firstWeekTotal = 0;
|
||||
let lastWeekTotal = 0;
|
||||
|
||||
const startTime = start.getTime();
|
||||
const todayTime = today.getTime();
|
||||
const startTime = startLocal.getTime();
|
||||
const todayTime = todayLocal.getTime();
|
||||
const msInDay = 1000 * 60 * 60 * 24;
|
||||
|
||||
for (const entry of usageData) {
|
||||
if (entry.substance !== substance) continue;
|
||||
const entryTime = new Date(entry.date).getTime();
|
||||
|
||||
// Parse entry date as local
|
||||
const [ey, em, ed] = entry.date.split('-').map(Number);
|
||||
const entryTime = new Date(ey, em - 1, ed).getTime();
|
||||
|
||||
const daysSinceEntryStart = Math.floor((entryTime - startTime) / msInDay);
|
||||
const daysAgo = Math.floor((todayTime - entryTime) / msInDay);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user