import React, { useState, useEffect, useRef, useLayoutEffect } from 'react'; import { Trash2, Dumbbell, Utensils, User, ChevronRight, ChevronDown, ChevronUp, Activity, X, Droplets, BookOpen, Info, PlusCircle, Sparkles, Camera, ClipboardList, RefreshCw, Copy, Settings, LogOut, ShoppingCart, Loader2, Phone, Plus, Heart, GripVertical, Upload, Video } from 'lucide-react'; import { GoogleGenerativeAI } from "@google/generative-ai"; import SignupScreen from './SignupScreen'; import { WORKOUT_ROUTINES, TRAINING_PLANS } from './workoutData'; import { getCurrentUser, signInWithGoogle, signOut } from './firebase'; import { isHealthKitAvailable, requestHealthKitAuthorization, getTodaySummary, getHealthSyncSettings, saveHealthSyncSettings, getAuthorizationStatus, getHealthDataForDate } from './healthKit'; // --- CONFIGURATION --- const GEMINI_API_KEY = import.meta.env.VITE_GEMINI_API_KEY; // Updated Component: Accepts onRemoveWater and splits click area, with drag support const GlassDashboard = ({ stats, onAddWater, onRemoveWater }) => { const dragRef = React.useRef({ isDragging: false, startY: 0, lastUpdate: 0, rafId: null }); const totalPct = Math.min((stats.food / stats.adjustedGoal) * 100, 100); const waterPct = Math.min((stats.dailyWater / stats.waterGoal) * 100, 100); const proteinCals = stats.proteinTotal * 4; const carbsCals = stats.carbsTotal * 4; const fatCals = stats.fatTotal * 9; const totalMacroCals = proteinCals + carbsCals + fatCals || 1; const pPct = (proteinCals / totalMacroCals) * 100; const cPct = (carbsCals / totalMacroCals) * 100; const fPct = (fatCals / totalMacroCals) * 100; // Drag handlers for water - highly optimized with RAF const handleDragStart = (e) => { const clientY = e.touches ? e.touches[0].clientY : e.clientY; dragRef.current.isDragging = true; dragRef.current.startY = clientY; dragRef.current.lastUpdate = clientY; e.preventDefault(); }; const handleDragMove = (e) => { if (!dragRef.current.isDragging) return; // Cancel any pending animation frame if (dragRef.current.rafId) { cancelAnimationFrame(dragRef.current.rafId); } // Use RAF for smooth updates dragRef.current.rafId = requestAnimationFrame(() => { const clientY = e.touches ? e.touches[0].clientY : e.clientY; const deltaFromLast = dragRef.current.lastUpdate - clientY; const threshold = 40; // pixels per water increment if (Math.abs(deltaFromLast) >= threshold) { if (deltaFromLast > 0) { onAddWater(); } else { onRemoveWater(); } dragRef.current.lastUpdate = clientY; } }); }; const handleDragEnd = () => { dragRef.current.isDragging = false; if (dragRef.current.rafId) { cancelAnimationFrame(dragRef.current.rafId); dragRef.current.rafId = null; } }; return (
{/* Floating orbs */}
{/* TOP SECTION: Tighter marginBottom (4px) to reduce overall height */}
{/* Circular progress */}
Calories
{stats.remaining}
remaining
{/* Water Glass - Updated with Click Zones and Drag Support */}
{/* Label "WATER" above the glass */}
Water
{/* Top Click Zone (Add) - only works when not dragging */}
{ e.stopPropagation(); if (!dragRef.current.isDragging) onAddWater(); }} style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '50%', zIndex: 10, cursor: 'pointer' }} /> {/* Bottom Click Zone (Remove) - only works when not dragging */}
{ e.stopPropagation(); if (!dragRef.current.isDragging) onRemoveWater(); }} style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '50%', zIndex: 10, cursor: 'pointer' }} />
{/* Updated Text with 'cups' */}
{stats.dailyWater}/{stats.waterGoal}
cups
{/* PIE CHART ROW - Tighter vertical gap (8px) and vertical labels */}
{Math.round(pPct)}% Protein {Math.round(cPct)}% Carbs {Math.round(fPct)}% Fat
{/* Macro Bars */}
{[ { label: 'Protein', current: stats.proteinTotal, goal: stats.proteinGoal, color: '#60a5fa' }, { label: 'Carbs', current: stats.carbsTotal, goal: stats.carbsGoal, color: '#fb923c' }, { label: 'Fat', current: stats.fatTotal, goal: stats.fatGoal, color: '#facc15' } ].map((macro, i) => (
{macro.label}
{macro.current}/{macro.goal}g
))}
); }; export default function App() { // --- STATE: USER PROFILE --- const [user, setUser] = useState(() => { const saved = localStorage.getItem('my_profile'); const loaded = saved ? JSON.parse(saved) : null; if (loaded) { // Ensure activityHoursPerWeek defaults to 5 if not present if (loaded.activityHoursPerWeek === undefined) loaded.activityHoursPerWeek = 5; // Ensure trainingDifficulty defaults to 'Intermediate' if not present (for backward compatibility) if (!loaded.trainingDifficulty) loaded.trainingDifficulty = 'Intermediate'; } return loaded; }); // --- AUTH USER (Firebase) --- const [authUser, setAuthUser] = useState(() => getCurrentUser()); const [authLoading, setAuthLoading] = useState(false); const handleGoogleLogin = async () => { setAuthLoading(true); try { const user = await signInWithGoogle(); setAuthUser(user); } catch (err) { alert('Login failed: ' + err.message); } setAuthLoading(false); }; const handleLogout = async () => { await signOut(); setAuthUser(null); }; // --- STATE: DATA --- const [workouts, setWorkouts] = useState(() => { const saved = localStorage.getItem('my_workouts'); return saved ? JSON.parse(saved) : []; }); const [meals, setMeals] = useState(() => { const saved = localStorage.getItem('my_meals'); return saved ? JSON.parse(saved) : []; }); const [water, setWater] = useState(() => { const saved = localStorage.getItem('my_water'); if (saved) { const parsed = JSON.parse(saved); if (Array.isArray(parsed)) return parsed; if (parsed.date) return [parsed]; } return []; }); // --- STATE: HIDDEN HISTORY --- const [hiddenHistory, setHiddenHistory] = useState(() => { const saved = localStorage.getItem('my_hidden_history'); return saved ? JSON.parse(saved) : []; }); // --- STATE: UI & FORMS --- const [selectedDate, setSelectedDate] = useState(new Date().toLocaleDateString()); const [modal, setModal] = useState(null); // Default workout tab is now 'plan' const [workoutTab, setWorkoutTab] = useState('plan'); const [foodTab, setFoodTab] = useState('plan'); const [searchHistory, setSearchHistory] = useState(''); const [planTab, setPlanTab] = useState('Push'); const [selectedRoutineId, setSelectedRoutineId] = useState('train-right'); const [showRoutinePicker, setShowRoutinePicker] = useState(false); const [exerciseSearchResults, setExerciseSearchResults] = useState([]); const [exerciseSearching, setExerciseSearching] = useState(false); const [showDoctorsOrders, setShowDoctorsOrders] = useState(false); // NEW: Profile Modal State const [showProfileMenu, setShowProfileMenu] = useState(false); const [customCalInput, setCustomCalInput] = useState(null); const [showCallWebView, setShowCallWebView] = useState(false); // Health Sync State const [healthSyncAvailable, setHealthSyncAvailable] = useState(false); const [healthSyncSettings, setHealthSyncSettings] = useState(getHealthSyncSettings()); const [healthSyncLoading, setHealthSyncLoading] = useState(false); const [healthData, setHealthData] = useState(null); // NEW: Workout Mode State (Strength vs Cardio) const [workoutMode, setWorkoutMode] = useState('Strength'); // trainingDifficulty removed from here, now in user object const [activePlanExercises, setActivePlanExercises] = useState([]); const [aiLoading, setAiLoading] = useState(false); const [regeneratingIndex, setRegeneratingIndex] = useState(-1); // Workout Analysis State const [analysisVideo, setAnalysisVideo] = useState(null); const [analysisDescription, setAnalysisDescription] = useState(''); const [analysisResult, setAnalysisResult] = useState(null); const [analysisLoading, setAnalysisLoading] = useState(false); const videoInputRef = useRef(null); // Load meal plan from storage const [generatedMealPlan, setGeneratedMealPlan] = useState(() => { const saved = localStorage.getItem('my_ai_plan'); return saved ? JSON.parse(saved) : null; }); const [onboardForm, setOnboardForm] = useState({ name: '', age: '', weight: '', feet: '', inches: '', goal: 'lose' }); // UPDATED: Defaults set to 3 sets / 6 reps const [wForm, setWForm] = useState({ exercise: '', weight: '', sets: '3', reps: '6', cals: '', time: '', effort: 'High' }); const [mForm, setMForm] = useState({ item: '', cals: '', protein: '', carbs: '', fat: '', servings: 1 }); const fileInputRef = useRef(null); const scrollRef = useRef(null); // --- EFFECT: PERSIST DATA --- useEffect(() => { localStorage.setItem('my_profile', JSON.stringify(user)); }, [user]); useEffect(() => { localStorage.setItem('my_workouts', JSON.stringify(workouts)); }, [workouts]); useEffect(() => { localStorage.setItem('my_meals', JSON.stringify(meals)); }, [meals]); useEffect(() => { localStorage.setItem('my_water', JSON.stringify(water)); }, [water]); useEffect(() => { localStorage.setItem('my_hidden_history', JSON.stringify(hiddenHistory)); }, [hiddenHistory]); useEffect(() => { localStorage.setItem('my_ai_plan', JSON.stringify(generatedMealPlan)); }, [generatedMealPlan]); // --- EFFECT: HEALTH SYNC INITIALIZATION --- useEffect(() => { const initHealthSync = async () => { const available = await isHealthKitAvailable(); setHealthSyncAvailable(available); if (available && healthSyncSettings.enabled) { // Sync inline — can't call syncHealthDataNow() here because // healthSyncAvailable state hasn't re-rendered yet setHealthSyncLoading(true); try { const dataByDate = {}; const promises = []; for (let i = 0; i < 7; i++) { const d = new Date(); d.setDate(d.getDate() - i); const dateStr = d.toLocaleDateString(); promises.push( getHealthDataForDate(d) .then(data => { dataByDate[dateStr] = data; }) ); } await Promise.all(promises); setHealthData(dataByDate); setHealthSyncSettings(prev => { const updated = { ...prev, lastSync: new Date().toISOString() }; saveHealthSyncSettings(updated); return updated; }); } catch (e) { console.error('Health auto-sync error:', e); } setHealthSyncLoading(false); } }; initHealthSync(); }, []); // --- EFFECT: LOAD PLAN --- useEffect(() => { const routine = WORKOUT_ROUTINES.find(r => r.id === selectedRoutineId) || WORKOUT_ROUTINES[0]; if (routine.hasSplits) { const activityHours = user?.activityHoursPerWeek || 0; const difficulty = getTrainingDifficulty(activityHours); const splits = routine.splits[difficulty] || routine.splits['Intermediate']; if (splits && splits[planTab]) { setActivePlanExercises(splits[planTab]); } else { setActivePlanExercises(splits?.Push || []); } } else { setActivePlanExercises(routine.exercises || []); } setExerciseSearchResults([]); }, [planTab, user, selectedRoutineId]); // --- EFFECT: SCROLL TO END (TODAY) --- useLayoutEffect(() => { if (scrollRef.current) { scrollRef.current.scrollLeft = scrollRef.current.scrollWidth; setTimeout(() => { if(scrollRef.current) { scrollRef.current.scrollLeft = scrollRef.current.scrollWidth; } }, 100); } }, []); // --- LOGIC: DATE & HISTORY --- const getItemsForDate = (list, dateStr) => { return list.filter(item => item.date === dateStr); }; const getWaterForDate = (dateStr) => { const entry = water.find(w => w.date === dateStr); return entry ? entry.count : 0; }; const getPastDays = () => { const days = []; for (let i = 0; i < 14; i++) { const d = new Date(); d.setDate(d.getDate() - i); days.push(d); } return days.reverse(); }; // --- LOGIC: HELPER FUNCTIONS --- // Calculate TDEE multiplier based on activity hours per week (Katch-McArdle) // Linear interpolation for smooth, responsive changes const getTDEEMultiplier = (activityHours) => { if (activityHours === 0) return 1.15; // Sedentary if (activityHours <= 3) { // Linear from 1.2 to 1.35 over 1-3 hours return 1.2 + ((activityHours - 1) / 2) * 0.15; } if (activityHours <= 6) { // Linear from 1.4 to 1.55 over 4-6 hours return 1.4 + ((activityHours - 4) / 2) * 0.15; } if (activityHours <= 9) { // Linear from 1.6 to 1.75 over 7-9 hours return 1.6 + ((activityHours - 7) / 2) * 0.15; } // Linear from 1.8 to 1.95 for 10-15 hours, capped at 1.95 return Math.min(1.95, 1.8 + ((activityHours - 10) / 5) * 0.15); }; // Map activity hours to training difficulty for backward compatibility const getTrainingDifficulty = (activityHours) => { if (activityHours <= 3) return 'Beginner'; if (activityHours <= 6) return 'Intermediate'; return 'Advanced'; }; // --- HEALTH SYNC FUNCTIONS --- const syncHealthDataNow = async () => { if (!healthSyncAvailable) return; setHealthSyncLoading(true); try { // Fetch last 7 days of health data const dataByDate = {}; const promises = []; for (let i = 0; i < 7; i++) { const d = new Date(); d.setDate(d.getDate() - i); const dateStr = d.toLocaleDateString(); promises.push( getHealthDataForDate(d) .then(data => { dataByDate[dateStr] = data; }) ); } await Promise.all(promises); setHealthData(dataByDate); // Use functional update to avoid stale closure overwriting enabled flag setHealthSyncSettings(prev => { const updated = { ...prev, lastSync: new Date().toISOString() }; saveHealthSyncSettings(updated); return updated; }); } catch (e) { console.error('Health sync error:', e); } setHealthSyncLoading(false); }; const enableHealthSync = async () => { if (healthSyncLoading) return; // Prevent double-tap setHealthSyncLoading(true); try { const authorized = await requestHealthKitAuthorization(); if (authorized) { setHealthSyncSettings(prev => { const updated = { ...prev, enabled: true }; saveHealthSyncSettings(updated); return updated; }); await syncHealthDataNow(); } else { alert('Health permissions were not granted. Please enable them in Settings > Privacy > Health.'); } } catch (e) { console.error('Failed to enable health sync:', e); alert('Failed to connect to Apple Health. Please try again.'); } setHealthSyncLoading(false); }; const disableHealthSync = () => { setHealthSyncSettings(prev => { const updated = { ...prev, enabled: false }; saveHealthSyncSettings(updated); return updated; }); setHealthData(null); }; // --- LOGIC: CALCULATIONS --- const calculateStats = () => { if (!user) return { baseGoal: 2000, adjustedGoal: 2000, food: 0, exercise: 0, remaining: 2000, waterGoal: 8, proteinTotal: 0, carbsTotal: 0, fatTotal: 0, proteinGoal: 0, carbsGoal: 0, fatGoal: 0, dietLabel: "Maintenance", dailyWater: 0, bmi: 0, dailyTotalBurned: 0, tdeeActivityBudget: 0, activityBudgetFilled: 0, bonusWork: 0, sedentaryBurn: 0, dailyDeficit: 0, weeklyPounds: 0, hasHealthCalories: false }; const totalInches = (parseInt(user.feet) * 12) + parseInt(user.inches); const bmi = totalInches > 0 ? ((parseInt(user.weight) / (totalInches * totalInches)) * 703).toFixed(1) : 0; // Calculate BMR and TDEE using activity-based multiplier const bmr = Math.round(parseInt(user.weight) * 10); // Simplified BMR approximation const activityHours = user.activityHoursPerWeek || 0; const activityMultiplier = getTDEEMultiplier(activityHours); const sedentaryBurn = Math.round(bmr * 1.15); // True sedentary TDEE (no formal exercise) const expectedActivityTDEE = Math.round(bmr * activityMultiplier); // TDEE with user's activity level const tdeeActivityBudget = expectedActivityTDEE - sedentaryBurn; // Expected activity calories let baseGoal; if (user.customCalories) { baseGoal = parseInt(user.customCalories); } else { const maintenance = Math.round(parseInt(user.weight) * 15); baseGoal = maintenance; if (user.goal === 'lose' || user.plan === 'lose') baseGoal -= 500; if (user.goal === 'gain' || user.plan === 'gain') baseGoal += 300; } let proteinRatio = 0.3, carbRatio = 0.45, fatRatio = 0.25, dietLabel = "Maintenance"; if (user.goal === 'lose' || user.plan === 'lose') { proteinRatio = 0.40; carbRatio = 0.40; fatRatio = 0.20; dietLabel = "Cutting (40/40/20)"; } else if (user.goal === 'gain' || user.plan === 'gain') { proteinRatio = 0.25; carbRatio = 0.55; fatRatio = 0.20; dietLabel = "Bulking (25/55/20)"; } const todaysMeals = getItemsForDate(meals, selectedDate); const todaysWorkouts = getItemsForDate(workouts, selectedDate); const dailyWater = getWaterForDate(selectedDate); const waterGoal = Math.ceil((parseInt(user.weight) * 0.5) / 8); const foodTotal = todaysMeals.reduce((sum, m) => sum + Number(m.cals || 0), 0); const proteinTotal = todaysMeals.reduce((sum, m) => sum + Number(m.protein || 0), 0); const carbsTotal = todaysMeals.reduce((sum, m) => sum + Number(m.carbs || 0), 0); const fatTotal = todaysMeals.reduce((sum, m) => sum + Number(m.fat || 0), 0); const exerciseTotal = todaysWorkouts.reduce((sum, w) => sum + Number(w.cals || 0), 0); // Use Apple Health active calories when available for any synced date const healthDayData = healthData && healthSyncSettings.enabled ? healthData[selectedDate] : null; const healthActiveCalories = healthDayData ? (healthDayData.activeCalories || 0) : 0; const hasHealthCalories = healthActiveCalories > 0; const realActivityCalories = hasHealthCalories ? healthActiveCalories : exerciseTotal; const activityBudgetFilled = Math.min(tdeeActivityBudget, realActivityCalories); const bonusWork = Math.max(0, realActivityCalories - tdeeActivityBudget); // Daily burn: if Apple Health connected, use real calories; otherwise use TDEE activity estimate const dailyTotalBurned = sedentaryBurn + (hasHealthCalories ? realActivityCalories : Math.max(tdeeActivityBudget, realActivityCalories)); const adjustedGoal = baseGoal; const remaining = adjustedGoal - foodTotal; const proteinGoal = Math.round((adjustedGoal * proteinRatio) / 4); const carbsGoal = Math.round((adjustedGoal * carbRatio) / 4); const fatGoal = Math.round((adjustedGoal * fatRatio) / 9); const dailyDeficit = dailyTotalBurned - adjustedGoal; const weeklyPounds = (dailyDeficit / 500).toFixed(1); return { bmi, dailyTotalBurned, sedentaryBurn, adjustedGoal: baseGoal, tdeeActivityBudget, activityBudgetFilled, bonusWork, hasHealthCalories, healthDayData, realActivityCalories, food: foodTotal, remaining, waterGoal, dailyWater, proteinTotal, carbsTotal, fatTotal, proteinGoal, carbsGoal, fatGoal, dietLabel, dailyDeficit, weeklyPounds }; }; const stats = calculateStats(); // --- AI LOGIC: GENERATORS --- const handleAILookup = async () => { if (!GEMINI_API_KEY) return alert("API Key missing! Add VITE_GEMINI_API_KEY to HF Secrets."); if (!mForm.item) return alert("Please type a food name first!"); setAiLoading(true); try { const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); const prompt = `Estimate the nutrition facts for 1 standard serving of: "${mForm.item}". Return ONLY a JSON object (no markdown) with these keys: cals, protein, carbs, fat. All values should be numbers (integers).`; const result = await model.generateContent(prompt); const response = await result.response; const text = response.text(); const jsonStr = text.replace(/```json|```/g, '').trim(); const data = JSON.parse(jsonStr); // Update state with base values stored setMForm(prev => ({ ...prev, ...data, baseCals: data.cals, baseProtein: data.protein, baseCarbs: data.carbs, baseFat: data.fat })); setAiLoading(false); return data; } catch (error) { console.error("AI Error:", error); alert("AI Error. Check console."); setAiLoading(false); return null; } }; // --- NEW: REGENERATE SINGLE EXERCISE --- const handleRegenerateExercise = async (index, exercise) => { if (!GEMINI_API_KEY) return alert("API Key missing!"); setRegeneratingIndex(index); try { const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); const activityHours = user?.activityHoursPerWeek || 0; const difficulty = getTrainingDifficulty(activityHours); const prompt = `Suggest ONE alternative ${difficulty} level exercise to replace "${exercise.name}" for a ${planTab} workout. Return ONLY a JSON object with keys: name, sets, reps, notes.`; const result = await model.generateContent(prompt); const response = await result.response; const text = response.text(); const jsonStr = text.replace(/```json|```/g, '').trim(); const newData = JSON.parse(jsonStr); const newPlan = [...activePlanExercises]; newPlan[index] = newData; setActivePlanExercises(newPlan); } catch (error) { console.error("Regen Error", error); alert("Failed to swap exercise."); } setRegeneratingIndex(-1); }; const removeExerciseFromPlan = (index) => { const newPlan = [...activePlanExercises]; newPlan.splice(index, 1); setActivePlanExercises(newPlan); }; const handleGenerateMealPlan = async () => { if (!GEMINI_API_KEY) return alert("API Key missing!"); setAiLoading(true); try { const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); const prompt = `Create a 1-day meal plan for a person with these targets: ${stats.adjustedGoal} Calories, ${stats.proteinGoal}g Protein, ${stats.carbsGoal}g Carbs, ${stats.fatGoal}g Fat. CRITICAL RULES: 1. Keep it "EASY AND SIMPLE". Use convenience foods. 2. NO complex cooking instructions (e.g. "8 scrambled egg whites"). 3. YES to: "2 Starbucks Egg Bites", "1 Cup Liquid Egg Whites", "Rotisserie Chicken", "Pre-cooked Rice", "Greek Yogurt". 4. "Item" field must be the simple food name + quantity (e.g. "2 Hard Boiled Eggs"). Return ONLY a JSON array of 4 objects (Breakfast, Lunch, Dinner, Snack). Each object must have: label, item, cals, protein, carbs, fat.`; const result = await model.generateContent(prompt); const response = await result.response; const text = response.text(); const jsonStr = text.replace(/```json|```/g, '').trim(); const data = JSON.parse(jsonStr); setGeneratedMealPlan(data); } catch (error) { console.error("Meal Plan Error", error); alert("Could not generate plan."); } setAiLoading(false); }; const handleRegenerateSingleMeal = async (index, currentMeal) => { if (!GEMINI_API_KEY) return alert("API Key missing!"); setRegeneratingIndex(index); try { const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); const prompt = `Suggest ONE alternative meal option for ${currentMeal.label} that replaces "${currentMeal.item}". Targets: ${currentMeal.cals} calories, ${currentMeal.protein}g protein, ${currentMeal.carbs}g carbs, ${currentMeal.fat}g fat. KEEP IT SIMPLE/CONVENIENCE FOOD (e.g. "Protein Shake", "Pre-made Wrap"). No cooking. IMPORTANT: The 'item' field must be a comma-separated list of ingredients. Return ONLY a JSON object with keys: label, item, cals, protein, carbs, fat.`; const result = await model.generateContent(prompt); const response = await result.response; const text = response.text(); const jsonStr = text.replace(/```json|```/g, '').trim(); const newData = JSON.parse(jsonStr); const newPlan = [...generatedMealPlan]; newPlan[index] = { ...newData, label: currentMeal.label }; setGeneratedMealPlan(newPlan); } catch (error) { console.error("Regen Error", error); alert("Failed to refresh meal."); } setRegeneratingIndex(-1); }; const handleWorkoutAILookup = async () => { // REMOVED: setAiLoading(true) and API call logic from here let targetExercise = wForm.exercise; let targetSets = wForm.sets; let targetReps = wForm.reps; let targetWeight = wForm.weight; if (!targetExercise) { const todaysLogs = getItemsForDate(workouts, selectedDate); // FIX: Use activePlanExercises instead of trainingPlan const dailyPlan = activePlanExercises; const nextUp = dailyPlan.find(ex => !todaysLogs.some(log => log.exercise === ex.name) ); if (nextUp) { targetExercise = nextUp.name; targetSets = nextUp.sets; targetReps = nextUp.reps; const history = getLastLog(nextUp.name); if (history) targetWeight = history.weight; // ONLY update the text fields, do not trigger AI yet setWForm(prev => ({ ...prev, exercise: targetExercise, sets: targetSets, reps: targetReps, weight: targetWeight, cals: '' // Keep cals empty so submit triggers the AI })); updateLastLog(targetExercise); } else { alert(`All ${planTab} exercises completed for today!`); } } // Note: We don't return AI data here anymore because we aren't calling the AI here }; const calculateWorkoutCalories = async (data) => { if (!GEMINI_API_KEY) return { cals: 0 }; setAiLoading(true); try { const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); const userWeight = user?.weight || 150; // ROBUST AI CALORIE CALCULATOR const prompt = `Estimate calories burned for a person weighing ${userWeight} lbs performing: "${data.exercise}". Details: ${data.sets ? `- ${data.sets} sets of ${data.reps} reps at ${data.weight} lbs` : ''} ${data.time ? `- ${data.time} minutes at ${data.effort} intensity` : ''} CRITICAL: Return ONLY a valid JSON object. No other text. Format: { "cals": 120 }`; const result = await model.generateContent(prompt); const response = await result.response; const text = response.text(); // Improved Parsing: Find the JSON object inside potential extra text const jsonMatch = text.match(/\{[\s\S]*\}/); if (jsonMatch) { const jsonStr = jsonMatch[0]; return JSON.parse(jsonStr); } else { throw new Error("No JSON found in response"); } } catch (error) { console.error("AI Error:", error); // FALLBACK FORMULA IF AI FAILS const MET = data.effort === 'High' ? 8 : 4; const duration = data.time ? parseInt(data.time) : (parseInt(data.sets) * 3); // Est 3 mins per set const weightKg = (user?.weight || 150) / 2.2; const fallbackCals = Math.round((MET * 3.5 * weightKg / 200) * duration); return { cals: fallbackCals }; } finally { setAiLoading(false); } }; const handleImageUpload = async (e) => { if (!GEMINI_API_KEY) return alert("API Key missing! Add VITE_GEMINI_API_KEY to HF Secrets."); const file = e.target.files[0]; if (!file) return; setAiLoading(true); try { const reader = new FileReader(); reader.readAsDataURL(file); reader.onloadend = async () => { const base64Data = reader.result.split(',')[1]; const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); const imagePart = { inlineData: { data: base64Data, mimeType: file.type }, }; const prompt = "Identify this food and estimate nutrition for 1 standard serving. Return ONLY a JSON object with keys: item (short name), cals, protein, carbs, fat."; const result = await model.generateContent([prompt, imagePart]); const response = await result.response; const text = response.text(); const jsonStr = text.replace(/```json|```/g, '').trim(); const data = JSON.parse(jsonStr); setMForm(data); setAiLoading(false); } } catch (error) { console.error("Image AI Error:", error); alert("Could not analyze image."); setAiLoading(false); } }; // --- WORKOUT VIDEO ANALYSIS --- const handleWorkoutAnalysis = async () => { if (!analysisVideo) return alert("Please upload a video first."); if (!analysisDescription.trim()) return alert("Please describe what sport or workout is being performed."); if (!GEMINI_API_KEY) return alert("API Key missing! Add VITE_GEMINI_API_KEY to HF Secrets."); setAnalysisLoading(true); setAnalysisResult(null); try { const reader = new FileReader(); reader.readAsDataURL(analysisVideo); reader.onloadend = async () => { try { const base64Data = reader.result.split(',')[1]; const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); const videoPart = { inlineData: { data: base64Data, mimeType: analysisVideo.type }, }; const prompt = `You are an expert sports and fitness coach. Analyze this workout/sports video. The user describes the activity as: "${analysisDescription}" Analyze the person's form, technique, and overall performance. Return ONLY a JSON object with these keys: { "score": , "summary": "<1-2 sentence overall assessment>", "strengths": ["", "", ""], "improvements": ["", "", ""], "tips": "" } SCORING GUIDE: 1 = Needs significant work on fundamentals 2 = Below average, several form issues 3 = Average, decent form with room to improve 4 = Good form, minor adjustments needed 5 = Excellent technique and execution Be specific and reference what you actually see in the video. Focus on form, technique, range of motion, tempo, and safety.`; const result = await model.generateContent([prompt, videoPart]); const response = await result.response; const text = response.text(); const jsonStr = text.replace(/```json|```/g, '').trim(); const data = JSON.parse(jsonStr); setAnalysisResult(data); } catch (err) { console.error("Gemini analysis error:", err); alert("Analysis failed. The video may be too large — try a shorter clip (under 30 seconds)."); } setAnalysisLoading(false); }; } catch (error) { console.error("Video read error:", error); alert("Could not read video file."); setAnalysisLoading(false); } }; // --- HELPER FOR UPDATING SERVINGS --- const updateServings = (delta) => { setMForm(prev => { const newServings = Math.max(0.5, prev.servings + delta); if (newServings === prev.servings) return prev; // Determine base values (per 1 serving) // If we haven't stored base values yet, derive them from current visible values const bCals = prev.baseCals ?? (Number(prev.cals) / prev.servings); const bP = prev.baseProtein ?? (Number(prev.protein) / prev.servings); const bC = prev.baseCarbs ?? (Number(prev.carbs) / prev.servings); const bF = prev.baseFat ?? (Number(prev.fat) / prev.servings); return { ...prev, servings: newServings, baseCals: bCals, baseProtein: bP, baseCarbs: bC, baseFat: bF, cals: Math.round(bCals * newServings) || '', protein: Math.round(bP * newServings) || '', carbs: Math.round(bC * newServings) || '', fat: Math.round(bF * newServings) || '' }; }); }; // --- ACTIONS --- const handleOnboardSubmit = (e) => { e.preventDefault(); if(!onboardForm.name || !onboardForm.weight) return; setUser(onboardForm); }; // NEW: Profile Actions const handleSaveCalories = () => { const newCals = parseInt(customCalInput); if(newCals > 500 && newCals < 10000) { setUser({...user, customCalories: newCals}); setCustomCalInput(null); alert("Base calorie goal updated!"); } else { alert("Please enter a valid calorie number."); } }; const handleResetCalories = () => { const { customCalories, ...rest } = user; setUser(rest); alert("Reverted to automatic calculation based on weight."); }; // 2. UPDATE ADD FUNCTION (Include 'sets') const addWorkout = async (e) => { e.preventDefault(); if (!wForm.exercise) return; let finalCals = wForm.cals; // 1. Trigger AI calculation if calories are missing if (finalCals === '' || finalCals === 0) { const aiResult = await calculateWorkoutCalories(wForm); finalCals = aiResult.cals; } // 2. Prepare the workout object const workoutEntry = { id: wForm.id || Date.now(), // Use existing ID if editing date: selectedDate, exercise: wForm.exercise, weight: wForm.weight, sets: wForm.sets, reps: wForm.reps, cals: Number(finalCals || 0), time: wForm.time, effort: wForm.effort, type: workoutMode // Saving the mode (Strength/Cardio) }; // 3. Update or Add if (wForm.id) { setWorkouts(workouts.map(w => w.id === wForm.id ? workoutEntry : w)); } else { setWorkouts([workoutEntry, ...workouts]); } // 4. Reset form and close modal setWForm({ exercise: '', weight: '', sets: '3', reps: '6', cals: '', time: '', effort: 'High' }); setLastLog(null); setModal(null); }; // --- HELPER: GET LAST WORKOUT STATS --- const getLastLog = (exerciseName) => { if (!exerciseName) return null; // Filter history for this exercise, sort by newest (descending ID) const history = workouts .filter(w => w.exercise && w.exercise.toLowerCase().trim() === exerciseName.toLowerCase().trim()) .sort((a, b) => b.id - a.id); return history.length > 0 ? history[0] : null; }; // derived state for the UI — only update on blur to prevent layout jitter while typing const [lastLog, setLastLog] = useState(null); // Update lastLog when exercise name is finalized (blur or programmatic set) const updateLastLog = (name) => setLastLog(getLastLog(name)); // 3. UPDATE LOG FROM PLAN (Accept full exercise object) const logFromPlan = (ex) => { setWForm({ exercise: ex.name, weight: '', sets: ex.sets, // Auto-fills sets reps: ex.reps, // Auto-fills reps cals: '' }); updateLastLog(ex.name); setWorkoutTab('log'); }; // LOG ALL: Add every exercise in the current plan to today's workouts const logAllFromPlan = async () => { if (activePlanExercises.length === 0) return; setAiLoading(true); const entries = activePlanExercises.map((ex, idx) => ({ id: Date.now() + idx, date: selectedDate, exercise: ex.name, weight: '', sets: ex.sets, reps: ex.reps, cals: 0, time: '', effort: 'High', type: 'Strength' })); setWorkouts([...entries, ...workouts]); setAiLoading(false); setModal(null); }; const logMealFromPlan = (meal) => { setMForm({ item: meal.item, cals: meal.cals, protein: meal.protein, carbs: meal.carbs, fat: meal.fat }); setFoodTab('log'); }; const editItem = (item) => { if (item.exercise) { // Fill Workout Form setWForm({ id: item.id, // Store ID so we know which one to update exercise: item.exercise, weight: item.weight, sets: item.sets, reps: item.reps, cals: item.cals }); setModal('workout'); setWorkoutTab('log'); } else { // Fill Food Form setMForm({ id: item.id, // Store ID so we know which one to update item: item.item, cals: item.cals, protein: item.protein, carbs: item.carbs, fat: item.fat, servings: item.servings || 1 }); setModal('food'); setFoodTab('log'); } }; const addMeal = async (e) => { e.preventDefault(); if (!mForm.item) return; let finalData = { ...mForm }; const servingMultiplier = parseFloat(mForm.servings) || 1; // 1. If calories are empty, fetch from AI first if (mForm.cals === '') { const aiData = await handleAILookup(); if (!aiData) return; // Note: aiData is per 1 serving. Since we are auto-fetching right before add, // we must scale it here. If cals were not empty, they would already be scaled in UI. finalData = { ...finalData, ...aiData, cals: Math.round(aiData.cals * servingMultiplier), protein: Math.round(aiData.protein * servingMultiplier), carbs: Math.round(aiData.carbs * servingMultiplier), fat: Math.round(aiData.fat * servingMultiplier) }; } // 2. Prepare the meal object with scaled values // UPDATE: We now assume 'finalData' (from mForm) ALREADY contains the scaled totals // (unless we just fetched it above). So we remove the '* servingMultiplier' here. const mealEntry = { id: mForm.id || Date.now(), // Use existing ID if editing date: selectedDate, item: servingMultiplier > 1 ? `${finalData.item} (x${servingMultiplier})` : finalData.item, cals: Math.round(Number(finalData.cals || 0)), protein: Math.round(Number(finalData.protein || 0)), carbs: Math.round(Number(finalData.carbs || 0)), fat: Math.round(Number(finalData.fat || 0)), servings: servingMultiplier }; // 3. Update or Add if (mForm.id) { setMeals(meals.map(m => m.id === mForm.id ? mealEntry : m)); } else { setMeals([mealEntry, ...meals]); } // 4. Reset form and close modal setMForm({ item: '', cals: '', protein: '', carbs: '', fat: '', servings: 1 }); setModal(null); }; // Add meal from history directly to today const addMealFromHistory = (meal) => { const mealEntry = { id: Date.now(), date: selectedDate, item: meal.item, cals: Math.round(Number(meal.cals || 0)), protein: Math.round(Number(meal.protein || 0)), carbs: Math.round(Number(meal.carbs || 0)), fat: Math.round(Number(meal.fat || 0)), servings: meal.servings || 1 }; setMeals([mealEntry, ...meals]); }; // Add all meals from a date in history const addAllMealsFromHistory = (mealsToAdd) => { const newEntries = mealsToAdd.map((meal, idx) => ({ id: Date.now() + idx, date: selectedDate, item: meal.item, cals: Math.round(Number(meal.cals || 0)), protein: Math.round(Number(meal.protein || 0)), carbs: Math.round(Number(meal.carbs || 0)), fat: Math.round(Number(meal.fat || 0)), servings: meal.servings || 1 })); setMeals([...newEntries, ...meals]); }; const addWorkoutFromHistory = (workout) => { const workoutEntry = { id: Date.now(), date: selectedDate, exercise: workout.exercise, type: workout.type, sets: workout.sets, reps: workout.reps, weight: workout.weight, time: workout.time, effort: workout.effort }; setWorkouts([workoutEntry, ...workouts]); }; // Add all workouts from a date in history const addAllWorkoutsFromHistory = (workoutsToAdd) => { const newEntries = workoutsToAdd.map((workout, idx) => ({ id: Date.now() + idx, date: selectedDate, exercise: workout.exercise, type: workout.type, sets: workout.sets, reps: workout.reps, weight: workout.weight, time: workout.time, effort: workout.effort })); setWorkouts([...newEntries, ...workouts]); }; const addWater = () => { const existingIndex = water.findIndex(w => w.date === selectedDate); if (existingIndex >= 0) { const newWater = [...water]; newWater[existingIndex].count += 1; setWater(newWater); } else { setWater([...water, { date: selectedDate, count: 1 }]); } }; const removeWater = () => { const existingIndex = water.findIndex(w => w.date === selectedDate); if (existingIndex >= 0) { const newWater = [...water]; if (newWater[existingIndex].count > 0) { newWater[existingIndex].count -= 1; setWater(newWater); } } }; const deleteItem = (id, type) => { if (type === 'workout') setWorkouts(workouts.filter(w => w.id !== id)); if (type === 'meal') setMeals(meals.filter(m => m.id !== id)); }; // --- NEW: DELETE HISTORY ITEM (HIDES FROM VIEW) --- const deleteHistoryItem = (itemName, type) => { if(confirm(`Hide "${itemName}" from your history list? This will NOT delete past logs.`)) { setHiddenHistory([...hiddenHistory, itemName]); } } const logout = () => { if(confirm("Are you sure you want to delete all profile data and logs?")) { setUser(null); setMeals([]); setWorkouts([]); setWater([]); setHiddenHistory([]); // Clear hidden history on logout setOnboardForm({ name: '', age: '', weight: '', feet: '', inches: '', goal: 'lose' }); setShowProfileMenu(false); } }; // --- UI HELPERS --- const TabButton = ({ active, label, onClick }) => ( ); // --- NEW COMPONENT: SINGLE SHOPPING LIST ROW --- const ShoppingListRow = ({ item, onLog, onDelete }) => { // REMOVE ANYTHING IN BRACKETS FOR SEARCH QUERY const cleanItem = item.replace(/\s*\(.*?\)\s*/g, '').trim(); const instacartUrl = `https://www.instacart.com/store/s?k=${encodeURIComponent(cleanItem)}`; return (
{item.trim()}
); }; // --- STYLES --- const container = { maxWidth: '480px', margin: '0 auto', fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", system-ui, sans-serif', padding: '16px', paddingBottom: '100px', minHeight: 'auto', background: '#f5f5f7', position: 'relative' }; const card = { background: 'white', borderRadius: '14px', padding: '14px', boxShadow: '0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.06)', marginBottom: '10px' }; const input = { width: '100%', padding: '12px', borderRadius: '10px', border: '1px solid #e5e7eb', marginBottom: '10px', fontSize: '16px', boxSizing: 'border-box' }; const btnPrimary = { width: '100%', padding: '14px', borderRadius: '12px', border: 'none', background: '#2563eb', color: 'white', fontSize: '16px', fontWeight: '600', cursor: 'pointer', display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '8px' }; const btnAI = { padding: '10px', borderRadius: '10px', border: 'none', background: '#eff6ff', color: '#2563eb', fontSize: '13px', fontWeight: '600', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '5px', whiteSpace: 'nowrap' }; // NEW: Add exercise to plan via AI search const [addExerciseInput, setAddExerciseInput] = useState(''); const [addingExercise, setAddingExercise] = useState(false); const handleSearchExercises = async () => { if (!addExerciseInput.trim()) return; if (!GEMINI_API_KEY) { // Fallback: just add it directly setActivePlanExercises([...activePlanExercises, { name: addExerciseInput, sets: '3', reps: '8-10', notes: '' }]); setAddExerciseInput(''); return; } setExerciseSearching(true); setExerciseSearchResults([]); try { const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); const difficulty = getTrainingDifficulty(user?.activityHoursPerWeek || 0); const prompt = `Suggest 4 exercises matching "${addExerciseInput}" for a ${difficulty} level trainee. Include variations and alternatives. Return ONLY a JSON array of objects with keys: name, sets, reps, notes. Keep notes to 1 short tip each.`; const result = await model.generateContent(prompt); const text = result.response.text().replace(/```json|```/g, '').trim(); const suggestions = JSON.parse(text); setExerciseSearchResults(Array.isArray(suggestions) ? suggestions : [suggestions]); } catch (e) { console.error('Exercise search error:', e); setActivePlanExercises([...activePlanExercises, { name: addExerciseInput, sets: '3', reps: '8-10', notes: '' }]); setAddExerciseInput(''); } setExerciseSearching(false); }; const addSearchResultToPlan = (exercise) => { setActivePlanExercises([...activePlanExercises, exercise]); setExerciseSearchResults([]); setAddExerciseInput(''); }; const bottomNav = { position: 'fixed', bottom: 0, left: '50%', transform: 'translateX(-50%)', width: '100%', maxWidth: '480px', background: 'rgba(255,255,255,0.92)', backdropFilter: 'blur(20px)', WebkitBackdropFilter: 'blur(20px)', padding: '8px 16px 20px', display: 'flex', justifyContent: 'space-around', borderTop: '1px solid rgba(0,0,0,0.06)', zIndex: 10 }; const navBtn = (isActive) => ({ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '3px', background: 'none', border: 'none', cursor: 'pointer', fontSize: '10px', fontWeight: '600', color: isActive ? '#2563eb' : '#8e8e93', transition: 'color 0.2s', letterSpacing: '0.2px' }); const navIconBox = (color, isActive) => ({ background: isActive ? color : '#e5e7eb', padding: '8px', borderRadius: '12px', color: 'white', transition: 'all 0.2s', transform: isActive ? 'scale(1.05)' : 'scale(1)' }); const modalOverlay = { position: 'fixed', top: 0, left: 0, right: 0, height: '100%', background: 'rgba(0,0,0,0.4)', backdropFilter: 'blur(4px)', WebkitBackdropFilter: 'blur(4px)', zIndex: 50, display: 'flex', flexDirection: 'column', alignItems: 'center', overflow: 'hidden' }; const modalContent = { background: 'white', width: '100%', maxWidth: '480px', borderRadius: '0', padding: '20px', paddingTop: '20px', animation: 'slideUp 0.3s cubic-bezier(0.32, 0.72, 0, 1)', flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }; // --- RENDER: ONBOARDING --- if (!user) { return setUser(data)} />; } // --- RENDER: MAIN APP --- return (
{/* HEADER: Profile Avatar + Welcome + Date Scroller */}
{/* Profile Avatar Button */} {/* Welcome Text - Updated to split name at space */}

