619 lines
20 KiB
TypeScript

// 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<UserPreferences> {
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<void> {
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<UsageEntry[]> {
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<void> {
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<void> {
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<void> {
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<Achievement[]> {
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<ReminderSettings> {
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<void> {
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<SavingsConfig | null> {
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<void> {
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<void> {
// This would need a dedicated API endpoint
console.warn('clearAllData not implemented for API-based storage');
}
export function clearAllData(_userId?: string): void {
clearAllDataAsync();
}