'use client'; import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import React 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, StorageRequestError, } from '@/lib/storage'; import { UserHeader } from './UserHeader'; import { SetupWizard } from './SetupWizard'; import { UsageCalendar } from './UsageCalendar'; import { StatsCard } from './StatsCard'; import { UnifiedQuitPlanCard } from './UnifiedQuitPlanCard'; import { AchievementsCard } from './AchievementsCard'; import { CelebrationAnimation } from './CelebrationAnimation'; import { HealthTimelineCard } from './HealthTimelineCard'; import { SavingsTrackerCard } from './SavingsTrackerCard'; import { MoodTracker } from './MoodTracker'; import { DailyInspirationCard } from './DailyInspirationCard'; import { ScrollWheelLogger } from './ScrollWheelLogger'; import { UsageLoggerDropUp } from './UsageLoggerDropUp'; import { VersionUpdateModal } from './VersionUpdateModal'; import { Button } from '@/components/ui/button'; import { PlusCircle, X } from 'lucide-react'; import { useTheme } from '@/lib/theme-context'; import { getTodayString } from '@/lib/date-utils'; interface DashboardProps { user: User; } const MOBILE_SLIDES = [ { id: 'mood', label: 'How Are You Feeling' }, { id: 'plan', label: 'Quit Journey Plan' }, { id: 'stats', label: 'Usage Stats' }, { id: 'recovery', label: 'Health Recovery' }, { id: 'achievements', label: 'Achievements' }, { id: 'savings', label: 'Savings' }, { id: 'calendar', label: 'Usage Calendar' }, ]; const GLOBAL_BADGE_IDS = new Set(['first_day']); export function Dashboard({ user }: DashboardProps) { const [preferences, setPreferences] = useState(null); const [usageData, setUsageData] = useState([]); const [achievements, setAchievements] = useState([]); const [savingsConfig, setSavingsConfig] = useState(null); const [showSetup, setShowSetup] = useState(false); const [showCelebration, setShowCelebration] = useState(false); const [isSubstancePickerOpen, setIsSubstancePickerOpen] = useState(false); const [activeLoggingSubstance, setActiveLoggingSubstance] = useState<'nicotine' | 'weed' | null>(null); const [newBadge, setNewBadge] = useState(null); const [isLoading, setIsLoading] = useState(true); const [loadError, setLoadError] = useState(null); const [refreshKey, setRefreshKey] = useState(0); const [currentPage, setCurrentPage] = useState(0); const [modalOpenCount, setModalOpenCount] = useState(0); const swipeContainerRef = useRef(null); const { theme } = useTheme(); const isModalOpen = modalOpenCount > 0 || showSetup || showCelebration; const totalPages = MOBILE_SLIDES.length; const handleModalStateChange = useCallback((isOpen: boolean) => { setModalOpenCount(prev => isOpen ? prev + 1 : Math.max(0, prev - 1)); }, []); const handleScroll = useCallback(() => { const container = swipeContainerRef.current; if (!container) return; const slides = Array.from(container.querySelectorAll('.swipe-item')); if (slides.length === 0) return; let nearestIndex = 0; let nearestDistance = Number.POSITIVE_INFINITY; slides.forEach((slide, index) => { const distance = Math.abs(container.scrollLeft - slide.offsetLeft); if (distance < nearestDistance) { nearestDistance = distance; nearestIndex = index; } }); if (nearestIndex !== currentPage) { setCurrentPage(nearestIndex); } }, [currentPage]); const scrollToPage = (pageIndex: number) => { const container = swipeContainerRef.current; if (!container) return; const slides = Array.from(container.querySelectorAll('.swipe-item')); if (slides.length === 0) return; const boundedPage = Math.min(Math.max(pageIndex, 0), totalPages - 1); const targetSlide = slides[boundedPage]; container.scrollTo({ left: targetSlide?.offsetLeft ?? 0, behavior: 'smooth' }); }; useEffect(() => { if (typeof window === 'undefined') return; const savedPage = Number(localStorage.getItem('quittraq_mobile_dashboard_page')); if (Number.isNaN(savedPage) || savedPage < 0 || savedPage >= totalPages) return; setCurrentPage(savedPage); const timeout = window.setTimeout(() => { scrollToPage(savedPage); }, 0); return () => window.clearTimeout(timeout); }, [totalPages]); useEffect(() => { if (typeof window === 'undefined') return; localStorage.setItem('quittraq_mobile_dashboard_page', String(currentPage)); }, [currentPage]); const loadData = useCallback(async () => { try { const [prefs, usage, achvs, savings] = await Promise.all([ fetchPreferences(), fetchUsageData(), fetchAchievements(), fetchSavingsConfig(), ]); setPreferences(prefs); setUsageData(usage); setAchievements(achvs); setSavingsConfig(savings); setLoadError(null); console.log('[Dashboard] Loaded prefs:', prefs); setRefreshKey(prev => prev + 1); return { prefs, usage, achvs }; } catch (error) { const message = error instanceof StorageRequestError ? error.message : 'Unable to sync your dashboard right now. Please try again.'; setLoadError(message); throw error; } }, []); const checkAndUnlockAchievements = useCallback(async ( usage: UsageEntry[], prefs: UserPreferences, currentAchievements: Achievement[] ) => { // Current unlocked set (local + server) const unlockedIds = new Set(currentAchievements.map(a => `${a.badgeId}-${a.substance}`)); const unlockedGlobalBadges = new Set( currentAchievements .filter((a) => GLOBAL_BADGE_IDS.has(a.badgeId)) .map((a) => a.badgeId) ); const newUnlocked: Achievement[] = []; let badgeToCelebrate: BadgeDefinition | null = null; const hasUsageBySubstance = { nicotine: usage.some((entry) => entry.substance === 'nicotine' && entry.count > 0), weed: usage.some((entry) => entry.substance === 'weed' && entry.count > 0), }; for (const badge of BADGE_DEFINITIONS) { if (GLOBAL_BADGE_IDS.has(badge.id)) { if (unlockedGlobalBadges.has(badge.id)) continue; const isEligible = checkBadgeEligibility(badge.id, usage, prefs, 'nicotine') || checkBadgeEligibility(badge.id, usage, prefs, 'weed'); if (!isEligible) continue; try { const result = await unlockAchievement(badge.id, 'both'); if (result.isNew && result.achievement) { newUnlocked.push(result.achievement); unlockedGlobalBadges.add(badge.id); if (!badgeToCelebrate) { badgeToCelebrate = badge; } } } catch (e) { console.error('Error unlocking global achievement:', e); } continue; } for (const substance of ['nicotine', 'weed'] as const) { if (!hasUsageBySubstance[substance]) continue; const key = `${badge.id}-${substance}`; if (unlockedIds.has(key)) continue; const isEligible = checkBadgeEligibility(badge.id, usage, prefs, substance); if (isEligible) { try { const result = await unlockAchievement(badge.id, substance); if (result.isNew && result.achievement) { newUnlocked.push(result.achievement); // Prioritize celebrating the first one found if (!badgeToCelebrate) { badgeToCelebrate = badge; } } } catch (e) { console.error('Error unlocking achievement:', e); } } } } if (newUnlocked.length > 0) { // Update local state with ALL new achievements setAchievements(prev => [...prev, ...newUnlocked]); // Show celebration for determining badge if (badgeToCelebrate) { setNewBadge(badgeToCelebrate); setShowCelebration(true); } } return newUnlocked.length > 0; }, []); useEffect(() => { const init = async () => { try { 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) // No longer automatically showing substance picker if (shouldShowUsagePrompt()) { markPromptShown(); } } } catch (error) { console.error('Dashboard init error:', error); } finally { 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); setIsSubstancePickerOpen(true); setRefreshKey(prev => prev + 1); }; const handleUsageSubmit = async (count: number, substance: 'nicotine' | 'weed') => { if (!preferences) { setIsSubstancePickerOpen(false); setActiveLoggingSubstance(null); return; } let latestPrefs = preferences; if (count > 0) { const today = getTodayString(); const now = new Date().toISOString(); await saveUsageEntryAsync({ date: today, count, substance, }); // Update preferences with last usage time latestPrefs = { ...preferences, [substance === 'nicotine' ? 'lastNicotineUsageTime' : 'lastWeedUsageTime']: now, }; // Force specific fields to be present to avoid partial update issues // This ensures that even if preferences is stale, we explicitly set the usage time const payload: UserPreferences = { ...latestPrefs, lastNicotineUsageTime: substance === 'nicotine' ? now : (latestPrefs.lastNicotineUsageTime ?? null), lastWeedUsageTime: substance === 'weed' ? now : (latestPrefs.lastWeedUsageTime ?? null), }; await savePreferencesAsync(payload); setPreferences(payload); } setActiveLoggingSubstance(null); setIsSubstancePickerOpen(false); // Reload data and force calendar refresh const usage = await fetchUsageData(); setUsageData(usage); setRefreshKey(prev => prev + 1); // Check for new achievements metrics FIRST await checkAndUnlockAchievements(usage, latestPrefs, achievements); // Force a fresh fetch of all data to ensure UI sync const freshAchievements = await fetchAchievements(); setAchievements(freshAchievements); // THEN refresh UI components setRefreshKey(prev => prev + 1); }; const handleGeneratePlan = async (targetSubstance: 'nicotine' | 'weed') => { if (!preferences) return; const plan = generateQuitPlan(targetSubstance); // Construct new state const currentQuitState = preferences.quitState || { nicotine: { plan: null, startDate: null }, weed: { plan: null, startDate: null } }; const updatedQuitState = { ...currentQuitState, [targetSubstance]: { plan, startDate: currentQuitState[targetSubstance].startDate || (preferences.substance === targetSubstance ? preferences.trackingStartDate : null) || getTodayString() } }; const updatedPrefs = { ...preferences, quitState: updatedQuitState }; 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 (
Loading...
); } return (
{loadError && (
{loadError}
)} {!preferences && !isLoading && (

Your dashboard data is unavailable right now.

)} {preferences && ( <> {/* Floating Log Button - Simplified to toggle Picker */}
{/* Dashboard Sections */}
{/* DESKTOP LAYOUT - Hidden on mobile */}
{/* Row 1: Mood + Quit Plan */}
{/* Row 2: Calendar/Quote */}
{ const updatedPrefs = { ...preferences, religion }; setPreferences(updatedPrefs); await savePreferencesAsync(updatedPrefs); }} showInspirationPanel preferences={preferences} onPreferencesUpdate={async (updatedPrefs: UserPreferences) => { await savePreferencesAsync(updatedPrefs); setPreferences(updatedPrefs); }} />
{/* Row 3: Achievements + Health Recovery */}
{/* Row 4: Savings + Stats */}
{/* MOBILE SWIPE LAYOUT - Hidden on desktop */}
{ if (event.key === 'ArrowRight') { event.preventDefault(); scrollToPage(currentPage + 1); } if (event.key === 'ArrowLeft') { event.preventDefault(); scrollToPage(currentPage - 1); } }} className="swipe-container sm:hidden" tabIndex={0} role="region" aria-label="Mobile dashboard sections" > {/* SLIDE 1: Mood */}

How Are You Feeling

{ const updatedPrefs = { ...preferences, religion }; setPreferences(updatedPrefs); await savePreferencesAsync(updatedPrefs); }} />
{/* SLIDE 2: Quit Plan */}

Quit Journey Plan

{/* SLIDE 3: Stats */}

Usage Stats

{/* SLIDE 4: Recovery */}

Health Recovery

{/* SLIDE 5: Achievements */}

Achievements

{/* SLIDE 6: Savings */}

Savings

{/* SLIDE 7: Calendar */}

Usage Calendar

{ await savePreferencesAsync(updatedPrefs); setPreferences(updatedPrefs); }} />
{MOBILE_SLIDES[currentPage]?.label}
{MOBILE_SLIDES.map((slide, index) => (
)}
{ setActiveLoggingSubstance(substance); setIsSubstancePickerOpen(false); }} onClose={() => setIsSubstancePickerOpen(false)} /> {activeLoggingSubstance && ( setActiveLoggingSubstance(null)} /> )} { if (!preferences) return; const nextPreferences = { ...preferences, lastSeenReleaseNotesVersion: version, }; setPreferences(nextPreferences); await savePreferencesAsync(nextPreferences); }} /> {showCelebration && newBadge && ( )}
); }