From e8f47993a995f3be9adf97f7ac635908621db153 Mon Sep 17 00:00:00 2001 From: Avery Felts Date: Tue, 24 Feb 2026 02:31:04 -0700 Subject: [PATCH] fix desktop dashboard regressions and one-time v1.1 release notes --- migrations/0008_add_release_notes_version.sql | 1 + prisma/d1_schema.sql | 2 +- prisma/schema.prisma | 1 + src/app/api/preferences/route.ts | 14 +++ src/app/globals.css | 3 +- src/components/Dashboard.tsx | 28 ++++- src/components/UnifiedQuitPlanCard.tsx | 109 +++++++++++++----- src/components/UsageCalendar.tsx | 108 ++++++++++------- src/components/VersionUpdateModal.tsx | 74 +++++++----- src/lib/d1.ts | 10 +- src/lib/storage.ts | 2 + 11 files changed, 236 insertions(+), 116 deletions(-) create mode 100644 migrations/0008_add_release_notes_version.sql diff --git a/migrations/0008_add_release_notes_version.sql b/migrations/0008_add_release_notes_version.sql new file mode 100644 index 0000000..e66bcb0 --- /dev/null +++ b/migrations/0008_add_release_notes_version.sql @@ -0,0 +1 @@ +ALTER TABLE UserPreferences ADD COLUMN lastSeenReleaseNotesVersion TEXT; diff --git a/prisma/d1_schema.sql b/prisma/d1_schema.sql index 63f8150..ea3135f 100644 --- a/prisma/d1_schema.sql +++ b/prisma/d1_schema.sql @@ -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"); - diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2db568f..f7e9412 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -24,6 +24,7 @@ model UserPreferences { religion String? lastNicotineUsageTime String? lastWeedUsageTime String? + lastSeenReleaseNotesVersion String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/src/app/api/preferences/route.ts b/src/app/api/preferences/route.ts index 4d87fec..fa5a58d 100644 --- a/src/app/api/preferences/route.ts +++ b/src/app/api/preferences/route.ts @@ -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); diff --git a/src/app/globals.css b/src/app/globals.css index 288ec83..04bc7ae 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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; diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 465e89e..aae0306 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -408,7 +408,7 @@ export function Dashboard({ user }: DashboardProps) { onModalStateChange={handleModalStateChange} /> -
+
{loadError && (
@@ -476,6 +476,7 @@ export function Dashboard({ user }: DashboardProps) { usageData={usageData} onGeneratePlan={handleGeneratePlan} refreshKey={refreshKey} + variant="desktop" />
@@ -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" />
@@ -665,8 +674,8 @@ export function Dashboard({ user }: DashboardProps) { -
-
+
+
{MOBILE_SLIDES[currentPage]?.label}
@@ -708,7 +717,18 @@ export function Dashboard({ user }: DashboardProps) { /> )} - + { + if (!preferences) return; + const nextPreferences = { + ...preferences, + lastSeenReleaseNotesVersion: version, + }; + setPreferences(nextPreferences); + await savePreferencesAsync(nextPreferences); + }} + /> {showCelebration && newBadge && ( 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({
{/* HEADER / SUMMARY ROW */}
@@ -118,11 +126,13 @@ function SubstancePlanSection({ {todayUsage}{activePlan ? ` / ${currentTarget}` : ''}
+ {onToggle && (isExpanded ? : )}
{/* EXPANDED CONTENT */} -
+ {isExpanded && ( +
{!activePlan ? ( @@ -147,7 +157,7 @@ function SubstancePlanSection({

Baseline established: {currentAverage} {unitLabel}/day

-
@@ -251,7 +261,8 @@ function SubstancePlanSection({
)} -
+
+ )}
); } @@ -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 ( @@ -280,31 +317,39 @@ export function UnifiedQuitPlanCard({ - e.substance === 'nicotine').sort((a, b) => a.date.localeCompare(b.date))[0]?.date || - null - } - onGeneratePlan={() => onGeneratePlan('nicotine')} - /> + {showNicotine && ( + 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')} + /> + )} - e.substance === 'weed').sort((a, b) => a.date.localeCompare(b.date))[0]?.date || - null - } - onGeneratePlan={() => onGeneratePlan('weed')} - /> + {showWeed && ( + 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')} + /> + )} ); diff --git a/src/components/UsageCalendar.tsx b/src/components/UsageCalendar.tsx index 50f2101..6db0458 100644 --- a/src/components/UsageCalendar.tsx +++ b/src/components/UsageCalendar.tsx @@ -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; } -function UsageCalendarComponent({ usageData, onDataUpdate, preferences, onPreferencesUpdate }: UsageCalendarProps) { +function UsageCalendarComponent({ usageData, onDataUpdate, religion, onReligionUpdate, showInspirationPanel = false, preferences, onPreferencesUpdate }: UsageCalendarProps) { const [selectedDate, setSelectedDate] = useState(undefined); const [editNicotineCount, setEditNicotineCount] = useState(''); const [editWeedCount, setEditWeedCount] = useState(''); @@ -255,49 +259,67 @@ function UsageCalendarComponent({ usageData, onDataUpdate, preferences, onPrefer
-
- {/* Calendar */} -
-
- ( - - ), - Chevron: ({ orientation }) => ( -
- {orientation === 'left' ? ( - - ) : ( - - )} -
- ), - }} - /> +
+
+ {/* Calendar */} +
+
+ ( + + ), + Chevron: ({ orientation }) => ( +
+ {orientation === 'left' ? ( + + ) : ( + + )} +
+ ), + }} + /> +
+ + {showInspirationPanel && ( + <> +
+
+ +
+ + )}
diff --git a/src/components/VersionUpdateModal.tsx b/src/components/VersionUpdateModal.tsx index 2bee79c..9ebd0a5 100644 --- a/src/components/VersionUpdateModal.tsx +++ b/src/components/VersionUpdateModal.tsx @@ -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; +} + +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 ( - !open && handleClose()}> + { if (!open) void handleClose(); }}>
- Version 1.0 is Live! + Version 1.1 is Live! - The biggest update to QuitTraq is finally here. + Desktop is restored, mobile is cleaner, and swipe flow is smoother.
@@ -60,9 +69,9 @@ export function VersionUpdateModal() {
-

Major Security Overhaul

+

Desktop Layout Restored

- implemented complete security audit and fixes. This is why you had to login again—we've secured your session data with industry-standard encryption. + Restored the desktop dashboard structure, including better section balance and the quote/calendar pairing.

@@ -73,12 +82,12 @@ export function VersionUpdateModal() {
-

Fresh New Look

+

Mobile Navigation Polish

- 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.

- iOS Users: 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). + Note: The pager/footer now stays on-screen so you can always jump between mobile sections.
@@ -87,17 +96,17 @@ export function VersionUpdateModal() {
-

Smart Notifications

+

One-Time Account Release Notes

- 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.

-

PWA & UI Polish

+

Swipe Feel Improvements

- 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.

@@ -108,9 +117,9 @@ export function VersionUpdateModal() {
-

Refined Tracking

+

Tracking + Visibility Fixes

- 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.

@@ -119,17 +128,17 @@ export function VersionUpdateModal() {
-

Independent Goals

+

Achievements Reliability

- Set separate quit plans for nicotine and marijuana. Fixed achievement unlocking celebrations. + Improved unlock behavior to prevent duplicate first-step unlock confusion.

-

Mood Tracker 2.0

+

Overall Experience Cleanup

- Friendlier messages, more mood options, daily average charts, and weekly score tracking. + Better spacing, cleaner transitions, and more predictable behavior across desktop and mobile.

@@ -140,9 +149,10 @@ export function VersionUpdateModal() {
diff --git a/src/lib/d1.ts b/src/lib/d1.ts index 02c2b8f..8c13576 100644 --- a/src/lib/d1.ts +++ b/src/lib/d1.ts @@ -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