fix desktop dashboard regressions and one-time v1.1 release notes

This commit is contained in:
Avery Felts 2026-02-24 02:31:04 -07:00
parent e5b3f649be
commit e8f47993a9
11 changed files with 236 additions and 116 deletions

View File

@ -0,0 +1 @@
ALTER TABLE UserPreferences ADD COLUMN lastSeenReleaseNotesVersion TEXT;

View File

@ -11,6 +11,7 @@ CREATE TABLE "UserPreferences" (
"religion" TEXT,
"lastNicotineUsageTime" TEXT,
"lastWeedUsageTime" TEXT,
"lastSeenReleaseNotesVersion" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"quitPlanJson" TEXT
@ -79,4 +80,3 @@ CREATE UNIQUE INDEX "ReminderSettings_userId_key" ON "ReminderSettings"("userId"
-- CreateIndex
CREATE UNIQUE INDEX "SavingsConfig_userId_key" ON "SavingsConfig"("userId");

View File

@ -24,6 +24,7 @@ model UserPreferences {
religion String?
lastNicotineUsageTime String?
lastWeedUsageTime String?
lastSeenReleaseNotesVersion String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

View File

@ -20,6 +20,10 @@ export async function GET() {
quitPlan: null,
userName: null,
userAge: null,
religion: null,
lastNicotineUsageTime: null,
lastWeedUsageTime: null,
lastSeenReleaseNotesVersion: null,
});
}
@ -43,6 +47,7 @@ export async function GET() {
religion: preferences.religion,
lastNicotineUsageTime: preferences.lastNicotineUsageTime,
lastWeedUsageTime: preferences.lastWeedUsageTime,
lastSeenReleaseNotesVersion: preferences.lastSeenReleaseNotesVersion,
});
} catch (error) {
console.error('Error fetching preferences:', error);
@ -69,6 +74,7 @@ export async function POST(request: NextRequest) {
religion?: string;
lastNicotineUsageTime?: string;
lastWeedUsageTime?: string;
lastSeenReleaseNotesVersion?: string;
};
// Validation & Normalization
@ -94,6 +100,12 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Invalid religion' }, { status: 400 });
}
if (body.lastSeenReleaseNotesVersion !== undefined && body.lastSeenReleaseNotesVersion !== null) {
if (typeof body.lastSeenReleaseNotesVersion !== 'string' || body.lastSeenReleaseNotesVersion.length > 32) {
return NextResponse.json({ error: 'Invalid lastSeenReleaseNotesVersion' }, { status: 400 });
}
}
// If quitState is provided in body, save it to quitPlanJson
const quitPlanJson = body.quitState
? JSON.stringify(body.quitState)
@ -110,6 +122,7 @@ export async function POST(request: NextRequest) {
religion: body.religion,
lastNicotineUsageTime: body.lastNicotineUsageTime,
lastWeedUsageTime: body.lastWeedUsageTime,
lastSeenReleaseNotesVersion: body.lastSeenReleaseNotesVersion,
});
if (!preferences) {
@ -136,6 +149,7 @@ export async function POST(request: NextRequest) {
religion: preferences.religion,
lastNicotineUsageTime: preferences.lastNicotineUsageTime,
lastWeedUsageTime: preferences.lastWeedUsageTime,
lastSeenReleaseNotesVersion: preferences.lastSeenReleaseNotesVersion,
});
} catch (error) {
console.error('Error saving preferences:', error);

View File

@ -616,7 +616,7 @@
align-items: flex-start;
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: x proximity;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
touch-action: pan-x pinch-zoom;
@ -640,7 +640,6 @@
flex: 0 0 calc(100vw - 3.25rem);
width: calc(100vw - 3.25rem);
scroll-snap-align: start;
scroll-snap-stop: always;
display: flex;
flex-direction: column;
justify-content: flex-start;

View File

@ -408,7 +408,7 @@ export function Dashboard({ user }: DashboardProps) {
onModalStateChange={handleModalStateChange}
/>
<main className="container mx-auto px-4 py-4 sm:py-8 pb-4 sm:pb-8 max-w-full">
<main className="container mx-auto px-4 py-4 sm:py-8 pb-28 sm:pb-8 max-w-full">
{loadError && (
<div className={`mb-4 rounded-xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm ${theme === 'light' ? 'text-amber-800' : 'text-amber-100'}`}>
<div className="flex items-center justify-between gap-3">
@ -476,6 +476,7 @@ export function Dashboard({ user }: DashboardProps) {
usageData={usageData}
onGeneratePlan={handleGeneratePlan}
refreshKey={refreshKey}
variant="desktop"
/>
</div>
@ -486,6 +487,13 @@ export function Dashboard({ user }: DashboardProps) {
usageData={usageData}
onDataUpdate={loadData}
userId={user.id}
religion={preferences.religion}
onReligionUpdate={async (religion: 'christian' | 'secular') => {
const updatedPrefs = { ...preferences, religion };
setPreferences(updatedPrefs);
await savePreferencesAsync(updatedPrefs);
}}
showInspirationPanel
preferences={preferences}
onPreferencesUpdate={async (updatedPrefs: UserPreferences) => {
await savePreferencesAsync(updatedPrefs);
@ -575,6 +583,7 @@ export function Dashboard({ user }: DashboardProps) {
usageData={usageData}
onGeneratePlan={handleGeneratePlan}
refreshKey={refreshKey}
variant="mobile"
/>
</div>
</div>
@ -665,8 +674,8 @@ export function Dashboard({ user }: DashboardProps) {
</div>
<div className="sm:hidden mt-1 mb-1 px-2">
<div className="rounded-xl border border-white/10 bg-black/20 px-3 py-2">
<div className="sm:hidden fixed bottom-[calc(env(safe-area-inset-bottom)+0.75rem)] left-1/2 -translate-x-1/2 z-30 w-full px-3 pointer-events-none">
<div className="mx-auto max-w-sm rounded-xl border border-white/10 bg-black/25 backdrop-blur-xl px-3 py-2 pointer-events-auto">
<div className="text-[10px] uppercase tracking-[0.2em] opacity-60 text-center mb-2">
{MOBILE_SLIDES[currentPage]?.label}
</div>
@ -708,7 +717,18 @@ export function Dashboard({ user }: DashboardProps) {
/>
)}
<VersionUpdateModal />
<VersionUpdateModal
preferences={preferences}
onAcknowledge={async (version) => {
if (!preferences) return;
const nextPreferences = {
...preferences,
lastSeenReleaseNotesVersion: version,
};
setPreferences(nextPreferences);
await savePreferencesAsync(nextPreferences);
}}
/>
{showCelebration && newBadge && (
<CelebrationAnimation

View File

@ -1,10 +1,10 @@
'use client';
import React, { useMemo } from 'react';
import React, { useEffect, useMemo, useState } 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 { TrendingDown, Cigarette, Leaf, AlertTriangle, XCircle } from 'lucide-react';
import { TrendingDown, ChevronDown, ChevronUp, Cigarette, Leaf, AlertTriangle, XCircle } from 'lucide-react';
import { useTheme } from '@/lib/theme-context';
import { getTodayString } from '@/lib/date-utils';
import { cn } from '@/lib/utils';
@ -15,6 +15,8 @@ interface SubstancePlanSectionProps {
usageData: UsageEntry[];
trackingStartDate: string | null;
onGeneratePlan: () => void;
isExpanded: boolean;
onToggle?: () => void;
}
function SubstancePlanSection({
@ -22,7 +24,9 @@ function SubstancePlanSection({
plan,
usageData,
trackingStartDate,
onGeneratePlan
onGeneratePlan,
isExpanded,
onToggle
}: SubstancePlanSectionProps) {
const { theme } = useTheme();
@ -97,7 +101,11 @@ function SubstancePlanSection({
<div className={cn("rounded-xl border transition-all duration-300 overflow-hidden mb-3", bgColor, borderColor)}>
{/* HEADER / SUMMARY ROW */}
<div
className="flex items-center justify-between p-4"
onClick={onToggle}
className={cn(
"flex items-center justify-between p-4",
onToggle && "cursor-pointer hover:bg-black/5 active:bg-black/10 transition-colors"
)}
>
<div className="flex items-center gap-3">
<div className={cn("p-2 rounded-lg", isNicotine ? "bg-yellow-500/20" : "bg-emerald-500/20")}>
@ -118,11 +126,13 @@ function SubstancePlanSection({
{todayUsage}{activePlan ? ` / ${currentTarget}` : ''}
</span>
</div>
{onToggle && (isExpanded ? <ChevronUp className="h-5 w-5 opacity-30" /> : <ChevronDown className="h-5 w-5 opacity-30" />)}
</div>
</div>
{/* EXPANDED CONTENT */}
<div className="px-4 pb-4">
{isExpanded && (
<div className="px-4 pb-4 animate-in slide-in-from-top-2 duration-200">
<div className="h-px w-full bg-border mb-4 opacity-30" />
{!activePlan ? (
@ -147,7 +157,7 @@ function SubstancePlanSection({
<p className="text-sm">
Baseline established: <strong className={accentColor}>{currentAverage} {unitLabel}/day</strong>
</p>
<Button onClick={onGeneratePlan} size="sm" className={cn("w-full h-10 font-bold", progressFill, "text-white hover:opacity-90")}>
<Button onClick={(e) => { e.stopPropagation(); onGeneratePlan(); }} size="sm" className={cn("w-full h-10 font-bold", progressFill, "text-white hover:opacity-90")}>
Generate Plan
</Button>
</div>
@ -252,6 +262,7 @@ function SubstancePlanSection({
</div>
)}
</div>
)}
</div>
);
}
@ -261,16 +272,42 @@ interface UnifiedQuitPlanCardProps {
usageData: UsageEntry[];
onGeneratePlan: (substance: 'nicotine' | 'weed') => void;
refreshKey: number;
variant?: 'desktop' | 'mobile';
}
export function UnifiedQuitPlanCard({
preferences,
usageData,
onGeneratePlan,
refreshKey
refreshKey,
variant = 'mobile'
}: UnifiedQuitPlanCardProps) {
const [expandedSubstance, setExpandedSubstance] = useState<'nicotine' | 'weed' | 'none'>('nicotine');
if (!preferences) return null;
const isDesktopVariant = variant === 'desktop';
const showNicotine = isDesktopVariant
? (preferences.substance === 'nicotine' || usageData.some(e => e.substance === 'nicotine'))
: true;
const showWeed = isDesktopVariant
? (preferences.substance === 'weed' || usageData.some(e => e.substance === 'weed'))
: true;
useEffect(() => {
if (!isDesktopVariant) return;
if (expandedSubstance === 'none') return;
if (expandedSubstance === 'nicotine' && !showNicotine) {
setExpandedSubstance(showWeed ? 'weed' : 'none');
}
if (expandedSubstance === 'weed' && !showWeed) {
setExpandedSubstance(showNicotine ? 'nicotine' : 'none');
}
}, [expandedSubstance, isDesktopVariant, showNicotine, showWeed]);
if (!showNicotine && !showWeed) return null;
return (
<Card className="backdrop-blur-2xl shadow-2xl border-white/10 overflow-hidden bg-white/5">
<CardHeader className="pb-1 pt-6 px-4 sm:px-6">
@ -280,6 +317,7 @@ export function UnifiedQuitPlanCard({
</CardTitle>
</CardHeader>
<CardContent className="pt-2 p-2 sm:p-4">
{showNicotine && (
<SubstancePlanSection
substance="nicotine"
plan={preferences.quitState?.nicotine?.plan || (preferences.substance === 'nicotine' ? preferences.quitPlan : null)}
@ -290,9 +328,13 @@ export function UnifiedQuitPlanCard({
usageData.filter(e => e.substance === 'nicotine').sort((a, b) => a.date.localeCompare(b.date))[0]?.date ||
null
}
isExpanded={isDesktopVariant ? expandedSubstance === 'nicotine' : true}
onToggle={isDesktopVariant ? () => setExpandedSubstance(expandedSubstance === 'nicotine' ? 'none' : 'nicotine') : undefined}
onGeneratePlan={() => onGeneratePlan('nicotine')}
/>
)}
{showWeed && (
<SubstancePlanSection
substance="weed"
plan={preferences.quitState?.weed?.plan || (preferences.substance === 'weed' ? preferences.quitPlan : null)}
@ -303,8 +345,11 @@ export function UnifiedQuitPlanCard({
usageData.filter(e => e.substance === 'weed').sort((a, b) => a.date.localeCompare(b.date))[0]?.date ||
null
}
isExpanded={isDesktopVariant ? expandedSubstance === 'weed' : true}
onToggle={isDesktopVariant ? () => setExpandedSubstance(expandedSubstance === 'weed' ? 'none' : 'weed') : undefined}
onGeneratePlan={() => onGeneratePlan('weed')}
/>
)}
</CardContent>
</Card>
);

View File

@ -16,6 +16,7 @@ import { UsageEntry, UserPreferences, setUsageForDateAsync, clearDayDataAsync }
import { ChevronLeftIcon, ChevronRightIcon, Cigarette, Leaf, Sparkles } from 'lucide-react';
import { useTheme } from '@/lib/theme-context';
import { getLocalDateString, getTodayString } from '@/lib/date-utils';
import { DailyInspirationCard } from './DailyInspirationCard';
import { cn } from '@/lib/utils';
import React from 'react';
@ -24,11 +25,14 @@ interface UsageCalendarProps {
usageData: UsageEntry[];
onDataUpdate: () => void;
userId: string;
religion?: 'christian' | 'secular' | null;
onReligionUpdate?: (religion: 'christian' | 'secular') => void;
showInspirationPanel?: boolean;
preferences?: UserPreferences | null;
onPreferencesUpdate?: (prefs: UserPreferences) => Promise<void>;
}
function UsageCalendarComponent({ usageData, onDataUpdate, preferences, onPreferencesUpdate }: UsageCalendarProps) {
function UsageCalendarComponent({ usageData, onDataUpdate, religion, onReligionUpdate, showInspirationPanel = false, preferences, onPreferencesUpdate }: UsageCalendarProps) {
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
const [editNicotineCount, setEditNicotineCount] = useState('');
const [editWeedCount, setEditWeedCount] = useState('');
@ -255,9 +259,14 @@ function UsageCalendarComponent({ usageData, onDataUpdate, preferences, onPrefer
</div>
</div>
<div className="max-w-4xl mx-auto">
<div className={cn("mx-auto", showInspirationPanel ? "max-w-6xl" : "max-w-4xl")}>
<div className={cn(
showInspirationPanel
? "flex flex-col lg:flex-row gap-8 lg:gap-8 items-center lg:items-stretch justify-center"
: "w-full flex flex-col items-center"
)}>
{/* Calendar */}
<div className="w-full flex flex-col items-center">
<div className={cn("w-full flex flex-col items-center", showInspirationPanel && "lg:w-1/2")}>
<div className={cn(
"rounded-2xl p-2 sm:p-4 border shadow-inner transition-all duration-500 w-full",
theme === 'light' ? "bg-slate-50/50 border-slate-200/60" : "bg-black/20 border-white/5"
@ -299,6 +308,19 @@ function UsageCalendarComponent({ usageData, onDataUpdate, preferences, onPrefer
/>
</div>
</div>
{showInspirationPanel && (
<>
<div className="hidden lg:block w-px self-stretch bg-gradient-to-b from-transparent via-white/10 to-transparent" />
<div className="w-full lg:w-1/2 flex flex-col justify-center">
<DailyInspirationCard
initialReligion={religion}
onReligionChange={onReligionUpdate}
/>
</div>
</>
)}
</div>
</div>
</CardContent>

View File

@ -1,32 +1,41 @@
'use client';
import { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog';
import { Dialog, DialogContent, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Sparkles, Shield, Bell, Smartphone, Monitor, Trophy, Rocket, Scale, Heart } from 'lucide-react';
import { Sparkles, Shield, Bell, Smartphone, Trophy, Rocket, Scale, Heart } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useTheme } from '@/lib/theme-context';
import type { UserPreferences } from '@/lib/storage';
export function VersionUpdateModal() {
const RELEASE_VERSION = '1.1';
interface VersionUpdateModalProps {
preferences: UserPreferences | null;
onAcknowledge: (version: string) => Promise<void>;
}
export function VersionUpdateModal({ preferences, onAcknowledge }: VersionUpdateModalProps) {
const [isOpen, setIsOpen] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const { theme } = useTheme();
useEffect(() => {
// Check local storage for the flag
const hasSeenUpdate = localStorage.getItem('seen_version_1.0_update');
if (!hasSeenUpdate) {
setIsOpen(true);
}
}, []);
if (!preferences) return;
const hasSeenUpdate = preferences.lastSeenReleaseNotesVersion === RELEASE_VERSION;
setIsOpen(!hasSeenUpdate);
}, [preferences]);
const handleClose = () => {
// Set the flag in local storage
localStorage.setItem('seen_version_1.0_update', 'true');
const handleClose = async () => {
if (isSaving || !preferences) return;
setIsSaving(true);
await onAcknowledge(RELEASE_VERSION);
setIsOpen(false);
setIsSaving(false);
};
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
<Dialog open={isOpen} onOpenChange={(open) => { if (!open) void handleClose(); }}>
<DialogContent className={cn(
"sm:max-w-xl max-h-[85vh] overflow-y-auto border-0 shadow-2xl p-0 gap-0 rounded-3xl",
theme === 'light' ? 'bg-white' : 'bg-[#1a1b26]'
@ -40,9 +49,9 @@ export function VersionUpdateModal() {
<Rocket className="w-10 h-10 text-white" />
</div>
<div className="space-y-1">
<DialogTitle className="text-3xl font-bold tracking-tight">Version 1.0 is Live!</DialogTitle>
<DialogTitle className="text-3xl font-bold tracking-tight">Version 1.1 is Live!</DialogTitle>
<DialogDescription className="text-white/80 text-base font-medium">
The biggest update to QuitTraq is finally here.
Desktop is restored, mobile is cleaner, and swipe flow is smoother.
</DialogDescription>
</div>
</div>
@ -60,9 +69,9 @@ export function VersionUpdateModal() {
<Shield className="w-5 h-5 text-emerald-500" />
</div>
<div className="space-y-1">
<h4 className="font-bold text-sm">Major Security Overhaul</h4>
<h4 className="font-bold text-sm">Desktop Layout Restored</h4>
<p className="text-xs opacity-70 leading-relaxed">
implemented complete security audit and fixes. This is why you had to login againwe've secured your session data with industry-standard encryption.
Restored the desktop dashboard structure, including better section balance and the quote/calendar pairing.
</p>
</div>
</div>
@ -73,12 +82,12 @@ export function VersionUpdateModal() {
<Sparkles className="w-5 h-5 text-indigo-500" />
</div>
<div className="space-y-2">
<h4 className="font-bold text-sm">Fresh New Look</h4>
<h4 className="font-bold text-sm">Mobile Navigation Polish</h4>
<p className="text-xs opacity-70 leading-relaxed">
We've updated the app icon to be cleaner and more modern.
Swipe cards now move more naturally, and the mobile section indicator stays visible at the bottom.
</p>
<div className={cn("text-[10px] p-2 rounded-lg border", theme === 'light' ? 'bg-yellow-50 border-yellow-200 text-yellow-800' : 'bg-yellow-500/10 border-yellow-500/20 text-yellow-200')}>
<strong>iOS Users:</strong> To see the new icon, you'll need to remove the app from your home screen and re-add it (Share Add to Home Screen).
<strong>Note:</strong> The pager/footer now stays on-screen so you can always jump between mobile sections.
</div>
</div>
</div>
@ -87,17 +96,17 @@ export function VersionUpdateModal() {
<div className="grid sm:grid-cols-2 gap-4">
<div className={cn("p-4 rounded-2xl border space-y-2", theme === 'light' ? 'bg-slate-50 border-slate-100' : 'bg-white/5 border-white/5')}>
<Bell className="w-5 h-5 text-amber-500 mb-1" />
<h4 className="font-bold text-sm">Smart Notifications</h4>
<h4 className="font-bold text-sm">One-Time Account Release Notes</h4>
<p className="text-[10px] opacity-70 leading-relaxed">
Updated messaging system that adapts based on the time of day to keep you motivated.
This message now saves per account and will not appear again after you close it.
</p>
</div>
<div className={cn("p-4 rounded-2xl border space-y-2", theme === 'light' ? 'bg-slate-50 border-slate-100' : 'bg-white/5 border-white/5')}>
<Smartphone className="w-5 h-5 text-blue-500 mb-1" />
<h4 className="font-bold text-sm">PWA & UI Polish</h4>
<h4 className="font-bold text-sm">Swipe Feel Improvements</h4>
<p className="text-[10px] opacity-70 leading-relaxed">
New login screen, landing page, and optimization fixes for a smoother app-like experience.
Reduced sticky snapping so swiping between sections feels lighter and more direct.
</p>
</div>
</div>
@ -108,9 +117,9 @@ export function VersionUpdateModal() {
<Scale className="w-5 h-5 text-indigo-500" />
</div>
<div className="space-y-1">
<h4 className="font-bold text-sm">Refined Tracking</h4>
<h4 className="font-bold text-sm">Tracking + Visibility Fixes</h4>
<p className="text-xs opacity-70 leading-relaxed">
Fixed input logging on desktop, improved the independent track buttons for weed/nicotine, and fixed the scroll system.
Section navigation, card flow, and bottom spacing were tuned so key controls stay reachable on mobile.
</p>
</div>
</div>
@ -119,17 +128,17 @@ export function VersionUpdateModal() {
<div className="grid sm:grid-cols-2 gap-4">
<div className={cn("p-4 rounded-2xl border space-y-2", theme === 'light' ? 'bg-slate-50 border-slate-100' : 'bg-white/5 border-white/5')}>
<Trophy className="w-5 h-5 text-yellow-500 mb-1" />
<h4 className="font-bold text-sm">Independent Goals</h4>
<h4 className="font-bold text-sm">Achievements Reliability</h4>
<p className="text-[10px] opacity-70 leading-relaxed">
Set separate quit plans for nicotine and marijuana. Fixed achievement unlocking celebrations.
Improved unlock behavior to prevent duplicate first-step unlock confusion.
</p>
</div>
<div className={cn("p-4 rounded-2xl border space-y-2", theme === 'light' ? 'bg-slate-50 border-slate-100' : 'bg-white/5 border-white/5')}>
<Heart className="w-5 h-5 text-rose-500 mb-1" />
<h4 className="font-bold text-sm">Mood Tracker 2.0</h4>
<h4 className="font-bold text-sm">Overall Experience Cleanup</h4>
<p className="text-[10px] opacity-70 leading-relaxed">
Friendlier messages, more mood options, daily average charts, and weekly score tracking.
Better spacing, cleaner transitions, and more predictable behavior across desktop and mobile.
</p>
</div>
</div>
@ -140,9 +149,10 @@ export function VersionUpdateModal() {
<div className={cn("p-6 pt-2", theme === 'light' ? 'bg-slate-50/50' : 'bg-black/20')}>
<Button
className="w-full h-12 rounded-xl text-base font-bold bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white shadow-lg active:scale-95 transition-all"
onClick={handleClose}
onClick={() => void handleClose()}
disabled={isSaving}
>
Let's Go!
{isSaving ? 'Saving...' : "Let's Go!"}
</Button>
</div>

View File

@ -38,6 +38,7 @@ export interface UserPreferencesRow {
religion: string | null;
lastNicotineUsageTime: string | null;
lastWeedUsageTime: string | null;
lastSeenReleaseNotesVersion: string | null;
quitPlanJson: string | null;
createdAt: string;
updatedAt: string;
@ -84,6 +85,10 @@ export async function upsertPreferencesD1(userId: string, data: Partial<UserPref
updates.push('lastWeedUsageTime = ?');
values.push(data.lastWeedUsageTime);
}
if (data.lastSeenReleaseNotesVersion !== undefined) {
updates.push('lastSeenReleaseNotesVersion = ?');
values.push(data.lastSeenReleaseNotesVersion);
}
if (data.quitPlanJson !== undefined) { updates.push('quitPlanJson = ?'); values.push(data.quitPlanJson); }
@ -100,8 +105,8 @@ export async function upsertPreferencesD1(userId: string, data: Partial<UserPref
} else {
// Insert
await db.prepare(
`INSERT INTO UserPreferences (id, userId, substance, trackingStartDate, hasCompletedSetup, dailyGoal, userName, userAge, religion, lastNicotineUsageTime, lastWeedUsageTime, quitPlanJson, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
`INSERT INTO UserPreferences (id, userId, substance, trackingStartDate, hasCompletedSetup, dailyGoal, userName, userAge, religion, lastNicotineUsageTime, lastWeedUsageTime, lastSeenReleaseNotesVersion, quitPlanJson, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
).bind(
id,
userId,
@ -114,6 +119,7 @@ export async function upsertPreferencesD1(userId: string, data: Partial<UserPref
data.religion || null,
data.lastNicotineUsageTime || null,
data.lastWeedUsageTime || null,
data.lastSeenReleaseNotesVersion || null,
data.quitPlanJson || null,
now,
now

View File

@ -27,6 +27,7 @@ export interface UserPreferences {
religion: 'christian' | 'secular' | null;
lastNicotineUsageTime?: string | null;
lastWeedUsageTime?: string | null;
lastSeenReleaseNotesVersion?: string | null;
}
export interface QuitPlan {
@ -126,6 +127,7 @@ const defaultPreferences: UserPreferences = {
userName: null,
userAge: null,
religion: null,
lastSeenReleaseNotesVersion: null,
};
export const defaultReminderSettings: ReminderSettings = {