Hello, {user.name.split(' ')[0]}

Today's Overview

{/* DATE SCROLLER (Pushed to Right) */}
{getPastDays().map((dateObj) => { const dateStr = dateObj.toLocaleDateString(); const isSelected = selectedDate === dateStr; const isToday = dateStr === new Date().toLocaleDateString(); const dayName = dateObj.toLocaleDateString('en-US', { weekday: 'short' }); const dayNum = dateObj.getDate(); return ( ) })}
{/* --- PROFILE MODAL --- */} {showProfileMenu && (
{ if(e.target === e.currentTarget) setShowProfileMenu(false) }}>
{user.name.charAt(0).toUpperCase()}

Profile Settings

{/* STATS HIGHLIGHTS - 3 COLUMN BREAKDOWN */}
{/* Column 1: BMI */}
Current BMI
{stats.bmi}
{/* Column 2: DAILY BURN BREAKDOWN */}
Daily Burn
{stats.dailyTotalBurned}
{stats.sedentaryBurn} Sedentary
{stats.hasHealthCalories ? stats.realActivityCalories : stats.tdeeActivityBudget} {stats.hasHealthCalories ? 'Active (Health)' : 'TDEE Activity'}
{/* Column 3: WEEKLY TRACK */}
Weekly Track
= 0 ? '#10b981' : '#ef4444'}}> {stats.dailyDeficit >= 0 ? `-${stats.weeklyPounds}` : `+${Math.abs(stats.weeklyPounds)}`} lbs
vs TDEE Avg
{/* UPDATE WEIGHT */}
setUser({...user, weight: e.target.value})} placeholder="Weight in lbs" />

