Stop_smoking_website_ver2/src/components/HealthTimelineCard.tsx
2026-01-25 12:30:09 -07:00

274 lines
10 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { HEALTH_MILESTONES, UsageEntry, UserPreferences } from '@/lib/storage';
import { useTheme } from '@/lib/theme-context';
import {
Heart,
Wind,
HeartPulse,
Eye,
Activity,
TrendingUp,
Sparkles,
HeartHandshake,
CheckCircle2,
Cigarette,
Leaf
} from 'lucide-react';
interface HealthTimelineCardProps {
usageData: UsageEntry[];
preferences?: UserPreferences | null;
}
const iconMap: Record<string, React.ElementType> = {
Heart,
Wind,
HeartPulse,
Eye,
Activity,
TrendingUp,
Sparkles,
HeartHandshake,
};
// Simple, direct calculation of minutes since last usage
function calculateMinutesFree(
substance: 'nicotine' | 'weed',
usageData: UsageEntry[],
preferences: UserPreferences | null
): number {
const now = new Date();
// 1. Check for stored timestamp first (most accurate)
const lastUsageTime = substance === 'nicotine'
? preferences?.lastNicotineUsageTime
: preferences?.lastWeedUsageTime;
if (lastUsageTime) {
const lastTime = new Date(lastUsageTime);
const diffMs = now.getTime() - lastTime.getTime();
return Math.max(0, diffMs / (1000 * 60));
}
// 2. Find last recorded usage from usage data
const substanceData = usageData
.filter(e => e.substance === substance && e.count > 0)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
if (substanceData.length > 0) {
const lastDateStr = substanceData[0].date;
const todayStr = now.toISOString().split('T')[0];
// If last usage was today but no timestamp, count from now (0 minutes)
if (lastDateStr === todayStr) {
return 0;
}
// For past days, count from end of that day (23:59:59)
const lastDate = new Date(lastDateStr);
lastDate.setHours(23, 59, 59, 999);
const diffMs = now.getTime() - lastDate.getTime();
return Math.max(0, diffMs / (1000 * 60));
}
// 3. No usage ever - count from tracking start date
if (preferences?.trackingStartDate) {
const startDate = new Date(preferences.trackingStartDate);
startDate.setHours(0, 0, 0, 0);
const diffMs = now.getTime() - startDate.getTime();
return Math.max(0, diffMs / (1000 * 60));
}
return 0;
}
function formatDuration(minutes: number): string {
if (minutes < 1) return '< 1 min';
if (minutes < 60) return `${Math.floor(minutes)} min`;
if (minutes < 1440) return `${Math.floor(minutes / 60)} hrs`;
if (minutes < 10080) return `${Math.floor(minutes / 1440)} days`;
if (minutes < 43200) return `${Math.floor(minutes / 10080)} weeks`;
if (minutes < 525600) return `${Math.floor(minutes / 43200)} months`;
return `${Math.floor(minutes / 525600)} year${minutes >= 1051200 ? 's' : ''}`;
}
function formatTimeRemaining(currentMinutes: number, targetMinutes: number): string {
const remaining = targetMinutes - currentMinutes;
if (remaining <= 0) return 'Achieved!';
return `${formatDuration(remaining)} to go`;
}
interface TimelineColumnProps {
substance: 'nicotine' | 'weed';
minutesFree: number;
theme: 'light' | 'dark';
}
function TimelineColumn({ substance, minutesFree, theme }: TimelineColumnProps) {
// Find current milestone
let currentMilestoneIndex = -1;
for (let i = HEALTH_MILESTONES.length - 1; i >= 0; i--) {
if (minutesFree >= HEALTH_MILESTONES[i].timeMinutes) {
currentMilestoneIndex = i;
break;
}
}
// Find next milestone
const nextMilestoneIndex = currentMilestoneIndex + 1;
const nextMilestone = nextMilestoneIndex < HEALTH_MILESTONES.length
? HEALTH_MILESTONES[nextMilestoneIndex]
: null;
// Calculate progress to next milestone
let progressToNext = 100;
if (nextMilestone) {
const prevMinutes = currentMilestoneIndex >= 0
? HEALTH_MILESTONES[currentMilestoneIndex].timeMinutes
: 0;
const range = nextMilestone.timeMinutes - prevMinutes;
const progress = minutesFree - prevMinutes;
progressToNext = Math.min(100, Math.max(0, (progress / range) * 100));
}
const substanceLabel = substance === 'nicotine' ? 'Nicotine' : 'Marijuana';
const SubstanceIcon = substance === 'nicotine' ? Cigarette : Leaf;
const accentColorClass = substance === 'nicotine' ? 'text-red-500' : 'text-green-500';
const bgAccentClass = substance === 'nicotine' ? 'bg-red-500' : 'bg-green-500';
return (
<div className="flex flex-col h-full bg-black/5 dark:bg-white/5 rounded-xl border border-white/5 overflow-hidden">
{/* Header with live timer */}
<div className={`p-3 border-b border-white/5 flex items-center gap-2 ${theme === 'light' ? 'bg-white/50' : 'bg-black/20'}`}>
<SubstanceIcon className={`h-4 w-4 ${accentColorClass}`} />
<span className={`text-sm font-semibold ${theme === 'light' ? 'text-slate-900' : 'text-white'}`}>
{substanceLabel}
</span>
<span className="ml-auto text-xs opacity-70 font-medium tabular-nums">
{formatDuration(minutesFree)} free
</span>
</div>
<div className="p-3 flex-1 overflow-y-auto min-h-0 space-y-3 custom-scrollbar">
{/* Progress to next milestone */}
{nextMilestone && (
<div className={`p-3 rounded-lg border ${theme === 'light' ? 'bg-white border-slate-200 shadow-sm' : 'bg-white/5 border-white/10'}`}>
<div className="flex items-center justify-between mb-2">
<span className={`text-xs font-medium opacity-80 ${theme === 'light' ? 'text-slate-700' : 'text-white'}`}>Next Up</span>
<span className={`text-xs font-bold ${accentColorClass}`}>
{formatTimeRemaining(minutesFree, nextMilestone.timeMinutes)}
</span>
</div>
<div className="w-full bg-slate-200 dark:bg-white/10 rounded-full h-1.5 overflow-hidden">
<div
className={`h-1.5 rounded-full transition-all duration-700 ${bgAccentClass}`}
style={{ width: `${progressToNext}%`, opacity: 0.8 }}
/>
</div>
<p className={`text-xs mt-1.5 opacity-70 truncate ${theme === 'light' ? 'text-slate-600' : 'text-white/70'}`}>
{nextMilestone.title}
</p>
</div>
)}
{/* Timeline Items */}
{HEALTH_MILESTONES.map((milestone, index) => {
const isAchieved = minutesFree >= milestone.timeMinutes;
const isCurrent = index === currentMilestoneIndex;
const Icon = iconMap[milestone.icon] || Heart;
return (
<div
key={milestone.id}
className={`flex items-start gap-2.5 p-2 rounded-lg transition-all ${isAchieved
? (theme === 'light' ? 'bg-slate-100/50' : 'bg-white/5')
: 'opacity-50 grayscale'
} ${isCurrent ? 'ring-1 ring-offset-1 ring-offset-transparent ' + (substance === 'nicotine' ? 'ring-red-500/50' : 'ring-green-500/50') : ''}`}
>
{/* Icon */}
<div
className={`p-1.5 rounded-full shrink-0 mt-0.5 ${isAchieved
? (theme === 'light' ? 'bg-white text-slate-700 shadow-sm' : 'bg-white/10 text-white')
: 'bg-black/5 text-black/30 dark:bg-white/5 dark:text-white/30'
}`}
>
{isAchieved ? <CheckCircle2 className="h-3 w-3" /> : <Icon className="h-3 w-3" />}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<p className={`text-xs font-semibold ${theme === 'light' ? 'text-slate-900' : 'text-white'}`}>
{milestone.title}
</p>
<p className={`text-[10px] mt-0.5 leading-tight ${theme === 'light' ? 'text-slate-600' : 'text-white/50'}`}>
{milestone.description}
</p>
</div>
</div>
);
})}
</div>
</div>
);
}
export function HealthTimelineCard({ usageData, preferences }: HealthTimelineCardProps) {
const { theme } = useTheme();
// State for live timer values
const [nicotineMinutes, setNicotineMinutes] = useState(0);
const [weedMinutes, setWeedMinutes] = useState(0);
// Function to recalculate both timers
const updateTimers = useCallback(() => {
const prefs = preferences || null;
setNicotineMinutes(calculateMinutesFree('nicotine', usageData, prefs));
setWeedMinutes(calculateMinutesFree('weed', usageData, prefs));
}, [usageData, preferences]);
// Initial calculation and start interval
useEffect(() => {
// Calculate immediately
updateTimers();
// Update every second
const interval = setInterval(updateTimers, 1000);
return () => clearInterval(interval);
}, [updateTimers]);
const cardBackground =
theme === 'light'
? 'linear-gradient(135deg, rgba(236, 253, 245, 0.9) 0%, rgba(209, 250, 229, 0.8) 100%)'
: 'linear-gradient(135deg, rgba(20, 184, 166, 0.2) 0%, rgba(6, 182, 212, 0.15) 100%)';
return (
<Card
className="backdrop-blur-xl border border-teal-500/40 shadow-xl drop-shadow-lg hover-lift transition-all duration-300 overflow-hidden relative h-[500px] flex flex-col"
style={{ background: cardBackground }}
>
<div className="absolute top-0 right-0 w-40 h-40 bg-gradient-to-br from-teal-500/10 to-transparent rounded-full -translate-y-1/2 translate-x-1/2 pointer-events-none" />
<CardHeader className="relative z-10 pb-4 shrink-0">
<CardTitle className={`flex items-center gap-2 ${theme === 'light' ? 'text-teal-900' : 'text-white'} text-shadow-sm`}>
<Heart className="h-5 w-5 text-teal-500" />
<span>Health Recovery</span>
</CardTitle>
<p className={`text-sm ${theme === 'light' ? 'text-teal-700' : 'text-white/70'}`}>
Track your body&apos;s healing process for each substance independently.
</p>
</CardHeader>
<CardContent className="relative z-10 flex-1 min-h-0 pb-6 pt-0">
<div className="grid grid-cols-2 gap-4 h-full">
<TimelineColumn substance="nicotine" minutesFree={nicotineMinutes} theme={theme} />
<TimelineColumn substance="weed" minutesFree={weedMinutes} theme={theme} />
</div>
</CardContent>
</Card>
);
}