diff --git a/src/app/api/preferences/route.ts b/src/app/api/preferences/route.ts
index 6ba1b84..4d87fec 100644
--- a/src/app/api/preferences/route.ts
+++ b/src/app/api/preferences/route.ts
@@ -71,22 +71,25 @@ export async function POST(request: NextRequest) {
lastWeedUsageTime?: string;
};
- // Validation
+ // Validation & Normalization
if (body.substance && !['nicotine', 'weed'].includes(body.substance)) {
return NextResponse.json({ error: 'Invalid substance' }, { status: 400 });
}
if (body.trackingStartDate && !/^\d{4}-\d{2}-\d{2}$/.test(body.trackingStartDate)) {
return NextResponse.json({ error: 'Invalid trackingStartDate format' }, { status: 400 });
}
- if (body.dailyGoal !== undefined && (typeof body.dailyGoal !== 'number' || body.dailyGoal < 0)) {
+
+ // Loose type checking for numbers (allow strings that parse to numbers)
+ const dailyGoal = Number(body.dailyGoal);
+ if (body.dailyGoal !== undefined && body.dailyGoal !== null && (isNaN(dailyGoal) || dailyGoal < 0)) {
return NextResponse.json({ error: 'Invalid dailyGoal' }, { status: 400 });
}
- if (body.userName && (typeof body.userName !== 'string' || body.userName.length > 100)) {
- return NextResponse.json({ error: 'Invalid userName' }, { status: 400 });
- }
- if (body.userAge !== undefined && (typeof body.userAge !== 'number' || body.userAge < 0 || body.userAge > 120)) {
+
+ const userAge = Number(body.userAge);
+ if (body.userAge !== undefined && body.userAge !== null && (isNaN(userAge) || userAge < 0 || userAge > 120)) {
return NextResponse.json({ error: 'Invalid userAge' }, { status: 400 });
}
+
if (body.religion && !['christian', 'secular'].includes(body.religion)) {
return NextResponse.json({ error: 'Invalid religion' }, { status: 400 });
}
diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx
index 54ef562..ecaa87e 100644
--- a/src/components/Dashboard.tsx
+++ b/src/components/Dashboard.tsx
@@ -218,8 +218,17 @@ export function Dashboard({ user }: DashboardProps) {
...preferences,
[substance === 'nicotine' ? 'lastNicotineUsageTime' : 'lastWeedUsageTime']: now,
};
- await savePreferencesAsync(latestPrefs);
- setPreferences(latestPrefs);
+
+ // Force specific fields to be present to avoid partial update issues
+ // This ensures that even if preferences is stale, we explicitly set the usage time
+ const payload: UserPreferences = {
+ ...latestPrefs,
+ lastNicotineUsageTime: substance === 'nicotine' ? now : (latestPrefs.lastNicotineUsageTime ?? null),
+ lastWeedUsageTime: substance === 'weed' ? now : (latestPrefs.lastWeedUsageTime ?? null),
+ };
+
+ await savePreferencesAsync(payload);
+ setPreferences(payload);
}
setActiveLoggingSubstance(null);
diff --git a/src/components/HealthTimelineCard.tsx b/src/components/HealthTimelineCard.tsx
index e0d9647..6c604ef 100644
--- a/src/components/HealthTimelineCard.tsx
+++ b/src/components/HealthTimelineCard.tsx
@@ -18,6 +18,7 @@ import {
Cigarette,
Leaf
} from 'lucide-react';
+import { getTodayString, getLocalDateString } from '@/lib/date-utils';
interface HealthTimelineCardProps {
usageData: UsageEntry[];
@@ -225,29 +226,57 @@ function HealthTimelineCardComponent({
// Calculate last usage timestamps only when data changes
const lastUsageTimes = useMemo(() => {
const getTimestamp = (substance: 'nicotine' | 'weed') => {
- // 1. Check for stored timestamp first
- const stored = substance === 'nicotine' ? preferences?.lastNicotineUsageTime : preferences?.lastWeedUsageTime;
- if (stored) return new Date(stored).getTime();
+ let lastTime = 0;
- // 2. Fallback to usage data
+ // 1. Check for stored timestamp
+ const stored = substance === 'nicotine' ? preferences?.lastNicotineUsageTime : preferences?.lastWeedUsageTime;
+ if (stored) {
+ lastTime = new Date(stored).getTime();
+ }
+
+ // 2. Check usage data (usually more up-to-date for "just logged")
const lastEntry = usageData
.filter(e => e.substance === substance && e.count > 0)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())[0];
if (lastEntry) {
+ const todayStr = getTodayString();
+
+ // Calculate local midnight for today
+ const now = new Date();
+ const localMidnight = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
+
+ // If usage recorded today
+ if (lastEntry.date === todayStr) {
+ // Check if the stored timestamp belongs to today (is after midnight)
+ if (lastTime >= localMidnight) {
+ return lastTime;
+ }
+
+ // Fallback: If we have usage "Today" but no valid timestamp,
+ // we must assume it just happened or we missed the timestamp.
+ // Returning Date.now() resets the timer to 0.
+ return Date.now();
+ }
+
const d = new Date(lastEntry.date);
d.setHours(23, 59, 59, 999);
- return d.getTime();
+ const entryTime = d.getTime();
+
+ // Take the more recent of the two
+ lastTime = Math.max(lastTime, entryTime);
}
- // 3. Fallback to start date
- if (preferences?.trackingStartDate) {
+
+
+ // 3. Fallback to start date if no usage found
+ if (lastTime === 0 && preferences?.trackingStartDate) {
const d = new Date(preferences.trackingStartDate);
d.setHours(0, 0, 0, 0);
- return d.getTime();
+ lastTime = d.getTime();
}
- return null;
+ return lastTime || null;
};
return {
@@ -302,6 +331,7 @@ function HealthTimelineCardComponent({
+
);
diff --git a/src/lib/d1.ts b/src/lib/d1.ts
index 67fb451..ca7f74f 100644
--- a/src/lib/d1.ts
+++ b/src/lib/d1.ts
@@ -74,17 +74,29 @@ export async function upsertPreferencesD1(userId: string, data: Partial 1) { // At least updatedAt is always there
+ await db.prepare(
+ `UPDATE UserPreferences SET ${updates.join(', ')} WHERE userId = ?`
+ ).bind(...values).run();
+ }
+
} else {
// Insert
await db.prepare(
diff --git a/src/lib/storage.ts b/src/lib/storage.ts
index a90c365..b14990b 100644
--- a/src/lib/storage.ts
+++ b/src/lib/storage.ts
@@ -181,6 +181,9 @@ export async function savePreferencesAsync(preferences: UserPreferences): Promis
});
if (response.ok) {
preferencesCache = preferences;
+ } else {
+ const err = await response.json();
+ console.error('[Storage] Error saving preferences:', response.status, err);
}
} catch (error) {
console.error('Error saving preferences:', error);