310 lines
10 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);
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>
);
}