- Usage prompt auto-shows when app accessed as home screen shortcut (PWA mode) - Added blur gradient overlay below header for smooth scroll fade effect
317 lines
11 KiB
TypeScript
317 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { User } from '@/lib/session';
|
|
import {
|
|
fetchPreferences,
|
|
fetchUsageData,
|
|
savePreferencesAsync,
|
|
saveUsageEntryAsync,
|
|
shouldShowUsagePrompt,
|
|
markPromptShown,
|
|
generateQuitPlan,
|
|
fetchAchievements,
|
|
fetchSavingsConfig,
|
|
saveSavingsConfig,
|
|
unlockAchievement,
|
|
checkBadgeEligibility,
|
|
UserPreferences,
|
|
UsageEntry,
|
|
Achievement,
|
|
SavingsConfig,
|
|
BADGE_DEFINITIONS,
|
|
BadgeDefinition,
|
|
} from '@/lib/storage';
|
|
import { UserHeader } from './UserHeader';
|
|
import { SetupWizard } from './SetupWizard';
|
|
import { UsagePromptDialog } from './UsagePromptDialog';
|
|
import { UsageCalendar } from './UsageCalendar';
|
|
import { StatsCard } from './StatsCard';
|
|
import { QuitPlanCard } from './QuitPlanCard';
|
|
import { AchievementsCard } from './AchievementsCard';
|
|
import { CelebrationAnimation } from './CelebrationAnimation';
|
|
import { HealthTimelineCard } from './HealthTimelineCard';
|
|
import { SavingsTrackerCard } from './SavingsTrackerCard';
|
|
import { Button } from '@/components/ui/button';
|
|
import { PlusCircle } from 'lucide-react';
|
|
import { useTheme } from '@/lib/theme-context';
|
|
import { getTodayString } from '@/lib/date-utils';
|
|
|
|
interface DashboardProps {
|
|
user: User;
|
|
}
|
|
|
|
export function Dashboard({ user }: DashboardProps) {
|
|
const [preferences, setPreferences] = useState<UserPreferences | null>(null);
|
|
const [usageData, setUsageData] = useState<UsageEntry[]>([]);
|
|
const [achievements, setAchievements] = useState<Achievement[]>([]);
|
|
const [savingsConfig, setSavingsConfig] = useState<SavingsConfig | null>(null);
|
|
const [showSetup, setShowSetup] = useState(false);
|
|
const [showUsagePrompt, setShowUsagePrompt] = useState(false);
|
|
const [showCelebration, setShowCelebration] = useState(false);
|
|
const [newBadge, setNewBadge] = useState<BadgeDefinition | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
const { theme } = useTheme();
|
|
|
|
const loadData = useCallback(async () => {
|
|
const [prefs, usage, achvs, savings] = await Promise.all([
|
|
fetchPreferences(),
|
|
fetchUsageData(),
|
|
fetchAchievements(),
|
|
fetchSavingsConfig(),
|
|
]);
|
|
setPreferences(prefs);
|
|
setUsageData(usage);
|
|
setAchievements(achvs);
|
|
setSavingsConfig(savings);
|
|
console.log('[Dashboard] Loaded prefs:', prefs);
|
|
setRefreshKey(prev => prev + 1);
|
|
return { prefs, usage, achvs };
|
|
}, []);
|
|
|
|
const checkAndUnlockAchievements = useCallback(async (
|
|
usage: UsageEntry[],
|
|
prefs: UserPreferences,
|
|
currentAchievements: Achievement[]
|
|
) => {
|
|
const unlockedIds = new Set(currentAchievements.map(a => `${a.badgeId}-${a.substance}`));
|
|
|
|
for (const badge of BADGE_DEFINITIONS) {
|
|
for (const substance of ['nicotine', 'weed'] as const) {
|
|
const key = `${badge.id}-${substance}`;
|
|
if (unlockedIds.has(key)) continue;
|
|
|
|
const isEligible = checkBadgeEligibility(badge.id, usage, prefs, substance);
|
|
if (isEligible) {
|
|
const result = await unlockAchievement(badge.id, substance);
|
|
if (result.isNew && result.achievement) {
|
|
setNewBadge(badge);
|
|
setShowCelebration(true);
|
|
setAchievements(prev => [...prev, result.achievement!]);
|
|
return; // Only show one celebration at a time
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const init = async () => {
|
|
const { prefs, usage, achvs } = await loadData();
|
|
|
|
if (!prefs.hasCompletedSetup) {
|
|
setShowSetup(true);
|
|
} else {
|
|
// Check for achievements
|
|
await checkAndUnlockAchievements(usage, prefs, achvs);
|
|
|
|
// Check if running as PWA (home screen shortcut)
|
|
const isStandalone = window.matchMedia('(display-mode: standalone)').matches ||
|
|
(window.navigator as Navigator & { standalone?: boolean }).standalone === true;
|
|
|
|
// Always show usage prompt when accessed as PWA shortcut
|
|
if (isStandalone) {
|
|
setShowUsagePrompt(true);
|
|
} else if (shouldShowUsagePrompt()) {
|
|
setShowUsagePrompt(true);
|
|
markPromptShown();
|
|
}
|
|
}
|
|
|
|
setIsLoading(false);
|
|
};
|
|
|
|
init();
|
|
}, [loadData, checkAndUnlockAchievements]);
|
|
|
|
const handleSetupComplete = async (data: { substance: 'nicotine' | 'weed'; name: string; age: number; religion: 'christian' | 'secular' }) => {
|
|
const today = getTodayString();
|
|
const newPrefs: UserPreferences = {
|
|
substance: data.substance,
|
|
trackingStartDate: today,
|
|
hasCompletedSetup: true,
|
|
dailyGoal: null,
|
|
quitPlan: null,
|
|
userName: data.name,
|
|
userAge: data.age,
|
|
religion: data.religion,
|
|
};
|
|
await savePreferencesAsync(newPrefs);
|
|
setPreferences(newPrefs);
|
|
setShowSetup(false);
|
|
setShowUsagePrompt(true);
|
|
setRefreshKey(prev => prev + 1);
|
|
};
|
|
|
|
const handleUsageSubmit = async (count: number, substance: 'nicotine' | 'weed') => {
|
|
if (!preferences) {
|
|
setShowUsagePrompt(false);
|
|
return;
|
|
}
|
|
|
|
if (count > 0) {
|
|
const today = getTodayString();
|
|
const now = new Date().toISOString();
|
|
|
|
await saveUsageEntryAsync({
|
|
date: today,
|
|
count,
|
|
substance,
|
|
});
|
|
|
|
// Update preferences with last usage time
|
|
const updatedPrefs = {
|
|
...preferences,
|
|
[substance === 'nicotine' ? 'lastNicotineUsageTime' : 'lastWeedUsageTime']: now,
|
|
};
|
|
await savePreferencesAsync(updatedPrefs);
|
|
setPreferences(updatedPrefs);
|
|
}
|
|
|
|
setShowUsagePrompt(false);
|
|
// Reload data and force calendar refresh
|
|
const usage = await fetchUsageData();
|
|
setUsageData(usage);
|
|
setRefreshKey(prev => prev + 1);
|
|
};
|
|
|
|
const handleGeneratePlan = async () => {
|
|
if (!preferences) return;
|
|
|
|
const plan = generateQuitPlan(preferences.substance);
|
|
const updatedPrefs = {
|
|
...preferences,
|
|
quitPlan: plan,
|
|
};
|
|
await savePreferencesAsync(updatedPrefs);
|
|
setPreferences(updatedPrefs);
|
|
setRefreshKey(prev => prev + 1);
|
|
};
|
|
|
|
const handleSavingsConfigChange = async (config: SavingsConfig) => {
|
|
setSavingsConfig(config);
|
|
await saveSavingsConfig(config);
|
|
};
|
|
|
|
const handleCelebrationComplete = () => {
|
|
setShowCelebration(false);
|
|
setNewBadge(null);
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<div className="animate-pulse text-lg text-white">Loading...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
<div className="min-h-screen">
|
|
<UserHeader user={user} preferences={preferences} />
|
|
|
|
<main className="container mx-auto px-4 py-8">
|
|
{preferences && (
|
|
<>
|
|
{/* Floating Log Button */}
|
|
<div className="fixed bottom-4 right-4 sm:bottom-6 sm:right-6 z-50 opacity-0 animate-scale-in delay-500">
|
|
<Button
|
|
size="lg"
|
|
onClick={() => setShowUsagePrompt(true)}
|
|
className="h-14 px-6 sm:h-16 sm:px-8 text-base sm:text-lg rounded-full shadow-xl bg-gradient-to-r from-primary to-primary/80 hover:from-primary/90 hover:to-primary/70 drop-shadow-lg hover-lift transition-all duration-300 hover:scale-105 active:scale-95"
|
|
>
|
|
<PlusCircle className="mr-2 h-5 w-5 sm:h-6 sm:w-6" />
|
|
Log Usage
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
<div className="space-y-6">
|
|
<div className="opacity-0 animate-fade-in-up">
|
|
<UsageCalendar
|
|
key={refreshKey}
|
|
usageData={usageData}
|
|
onDataUpdate={loadData}
|
|
userId={user.id}
|
|
religion={preferences.religion}
|
|
onReligionUpdate={async (religion) => {
|
|
const updatedPrefs = { ...preferences, religion };
|
|
setPreferences(updatedPrefs);
|
|
await savePreferencesAsync(updatedPrefs);
|
|
}}
|
|
preferences={preferences}
|
|
onPreferencesUpdate={async (updatedPrefs) => {
|
|
await savePreferencesAsync(updatedPrefs);
|
|
setPreferences(updatedPrefs);
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="opacity-0 animate-fade-in-up delay-200">
|
|
<QuitPlanCard
|
|
key={`quit-plan-${refreshKey}`}
|
|
plan={preferences.quitPlan}
|
|
onGeneratePlan={handleGeneratePlan}
|
|
usageData={usageData}
|
|
/>
|
|
</div>
|
|
<div className="opacity-0 animate-fade-in-up delay-400">
|
|
<HealthTimelineCard
|
|
key={`health-${refreshKey}`}
|
|
usageData={usageData}
|
|
preferences={preferences}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-6">
|
|
<div className="opacity-0 animate-slide-in-right delay-100">
|
|
<StatsCard key={`stats-nicotine-${refreshKey}`} usageData={usageData} substance="nicotine" />
|
|
</div>
|
|
<div className="opacity-0 animate-slide-in-right delay-300">
|
|
<StatsCard key={`stats-weed-${refreshKey}`} usageData={usageData} substance="weed" />
|
|
</div>
|
|
<div className="opacity-0 animate-slide-in-right delay-400">
|
|
<AchievementsCard
|
|
key={`achievements-${refreshKey}`}
|
|
achievements={achievements}
|
|
substance={preferences.substance}
|
|
/>
|
|
</div>
|
|
<div className="opacity-0 animate-slide-in-right delay-500">
|
|
<SavingsTrackerCard
|
|
key={`savings-${refreshKey}`}
|
|
savingsConfig={savingsConfig}
|
|
usageData={usageData}
|
|
trackingStartDate={preferences.trackingStartDate}
|
|
onSavingsConfigChange={handleSavingsConfigChange}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</main>
|
|
|
|
<SetupWizard open={showSetup} onComplete={handleSetupComplete} />
|
|
|
|
{preferences && (
|
|
<UsagePromptDialog
|
|
open={showUsagePrompt}
|
|
onClose={() => setShowUsagePrompt(false)}
|
|
onSubmit={handleUsageSubmit}
|
|
userId={user.id}
|
|
/>
|
|
)}
|
|
|
|
{showCelebration && newBadge && (
|
|
<CelebrationAnimation
|
|
badge={newBadge}
|
|
onComplete={handleCelebrationComplete}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|