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,
|
"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");
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
@ -251,7 +261,8 @@ function SubstancePlanSection({
|
|||||||
</div>
|
</div>
|
||||||
</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,31 +317,39 @@ 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">
|
||||||
<SubstancePlanSection
|
{showNicotine && (
|
||||||
substance="nicotine"
|
<SubstancePlanSection
|
||||||
plan={preferences.quitState?.nicotine?.plan || (preferences.substance === 'nicotine' ? preferences.quitPlan : null)}
|
substance="nicotine"
|
||||||
usageData={usageData}
|
plan={preferences.quitState?.nicotine?.plan || (preferences.substance === 'nicotine' ? preferences.quitPlan : null)}
|
||||||
trackingStartDate={
|
usageData={usageData}
|
||||||
preferences.quitState?.nicotine?.startDate ||
|
trackingStartDate={
|
||||||
(preferences.substance === 'nicotine' ? preferences.trackingStartDate : null) ||
|
preferences.quitState?.nicotine?.startDate ||
|
||||||
usageData.filter(e => e.substance === 'nicotine').sort((a, b) => a.date.localeCompare(b.date))[0]?.date ||
|
(preferences.substance === 'nicotine' ? preferences.trackingStartDate : null) ||
|
||||||
null
|
usageData.filter(e => e.substance === 'nicotine').sort((a, b) => a.date.localeCompare(b.date))[0]?.date ||
|
||||||
}
|
null
|
||||||
onGeneratePlan={() => onGeneratePlan('nicotine')}
|
}
|
||||||
/>
|
isExpanded={isDesktopVariant ? expandedSubstance === 'nicotine' : true}
|
||||||
|
onToggle={isDesktopVariant ? () => setExpandedSubstance(expandedSubstance === 'nicotine' ? 'none' : 'nicotine') : undefined}
|
||||||
|
onGeneratePlan={() => onGeneratePlan('nicotine')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<SubstancePlanSection
|
{showWeed && (
|
||||||
substance="weed"
|
<SubstancePlanSection
|
||||||
plan={preferences.quitState?.weed?.plan || (preferences.substance === 'weed' ? preferences.quitPlan : null)}
|
substance="weed"
|
||||||
usageData={usageData}
|
plan={preferences.quitState?.weed?.plan || (preferences.substance === 'weed' ? preferences.quitPlan : null)}
|
||||||
trackingStartDate={
|
usageData={usageData}
|
||||||
preferences.quitState?.weed?.startDate ||
|
trackingStartDate={
|
||||||
(preferences.substance === 'weed' ? preferences.trackingStartDate : null) ||
|
preferences.quitState?.weed?.startDate ||
|
||||||
usageData.filter(e => e.substance === 'weed').sort((a, b) => a.date.localeCompare(b.date))[0]?.date ||
|
(preferences.substance === 'weed' ? preferences.trackingStartDate : null) ||
|
||||||
null
|
usageData.filter(e => e.substance === 'weed').sort((a, b) => a.date.localeCompare(b.date))[0]?.date ||
|
||||||
}
|
null
|
||||||
onGeneratePlan={() => onGeneratePlan('weed')}
|
}
|
||||||
/>
|
isExpanded={isDesktopVariant ? expandedSubstance === 'weed' : true}
|
||||||
|
onToggle={isDesktopVariant ? () => setExpandedSubstance(expandedSubstance === 'weed' ? 'none' : 'weed') : undefined}
|
||||||
|
onGeneratePlan={() => onGeneratePlan('weed')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,49 +259,67 @@ 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")}>
|
||||||
{/* Calendar */}
|
<div className={cn(
|
||||||
<div className="w-full flex flex-col items-center">
|
showInspirationPanel
|
||||||
<div className={cn(
|
? "flex flex-col lg:flex-row gap-8 lg:gap-8 items-center lg:items-stretch justify-center"
|
||||||
"rounded-2xl p-2 sm:p-4 border shadow-inner transition-all duration-500 w-full",
|
: "w-full flex flex-col items-center"
|
||||||
theme === 'light' ? "bg-slate-50/50 border-slate-200/60" : "bg-black/20 border-white/5"
|
)}>
|
||||||
)}>
|
{/* Calendar */}
|
||||||
<DayPicker
|
<div className={cn("w-full flex flex-col items-center", showInspirationPanel && "lg:w-1/2")}>
|
||||||
mode="single"
|
<div className={cn(
|
||||||
selected={selectedDate}
|
"rounded-2xl p-2 sm:p-4 border shadow-inner transition-all duration-500 w-full",
|
||||||
onSelect={handleDateSelect}
|
theme === 'light' ? "bg-slate-50/50 border-slate-200/60" : "bg-black/20 border-white/5"
|
||||||
className={cn(
|
)}>
|
||||||
"p-0 sm:p-2 w-full [&_.rdp-month]:w-full [&_.rdp-table]:w-full",
|
<DayPicker
|
||||||
theme === 'light' ? "text-slate-900" : "text-white"
|
mode="single"
|
||||||
)}
|
selected={selectedDate}
|
||||||
showOutsideDays={false}
|
onSelect={handleDateSelect}
|
||||||
components={{
|
className={cn(
|
||||||
DayButton: (props) => (
|
"p-0 sm:p-2 w-full [&_.rdp-month]:w-full [&_.rdp-table]:w-full",
|
||||||
<CustomDayButton
|
theme === 'light' ? "text-slate-900" : "text-white"
|
||||||
{...props}
|
)}
|
||||||
className={cn(
|
showOutsideDays={false}
|
||||||
props.className,
|
components={{
|
||||||
"aspect-square rounded-full flex items-center justify-center p-0"
|
DayButton: (props) => (
|
||||||
)}
|
<CustomDayButton
|
||||||
/>
|
{...props}
|
||||||
),
|
className={cn(
|
||||||
Chevron: ({ orientation }) => (
|
props.className,
|
||||||
<div className={cn(
|
"aspect-square rounded-full flex items-center justify-center p-0"
|
||||||
"p-1.5 rounded-full border transition-all duration-200",
|
)}
|
||||||
theme === 'light'
|
/>
|
||||||
? "bg-white border-slate-200 text-slate-600 hover:bg-slate-100 hover:scale-110 shadow-sm"
|
),
|
||||||
: "bg-white/5 border-white/10 text-white/70 hover:bg-white/10 hover:scale-110"
|
Chevron: ({ orientation }) => (
|
||||||
)}>
|
<div className={cn(
|
||||||
{orientation === 'left' ? (
|
"p-1.5 rounded-full border transition-all duration-200",
|
||||||
<ChevronLeftIcon className="h-4 w-4" />
|
theme === 'light'
|
||||||
) : (
|
? "bg-white border-slate-200 text-slate-600 hover:bg-slate-100 hover:scale-110 shadow-sm"
|
||||||
<ChevronRightIcon className="h-4 w-4" />
|
: "bg-white/5 border-white/10 text-white/70 hover:bg-white/10 hover:scale-110"
|
||||||
)}
|
)}>
|
||||||
</div>
|
{orientation === 'left' ? (
|
||||||
),
|
<ChevronLeftIcon className="h-4 w-4" />
|
||||||
}}
|
) : (
|
||||||
/>
|
<ChevronRightIcon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -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 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>
|
</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>
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user