Recalculates BMI and daily calorie targets automatically.

{/* CALORIE BUDGET */}
setCustomCalInput(e.target.value)} onFocus={(e) => { if(customCalInput === null) setCustomCalInput(e.target.value); }} />
{user.customCalories && ( )}
{/* ACTIVITY PER WEEK */}
setUser({...user, activityHoursPerWeek: e.target.value === '' ? '' : parseFloat(e.target.value)})} style={{ width: '100%', padding: '12px', borderRadius: '10px', border: '1px solid #e5e7eb', fontSize: '14px', fontWeight: '600', color: '#1f2937' }} placeholder="Enter hours (e.g., 5)" />
{/* APPLE HEALTH SYNC */} {healthSyncAvailable && (
{!healthSyncSettings.enabled ? ( ) : (
Connected
{healthData && healthData[new Date().toLocaleDateString()] && (() => { const todayHealth = healthData[new Date().toLocaleDateString()]; return ( <>
Today
{todayHealth.steps?.toLocaleString() || 0}
Steps
{todayHealth.activeCalories || 0}
Active Cal
{todayHealth.totalCalories || 0}
Total Burn
{todayHealth.distance || 0}
Miles
); })()} {healthSyncSettings.lastSync && (
Last synced: {new Date(healthSyncSettings.lastSync).toLocaleTimeString()}
)}
)}

Syncs steps, calories burned, and workouts from Apple Health for the last 7 days.

)}
)} {/* --- NEW GLASS MORPHISM DASHBOARD --- */}

