// Client-side storage utilities for tracking data // Now uses API calls to persist data in SQLite database export interface UsageEntry { date: string; // ISO date string YYYY-MM-DD count: number; substance: 'nicotine' | 'weed'; } export interface UserPreferences { substance: 'nicotine' | 'weed'; trackingStartDate: string | null; hasCompletedSetup: boolean; dailyGoal: number | null; quitPlan: QuitPlan | null; userName: string | null; userAge: number | null; religion: 'christian' | 'secular' | null; lastNicotineUsageTime?: string | null; // ISO timestamp of last usage lastWeedUsageTime?: string | null; // ISO timestamp of last usage } export interface QuitPlan { startDate: string; endDate: string; weeklyTargets: number[]; baselineAverage: number; } // ============ NEW FEATURE INTERFACES ============ export interface Achievement { badgeId: string; unlockedAt: string; substance: 'nicotine' | 'weed' | 'both'; } export interface ReminderSettings { enabled: boolean; reminderTime: string; // HH:MM format frequency: 'daily' | 'hourly'; } export interface SavingsConfig { costPerUnit: number; unitsPerDay: number; savingsGoal: number | null; goalName: string | null; currency: string; substance: 'nicotine' | 'weed'; } export interface BadgeDefinition { id: string; name: string; description: string; howToUnlock: string; icon: string; } export interface HealthMilestone { id: string; timeMinutes: number; title: string; description: string; icon: string; } // ============ BADGE DEFINITIONS ============ export const BADGE_DEFINITIONS: BadgeDefinition[] = [ { id: 'first_day', name: 'First Step', description: 'Logged your first usage', howToUnlock: 'Log your usage for the first time', icon: 'Footprints' }, { id: 'streak_3', name: 'Hat Trick', description: '3 days substance-free', howToUnlock: 'Go 3 consecutive days without using a tracked substance', icon: 'Flame' }, { id: 'streak_7', name: 'Week Warrior', description: 'Tracked for one week', howToUnlock: 'Track your usage for 7 days', icon: 'Shield' }, { id: 'fighter', name: 'Fighter', description: '7 days substance-free', howToUnlock: 'Go 7 consecutive days without using any substance', icon: 'Swords' }, { id: 'one_month', name: 'Monthly Master', description: 'One month tracked with 50% reduction', howToUnlock: 'Track for 30 days and reduce your usage by at least 50%', icon: 'Crown' }, { id: 'goal_crusher', name: 'Goal Crusher', description: 'One month substance-free', howToUnlock: 'Go 30 consecutive days without using any substance', icon: 'Trophy' }, ]; // ============ HEALTH MILESTONES ============ export const HEALTH_MILESTONES: HealthMilestone[] = [ { id: '20min', timeMinutes: 20, title: 'Blood Pressure Normalizes', description: 'Your heart rate and blood pressure begin to drop', icon: 'Heart' }, { id: '8hr', timeMinutes: 480, title: 'Oxygen Levels Rise', description: 'Carbon monoxide levels drop, oxygen levels increase', icon: 'Wind' }, { id: '24hr', timeMinutes: 1440, title: 'Heart Attack Risk Drops', description: 'Your risk of heart attack begins to decrease', icon: 'HeartPulse' }, { id: '48hr', timeMinutes: 2880, title: 'Senses Sharpen', description: 'Taste and smell begin to improve', icon: 'Eye' }, { id: '72hr', timeMinutes: 4320, title: 'Breathing Easier', description: 'Bronchial tubes relax, energy levels increase', icon: 'Wind' }, { id: '2wk', timeMinutes: 20160, title: 'Circulation Improves', description: 'Blood circulation significantly improves', icon: 'Activity' }, { id: '1mo', timeMinutes: 43200, title: 'Lung Function Improves', description: 'Lung capacity increases up to 30%', icon: 'TrendingUp' }, { id: '3mo', timeMinutes: 129600, title: 'Cilia Regenerate', description: 'Lungs begin to heal, coughing decreases', icon: 'Sparkles' }, { id: '1yr', timeMinutes: 525600, title: 'Heart Disease Risk Halved', description: 'Risk of coronary heart disease cut in half', icon: 'HeartHandshake' }, ]; const defaultPreferences: UserPreferences = { substance: 'nicotine', trackingStartDate: null, hasCompletedSetup: false, dailyGoal: null, quitPlan: null, userName: null, userAge: null, religion: null, }; // Cache for preferences and usage data to avoid excessive API calls let preferencesCache: UserPreferences | null = null; let usageDataCache: UsageEntry[] | null = null; let achievementsCache: Achievement[] | null = null; let reminderSettingsCache: ReminderSettings | null = null; let savingsConfigCache: SavingsConfig | null = null; export function clearCache(): void { preferencesCache = null; usageDataCache = null; achievementsCache = null; reminderSettingsCache = null; savingsConfigCache = null; } // These functions are kept for backwards compatibility but no longer used export function setCurrentUserId(_userId: string): void { // No-op - user ID is now managed by session } export function getCurrentUserId(): string | null { return null; } // Async API functions export async function fetchPreferences(): Promise { if (preferencesCache) return preferencesCache; try { const response = await fetch('/api/preferences'); if (!response.ok) { console.error('Failed to fetch preferences'); return defaultPreferences; } const data = await response.json() as UserPreferences; preferencesCache = data; return data; } catch (error) { console.error('Error fetching preferences:', error); return defaultPreferences; } } export async function savePreferencesAsync(preferences: UserPreferences): Promise { try { const response = await fetch('/api/preferences', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(preferences), }); if (response.ok) { preferencesCache = preferences; } } catch (error) { console.error('Error saving preferences:', error); } } export async function fetchUsageData(): Promise { if (usageDataCache) return usageDataCache; try { const response = await fetch('/api/usage'); if (!response.ok) { console.error('Failed to fetch usage data'); return []; } const data = await response.json() as UsageEntry[]; usageDataCache = data; return data; } catch (error) { console.error('Error fetching usage data:', error); return []; } } export async function saveUsageEntryAsync(entry: UsageEntry): Promise { try { await fetch('/api/usage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(entry), }); usageDataCache = null; // Invalidate cache } catch (error) { console.error('Error saving usage entry:', error); } } export async function setUsageForDateAsync( date: string, count: number, substance: 'nicotine' | 'weed' ): Promise { try { await fetch('/api/usage', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ date, count, substance }), }); usageDataCache = null; // Invalidate cache } catch (error) { console.error('Error setting usage for date:', error); } } export async function clearDayDataAsync( date: string, substance: 'nicotine' | 'weed' ): Promise { try { await fetch(`/api/usage?date=${date}&substance=${substance}`, { method: 'DELETE', }); usageDataCache = null; // Invalidate cache } catch (error) { console.error('Error clearing day data:', error); } } // ============ ACHIEVEMENTS FUNCTIONS ============ export async function fetchAchievements(): Promise { if (achievementsCache) return achievementsCache; try { const response = await fetch('/api/achievements'); if (!response.ok) return []; const data = await response.json() as Achievement[]; achievementsCache = data; return data; } catch (error) { console.error('Error fetching achievements:', error); return []; } } export async function unlockAchievement( badgeId: string, substance: 'nicotine' | 'weed' | 'both' ): Promise<{ achievement: Achievement | null; isNew: boolean }> { try { const response = await fetch('/api/achievements', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ badgeId, substance }), }); if (response.ok) { const data = await response.json() as { badgeId: string; unlockedAt: string; substance: 'nicotine' | 'weed' | 'both'; alreadyUnlocked?: boolean }; achievementsCache = null; // Invalidate cache return { achievement: { badgeId: data.badgeId, unlockedAt: data.unlockedAt, substance: data.substance, }, isNew: !data.alreadyUnlocked, }; } return { achievement: null, isNew: false }; } catch (error) { console.error('Error unlocking achievement:', error); return { achievement: null, isNew: false }; } } export function getAchievements(): Achievement[] { return achievementsCache || []; } // ============ REMINDERS FUNCTIONS ============ export async function fetchReminderSettings(): Promise { if (reminderSettingsCache) return reminderSettingsCache; try { const response = await fetch('/api/reminders'); if (!response.ok) return { enabled: false, reminderTime: '09:00', frequency: 'daily' }; const data = await response.json() as ReminderSettings; reminderSettingsCache = data; return data; } catch (error) { console.error('Error fetching reminder settings:', error); return { enabled: false, reminderTime: '09:00', frequency: 'daily' }; } } export async function saveReminderSettings(settings: ReminderSettings): Promise { try { const response = await fetch('/api/reminders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settings), }); if (response.ok) { reminderSettingsCache = settings; } } catch (error) { console.error('Error saving reminder settings:', error); } } export function getReminderSettings(): ReminderSettings { return reminderSettingsCache || { enabled: false, reminderTime: '09:00', frequency: 'daily' }; } // ============ SAVINGS FUNCTIONS ============ export async function fetchSavingsConfig(): Promise { if (savingsConfigCache) return savingsConfigCache; try { const response = await fetch('/api/savings'); if (!response.ok) return null; const data = await response.json() as SavingsConfig | null; savingsConfigCache = data; return data; } catch (error) { console.error('Error fetching savings config:', error); return null; } } export async function saveSavingsConfig(config: SavingsConfig): Promise { try { const response = await fetch('/api/savings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config), }); if (response.ok) { savingsConfigCache = config; } } catch (error) { console.error('Error saving savings config:', error); } } export function getSavingsConfig(): SavingsConfig | null { return savingsConfigCache; } // ============ CALCULATION HELPERS ============ export function calculateStreak( usageData: UsageEntry[], substance: 'nicotine' | 'weed' ): number { let streak = 0; const today = new Date(); const substanceData = usageData.filter((e) => e.substance === substance); for (let i = 0; i <= 365; i++) { const checkDate = new Date(today); checkDate.setDate(checkDate.getDate() - i); const dateStr = checkDate.toISOString().split('T')[0]; const dayUsage = substanceData.find((e) => e.date === dateStr)?.count ?? -1; if (dayUsage === 0) { streak++; } else if (dayUsage > 0) { break; } // If dayUsage === -1 (no entry), we continue but don't count it as a streak day } return streak; } export function calculateTotalSaved( savingsConfig: SavingsConfig | null, usageData: UsageEntry[], startDate: string | null ): number { if (!savingsConfig || !startDate) return 0; const start = new Date(startDate); const today = new Date(); const daysSinceStart = Math.floor( (today.getTime() - start.getTime()) / (1000 * 60 * 60 * 24) ); if (daysSinceStart <= 0) return 0; // Expected spending if they continued at baseline (unitsPerDay is now unitsPerWeek) const weeksSinceStart = daysSinceStart / 7; const expectedSpend = weeksSinceStart * savingsConfig.costPerUnit * savingsConfig.unitsPerDay; // Actual usage converted to cost (assuming ~20 puffs/hits per unit) const relevantUsage = usageData.filter( (e) => e.substance === savingsConfig.substance && new Date(e.date) >= start ); const actualUnits = relevantUsage.reduce((sum, e) => sum + e.count, 0); const unitsPerPack = 20; // Average puffs per pack/unit const actualSpend = (actualUnits / unitsPerPack) * savingsConfig.costPerUnit; return Math.max(0, expectedSpend - actualSpend); } export function checkBadgeEligibility( badgeId: string, usageData: UsageEntry[], preferences: UserPreferences, substance: 'nicotine' | 'weed' ): boolean { const streak = calculateStreak(usageData, substance); const nicotineStreak = calculateStreak(usageData, 'nicotine'); const weedStreak = calculateStreak(usageData, 'weed'); const totalDays = new Set( usageData.filter((e) => e.substance === substance).map((e) => e.date) ).size; // Check if user has tracked for at least 30 days and reduced usage by 50% const checkMonthlyReduction = (): boolean => { if (!preferences.trackingStartDate) return false; const startDate = new Date(preferences.trackingStartDate); const today = new Date(); const daysSinceStart = Math.floor( (today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24) ); if (daysSinceStart < 30) return false; // Get first week's average const firstWeekData = usageData.filter((e) => { const entryDate = new Date(e.date); const daysSinceEntry = Math.floor( (entryDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24) ); return e.substance === substance && daysSinceEntry >= 0 && daysSinceEntry < 7; }); const firstWeekTotal = firstWeekData.reduce((sum, e) => sum + e.count, 0); const firstWeekAvg = firstWeekData.length > 0 ? firstWeekTotal / 7 : 0; // Get last week's average const lastWeekData = usageData.filter((e) => { const entryDate = new Date(e.date); const daysAgo = Math.floor( (today.getTime() - entryDate.getTime()) / (1000 * 60 * 60 * 24) ); return e.substance === substance && daysAgo >= 0 && daysAgo < 7; }); const lastWeekTotal = lastWeekData.reduce((sum, e) => sum + e.count, 0); const lastWeekAvg = lastWeekTotal / 7; // Check if reduced by at least 50% if (firstWeekAvg <= 0) return lastWeekAvg === 0; return lastWeekAvg <= firstWeekAvg * 0.5; }; switch (badgeId) { case 'first_day': // Log usage for the first time return totalDays >= 1; case 'streak_3': // 3 days off a tracked substance return streak >= 3; case 'streak_7': // Track usage for one week (7 days of entries) return totalDays >= 7; case 'fighter': // 7 days off ANY substance (both nicotine AND weed) return nicotineStreak >= 7 && weedStreak >= 7; case 'one_month': // Track one month and reduce usage by 50% return checkMonthlyReduction(); case 'goal_crusher': // One month substance free (both substances) return nicotineStreak >= 30 && weedStreak >= 30; default: return false; } } // Synchronous functions that use cache (for backwards compatibility) // These should be replaced with async versions in components export function getPreferences(_userId?: string): UserPreferences { return preferencesCache || defaultPreferences; } export function getUsageData(_userId?: string): UsageEntry[] { return usageDataCache || []; } export function savePreferences(preferences: UserPreferences, _userId?: string): void { preferencesCache = preferences; savePreferencesAsync(preferences); } export function saveUsageEntry(entry: UsageEntry, _userId?: string): void { saveUsageEntryAsync(entry); } export function setUsageForDate( date: string, count: number, substance: 'nicotine' | 'weed', _userId?: string ): void { setUsageForDateAsync(date, count, substance); } export function getUsageForDate( date: string, substance: 'nicotine' | 'weed', _userId?: string ): number { const data = usageDataCache || []; const entry = data.find((e) => e.date === date && e.substance === substance); return entry?.count ?? 0; } export function clearDayData( date: string, substance: 'nicotine' | 'weed', _userId?: string ): void { clearDayDataAsync(date, substance); } const LAST_PROMPT_KEY = 'quittraq_last_prompt_date'; export function shouldShowUsagePrompt(): boolean { if (typeof window === 'undefined') return false; const today = new Date().toISOString().split('T')[0]; const lastPromptDate = localStorage.getItem(LAST_PROMPT_KEY); return lastPromptDate !== today; } export function markPromptShown(): void { if (typeof window === 'undefined') return; const today = new Date().toISOString().split('T')[0]; localStorage.setItem(LAST_PROMPT_KEY, today); } export function getWeeklyData(substance: 'nicotine' | 'weed', _userId?: string): UsageEntry[] { const data = usageDataCache || []; const today = new Date(); const weekAgo = new Date(today); weekAgo.setDate(weekAgo.getDate() - 7); return data.filter((entry) => { const entryDate = new Date(entry.date); return entry.substance === substance && entryDate >= weekAgo && entryDate <= today; }); } export function calculateWeeklyAverage(substance: 'nicotine' | 'weed', _userId?: string): number { const weeklyData = getWeeklyData(substance); if (weeklyData.length === 0) return 0; const total = weeklyData.reduce((sum, entry) => sum + entry.count, 0); return Math.round(total / weeklyData.length); } export function hasOneWeekOfData(substance: 'nicotine' | 'weed', _userId?: string): boolean { const prefs = preferencesCache; if (!prefs?.trackingStartDate) return false; const startDate = new Date(prefs.trackingStartDate); const today = new Date(); const daysDiff = Math.floor((today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)); return daysDiff >= 7; } export function generateQuitPlan(substance: 'nicotine' | 'weed', _userId?: string): QuitPlan { const baseline = calculateWeeklyAverage(substance); const today = new Date(); const startDate = today.toISOString().split('T')[0]; // 4-week reduction plan with 25% weekly reduction const endDate = new Date(today); endDate.setDate(endDate.getDate() + 28); // Gradual reduction: each week reduce by 25% const weeklyTargets: number[] = []; let current = baseline; for (let i = 0; i < 4; i++) { current = Math.max(0, Math.round(current * 0.75)); weeklyTargets.push(current); } return { startDate, endDate: endDate.toISOString().split('T')[0], weeklyTargets, baselineAverage: baseline, }; } export function getCurrentWeekTarget(_userId?: string): number | null { const prefs = preferencesCache; if (!prefs?.quitPlan) return null; const startDate = new Date(prefs.quitPlan.startDate); const today = new Date(); const weekNumber = Math.floor((today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24 * 7)); if (weekNumber >= prefs.quitPlan.weeklyTargets.length) { return 0; // Goal achieved } return prefs.quitPlan.weeklyTargets[weekNumber]; } export async function clearAllDataAsync(): Promise { // This would need a dedicated API endpoint console.warn('clearAllData not implemented for API-based storage'); } export function clearAllData(_userId?: string): void { clearAllDataAsync(); }