dublfit / src /App.jsx
justinrothaug's picture
Update src/App.jsx
d1918cc verified
Raw
History Blame Contribute Delete
165 kB
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 (
<div style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)',
borderRadius: '24px',
padding: '3px',
position: 'relative',
marginBottom: '12px'
}}>
{/* Floating orbs */}
<div style={{ position: 'absolute', top: '10%', left: '5%', width: '80px', height: '80px', background: 'rgba(255,255,255,0.2)', borderRadius: '50%', filter: 'blur(25px)' }} />
<div style={{ position: 'absolute', bottom: '10%', right: '10%', width: '60px', height: '60px', background: 'rgba(255,200,255,0.3)', borderRadius: '50%', filter: 'blur(20px)' }} />
<div style={{
background: 'rgba(255,255,255,0.15)',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
borderRadius: '21px',
padding: '16px',
border: '1px solid rgba(255,255,255,0.2)',
position: 'relative'
}}>
{/* TOP SECTION: Tighter marginBottom (4px) to reduce overall height */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '4px',
position: 'relative'
}}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', position: 'relative' }}>
{/* Circular progress */}
<div style={{ position: 'relative', width: '150px', height: '150px' }}>
<svg style={{ position: 'absolute', transform: 'rotate(-90deg)' }} width="150" height="150">
<circle cx="75" cy="75" r="65" fill="none" stroke="rgba(255,255,255,0.1)" strokeWidth="10" />
<circle
cx="75" cy="75" r="65" fill="none"
stroke="rgba(255,255,255,0.9)"
strokeWidth="10"
strokeLinecap="round"
strokeDasharray={`${totalPct * 4.08} 408`}
style={{ transition: 'stroke-dasharray 0.5s ease' }}
/>
</svg>
<div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ fontSize: '10px', color: 'rgba(255,255,255,0.6)', textTransform: 'uppercase', letterSpacing: '1px' }}>Calories</div>
<div style={{ fontSize: '42px', fontWeight: '700', color: 'white', lineHeight: 1 }}>{stats.remaining}</div>
<div style={{ fontSize: '10px', color: 'rgba(255,255,255,0.6)', marginTop: '2px' }}>remaining</div>
</div>
</div>
</div>
{/* Water Glass - Updated with Click Zones and Drag Support */}
<div style={{ position: 'absolute', right: '8px', top: '50%', transform: 'translateY(-50%)', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
{/* Label "WATER" above the glass */}
<div style={{ fontSize: '10px', color: 'rgba(255,255,255,0.6)', textTransform: 'uppercase', letterSpacing: '1px', marginBottom: '4px', fontWeight: '700' }}>Water</div>
<div
style={{ width: '36px', height: '100px', position: 'relative', touchAction: 'none' }}
onMouseDown={handleDragStart}
onMouseMove={handleDragMove}
onMouseUp={handleDragEnd}
onMouseLeave={handleDragEnd}
onTouchStart={handleDragStart}
onTouchMove={handleDragMove}
onTouchEnd={handleDragEnd}
>
{/* Top Click Zone (Add) - only works when not dragging */}
<div
onClick={(e) => { 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 */}
<div
onClick={(e) => { e.stopPropagation(); if (!dragRef.current.isDragging) onRemoveWater(); }}
style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '50%', zIndex: 10, cursor: 'pointer' }}
/>
<div style={{ position: 'absolute', inset: 0, background: 'rgba(255,255,255,0.05)', borderRadius: '5px 5px 12px 12px', border: '2px solid rgba(125, 211, 252, 0.4)', overflow: 'hidden', pointerEvents: 'none' }}>
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: `${waterPct}%`, background: 'linear-gradient(180deg, #7dd3fc 0%, #0ea5e9 50%, #0284c7 100%)', transition: 'height 0.5s cubic-bezier(0.4, 0, 0.2, 1)', borderRadius: '0 0 10px 10px' }} />
</div>
</div>
{/* Updated Text with 'cups' */}
<div style={{ marginTop: '6px', textAlign: 'center', lineHeight: '1.1' }}>
<div style={{ fontSize: '11px', fontWeight: '700', color: '#7dd3fc' }}>
{stats.dailyWater}<span style={{ opacity: 0.6 }}>/{stats.waterGoal}</span>
</div>
<div style={{ fontSize: '9px', fontWeight: '500', color: '#7dd3fc', opacity: 0.8 }}>cups</div>
</div>
</div>
</div>
{/* PIE CHART ROW - Tighter vertical gap (8px) and vertical labels */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8, paddingLeft: 4 }}>
<div style={{ position: 'relative', width: 56, height: 56 }}>
<svg width="56" height="56" style={{ transform: 'rotate(-90deg)' }}>
<circle cx="28" cy="28" r="20" fill="none" stroke="#60a5fa" strokeWidth="12"
strokeDasharray={`${pPct * 1.256} 125.6`} strokeDashoffset="0"/>
<circle cx="28" cy="28" r="20" fill="none" stroke="#fb923c" strokeWidth="12"
strokeDasharray={`${cPct * 1.256} 125.6`} strokeDashoffset={`${-pPct * 1.256}`}/>
<circle cx="28" cy="28" r="20" fill="none" stroke="#facc15" strokeWidth="12"
strokeDasharray={`${fPct * 1.256} 125.6`} strokeDashoffset={`${-(pPct + cPct) * 1.256}`}/>
</svg>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, fontSize: 11 }}>
<span style={{ color: '#60a5fa', fontWeight: 800 }}>{Math.round(pPct)}% Protein</span>
<span style={{ color: '#fb923c', fontWeight: 800 }}>{Math.round(cPct)}% Carbs</span>
<span style={{ color: '#facc15', fontWeight: 800 }}>{Math.round(fPct)}% Fat</span>
</div>
</div>
{/* Macro Bars */}
<div style={{ display: 'flex', gap: '6px' }}>
{[
{ 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) => (
<div key={i} style={{ flex: 1, background: 'rgba(255,255,255,0.08)', borderRadius: '10px', padding: '8px 6px', textAlign: 'center' }}>
<div style={{ fontSize: '9px', color: macro.color, marginBottom: '2px', fontWeight: '600' }}>{macro.label}</div>
<div style={{ fontSize: '13px', fontWeight: '700', color: 'white' }}>{macro.current}<span style={{opacity: 0.5, fontSize: '9px'}}>/{macro.goal}g</span></div>
<div style={{ height: '3px', background: 'rgba(255,255,255,0.1)', borderRadius: '2px', marginTop: '4px', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${Math.min((macro.current / macro.goal) * 100, 100)}%`, background: macro.color, transition: 'width 0.3s' }} />
</div>
</div>
))}
</div>
</div>
</div>
);
};
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": <number 1-5>,
"summary": "<1-2 sentence overall assessment>",
"strengths": ["<specific thing done well>", "<another strength>", "<another strength>"],
"improvements": ["<specific actionable improvement>", "<another improvement>", "<another improvement>"],
"tips": "<one key coaching tip to focus on next session>"
}
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 }) => (
<button onClick={onClick} style={{flex: 1, padding: '10px', background: active ? '#2563eb' : '#f3f4f6', color: active ? 'white' : '#4b5563', border: 'none', borderRadius: '8px', cursor: 'pointer', fontWeight: '600', transition: 'all 0.2s', fontSize: '13px'}}>
{label}
</button>
);
// --- 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 (
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0', borderBottom: '1px solid #f9fafb'}}>
<span style={{fontSize: '13px', color: '#374151', fontWeight: '500', textAlign: 'left', flex: 1, paddingRight: '8px'}}>{item.trim()}</span>
<div style={{display: 'flex', gap: '4px', flexShrink: 0}}>
<button onClick={() => onLog(item)} style={{background: 'none', border: '1px solid #e5e7eb', borderRadius: '6px', cursor: 'pointer', color: '#2563eb', padding: '6px'}} title="Log Item">
<PlusCircle size={14} />
</button>
<a href={instacartUrl} target="_blank" rel="noopener noreferrer" style={{background: '#ecfdf5', border: '1px solid #a7f3d0', borderRadius: '6px', color: '#059669', display: 'flex', alignItems: 'center', padding: '6px'}} title="Search on Instacart">
<ShoppingCart size={14} />
</a>
<button onClick={onDelete} style={{background: 'none', border: '1px solid #fee2e2', borderRadius: '6px', cursor: 'pointer', color: '#ef4444', padding: '6px'}} title="Remove Item">
<Trash2 size={14} />
</button>
</div>
</div>
);
};
// --- 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 <SignupScreen onComplete={(data) => setUser(data)} />;
}
// --- RENDER: MAIN APP ---
return (
<div style={container}>
{/* HEADER: Profile Avatar + Welcome + Date Scroller */}
<header style={{display:'flex', alignItems:'center', gap: '12px', marginBottom:'16px'}}>
{/* Profile Avatar Button */}
<button
onClick={() => setShowProfileMenu(true)}
style={{
background: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
border: 'none',
cursor: 'pointer',
width: '42px',
height: '42px',
borderRadius: '12px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: '18px',
fontWeight: '800',
flexShrink: 0,
boxShadow: '0 4px 6px -1px rgba(37, 99, 235, 0.2)'
}}
>
{user.name.charAt(0).toUpperCase()}
</button>
{/* Welcome Text - Updated to split name at space */}
<div style={{flexShrink: 0, marginRight: '4px'}}>
<h1 style={{fontSize:'16px', margin:0, fontWeight: '700', lineHeight: 1.2}}>
Hello, {user.name.split(' ')[0]}
</h1>
<p style={{color:'#6b7280', margin:0, fontSize:'10px', fontWeight: '500', textTransform: 'uppercase', letterSpacing: '0.5px'}}>
Today's Overview
</p>
</div>
{/* DATE SCROLLER (Pushed to Right) */}
<div
ref={scrollRef}
style={{flex: 1, display: 'flex', overflowX: 'auto', gap: '6px', scrollbarWidth: 'none', alignItems: 'center', paddingLeft: '4px'}}
>
{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 (
<button
key={dateStr}
onClick={() => setSelectedDate(dateStr)}
style={{
minWidth: '36px',
padding: '6px 0',
borderRadius: '8px',
border: isSelected ? '2px solid #2563eb' : '1px solid #e5e7eb',
background: isSelected ? '#eff6ff' : 'white',
color: isSelected ? '#2563eb' : '#6b7280',
display: 'flex', flexDirection: 'column', alignItems: 'center',
cursor: 'pointer', flexShrink: 0,
transition: 'all 0.2s'
}}
>
<span style={{fontSize: '9px', fontWeight: '600', textTransform: 'uppercase'}}>{isToday ? 'Today' : dayName}</span>
<span style={{fontSize: '13px', fontWeight: '700'}}>{dayNum}</span>
</button>
)
})}
</div>
</header>
{/* --- PROFILE MODAL --- */}
{showProfileMenu && (
<div style={modalOverlay} onClick={(e) => { if(e.target === e.currentTarget) setShowProfileMenu(false) }}>
<div style={{...modalContent, paddingBottom: '30px', overflowY: 'auto'}}>
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom: '20px', alignItems: 'center'}}>
<div style={{display: 'flex', alignItems: 'center', gap: '12px'}}>
<div style={{
background: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
width: '40px', height: '40px', borderRadius: '10px',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'white', fontWeight: '800', fontSize: '18px'
}}>
{user.name.charAt(0).toUpperCase()}
</div>
<h2 style={{margin: 0, fontSize: '18px', fontWeight: '700'}}>Profile Settings</h2>
</div>
<button onClick={() => setShowProfileMenu(false)} style={{background: '#f3f4f6', border: 'none', cursor: 'pointer', padding: '6px', borderRadius: '50%', display: 'flex'}}><X size={20} color="#374151" /></button>
</div>
{/* STATS HIGHLIGHTS - 3 COLUMN BREAKDOWN */}
<div style={{display: 'flex', gap: '8px', marginBottom: '20px'}}>
{/* Column 1: BMI */}
<div style={{flex: 1, background: '#f8fafc', padding: '12px 4px', borderRadius: '12px', border: '1px solid #e2e8f0', textAlign: 'center'}}>
<div style={{fontSize: '9px', color: '#64748b', textTransform: 'uppercase', fontWeight: 'bold', marginBottom: '4px'}}>Current BMI</div>
<div style={{fontSize: '16px', fontWeight: '800', color: '#2563eb'}}>{stats.bmi}</div>
</div>
{/* Column 2: DAILY BURN BREAKDOWN */}
<div style={{flex: 1.5, background: '#f8fafc', padding: '12px 4px', borderRadius: '12px', border: '1px solid #e2e8f0', textAlign: 'center'}}>
<div style={{fontSize: '9px', color: '#64748b', textTransform: 'uppercase', fontWeight: 'bold', marginBottom: '4px'}}>Daily Burn</div>
<div style={{fontSize: '18px', fontWeight: '800', color: '#10b981'}}>{stats.dailyTotalBurned}</div>
<div style={{display: 'flex', flexDirection: 'column', gap: '1px', marginTop: '4px'}}>
<div style={{fontSize: '8px', color: '#64748b'}}>
<span style={{fontWeight: '700'}}>{stats.sedentaryBurn}</span> Sedentary
</div>
<div style={{fontSize: '8px', color: stats.hasHealthCalories ? '#10b981' : '#2563eb'}}>
<span style={{fontWeight: '700'}}>{stats.hasHealthCalories ? stats.realActivityCalories : stats.tdeeActivityBudget}</span> {stats.hasHealthCalories ? 'Active (Health)' : 'TDEE Activity'}
</div>
</div>
</div>
{/* Column 3: WEEKLY TRACK */}
<div style={{flex: 1, background: '#f8fafc', padding: '12px 4px', borderRadius: '12px', border: '1px solid #e2e8f0', textAlign: 'center'}}>
<div style={{fontSize: '9px', color: '#64748b', textTransform: 'uppercase', fontWeight: 'bold', marginBottom: '4px'}}>Weekly Track</div>
<div style={{fontSize: '16px', fontWeight: '800', color: stats.dailyDeficit >= 0 ? '#10b981' : '#ef4444'}}>
{stats.dailyDeficit >= 0 ? `-${stats.weeklyPounds}` : `+${Math.abs(stats.weeklyPounds)}`} <span style={{fontSize: '10px'}}>lbs</span>
</div>
<div style={{fontSize: '8px', color: '#94a3b8', marginTop: '2px'}}>vs TDEE Avg</div>
</div>
</div>
{/* UPDATE WEIGHT */}
<div style={{marginBottom: '20px'}}>
<label style={{display:'block', marginBottom:'8px', fontWeight:'600', fontSize: '14px'}}>Current Weight (lbs)</label>
<input
style={{...input, marginBottom: '4px'}}
type="number"
value={user.weight}
onChange={(e) => setUser({...user, weight: e.target.value})}
placeholder="Weight in lbs"
/>
<p style={{fontSize: '11px', color: '#6b7280'}}>Recalculates BMI and daily calorie targets automatically.</p>
</div>
{/* CALORIE BUDGET */}
<div style={{marginBottom: '20px'}}>
<label style={{display:'block', marginBottom:'8px', fontWeight:'600', fontSize: '14px'}}>Calorie Budget (Base)</label>
<div style={{display: 'flex', gap: '8px'}}>
<input
style={{...input, marginBottom: 0}}
type="number"
placeholder={stats.adjustedGoal}
value={customCalInput !== null ? customCalInput : (user.customCalories ? user.customCalories : stats.adjustedGoal)}
onChange={(e) => setCustomCalInput(e.target.value)}
onFocus={(e) => { if(customCalInput === null) setCustomCalInput(e.target.value); }}
/>
<button onClick={handleSaveCalories} style={{...btnPrimary, width: 'auto', padding: '0 20px'}}>Save</button>
</div>
{user.customCalories && (
<button onClick={handleResetCalories} style={{marginTop: '8px', fontSize: '12px', color: '#2563eb', background: 'none', border: 'none', cursor: 'pointer', padding: 0}}>
Reset to Automatic Formula
</button>
)}
</div>
{/* ACTIVITY PER WEEK */}
<div style={{marginBottom: '20px'}}>
<label style={{display:'block', marginBottom:'8px', fontWeight:'600', fontSize: '14px'}}>Activity Per Week (hours)</label>
<input
type="number"
min="0"
max="40"
step="0.5"
value={user.activityHoursPerWeek ?? ''}
onChange={(e) => 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)"
/>
</div>
{/* APPLE HEALTH SYNC */}
{healthSyncAvailable && (
<div style={{marginBottom: '20px'}}>
<label style={{display:'block', marginBottom:'8px', fontWeight:'600', fontSize: '14px'}}>
<Heart size={16} style={{display: 'inline', marginRight: '6px', verticalAlign: 'text-bottom'}} />
Apple Health Sync
</label>
{!healthSyncSettings.enabled ? (
<button
onClick={enableHealthSync}
disabled={healthSyncLoading}
style={{
width: '100%',
padding: '12px',
borderRadius: '10px',
border: '1px solid #fecaca',
background: 'linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)',
color: '#dc2626',
fontSize: '14px',
fontWeight: '600',
cursor: healthSyncLoading ? 'wait' : 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px'
}}
>
{healthSyncLoading ? (
<><Loader2 size={16} style={{animation: 'spin 1s linear infinite'}} /> Connecting...</>
) : (
<><Heart size={16} /> Connect to Apple Health</>
)}
</button>
) : (
<div style={{
background: '#f0fdf4',
border: '1px solid #bbf7d0',
borderRadius: '12px',
padding: '12px'
}}>
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px'}}>
<span style={{fontSize: '13px', fontWeight: '600', color: '#16a34a'}}>
Connected
</span>
<button
onClick={disableHealthSync}
style={{
fontSize: '12px',
color: '#6b7280',
background: 'none',
border: 'none',
cursor: 'pointer',
textDecoration: 'underline'
}}
>
Disconnect
</button>
</div>
{healthData && healthData[new Date().toLocaleDateString()] && (() => {
const todayHealth = healthData[new Date().toLocaleDateString()];
return (
<>
<div style={{fontSize: '11px', fontWeight: '600', color: '#374151', marginBottom: '6px'}}>Today</div>
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', marginBottom: '10px'}}>
<div style={{background: 'white', padding: '8px', borderRadius: '8px', textAlign: 'center'}}>
<div style={{fontSize: '18px', fontWeight: '700', color: '#2563eb'}}>{todayHealth.steps?.toLocaleString() || 0}</div>
<div style={{fontSize: '10px', color: '#6b7280', textTransform: 'uppercase'}}>Steps</div>
</div>
<div style={{background: 'white', padding: '8px', borderRadius: '8px', textAlign: 'center'}}>
<div style={{fontSize: '18px', fontWeight: '700', color: '#dc2626'}}>{todayHealth.activeCalories || 0}</div>
<div style={{fontSize: '10px', color: '#6b7280', textTransform: 'uppercase'}}>Active Cal</div>
</div>
<div style={{background: 'white', padding: '8px', borderRadius: '8px', textAlign: 'center'}}>
<div style={{fontSize: '18px', fontWeight: '700', color: '#10b981'}}>{todayHealth.totalCalories || 0}</div>
<div style={{fontSize: '10px', color: '#6b7280', textTransform: 'uppercase'}}>Total Burn</div>
</div>
<div style={{background: 'white', padding: '8px', borderRadius: '8px', textAlign: 'center'}}>
<div style={{fontSize: '18px', fontWeight: '700', color: '#8b5cf6'}}>{todayHealth.distance || 0}</div>
<div style={{fontSize: '10px', color: '#6b7280', textTransform: 'uppercase'}}>Miles</div>
</div>
</div>
</>
);
})()}
<button
onClick={syncHealthDataNow}
disabled={healthSyncLoading}
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid #bbf7d0',
background: 'white',
color: '#16a34a',
fontSize: '12px',
fontWeight: '600',
cursor: healthSyncLoading ? 'wait' : 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px'
}}
>
{healthSyncLoading ? (
<><Loader2 size={14} style={{animation: 'spin 1s linear infinite'}} /> Syncing...</>
) : (
<><RefreshCw size={14} /> Sync Now</>
)}
</button>
{healthSyncSettings.lastSync && (
<div style={{fontSize: '10px', color: '#6b7280', textAlign: 'center', marginTop: '6px'}}>
Last synced: {new Date(healthSyncSettings.lastSync).toLocaleTimeString()}
</div>
)}
</div>
)}
<p style={{fontSize: '11px', color: '#6b7280', marginTop: '6px'}}>
Syncs steps, calories burned, and workouts from Apple Health for the last 7 days.
</p>
</div>
)}
<hr style={{border: 'none', borderTop: '1px solid #e5e7eb', margin: '20px 0'}} />
<button onClick={logout} style={{width: '100%', padding: '12px', borderRadius: '10px', border: '1px solid #fee2e2', background: '#fef2f2', color: '#ef4444', fontSize: '14px', fontWeight: '600', cursor: 'pointer', display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '8px'}}>
<LogOut size={16}/> Logout / Reset All Data
</button>
</div>
</div>
)}
{/* --- NEW GLASS MORPHISM DASHBOARD --- */}
<GlassDashboard stats={stats} onAddWater={addWater} onRemoveWater={removeWater} />
<h3 style={{fontSize:'16px', marginBottom:'10px', fontWeight: '600'}}>
{selectedDate === new Date().toLocaleDateString() ? "Today's Activity" : `Log for ${selectedDate}`}
</h3>
{[...getItemsForDate(workouts, selectedDate), ...getItemsForDate(meals, selectedDate)].length === 0 && !stats.hasHealthCalories && (
<div style={{textAlign: 'center', color: '#9ca3af', padding: '20px', fontSize: '14px'}}>
No activity logged for this day. Tap below to log!
</div>
)}
{/* 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 (
<div key={`health-workout-${i}`} style={{...card, padding:'12px', display:'flex', alignItems:'center', marginBottom:'8px', background: 'linear-gradient(135deg, #eff6ff 0%, #f0f9ff 100%)', border: '1px solid #bfdbfe'}}>
<div style={{display:'flex', gap:'12px', alignItems:'center', flex: 1}}>
<div style={{background: '#dbeafe', padding: '8px', borderRadius: '10px'}}>
<Activity size={18} color="#2563eb"/>
</div>
<div>
<div style={{fontWeight: 600, fontSize: '14px', color: '#1e3a5f', display: 'flex', alignItems: 'center', gap: '6px'}}>
{typeLabel}
<span style={{fontSize: '9px', fontWeight: 600, color: '#10b981', background: '#dcfce7', padding: '1px 5px', borderRadius: '4px'}}>Health</span>
</div>
<div style={{fontSize: '12px', color: '#3b82f6', marginTop: '2px'}}>
{w.calories ? <strong>{Math.round(w.calories)} cal</strong> : null}
{duration ? `${w.calories ? ' • ' : ''}${duration} min` : ''}
{timeStr ? ` • ${timeStr}` : ''}
</div>
</div>
</div>
</div>
);
})}
{/* Apple Health steps/movement entry (non-workout activity) */}
{stats.hasHealthCalories && stats.healthDayData && (stats.healthDayData.steps > 0 || stats.healthDayData.movementCalories > 0) && (
<div style={{...card, padding:'12px', display:'flex', alignItems:'center', marginBottom:'8px', background: 'linear-gradient(135deg, #f0fdf4 0%, #ecfdf5 100%)', border: '1px solid #bbf7d0'}}>
<div style={{display:'flex', gap:'12px', alignItems:'center', flex: 1}}>
<div style={{background: '#dcfce7', padding: '8px', borderRadius: '10px'}}>
<Heart size={18} color="#10b981"/>
</div>
<div>
<div style={{fontWeight: 600, fontSize: '14px', color: '#065f46', display: 'flex', alignItems: 'center', gap: '6px'}}>
Steps & Movement
<span style={{fontSize: '9px', fontWeight: 600, color: '#10b981', background: '#dcfce7', padding: '1px 5px', borderRadius: '4px'}}>Health</span>
</div>
<div style={{fontSize: '12px', color: '#047857', marginTop: '2px'}}>
{(stats.healthDayData.steps || 0).toLocaleString()} steps
{stats.healthDayData.movementCalories > 0 ? <><strong>{Math.round(stats.healthDayData.movementCalories)} cal</strong></> : null}
{stats.healthDayData.distance ? ` • ${stats.healthDayData.distance} mi` : ''}
</div>
</div>
</div>
</div>
)}
{[...getItemsForDate(workouts, selectedDate), ...getItemsForDate(meals, selectedDate)]
.sort((a,b) => b.id - a.id)
.map(item => (
<div key={item.id} style={{...card, padding:'12px', display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:'8px'}}>
{/* This clickable div allows you to edit the item */}
<div
onClick={() => editItem(item)}
style={{display:'flex', gap:'12px', alignItems:'center', flex: 1, cursor: 'pointer'}}
>
<div style={{background: item.exercise ? '#eff6ff' : '#ecfdf5', padding: '8px', borderRadius: '10px'}}>
{item.exercise ? <Dumbbell size={18} color="#2563eb"/> : <Utensils size={18} color="#10b981"/>}
</div>
<div>
<div style={{fontWeight:600, fontSize: '14px'}}>{item.exercise || item.item}</div>
<div style={{fontSize:'12px', color:'#6b7280', marginTop: '2px'}}>
{item.exercise
? (item.type === 'Cardio' || item.time ? (
<span>{item.cals ? <strong>{item.cals} cal</strong> : '0 cal'} • {item.time} mins ({item.effort})</span>
) : (
<span>{item.cals ? <strong>{item.cals} cal</strong> : '0 cal'} • {item.weight}lbs × {item.reps}</span>
))
: <span><strong>{item.cals} cal</strong> <span style={{fontSize: '11px', opacity: 0.8}}> (P:{item.protein || 0} C:{item.carbs || 0} F:{item.fat || 0})</span></span>
}
</div>
</div>
</div>
<button
onClick={(e) => {
e.stopPropagation(); // Prevents clicking the trash from opening the edit modal
deleteItem(item.id, item.exercise ? 'workout' : 'meal');
}}
style={{border:'none', background:'none', color:'#ef4444', cursor:'pointer', padding: '4px'}}
>
<Trash2 size={16}/>
</button>
</div>
))}
<div style={bottomNav}>
<button style={navBtn(modal === 'food')} onClick={() => {setModal('food'); setFoodTab('plan');}}><div style={navIconBox('#10b981', modal === 'food')}><Utensils size={18} /></div>Food</button>
<button style={navBtn(modal === 'workout')} onClick={() => {setModal('workout'); setWorkoutTab('plan');}}><div style={navIconBox('#2563eb', modal === 'workout')}><Dumbbell size={18} /></div>Workout</button>
<button style={navBtn(showCallWebView)} onClick={() => setShowCallWebView(true)}><div style={navIconBox('#a855f7', showCallWebView)}><Phone size={18} /></div>Call</button>
</div>
{modal && (
<div style={modalOverlay} onClick={(e) => { if(e.target === e.currentTarget) setModal(null) }}>
<div style={modalContent}>
<div style={{flexShrink: 0}}>
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom: '20px', alignItems: 'center'}}>
<h2 style={{margin: 0, fontSize: '18px', fontWeight: '700'}}>
{modal === 'food' ? 'Food & Diet' : 'Workout & Training'}
</h2>
<button onClick={() => setModal(null)} style={{background: '#f3f4f6', border: 'none', cursor: 'pointer', padding: '6px', borderRadius: '50%', display: 'flex'}}><X size={20} color="#374151" /></button>
</div>
{/* MODAL TABS */}
<div style={{display: 'flex', gap: '10px', marginBottom: '20px'}}>
{modal === 'workout' ? (
<>
<TabButton active={workoutTab === 'plan'} label="Plan" onClick={() => setWorkoutTab('plan')} />
<TabButton active={workoutTab === 'log'} label="Log" onClick={() => setWorkoutTab('log')} />
<TabButton active={workoutTab === 'history'} label="History" onClick={() => setWorkoutTab('history')} />
<TabButton active={workoutTab === 'analyze'} label="Analyze" onClick={() => setWorkoutTab('analyze')} />
</>
) : (
<>
{/* UPDATED TAB ORDER: PLAN, LOG, HISTORY */}
<TabButton active={foodTab === 'plan'} label="Plan" onClick={() => setFoodTab('plan')} />
<TabButton active={foodTab === 'log'} label="Log" onClick={() => setFoodTab('log')} />
<TabButton active={foodTab === 'history'} label="History" onClick={() => setFoodTab('history')} />
</>
)}
</div>
</div>
{/* --- CONTENT AREA (SCROLLABLE) --- */}
<div style={{flex: 1, overflowY: 'auto', minHeight: 0}}>
{/* --- FOOD MODAL CONTENT --- */}
{modal === 'food' && foodTab === 'log' && (
<form onSubmit={addMeal}>
{/* NUTRITION LABEL CONTAINER */}
<div style={{
border: '2px solid black',
padding: '12px',
background: 'white',
color: 'black',
fontFamily: 'Helvetica, Arial, sans-serif',
marginBottom: '16px'
}}>
{/* HEADER: TITLE INPUT + ACTIONS */}
<div style={{display: 'flex', alignItems: 'stretch', justifyContent: 'space-between', marginBottom: '4px'}}>
<div style={{flex: 1}}>
<input
placeholder="Food Name"
autoFocus
value={mForm.item}
onChange={e => setMForm({...mForm, item: e.target.value})}
style={{
fontSize: '28px',
fontWeight: '900',
border: 'none',
outline: 'none',
width: '100%',
padding: 0,
margin: 0,
color: 'black'
}}
/>
</div>
{/* ACTION BUTTONS (SCAN ONLY) */}
<div style={{display: 'flex', flexDirection: 'column', gap: '4px', height: 'auto'}}>
<button type="button" onClick={() => fileInputRef.current.click()} style={{
background: 'black', color: 'white', border: 'none', padding: '0 12px',
height: '100%', // Fill height
borderRadius: '4px', fontWeight: 'bold', fontSize: '11px', cursor: 'pointer',
display: 'flex', alignItems: 'center', gap: '4px', justifyContent: 'center'
}}>
<Camera size={14}/> SCAN
</button>
</div>
</div>
<input type="file" ref={fileInputRef} onChange={handleImageUpload} style={{display: 'none'}} accept="image/*" />
{/* THICK BAR 1 */}
<div style={{height: '10px', background: 'black', marginBottom: '8px'}}></div>
{/* CALORIES ROW */}
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px'}}>
<span style={{fontSize: '28px', fontWeight: '900'}}>Calories</span>
<input
type="number"
placeholder="0"
value={mForm.cals}
onChange={e => 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
}}
/>
</div>
{/* MEDIUM BAR */}
<div style={{height: '5px', background: 'black', marginBottom: '12px'}}></div>
{/* 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) => (
<div key={macro.key} style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px 0',
borderBottom: '1px solid #a3a3a3',
fontSize: '15px'
}}>
<span style={{fontWeight: '700'}}>{macro.label}</span>
<div style={{display: 'flex', alignItems: 'center', gap: '4px'}}>
<input
type="number"
placeholder="0"
value={mForm[macro.key]}
onChange={e => setMForm({...mForm, [macro.key]: e.target.value})}
style={{
fontSize: '16px',
fontWeight: 'bold',
textAlign: 'right',
border: 'none',
background: '#f3f4f6',
padding: '4px',
borderRadius: '4px',
width: '60px'
}}
/>
<span style={{fontWeight: '600', fontSize: '12px'}}>{macro.unit}</span>
</div>
</div>
))}
<div style={{fontSize: '10px', marginTop: '10px', color: '#666'}}>
* Percent Daily Values are based on your {stats.adjustedGoal} calorie diet.
</div>
</div>
<div style={{display: 'flex', gap: '10px', marginTop: '12px', alignItems: 'flex-end'}}>
{/* Update Log Button */}
<button
type="submit"
disabled={aiLoading}
style={{
...btnPrimary,
flex: 3,
background: aiLoading ? '#525252' : 'black',
color: 'white',
cursor: aiLoading ? 'not-allowed' : 'pointer',
justifyContent: 'center',
marginTop: 0,
height: '56px',
fontSize: '18px'
}}
>
{aiLoading ? (
<>
<Loader2 size={20} style={{animation: 'spin 1s linear infinite'}} />
<span style={{marginLeft: '8px'}}>Analyzing...</span>
</>
) : (
"Update Log"
)}
</button>
{/* Servings Control (Bottom Right) */}
<div style={{flex: 1, display: 'flex', flexDirection: 'column', gap: '4px'}}>
{/* +/- Buttons Stacked */}
<div style={{display: 'flex', gap: '4px', height: '40px'}}>
<button
type="button"
onClick={() => updateServings(-0.5)}
style={{ flex: 1, border: 'none', background: '#e5e7eb', borderRadius: '6px', fontWeight: 'bold', cursor: 'pointer', fontSize: '18px', color: '#374151' }}
>-</button>
<button
type="button"
onClick={() => updateServings(0.5)}
style={{ flex: 1, border: 'none', background: '#e5e7eb', borderRadius: '6px', fontWeight: 'bold', cursor: 'pointer', fontSize: '18px', color: '#374151' }}
>+</button>
</div>
{/* Number Box (Same height as Add Log button) */}
<div style={{
background: '#f3f4f6',
borderRadius: '12px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: '800',
fontSize: '20px',
height: '56px',
color: '#111827',
border: '1px solid #e5e7eb'
}}>
{mForm.servings}x
</div>
</div>
</div>
</form>
)}
{modal === 'food' && foodTab === 'plan' && (
<div>
{/* --- DIET SECTION (Top) --- */}
<div style={{background: '#f9fafb', padding: '16px', borderRadius: '12px', marginBottom: '20px', position: 'relative'}}>
<div style={{display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px'}}>
<h3 style={{margin: 0, fontSize: '16px', color: '#111827'}}>{stats.dietLabel}</h3>
<button
onClick={() => setShowDoctorsOrders(!showDoctorsOrders)}
style={{background: 'none', border: 'none', cursor: 'pointer', color: '#6b7280', padding: '4px', display: 'flex'}}
>
<Info size={16} />
</button>
{/* POPOVER */}
{showDoctorsOrders && (
<div style={{
position: 'absolute', top: '40px', left: '16px', right: '16px',
background: '#1f2937', color: 'white', padding: '16px', borderRadius: '12px',
zIndex: 20, boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
animation: 'fadeIn 0.2s'
}}>
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom: '8px'}}>
<h4 style={{fontSize: '14px', margin: 0, color: '#f3f4f6'}}>Doctor's Orders</h4>
<button onClick={() => setShowDoctorsOrders(false)} style={{background: 'none', border: 'none', color: '#9ca3af', cursor: 'pointer'}}><X size={14}/></button>
</div>
<ul style={{fontSize: '12px', paddingLeft: '16px', margin: 0, lineHeight: '1.6', color: '#d1d5db'}}>
<li><strong>Pre-Workout:</strong> 30-40g Protein + 30-40g Carbs (30-60 mins before).</li>
<li><strong>Post-Workout:</strong> 30-40g Protein + High Carbs (within 2 hours).</li>
<li><strong>Hydration:</strong> Drink 1:1 ratio of Water to Electrolytes if sweating heavily.</li>
<li><strong>Focus:</strong> Unprocessed foods (80% rule). Mediterranean sources preferred.</li>
</ul>
</div>
)}
</div>
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px', marginBottom: '0'}}>
<div style={{background: 'white', padding: '10px', borderRadius: '8px', border: '1px solid #e5e7eb'}}>
<div style={{fontSize: '11px', color: '#6b7280'}}>Calories</div>
<div style={{fontWeight: '700', fontSize: '16px'}}>{stats.adjustedGoal}</div>
</div>
<div style={{background: 'white', padding: '10px', borderRadius: '8px', border: '1px solid #e5e7eb'}}>
<div style={{fontSize: '11px', color: '#6b7280'}}>Protein (g)</div>
<div style={{fontWeight: '700', fontSize: '16px', color: '#3b82f6'}}>{stats.proteinGoal}</div>
</div>
<div style={{background: 'white', padding: '10px', borderRadius: '8px', border: '1px solid #e5e7eb'}}>
<div style={{fontSize: '11px', color: '#6b7280'}}>Carbs (g)</div>
<div style={{fontWeight: '700', fontSize: '16px', color: '#f97316'}}>{stats.carbsGoal}</div>
</div>
<div style={{background: 'white', padding: '10px', borderRadius: '8px', border: '1px solid #e5e7eb'}}>
<div style={{fontSize: '11px', color: '#6b7280'}}>Fat (g)</div>
<div style={{fontWeight: '700', fontSize: '16px', color: '#eab308'}}>{stats.fatGoal}</div>
</div>
</div>
</div>
{/* --- MEAL PLAN SECTION (Bottom) --- */}
<div style={{padding: '0 4px'}}>
<div style={{textAlign: 'center', marginBottom: '16px'}}>
<button onClick={handleGenerateMealPlan} disabled={aiLoading} style={{...btnAI, width: '100%', justifyContent: 'center', padding: '12px', background: '#8b5cf6', color: 'white'}}>
{aiLoading ? 'Creating Plan...' : <><Sparkles size={16}/> Generate Meal Plan</>}
</button>
<div style={{display: 'flex', justifyContent: 'space-between', marginTop: '8px', alignItems: 'center'}}>
<p style={{fontSize: '11px', color: '#6b7280', margin: 0}}>Based on {stats.adjustedGoal} cal target.</p>
</div>
</div>
{generatedMealPlan && (
<div style={{display: 'flex', flexDirection: 'column', gap: '12px'}}>
{generatedMealPlan.map((planItem, idx) => (
<div key={idx} style={{background: 'white', padding: '12px', borderRadius: '12px', border: '1px solid #e5e7eb', boxShadow: '0 1px 2px rgba(0,0,0,0.05)', position: 'relative'}}>
{regeneratingIndex === idx && (
<div style={{position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(255,255,255,0.8)', zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '12px'}}>
<Loader2 size={24} className="animate-spin text-blue-600" color="#2563eb" />
</div>
)}
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '8px'}}>
<div>
<span style={{fontSize: '12px', fontWeight: '800', color: '#8b5cf6', textTransform: 'uppercase'}}>{planItem.label}</span>
<div style={{fontSize: '11px', color: '#6b7280', marginTop: '2px'}}>
{planItem.cals} cal <span style={{opacity: 0.5}}>|</span> P:{planItem.protein} C:{planItem.carbs} F:{planItem.fat}
</div>
</div>
<div style={{display: 'flex', gap: '4px'}}>
<button onClick={() => handleRegenerateSingleMeal(idx, planItem)} disabled={regeneratingIndex === idx} style={{border: 'none', background: '#f3f4f6', color: '#6b7280', borderRadius: '20px', padding: '6px', cursor: 'pointer', display: 'flex', alignItems: 'center'}}>
<RefreshCw size={14}/>
</button>
<button onClick={() => logMealFromPlan(planItem)} style={{border: 'none', background: '#eff6ff', color: '#2563eb', borderRadius: '20px', padding: '4px 10px', fontSize: '11px', fontWeight: '600', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '4px'}}>
<PlusCircle size={14}/> Log
</button>
</div>
</div>
<div style={{borderTop: '1px solid #f3f4f6', paddingTop: '8px'}}>
{planItem.item.split(',').map((foodStr, i) => (
<ShoppingListRow
key={i}
item={foodStr}
onLog={() => { 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);
}}
/>
))}
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* --- NEW FOOD HISTORY SECTION - GROUPED BY DAY --- */}
{modal === 'food' && foodTab === 'history' && (
<div>
<input
style={input}
placeholder="Search history..."
value={searchHistory}
onChange={e => setSearchHistory(e.target.value)}
/>
<div style={{maxHeight: '400px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '12px', paddingTop: '8px'}}>
{(() => {
// 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 <div style={{textAlign: 'center', padding: '40px', color: '#9ca3af', fontSize: '14px'}}>
{searchHistory ? 'No matching meals found' : 'No food history yet'}
</div>;
}
return sortedDates.map((date, dateIdx) => {
const mealsForDate = mealsByDate[date];
return (
<div key={dateIdx} style={{borderRadius: '12px', border: '2px solid #e5e7eb', overflow: 'visible', background: 'white'}}>
{/* Date Header */}
<div style={{background: '#f3f4f6', padding: '12px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #e5e7eb'}}>
<div style={{fontWeight: '700', color: '#374151', fontSize: '14px'}}>
{date === selectedDate ? 'Today' : date}
</div>
<button
onClick={() => addAllMealsFromHistory(mealsForDate)}
style={{
background: '#10b981',
color: 'white',
border: 'none',
borderRadius: '8px',
padding: '6px 12px',
fontSize: '12px',
fontWeight: '600',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}
>
<Plus size={14}/> Add All ({mealsForDate.length})
</button>
</div>
{/* ALL MEALS - NO DEDUPLICATION */}
<div style={{display: 'flex', flexDirection: 'column', background: 'white'}}>
{mealsForDate.map((m, i) => (
<div
key={m.id || i}
onClick={() => 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'}
>
<div style={{flex: 1, minWidth: 0}}>
<div style={{fontWeight: '600', color: '#111827', marginBottom: '6px', fontSize: '15px'}}>{m.item}</div>
<div style={{fontSize: '13px', color: '#6b7280', fontWeight: '500'}}>
{m.cals || 0} cal • P:{m.protein || 0} C:{m.carbs || 0} F:{m.fat || 0}
</div>
</div>
<button
onClick={(e) => { e.stopPropagation(); deleteHistoryItem(m.item, 'meal'); }}
style={{border: 'none', background: 'none', color: '#ef4444', padding: '8px', cursor: 'pointer', flexShrink: 0, marginLeft: '8px'}}
>
<Trash2 size={18}/>
</button>
</div>
))}
</div>
</div>
);
});
})()}
</div>
</div>
)}
{/* --- WORKOUT MODAL CONTENT --- */}
{modal === 'workout' && workoutTab === 'log' && (
<form onSubmit={addWorkout}>
{/* EXERCISE NAME */}
<div style={{marginBottom: '10px'}}>
<label style={{display:'block', marginBottom:'5px', fontWeight:'500', fontSize: '14px'}}>Exercise</label>
<div style={{display: 'flex', gap: '8px'}}>
<input
style={input}
placeholder="e.g. Bench Press"
autoFocus
value={wForm.exercise}
onChange={e => setWForm({...wForm, exercise: e.target.value})}
onBlur={e => updateLastLog(e.target.value)}
/>
<button type="button" onClick={handleWorkoutAILookup} disabled={aiLoading} style={btnAI}>
{aiLoading ? 'Thinking...' : <><Sparkles size={16}/> Next-Up!</>}
</button>
</div>
</div>
{/* SMART TARGET CARD (TONAL STYLE) */}
{lastLog && lastLog.weight && lastLog.sets && lastLog.reps && (
<div style={{
background: '#1f2937',
color: 'white',
borderRadius: '12px',
padding: '12px',
marginBottom: '16px',
border: '1px solid #374151',
animation: 'fadeIn 0.3s'
}}>
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px'}}>
<span style={{fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px', color: '#9ca3af', fontWeight: '600'}}>
Last Session ({lastLog.date})
</span>
<button
type="button"
onClick={() => setWForm({...wForm, weight: lastLog.weight, sets: lastLog.sets, reps: lastLog.reps})}
style={{background: '#374151', border: 'none', color: '#e5e7eb', fontSize: '10px', padding: '4px 8px', borderRadius: '4px', cursor: 'pointer'}}
>
Use These Stats
</button>
</div>
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end'}}>
<div>
<div style={{fontSize: '24px', fontWeight: '700', lineHeight: '1'}}>
{lastLog.weight}<span style={{fontSize: '14px', fontWeight: '400', color: '#d1d5db'}}>lbs</span>
</div>
<div style={{fontSize: '13px', color: '#9ca3af', marginTop: '4px'}}>
{lastLog.sets} sets × {lastLog.reps} reps
</div>
</div>
<div style={{textAlign: 'right'}}>
<div style={{fontSize: '11px', color: '#10b981', fontWeight: '700', marginBottom: '2px'}}>
BEAT IT
</div>
<div style={{fontSize: '18px', fontWeight: '700', color: '#34d399'}}>
{parseInt(lastLog.weight) + 5}<span style={{fontSize: '12px', fontWeight: '400'}}>lbs</span>
</div>
</div>
</div>
</div>
)}
{/* --- UNIFIED DETAILS SECTION --- */}
<div style={{marginBottom: '16px', background: '#f9fafb', padding: '12px', borderRadius: '12px', border: '1px solid #e5e7eb', animation: 'fadeIn 0.2s'}}>
{/* Header Toggle */}
<div style={{display: 'flex', justifyContent: 'center', marginBottom: '12px'}}>
<div style={{display: 'flex', background: '#e5e7eb', borderRadius: '8px', padding: '2px', width: '100%'}}>
<button
type="button"
onClick={() => setWorkoutMode('Strength')}
style={{flex: 1, padding: '8px', borderRadius: '6px', border: 'none', background: workoutMode === 'Strength' ? 'white' : 'transparent', color: workoutMode === 'Strength' ? '#2563eb' : '#6b7280', fontWeight: '700', fontSize: '12px', cursor: 'pointer', boxShadow: workoutMode === 'Strength' ? '0 1px 2px rgba(0,0,0,0.1)' : 'none', transition: 'all 0.2s'}}
>Strength Details</button>
<button
type="button"
onClick={() => setWorkoutMode('Cardio')}
style={{flex: 1, padding: '8px', borderRadius: '6px', border: 'none', background: workoutMode === 'Cardio' ? 'white' : 'transparent', color: workoutMode === 'Cardio' ? '#2563eb' : '#6b7280', fontWeight: '700', fontSize: '12px', cursor: 'pointer', boxShadow: workoutMode === 'Cardio' ? '0 1px 2px rgba(0,0,0,0.1)' : 'none', transition: 'all 0.2s'}}
>Cardio Details</button>
</div>
</div>
{/* Inputs */}
{workoutMode === 'Strength' ? (
<div style={{display:'flex', gap:'10px'}}>
<div style={{flex: 1}}>
<label style={{fontSize: '11px', color: '#6b7280'}}>Lbs</label>
<input style={{...input, marginBottom: 0}} type="number" placeholder="0" value={wForm.weight} onChange={e => setWForm({...wForm, weight: e.target.value})} />
</div>
<div style={{flex: 1}}>
<label style={{fontSize: '11px', color: '#6b7280'}}>Sets</label>
<input style={{...input, marginBottom: 0}} type="text" placeholder="3" value={wForm.sets} onChange={e => setWForm({...wForm, sets: e.target.value})} />
</div>
<div style={{flex: 1}}>
<label style={{fontSize: '11px', color: '#6b7280'}}>Reps</label>
<input style={{...input, marginBottom: 0}} type="text" placeholder="6" value={wForm.reps} onChange={e => setWForm({...wForm, reps: e.target.value})} />
</div>
</div>
) : (
<div style={{display:'flex', gap:'10px', alignItems: 'flex-end'}}>
<div style={{flex: 1}}>
<label style={{fontSize: '11px', color: '#6b7280'}}>Time (Mins)</label>
<input style={{...input, marginBottom: 0}} type="number" placeholder="0" value={wForm.time} onChange={e => setWForm({...wForm, time: e.target.value})} />
</div>
<div style={{flex: 1}}>
<label style={{fontSize: '11px', color: '#6b7280'}}>Intensity</label>
<div style={{display: 'flex', background: 'white', borderRadius: '10px', border: '1px solid #e5e7eb', overflow: 'hidden'}}>
<button
type="button"
onClick={() => setWForm({...wForm, effort: 'Low'})}
style={{flex: 1, border: 'none', background: wForm.effort === 'Low' ? '#eff6ff' : 'white', color: wForm.effort === 'Low' ? '#2563eb' : '#6b7280', padding: '10px', fontSize: '12px', fontWeight: '600', cursor: 'pointer'}}
>Low</button>
<div style={{width: '1px', background: '#e5e7eb'}}></div>
<button
type="button"
onClick={() => setWForm({...wForm, effort: 'High'})}
style={{flex: 1, border: 'none', background: wForm.effort === 'High' ? '#fef2f2' : 'white', color: wForm.effort === 'High' ? '#ef4444' : '#6b7280', padding: '10px', fontSize: '12px', fontWeight: '600', cursor: 'pointer'}}
>High</button>
</div>
</div>
</div>
)}
</div>
<button
type="submit"
disabled={aiLoading}
style={{
...btnPrimary,
marginTop: '10px',
background: aiLoading ? '#525252' : '#2563eb'
}}
>
{aiLoading ? (
<>
<Loader2 size={20} style={{animation: 'spin 1s linear infinite'}} />
<span style={{marginLeft: '8px'}}>Calculating...</span>
</>
) : (
"Add Workout"
)}
</button>
</form>
)}
{modal === 'workout' && workoutTab === 'plan' && (
<div>
{/* --- ROUTINE SELECTOR --- */}
<div style={{position: 'relative', marginBottom: '14px'}}>
<button
onClick={() => setShowRoutinePicker(!showRoutinePicker)}
style={{
width: '100%',
padding: '12px 14px',
borderRadius: '12px',
border: '2px solid #2563eb',
background: '#eff6ff',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
<div style={{display: 'flex', alignItems: 'center', gap: '10px'}}>
<span style={{fontSize: '20px'}}>{(WORKOUT_ROUTINES.find(r => r.id === selectedRoutineId) || WORKOUT_ROUTINES[0]).emoji}</span>
<div style={{textAlign: 'left'}}>
<div style={{fontWeight: '700', fontSize: '15px', color: '#1e40af'}}>{(WORKOUT_ROUTINES.find(r => r.id === selectedRoutineId) || WORKOUT_ROUTINES[0]).name}</div>
<div style={{fontSize: '11px', color: '#6b7280'}}>{(WORKOUT_ROUTINES.find(r => r.id === selectedRoutineId) || WORKOUT_ROUTINES[0]).description}</div>
</div>
</div>
<ChevronDown size={18} color="#2563eb" style={{transform: showRoutinePicker ? 'rotate(180deg)' : 'rotate(0)', transition: 'transform 0.2s'}} />
</button>
{/* Routine Picker Dropdown - Overlay */}
{showRoutinePicker && (
<div style={{
position: 'absolute',
top: '100%',
left: 0,
right: 0,
zIndex: 20,
border: '2px solid #e5e7eb',
borderTop: '1px solid #e5e7eb',
borderRadius: '0 0 12px 12px',
maxHeight: '260px',
overflowY: 'auto',
background: 'white',
boxShadow: '0 8px 24px rgba(0,0,0,0.15)'
}}>
{WORKOUT_ROUTINES.map(routine => (
<button
key={routine.id}
onClick={() => { setSelectedRoutineId(routine.id); setShowRoutinePicker(false); if (routine.hasSplits) setPlanTab('Push'); }}
style={{
width: '100%',
padding: '10px 14px',
border: 'none',
borderBottom: '1px solid #f3f4f6',
background: selectedRoutineId === routine.id ? '#eff6ff' : 'white',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '10px',
textAlign: 'left'
}}
>
<span style={{fontSize: '18px', flexShrink: 0}}>{routine.emoji}</span>
<div style={{flex: 1, minWidth: 0}}>
<div style={{fontWeight: '600', fontSize: '13px', color: selectedRoutineId === routine.id ? '#1e40af' : '#374151'}}>{routine.name}</div>
<div style={{fontSize: '11px', color: '#9ca3af'}}>{routine.description}</div>
</div>
{selectedRoutineId === routine.id && <div style={{width: '8px', height: '8px', borderRadius: '50%', background: '#2563eb', flexShrink: 0}} />}
</button>
))}
</div>
)}
</div>
{/* Split pills for Train Right only */}
{selectedRoutineId === 'train-right' && (
<div style={{display: 'flex', gap: '6px', marginBottom: '14px'}}>
{['Push', 'Pull', 'Legs', 'Cardio'].map(split => (
<button
key={split}
onClick={() => setPlanTab(split)}
style={{
flex: 1,
padding: '8px 4px',
borderRadius: '8px',
border: planTab === split ? '2px solid #2563eb' : '1px solid #e5e7eb',
background: planTab === split ? '#eff6ff' : 'white',
color: planTab === split ? '#2563eb' : '#6b7280',
fontWeight: '700',
fontSize: '12px',
cursor: 'pointer',
transition: 'all 0.15s'
}}
>{split}</button>
))}
</div>
)}
{/* Header + Log All */}
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px'}}>
<div>
<h3 style={{margin: 0, fontSize: '15px', fontWeight: '700', color: '#111827'}}>
{selectedRoutineId === 'train-right' ? `${planTab} Day` : 'Exercises'}
</h3>
<span style={{fontSize: '11px', color: '#9ca3af', fontWeight: '500'}}>
{getTrainingDifficulty(user?.activityHoursPerWeek || 0)} · {activePlanExercises.length} exercises
</span>
</div>
<button
onClick={logAllFromPlan}
style={{
background: '#10b981',
color: 'white',
border: 'none',
borderRadius: '8px',
padding: '8px 14px',
fontSize: '12px',
fontWeight: '700',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '5px'
}}
>
<PlusCircle size={14}/> Log All
</button>
</div>
{/* Exercise List - Compact */}
{activePlanExercises.map((ex, i) => (
<div key={i} style={{
background: 'white',
border: '1px solid #e5e7eb',
borderRadius: '10px',
padding: '10px 12px',
marginBottom: '6px',
display: 'flex',
alignItems: 'center',
gap: '10px'
}}>
<div style={{flex: 1, minWidth: 0}}>
<div style={{display: 'flex', alignItems: 'center', gap: '8px'}}>
<span style={{fontWeight: '600', fontSize: '13px', color: '#111827'}}>{ex.name}</span>
<span style={{fontSize: '11px', color: '#8b5cf6', fontWeight: '700', background: '#f5f3ff', padding: '1px 6px', borderRadius: '4px', flexShrink: 0}}>{ex.sets}×{ex.reps}</span>
</div>
{ex.notes && <div style={{fontSize: '11px', color: '#9ca3af', marginTop: '2px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'}}>{ex.notes}</div>}
</div>
<div style={{display: 'flex', gap: '4px', flexShrink: 0}}>
<button onClick={() => handleRegenerateExercise(i, ex)} disabled={regeneratingIndex === i} style={{border: 'none', background: '#f3f4f6', color: '#6b7280', borderRadius: '6px', padding: '6px', cursor: 'pointer', display: 'flex'}} title="Swap">
<RefreshCw size={13} style={regeneratingIndex === i ? {animation: 'spin 1s linear infinite'} : {}}/>
</button>
<button onClick={() => logFromPlan(ex)} style={{border: 'none', background: '#eff6ff', color: '#2563eb', borderRadius: '6px', padding: '6px', cursor: 'pointer', display: 'flex'}} title="Log">
<PlusCircle size={13}/>
</button>
<button onClick={() => removeExerciseFromPlan(i)} style={{border: 'none', background: '#fef2f2', color: '#ef4444', borderRadius: '6px', padding: '6px', cursor: 'pointer', display: 'flex'}} title="Remove">
<Trash2 size={13}/>
</button>
</div>
</div>
))}
{/* Gemini Exercise Search */}
<div style={{marginTop: '12px', position: 'relative', background: '#f9fafb', borderRadius: '12px', padding: '12px', border: '1px solid #e5e7eb'}}>
<div style={{display: 'flex', gap: '8px', alignItems: 'center'}}>
<Sparkles size={16} color="#8b5cf6" />
<span style={{fontSize: '12px', fontWeight: '700', color: '#6b7280', textTransform: 'uppercase', letterSpacing: '0.5px'}}>Find Exercises with AI</span>
</div>
<div style={{display: 'flex', gap: '8px', marginTop: '8px'}}>
<input
type="text"
placeholder="e.g. chest exercises, bicep curls..."
value={addExerciseInput}
onChange={e => 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'
}}
/>
<button
onClick={handleSearchExercises}
disabled={exerciseSearching || !addExerciseInput.trim()}
style={{
background: addExerciseInput.trim() ? '#8b5cf6' : '#e5e7eb',
color: 'white', border: 'none', borderRadius: '8px',
padding: '10px 14px', cursor: addExerciseInput.trim() ? 'pointer' : 'default',
display: 'flex', alignItems: 'center', fontWeight: '600', fontSize: '13px'
}}
>
{exerciseSearching ? <Loader2 size={16} style={{animation: 'spin 1s linear infinite'}}/> : 'Search'}
</button>
</div>
{/* Search Results - Popover */}
{exerciseSearchResults.length > 0 && (
<div style={{
position: 'absolute',
bottom: '100%',
left: 0,
right: 0,
zIndex: 20,
background: 'white',
borderRadius: '12px',
border: '2px solid #8b5cf6',
boxShadow: '0 -8px 24px rgba(0,0,0,0.15)',
marginBottom: '4px',
overflow: 'hidden'
}}>
<div style={{padding: '10px 12px', background: '#f5f3ff', borderBottom: '1px solid #e5e7eb', display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
<span style={{fontSize: '12px', fontWeight: '700', color: '#7c3aed'}}>Tap to add</span>
<button onClick={() => setExerciseSearchResults([])} style={{border: 'none', background: 'none', cursor: 'pointer', color: '#9ca3af', padding: '2px', display: 'flex'}}><X size={14}/></button>
</div>
{exerciseSearchResults.map((ex, i) => (
<button
key={i}
onClick={() => addSearchResultToPlan(ex)}
style={{
width: '100%', padding: '10px 12px',
border: 'none', borderBottom: i < exerciseSearchResults.length - 1 ? '1px solid #f3f4f6' : 'none',
background: 'white', cursor: 'pointer',
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
textAlign: 'left'
}}
>
<div>
<div style={{fontWeight: '600', fontSize: '13px', color: '#111827'}}>{ex.name}</div>
<div style={{fontSize: '11px', color: '#6b7280'}}>{ex.sets}×{ex.reps} {ex.notes ? `· ${ex.notes}` : ''}</div>
</div>
<Plus size={16} color="#2563eb" />
</button>
))}
</div>
)}
</div>
</div>
)}
{/* --- NEW WORKOUT HISTORY SECTION --- */}
{/* --- WORKOUT ANALYZE TAB --- */}
{modal === 'workout' && workoutTab === 'analyze' && (
<div style={{display: 'flex', flexDirection: 'column', gap: '16px'}}>
{/* Upload Area */}
<div
onClick={() => 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 ? (
<div>
<video
src={URL.createObjectURL(analysisVideo)}
controls
style={{width: '100%', borderRadius: '12px', maxHeight: '200px'}}
onClick={e => e.stopPropagation()}
/>
<div style={{fontSize: '12px', color: '#6b7280', marginTop: '8px'}}>
{analysisVideo.name} — Tap to change
</div>
</div>
) : (
<div>
<Upload size={32} color="#9ca3af" style={{margin: '0 auto 8px'}} />
<div style={{fontWeight: '600', color: '#374151', fontSize: '15px'}}>Upload Workout Video</div>
<div style={{fontSize: '12px', color: '#9ca3af', marginTop: '4px'}}>Any sport or exercise — keep it under 30s for best results</div>
</div>
)}
</div>
<input
type="file"
ref={videoInputRef}
onChange={e => { if (e.target.files[0]) { setAnalysisVideo(e.target.files[0]); setAnalysisResult(null); } }}
style={{display: 'none'}}
accept="video/*"
/>
{/* Description Input */}
<textarea
placeholder="Describe the workout or sport (e.g. 'barbell back squat', 'basketball free throw', 'deadlift form check')..."
value={analysisDescription}
onChange={e => setAnalysisDescription(e.target.value)}
rows={2}
style={{
width: '100%',
padding: '12px',
borderRadius: '12px',
border: '2px solid #e5e7eb',
fontSize: '14px',
fontFamily: 'inherit',
resize: 'none',
outline: 'none',
boxSizing: 'border-box'
}}
/>
{/* Analyze Button */}
<button
onClick={handleWorkoutAnalysis}
disabled={analysisLoading || !analysisVideo || !analysisDescription.trim()}
style={{
width: '100%',
padding: '14px',
background: analysisLoading || !analysisVideo || !analysisDescription.trim() ? '#d1d5db' : 'linear-gradient(135deg, #2563eb, #7c3aed)',
color: 'white',
border: 'none',
borderRadius: '12px',
fontSize: '15px',
fontWeight: '700',
cursor: analysisLoading ? 'wait' : 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px'
}}
>
{analysisLoading ? <><Loader2 size={18} style={{animation: 'spin 1s linear infinite'}} /> Analyzing...</> : <><Sparkles size={18} /> Analyze My Form</>}
</button>
{/* Results */}
{analysisResult && (
<div style={{background: 'white', borderRadius: '16px', border: '2px solid #e5e7eb', overflow: 'hidden'}}>
{/* Score Header */}
<div style={{
background: analysisResult.score >= 4 ? 'linear-gradient(135deg, #10b981, #059669)' : analysisResult.score >= 3 ? 'linear-gradient(135deg, #f59e0b, #d97706)' : 'linear-gradient(135deg, #ef4444, #dc2626)',
padding: '20px',
textAlign: 'center',
color: 'white'
}}>
<div style={{fontSize: '42px', fontWeight: '800'}}>{analysisResult.score}<span style={{fontSize: '20px', opacity: 0.8}}>/5</span></div>
<div style={{fontSize: '13px', opacity: 0.9, marginTop: '4px'}}>{analysisResult.summary}</div>
</div>
{/* Strengths */}
<div style={{padding: '16px', borderBottom: '1px solid #f3f4f6'}}>
<div style={{fontWeight: '700', fontSize: '14px', color: '#10b981', marginBottom: '8px'}}>What You Did Well</div>
{analysisResult.strengths?.map((s, i) => (
<div key={i} style={{fontSize: '13px', color: '#374151', padding: '6px 0', display: 'flex', gap: '8px', alignItems: 'flex-start'}}>
<span style={{color: '#10b981', flexShrink: 0}}>+</span> {s}
</div>
))}
</div>
{/* Improvements */}
<div style={{padding: '16px', borderBottom: '1px solid #f3f4f6'}}>
<div style={{fontWeight: '700', fontSize: '14px', color: '#f59e0b', marginBottom: '8px'}}>Things to Improve</div>
{analysisResult.improvements?.map((s, i) => (
<div key={i} style={{fontSize: '13px', color: '#374151', padding: '6px 0', display: 'flex', gap: '8px', alignItems: 'flex-start'}}>
<span style={{color: '#f59e0b', flexShrink: 0}}>!</span> {s}
</div>
))}
</div>
{/* Coaching Tip */}
{analysisResult.tips && (
<div style={{padding: '16px', background: '#f0f9ff'}}>
<div style={{fontWeight: '700', fontSize: '14px', color: '#2563eb', marginBottom: '6px'}}>Coach's Tip</div>
<div style={{fontSize: '13px', color: '#374151'}}>{analysisResult.tips}</div>
</div>
)}
</div>
)}
</div>
)}
{modal === 'workout' && workoutTab === 'history' && (
<div>
<input
style={input}
placeholder="Search history..."
value={searchHistory}
onChange={e => setSearchHistory(e.target.value)}
/>
<div style={{maxHeight: '400px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '12px', paddingTop: '8px'}}>
{(() => {
// Filter workouts
const filteredWorkouts = workouts
.filter(w => w && w.exercise)
.filter(w => !hiddenHistory.includes(w.exercise))
.filter(w => searchHistory === '' || w.exercise.toLowerCase().includes(searchHistory.toLowerCase()));
// Group by date
const workoutsByDate = filteredWorkouts.reduce((acc, workout) => {
if (!acc[workout.date]) acc[workout.date] = [];
acc[workout.date].push(workout);
return acc;
}, {});
// Sort dates descending (newest first) - parse as actual dates
const sortedDates = Object.keys(workoutsByDate).sort((a, b) => {
const dateA = new Date(a);
const dateB = new Date(b);
return dateB - dateA; // descending
});
if (sortedDates.length === 0) {
return <div style={{textAlign: 'center', padding: '40px', color: '#9ca3af', fontSize: '14px'}}>
{searchHistory ? 'No matching exercises found' : 'No workout history yet'}
</div>;
}
return sortedDates.map((date, dateIdx) => {
const workoutsForDate = workoutsByDate[date];
return (
<div key={dateIdx} style={{borderRadius: '12px', border: '2px solid #e5e7eb', overflow: 'visible', background: 'white'}}>
{/* Date Header */}
<div style={{background: '#f3f4f6', padding: '12px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #e5e7eb'}}>
<div style={{fontWeight: '700', color: '#374151', fontSize: '14px'}}>
{date === selectedDate ? 'Today' : date}
</div>
<button
onClick={() => addAllWorkoutsFromHistory(workoutsForDate)}
style={{
background: '#10b981',
color: 'white',
border: 'none',
borderRadius: '8px',
padding: '6px 12px',
fontSize: '12px',
fontWeight: '600',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}
>
<Plus size={14}/> Add All ({workoutsForDate.length})
</button>
</div>
{/* ALL WORKOUTS - NO DEDUPLICATION */}
<div style={{display: 'flex', flexDirection: 'column', background: 'white'}}>
{workoutsForDate.map((w, i) => (
<div
key={w.id || i}
onClick={() => addWorkoutFromHistory(w)}
style={{
padding: '14px 12px',
cursor: 'pointer',
borderBottom: i < workoutsForDate.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'}
>
<div style={{flex: 1, minWidth: 0}}>
<div style={{fontWeight: '600', color: '#111827', marginBottom: '6px', fontSize: '15px'}}>{w.exercise}</div>
<div style={{fontSize: '13px', color: '#6b7280', fontWeight: '500'}}>
{w.type === 'Cardio' || w.time
? `${w.time} mins (${w.effort})`
: `${w.weight}lbs • ${w.sets} sets × ${w.reps} reps`
}
</div>
</div>
<button
onClick={(e) => { e.stopPropagation(); deleteHistoryItem(w.exercise, 'workout'); }}
style={{border: 'none', background: 'none', color: '#ef4444', padding: '8px', cursor: 'pointer', flexShrink: 0, marginLeft: '8px'}}
>
<Trash2 size={18}/>
</button>
</div>
))}
</div>
</div>
);
});
})()}
</div>
</div>
)}
</div>
</div>
</div>
)}
{/* --- CALL WEBVIEW MODAL --- */}
{showCallWebView && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: '#000',
zIndex: 100,
display: 'flex',
flexDirection: 'column'
}}>
{/* Header with close button */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '12px 16px',
background: '#1a1a2e',
borderBottom: '1px solid #333'
}}>
<div style={{display: 'flex', alignItems: 'center', gap: '10px'}}>
<div style={{background: '#a855f7', padding: '8px', borderRadius: '10px'}}>
<Phone size={18} color="white" />
</div>
<span style={{color: 'white', fontWeight: '600', fontSize: '16px'}}>dubltalk</span>
{authUser && (
<span
onClick={() => { if(confirm('Logout?')) handleLogout(); }}
style={{color: '#a855f7', fontSize: '12px', cursor: 'pointer'}}
>{authUser.email}</span>
)}
</div>
<button
onClick={() => setShowCallWebView(false)}
style={{
background: '#ef4444',
border: 'none',
cursor: 'pointer',
padding: '8px 16px',
borderRadius: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px',
color: 'white',
fontWeight: '600',
fontSize: '14px'
}}
>
<X size={18} color="white" />
Close
</button>
</div>
<div style={{ flex: 1, position: 'relative' }}>
<iframe
key={`call-${authUser?.uid || 'anon'}`}
src={`https://dubltalk.onrender.com/avatar-selected?avatar=dr.ryan&mode=audio&embed=true${authUser?.uid ? `&userId=${authUser.uid}` : ''}`}
style={{
width: '100%',
height: '100%',
border: 'none',
background: '#000'
}}
allow="microphone; camera; autoplay"
title="dubltalk"
/>
{/* Login overlay - covers the call button when not logged in */}
{!authUser && (
<div style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: '220px',
background: 'linear-gradient(transparent 0%, rgba(0,0,0,1) 30%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
paddingTop: '40px'
}}>
<button
onClick={handleGoogleLogin}
disabled={authLoading}
style={{
background: '#4285f4',
color: 'white',
border: 'none',
borderRadius: '30px',
padding: '16px 40px',
fontSize: '16px',
fontWeight: '600',
cursor: 'pointer',
boxShadow: '0 4px 15px rgba(66,133,244,0.4)'
}}
>
{authLoading ? 'Signing in...' : 'Sign in to Start Call'}
</button>
</div>
)}
</div>
</div>
)}
<style>{`
@keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes wave {
0%, 100% { transform: translateX(-5%) rotate(-2deg); }
50% { transform: translateX(5%) rotate(2deg); }
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden; /* Prevents the 'pull' on the whole screen */
position: fixed; /* Hard-locks the background */
}
#root {
width: 100%;
height: 100%;
overflow-y: auto; /* Only allows scrolling if content is actually taller than screen */
-webkit-overflow-scrolling: touch;
}
body {
-webkit-tap-highlight-color: transparent;
overscroll-behavior-y: none;
}
`}</style>
</div>
);
}