{selectedDate === new Date().toLocaleDateString() ? "Today's Activity" : `Log for ${selectedDate}`}

{[...getItemsForDate(workouts, selectedDate), ...getItemsForDate(meals, selectedDate)].length === 0 && !stats.hasHealthCalories && (
No activity logged for this day. Tap below to log!
)} {/* Apple Health individual workout entries */} {stats.hasHealthCalories && stats.healthDayData && stats.healthDayData.workouts && stats.healthDayData.workouts.map((w, i) => { const typeLabel = (w.type || 'workout').replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase()).trim(); const duration = w.duration ? Math.round(w.duration / 60) : (w.startDate && w.endDate ? Math.round((new Date(w.endDate) - new Date(w.startDate)) / 60000) : null); const timeStr = w.startDate ? new Date(w.startDate).toLocaleTimeString([], {hour: 'numeric', minute: '2-digit'}) : ''; return (
{typeLabel} Health
{w.calories ? {Math.round(w.calories)} cal : null} {duration ? `${w.calories ? ' • ' : ''}${duration} min` : ''} {timeStr ? ` • ${timeStr}` : ''}
); })} {/* Apple Health steps/movement entry (non-workout activity) */} {stats.hasHealthCalories && stats.healthDayData && (stats.healthDayData.steps > 0 || stats.healthDayData.movementCalories > 0) && (
Steps & Movement Health
{(stats.healthDayData.steps || 0).toLocaleString()} steps {stats.healthDayData.movementCalories > 0 ? <> • {Math.round(stats.healthDayData.movementCalories)} cal : null} {stats.healthDayData.distance ? ` • ${stats.healthDayData.distance} mi` : ''}
)} {[...getItemsForDate(workouts, selectedDate), ...getItemsForDate(meals, selectedDate)] .sort((a,b) => b.id - a.id) .map(item => (
{/* This clickable div allows you to edit the item */}
editItem(item)} style={{display:'flex', gap:'12px', alignItems:'center', flex: 1, cursor: 'pointer'}} >
{item.exercise ? : }
{item.exercise || item.item}
{item.exercise ? (item.type === 'Cardio' || item.time ? ( {item.cals ? {item.cals} cal : '0 cal'} • {item.time} mins ({item.effort}) ) : ( {item.cals ? {item.cals} cal : '0 cal'} • {item.weight}lbs × {item.reps} )) : {item.cals} cal (P:{item.protein || 0} C:{item.carbs || 0} F:{item.fat || 0}) }
))}
{modal && (
{ if(e.target === e.currentTarget) setModal(null) }}>

{modal === 'food' ? 'Food & Diet' : 'Workout & Training'}

{/* MODAL TABS */}
{modal === 'workout' ? ( <> setWorkoutTab('plan')} /> setWorkoutTab('log')} /> setWorkoutTab('history')} /> setWorkoutTab('analyze')} /> ) : ( <> {/* UPDATED TAB ORDER: PLAN, LOG, HISTORY */} setFoodTab('plan')} /> setFoodTab('log')} /> setFoodTab('history')} /> )}
{/* --- CONTENT AREA (SCROLLABLE) --- */}
{/* --- FOOD MODAL CONTENT --- */} {modal === 'food' && foodTab === 'log' && (
{/* NUTRITION LABEL CONTAINER */}
{/* HEADER: TITLE INPUT + ACTIONS */}
setMForm({...mForm, item: e.target.value})} style={{ fontSize: '28px', fontWeight: '900', border: 'none', outline: 'none', width: '100%', padding: 0, margin: 0, color: 'black' }} />
{/* ACTION BUTTONS (SCAN ONLY) */}
{/* THICK BAR 1 */}
{/* CALORIES ROW */}
Calories setMForm({...mForm, cals: e.target.value})} style={{ fontSize: '32px', fontWeight: '900', textAlign: 'right', border: 'none', borderBottom: '2px solid black', width: '120px', outline: 'none', padding: 0 }} />
{/* MEDIUM BAR */}
{/* MACRO ROWS */} {[ { label: 'Total Fat', key: 'fat', unit: 'g' }, { label: 'Total Carbohydrate', key: 'carbs', unit: 'g' }, { label: 'Protein', key: 'protein', unit: 'g' } ].map((macro, i) => (
{macro.label}
setMForm({...mForm, [macro.key]: e.target.value})} style={{ fontSize: '16px', fontWeight: 'bold', textAlign: 'right', border: 'none', background: '#f3f4f6', padding: '4px', borderRadius: '4px', width: '60px' }} /> {macro.unit}
))}
* Percent Daily Values are based on your {stats.adjustedGoal} calorie diet.
{/* Update Log Button */} {/* Servings Control (Bottom Right) */}
{/* +/- Buttons Stacked */}
{/* Number Box (Same height as Add Log button) */}
{mForm.servings}x
)} {modal === 'food' && foodTab === 'plan' && (
{/* --- DIET SECTION (Top) --- */}

{stats.dietLabel}

{/* POPOVER */} {showDoctorsOrders && (

Doctor's Orders

  • Pre-Workout: 30-40g Protein + 30-40g Carbs (30-60 mins before).
  • Post-Workout: 30-40g Protein + High Carbs (within 2 hours).
  • Hydration: Drink 1:1 ratio of Water to Electrolytes if sweating heavily.
  • Focus: Unprocessed foods (80% rule). Mediterranean sources preferred.
)}
Calories
{stats.adjustedGoal}
Protein (g)
{stats.proteinGoal}
Carbs (g)
{stats.carbsGoal}
Fat (g)
{stats.fatGoal}
{/* --- MEAL PLAN SECTION (Bottom) --- */}

Based on {stats.adjustedGoal} cal target.

{generatedMealPlan && (
{generatedMealPlan.map((planItem, idx) => (
{regeneratingIndex === idx && (
)}
{planItem.label}
{planItem.cals} cal | P:{planItem.protein} C:{planItem.carbs} F:{planItem.fat}
{planItem.item.split(',').map((foodStr, i) => ( { setMForm({ item: foodStr.trim(), cals: '', protein: '', carbs: '', fat: '', servings: 1 }); setFoodTab('log'); }} onDelete={() => { const newPlan = [...generatedMealPlan]; const ingredients = newPlan[idx].item.split(',').map(s => s.trim()); ingredients.splice(i, 1); newPlan[idx].item = ingredients.join(', '); setGeneratedMealPlan(newPlan); }} /> ))}
))}
)}
)} {/* --- NEW FOOD HISTORY SECTION - GROUPED BY DAY --- */} {modal === 'food' && foodTab === 'history' && (
setSearchHistory(e.target.value)} />
{(() => { // Filter meals const filteredMeals = meals .filter(m => m && m.item) .filter(m => !hiddenHistory.includes(m.item)) .filter(m => searchHistory === '' || m.item.toLowerCase().includes(searchHistory.toLowerCase())); // Group by date const mealsByDate = filteredMeals.reduce((acc, meal) => { if (!acc[meal.date]) acc[meal.date] = []; acc[meal.date].push(meal); return acc; }, {}); // Sort dates descending (newest first) - parse as actual dates const sortedDates = Object.keys(mealsByDate).sort((a, b) => { const dateA = new Date(a); const dateB = new Date(b); return dateB - dateA; // descending }); if (sortedDates.length === 0) { return
{searchHistory ? 'No matching meals found' : 'No food history yet'}
; } return sortedDates.map((date, dateIdx) => { const mealsForDate = mealsByDate[date]; return (
{/* Date Header */}
{date === selectedDate ? 'Today' : date}
{/* ALL MEALS - NO DEDUPLICATION */}
{mealsForDate.map((m, i) => (
addMealFromHistory(m)} style={{ padding: '14px 12px', cursor: 'pointer', borderBottom: i < mealsForDate.length - 1 ? '1px solid #e5e7eb' : 'none', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: 'white', transition: 'background 0.15s', minHeight: '68px' }} onMouseEnter={(e) => e.currentTarget.style.background = '#f9fafb'} onMouseLeave={(e) => e.currentTarget.style.background = 'white'} >
{m.item}
{m.cals || 0} cal • P:{m.protein || 0} C:{m.carbs || 0} F:{m.fat || 0}
))}
); }); })()}
)} {/* --- WORKOUT MODAL CONTENT --- */} {modal === 'workout' && workoutTab === 'log' && (
{/* EXERCISE NAME */}
setWForm({...wForm, exercise: e.target.value})} onBlur={e => updateLastLog(e.target.value)} />
{/* SMART TARGET CARD (TONAL STYLE) */} {lastLog && lastLog.weight && lastLog.sets && lastLog.reps && (
Last Session ({lastLog.date})
{lastLog.weight}lbs
{lastLog.sets} sets × {lastLog.reps} reps
BEAT IT
{parseInt(lastLog.weight) + 5}lbs
)} {/* --- UNIFIED DETAILS SECTION --- */}
{/* Header Toggle */}
{/* Inputs */} {workoutMode === 'Strength' ? (
setWForm({...wForm, weight: e.target.value})} />
setWForm({...wForm, sets: e.target.value})} />
setWForm({...wForm, reps: e.target.value})} />
) : (
setWForm({...wForm, time: e.target.value})} />
)}
)} {modal === 'workout' && workoutTab === 'plan' && (
{/* --- ROUTINE SELECTOR --- */}
{/* Routine Picker Dropdown - Overlay */} {showRoutinePicker && (
{WORKOUT_ROUTINES.map(routine => ( ))}
)}
{/* Split pills for Train Right only */} {selectedRoutineId === 'train-right' && (
{['Push', 'Pull', 'Legs', 'Cardio'].map(split => ( ))}
)} {/* Header + Log All */}

{selectedRoutineId === 'train-right' ? `${planTab} Day` : 'Exercises'}

{getTrainingDifficulty(user?.activityHoursPerWeek || 0)} · {activePlanExercises.length} exercises
{/* Exercise List - Compact */} {activePlanExercises.map((ex, i) => (
{ex.name} {ex.sets}×{ex.reps}
{ex.notes &&
{ex.notes}
}
))} {/* Gemini Exercise Search */}
Find Exercises with AI
setAddExerciseInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSearchExercises()} style={{ flex: 1, padding: '10px 12px', borderRadius: '8px', border: '1px solid #e5e7eb', fontSize: '14px', outline: 'none', background: 'white' }} />
{/* Search Results - Popover */} {exerciseSearchResults.length > 0 && (
Tap to add
{exerciseSearchResults.map((ex, i) => ( ))}
)}
)} {/* --- NEW WORKOUT HISTORY SECTION --- */} {/* --- WORKOUT ANALYZE TAB --- */} {modal === 'workout' && workoutTab === 'analyze' && (
{/* Upload Area */}
videoInputRef.current?.click()} style={{ border: '2px dashed #d1d5db', borderRadius: '16px', padding: analysisVideo ? '12px' : '32px', textAlign: 'center', cursor: 'pointer', background: analysisVideo ? '#f0fdf4' : '#f9fafb', transition: 'all 0.2s' }} > {analysisVideo ? (
) : (
Upload Workout Video
Any sport or exercise — keep it under 30s for best results
)}
{ if (e.target.files[0]) { setAnalysisVideo(e.target.files[0]); setAnalysisResult(null); } }} style={{display: 'none'}} accept="video/*" /> {/* Description Input */}