fix desktop dashboard regressions and one-time v1.1 release notes
This commit is contained in:
parent
e5b3f649be
commit
e8f47993a9
1
migrations/0008_add_release_notes_version.sql
Normal file
1
migrations/0008_add_release_notes_version.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE UserPreferences ADD COLUMN lastSeenReleaseNotesVersion TEXT;
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ model UserPreferences {
|
||||
religion String?
|
||||
lastNicotineUsageTime String?
|
||||
lastWeedUsageTime String?
|
||||
lastSeenReleaseNotesVersion String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 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.
|
||||
</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>
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user