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, "religion" TEXT,
"lastNicotineUsageTime" TEXT, "lastNicotineUsageTime" TEXT,
"lastWeedUsageTime" TEXT, "lastWeedUsageTime" TEXT,
"lastSeenReleaseNotesVersion" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL, "updatedAt" DATETIME NOT NULL,
"quitPlanJson" TEXT "quitPlanJson" TEXT
@ -79,4 +80,3 @@ CREATE UNIQUE INDEX "ReminderSettings_userId_key" ON "ReminderSettings"("userId"
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "SavingsConfig_userId_key" ON "SavingsConfig"("userId"); CREATE UNIQUE INDEX "SavingsConfig_userId_key" ON "SavingsConfig"("userId");

View File

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

View File

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

View File

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

View File

@ -408,7 +408,7 @@ export function Dashboard({ user }: DashboardProps) {
onModalStateChange={handleModalStateChange} 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 && ( {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={`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"> <div className="flex items-center justify-between gap-3">
@ -476,6 +476,7 @@ export function Dashboard({ user }: DashboardProps) {
usageData={usageData} usageData={usageData}
onGeneratePlan={handleGeneratePlan} onGeneratePlan={handleGeneratePlan}
refreshKey={refreshKey} refreshKey={refreshKey}
variant="desktop"
/> />
</div> </div>
@ -486,6 +487,13 @@ export function Dashboard({ user }: DashboardProps) {
usageData={usageData} usageData={usageData}
onDataUpdate={loadData} onDataUpdate={loadData}
userId={user.id} userId={user.id}
religion={preferences.religion}
onReligionUpdate={async (religion: 'christian' | 'secular') => {
const updatedPrefs = { ...preferences, religion };
setPreferences(updatedPrefs);
await savePreferencesAsync(updatedPrefs);
}}
showInspirationPanel
preferences={preferences} preferences={preferences}
onPreferencesUpdate={async (updatedPrefs: UserPreferences) => { onPreferencesUpdate={async (updatedPrefs: UserPreferences) => {
await savePreferencesAsync(updatedPrefs); await savePreferencesAsync(updatedPrefs);
@ -575,6 +583,7 @@ export function Dashboard({ user }: DashboardProps) {
usageData={usageData} usageData={usageData}
onGeneratePlan={handleGeneratePlan} onGeneratePlan={handleGeneratePlan}
refreshKey={refreshKey} refreshKey={refreshKey}
variant="mobile"
/> />
</div> </div>
</div> </div>
@ -665,8 +674,8 @@ export function Dashboard({ user }: DashboardProps) {
</div> </div>
<div className="sm:hidden mt-1 mb-1 px-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="rounded-xl border border-white/10 bg-black/20 px-3 py-2"> <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"> <div className="text-[10px] uppercase tracking-[0.2em] opacity-60 text-center mb-2">
{MOBILE_SLIDES[currentPage]?.label} {MOBILE_SLIDES[currentPage]?.label}
</div> </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 && ( {showCelebration && newBadge && (
<CelebrationAnimation <CelebrationAnimation

View File

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

View File

@ -16,6 +16,7 @@ import { UsageEntry, UserPreferences, setUsageForDateAsync, clearDayDataAsync }
import { ChevronLeftIcon, ChevronRightIcon, Cigarette, Leaf, Sparkles } from 'lucide-react'; import { ChevronLeftIcon, ChevronRightIcon, Cigarette, Leaf, Sparkles } from 'lucide-react';
import { useTheme } from '@/lib/theme-context'; import { useTheme } from '@/lib/theme-context';
import { getLocalDateString, getTodayString } from '@/lib/date-utils'; import { getLocalDateString, getTodayString } from '@/lib/date-utils';
import { DailyInspirationCard } from './DailyInspirationCard';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import React from 'react'; import React from 'react';
@ -24,11 +25,14 @@ interface UsageCalendarProps {
usageData: UsageEntry[]; usageData: UsageEntry[];
onDataUpdate: () => void; onDataUpdate: () => void;
userId: string; userId: string;
religion?: 'christian' | 'secular' | null;
onReligionUpdate?: (religion: 'christian' | 'secular') => void;
showInspirationPanel?: boolean;
preferences?: UserPreferences | null; preferences?: UserPreferences | null;
onPreferencesUpdate?: (prefs: UserPreferences) => Promise<void>; 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 [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
const [editNicotineCount, setEditNicotineCount] = useState(''); const [editNicotineCount, setEditNicotineCount] = useState('');
const [editWeedCount, setEditWeedCount] = useState(''); const [editWeedCount, setEditWeedCount] = useState('');
@ -255,9 +259,14 @@ function UsageCalendarComponent({ usageData, onDataUpdate, preferences, onPrefer
</div> </div>
</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 */} {/* 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( <div className={cn(
"rounded-2xl p-2 sm:p-4 border shadow-inner transition-all duration-500 w-full", "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" 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>
</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> </div>
</CardContent> </CardContent>

View File

@ -1,32 +1,41 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; 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 { 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 { cn } from '@/lib/utils';
import { useTheme } from '@/lib/theme-context'; 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 [isOpen, setIsOpen] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const { theme } = useTheme(); const { theme } = useTheme();
useEffect(() => { useEffect(() => {
// Check local storage for the flag if (!preferences) return;
const hasSeenUpdate = localStorage.getItem('seen_version_1.0_update'); const hasSeenUpdate = preferences.lastSeenReleaseNotesVersion === RELEASE_VERSION;
if (!hasSeenUpdate) { setIsOpen(!hasSeenUpdate);
setIsOpen(true); }, [preferences]);
}
}, []);
const handleClose = () => { const handleClose = async () => {
// Set the flag in local storage if (isSaving || !preferences) return;
localStorage.setItem('seen_version_1.0_update', 'true'); setIsSaving(true);
await onAcknowledge(RELEASE_VERSION);
setIsOpen(false); setIsOpen(false);
setIsSaving(false);
}; };
return ( return (
<Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}> <Dialog open={isOpen} onOpenChange={(open) => { if (!open) void handleClose(); }}>
<DialogContent className={cn( <DialogContent className={cn(
"sm:max-w-xl max-h-[85vh] overflow-y-auto border-0 shadow-2xl p-0 gap-0 rounded-3xl", "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]' theme === 'light' ? 'bg-white' : 'bg-[#1a1b26]'
@ -40,9 +49,9 @@ export function VersionUpdateModal() {
<Rocket className="w-10 h-10 text-white" /> <Rocket className="w-10 h-10 text-white" />
</div> </div>
<div className="space-y-1"> <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"> <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> </DialogDescription>
</div> </div>
</div> </div>
@ -60,9 +69,9 @@ export function VersionUpdateModal() {
<Shield className="w-5 h-5 text-emerald-500" /> <Shield className="w-5 h-5 text-emerald-500" />
</div> </div>
<div className="space-y-1"> <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"> <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> </p>
</div> </div>
</div> </div>
@ -73,12 +82,12 @@ export function VersionUpdateModal() {
<Sparkles className="w-5 h-5 text-indigo-500" /> <Sparkles className="w-5 h-5 text-indigo-500" />
</div> </div>
<div className="space-y-2"> <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"> <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> </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')}> <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> </div>
</div> </div>
@ -87,17 +96,17 @@ export function VersionUpdateModal() {
<div className="grid sm:grid-cols-2 gap-4"> <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')}> <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" /> <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"> <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> </p>
</div> </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')}> <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" /> <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"> <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> </p>
</div> </div>
</div> </div>
@ -108,9 +117,9 @@ export function VersionUpdateModal() {
<Scale className="w-5 h-5 text-indigo-500" /> <Scale className="w-5 h-5 text-indigo-500" />
</div> </div>
<div className="space-y-1"> <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"> <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> </p>
</div> </div>
</div> </div>
@ -119,17 +128,17 @@ export function VersionUpdateModal() {
<div className="grid sm:grid-cols-2 gap-4"> <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')}> <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" /> <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"> <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> </p>
</div> </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')}> <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" /> <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"> <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> </p>
</div> </div>
</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')}> <div className={cn("p-6 pt-2", theme === 'light' ? 'bg-slate-50/50' : 'bg-black/20')}>
<Button <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" 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> </Button>
</div> </div>

View File

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

View File

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