import React, { useState, useEffect, useRef } from 'react'; import { initializeApp, getApps, getApp } from 'firebase/app'; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth'; import { getFirestore, collection, addDoc, query, orderBy, onSnapshot, serverTimestamp, Timestamp } from 'firebase/firestore'; import { Line } from 'react-chartjs-2'; import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend } from 'chart.js'; // Register Chart.js components ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend); // Define global variables for Firebase configuration (provided by the environment) const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {}; const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null; // Initialize Firebase (will be done once in useEffect) let app = null; let db = null; let auth = null; // Check if Firebase app is already initialized if (!getApps().length) { app = initializeApp(firebaseConfig); db = getFirestore(app); auth = getAuth(app); } function App() { const [weight, setWeight] = useState(''); const [height, setHeight] = useState(''); const [age, setAge] = useState(''); const [neckCircumference, setNeckCircumference] = useState(''); const [waistCircumference, setWaistCircumference] = useState(''); const [hipCircumference, setHipCircumference] = useState(''); // Only for female formula const [femininityPercentage, setFemininityPercentage] = useState(50); // 0-100, 0=Masculine, 100=Feminine const [calculatedBMI, setCalculatedBMI] = useState(null); const [calculatedBF, setCalculatedBF] = useState(null); const [calculatedLeanMass, setCalculatedLeanMass] = useState(null); const [calculatedFatMass, setCalculatedFatMass] = useState(null); const [notes, setNotes] = useState(''); const [history, setHistory] = useState([]); const [userId, setUserId] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [message, setMessage] = useState(''); const [unitSystem, setUnitSystem] = useState('imperial'); // 'imperial' or 'metric' const [inputDate, setInputDate] = useState(new Date().toISOString().split('T')[0]); // Default to current date const isFirebaseInitialized = useRef(false); const appVersion = "0.6"; // Define the app version // Initialize Firebase and set up authentication useEffect(() => { const initializeFirebase = async () => { if (isFirebaseInitialized.current) return; // Use the already initialized app if it exists if (!app) { app = initializeApp(firebaseConfig); db = getFirestore(app); auth = getAuth(app); } try { // Sign in with custom token if available, otherwise anonymously if (initialAuthToken) { await signInWithCustomToken(auth, initialAuthToken); } else { await signInAnonymously(auth); } // Listen for auth state changes onAuthStateChanged(auth, (user) => { if (user) { setUserId(user.uid); setLoading(false); } else { setUserId(crypto.randomUUID()); // Fallback for unauthenticated or anonymous users setLoading(false); } }); isFirebaseInitialized.current = true; } catch (err) { console.error("Firebase initialization error:", err); setError("Failed to initialize Firebase. Please try again later."); setLoading(false); } }; initializeFirebase(); }, []); // Fetch data from Firestore when userId is available useEffect(() => { if (!userId || !db) return; const q = query( collection(db, `artifacts/${appId}/users/${userId}/bodyCompositionData`), orderBy('date', 'desc') // Order by date descending for history ); const unsubscribe = onSnapshot(q, (snapshot) => { const data = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data(), // If customDate is present, use it, otherwise fall back to serverTimestamp date: doc.data().customDate || doc.data().date?.toDate().toISOString().split('T')[0] || 'N/A' })); setHistory(data); }, (err) => { console.error("Error fetching data:", err); setError("Failed to load historical data."); }); return () => unsubscribe(); // Cleanup listener on component unmount }, [userId]); // Depend on userId to re-run when auth state is ready // Conversion functions const kgToLbs = (kg) => kg * 2.20462; const lbsToKg = (lbs) => lbs / 2.20462; const cmToInches = (cm) => cm * 0.393701; const inchesToCm = (inches) => inches / 0.393701; // Function to calculate BMI (always uses lbs and inches internally) const calculateBMI = (weightLbs, heightInches) => { if (!weightLbs || !heightInches) return null; return (weightLbs / (heightInches * heightInches)) * 703; }; // Function to calculate Body Fat Percentage using U.S. Navy Method (always uses inches internally) const calculateBodyFatNavyMethod = (weightLbs, heightInches, neckInches, waistInches, hipInches, femininity) => { if (!weightLbs || !heightInches || !neckInches || !waistInches) return null; // Rounding rules for U.S. Navy Method const roundedNeck = Math.ceil(neckInches * 2) / 2; // round up to nearest 0.5 inch const roundedWaist = Math.floor(waistInches * 2) / 2; // round down to nearest 0.5 inch const roundedHeight = Math.round(heightInches * 2) / 2; // round to nearest 0.5 inch // Male formula const bfMale = 86.010 * Math.log10(roundedWaist - roundedNeck) - 70.041 * Math.log10(roundedHeight) + 36.76; // Female formula (requires hip circumference) let bfFemale = null; if (hipInches) { const roundedHip = Math.floor(hipInches * 2) / 2; // round down to nearest 0.5 inch bfFemale = 163.205 * Math.log10(roundedWaist + roundedHip - roundedNeck) - 97.684 * Math.log10(roundedHeight) - 78.387; } // Interpolate based on femininity percentage if (bfFemale === null) { // If hip is not provided, we can't calculate female formula, so just use male. // Or, we could default to a more general formula if available, but for now, // we'll highlight that hip is needed for full interpolation. return bfMale; // Fallback to male if female calculation is not possible } // Linear interpolation: 0% femininity = 100% Male, 100% femininity = 100% Female const interpolationFactor = femininity / 100; const interpolatedBF = bfMale * (1 - interpolationFactor) + bfFemale * interpolationFactor; return interpolatedBF; }; const handleCalculate = () => { setError(''); setMessage(''); let w = parseFloat(weight); let h = parseFloat(height); let n = parseFloat(neckCircumference); let wa = parseFloat(waistCircumference); let hi = parseFloat(hipCircumference); const a = parseFloat(age); if (isNaN(w) || isNaN(h) || isNaN(a) || isNaN(n) || isNaN(wa)) { setError('Please enter valid numbers for all required fields (Weight, Height, Age, Neck, Waist).'); return; } // Convert to imperial units if current system is metric let weightLbs = w; let heightInches = h; let neckInches = n; let waistInches = wa; let hipInches = hi; if (unitSystem === 'metric') { weightLbs = kgToLbs(w); heightInches = cmToInches(h); neckInches = cmToInches(n); waistInches = cmToInches(wa); hipInches = hi ? cmToInches(hi) : null; } // Calculate BMI const bmi = calculateBMI(weightLbs, heightInches); setCalculatedBMI(bmi ? bmi.toFixed(2) : null); // Calculate Body Fat const bf = calculateBodyFatNavyMethod(weightLbs, heightInches, neckInches, waistInches, hipInches, femininityPercentage); setCalculatedBF(bf ? bf.toFixed(2) : null); // Calculate Lean Mass and Fat Mass if BF is available if (bf !== null) { const fatMassLbs = (bf / 100) * weightLbs; const leanMassLbs = weightLbs - fatMassLbs; // Convert back to metric if unit system is metric setCalculatedFatMass(unitSystem === 'metric' ? lbsToKg(fatMassLbs).toFixed(2) : fatMassLbs.toFixed(2)); setCalculatedLeanMass(unitSystem === 'metric' ? lbsToKg(leanMassLbs).toFixed(2) : leanMassLbs.toFixed(2)); } else { setCalculatedFatMass(null); setCalculatedLeanMass(null); } }; const handleSave = async () => { setError(''); setMessage(''); if (!userId) { setError("User not authenticated. Please wait or refresh."); return; } if (calculatedBMI === null || calculatedBF === null) { setError("Please calculate body composition before saving."); return; } try { // Use the inputDate if provided, otherwise use serverTimestamp const timestampToSave = inputDate ? inputDate : serverTimestamp(); await addDoc(collection(db, `artifacts/${appId}/users/${userId}/bodyCompositionData`), { date: serverTimestamp(), // Keep serverTimestamp for ordering customDate: inputDate, // Store the user-selected date string weight: parseFloat(weight), height: parseFloat(height), age: parseInt(age), neckCircumference: parseFloat(neckCircumference), waistCircumference: parseFloat(waistCircumference), hipCircumference: parseFloat(hipCircumference || 0), // Store 0 if not provided femininityPercentage: parseInt(femininityPercentage), calculatedBMI: parseFloat(calculatedBMI), calculatedBF: parseFloat(calculatedBF), calculatedLeanMass: parseFloat(calculatedLeanMass), calculatedFatMass: parseFloat(calculatedFatMass), notes: notes, unitSystem: unitSystem, // Save the unit system used for this entry }); setMessage("Data saved successfully!"); setNotes(''); // Clear notes after saving setInputDate(new Date().toISOString().split('T')[0]); // Reset date to current } catch (e) { console.error("Error adding document: ", e); setError("Failed to save data. Please try again."); } }; const handleExportToCSV = () => { if (history.length === 0) { setError("No data to export."); return; } // Define CSV headers const headers = [ 'Date', `Weight (${unitSystem === 'imperial' ? 'lbs' : 'kg'})`, `Height (${unitSystem === 'imperial' ? 'in' : 'cm'})`, 'Age', `Neck Circumference (${unitSystem === 'imperial' ? 'in' : 'cm'})`, `Waist Circumference (${unitSystem === 'imperial' ? 'in' : 'cm'})`, `Hip Circumference (${unitSystem === 'imperial' ? 'in' : 'cm'})`, 'Femininity Percentage', 'BMI', 'Body Fat %', `Lean Mass (${unitSystem === 'imperial' ? 'lbs' : 'kg'})`, `Fat Mass (${unitSystem === 'imperial' ? 'lbs' : 'kg'})`, 'Notes', 'Unit System' ]; // Format data rows const csvRows = history.map(entry => { // Ensure values are strings and handle commas in notes by enclosing in quotes const formattedNotes = `"${(entry.notes || '').replace(/"/g, '""')}"`; // Escape double quotes // Convert to currently selected unit system for export display const displayWeight = entry.unitSystem === unitSystem ? entry.weight : unitSystem === 'imperial' ? kgToLbs(entry.weight).toFixed(2) : lbsToKg(entry.weight).toFixed(2); const displayHeight = entry.unitSystem === unitSystem ? entry.height : unitSystem === 'imperial' ? cmToInches(entry.height).toFixed(2) : inchesToCm(entry.height).toFixed(2); const displayNeck = entry.unitSystem === unitSystem ? entry.neckCircumference : unitSystem === 'imperial' ? cmToInches(entry.neckCircumference).toFixed(2) : inchesToCm(entry.neckCircumference).toFixed(2); const displayWaist = entry.unitSystem === unitSystem ? entry.waistCircumference : unitSystem === 'imperial' ? cmToInches(entry.waistCircumference).toFixed(2) : inchesToCm(entry.waistCircumference).toFixed(2); const displayHip = entry.unitSystem === unitSystem ? (entry.hipCircumference || '') : (entry.hipCircumference ? (unitSystem === 'imperial' ? cmToInches(entry.hipCircumference).toFixed(2) : inchesToCm(entry.hipCircumference).toFixed(2)) : ''); return [ entry.date, displayWeight, displayHeight, entry.age, displayNeck, displayWaist, displayHip, entry.femininityPercentage, entry.calculatedBMI, entry.calculatedBF, entry.calculatedLeanMass, entry.calculatedFatMass, formattedNotes, entry.unitSystem ].join(','); }); // Combine headers and rows const csvContent = [ headers.join(','), ...csvRows ].join('\n'); // Create a Blob and trigger download const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'body_composition_data.csv'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); setMessage("Data exported successfully to body_composition_data.csv!"); }; // Prepare data for Chart.js const chartData = { labels: history.map(entry => entry.date).reverse(), // Reverse to show oldest first datasets: [ { label: `Weight (${unitSystem === 'imperial' ? 'lbs' : 'kg'})`, data: history.map(entry => unitSystem === 'imperial' ? entry.weight : lbsToKg(entry.weight)).reverse(), borderColor: 'rgb(75, 192, 192)', backgroundColor: 'rgba(75, 192, 192, 0.5)', yAxisID: 'y', }, { label: 'Body Fat %', data: history.map(entry => entry.calculatedBF).reverse(), borderColor: 'rgb(255, 99, 132)', backgroundColor: 'rgba(255, 99, 132, 0.5)', yAxisID: 'y1', }, { label: 'Femininity %', data: history.map(entry => entry.femininityPercentage).reverse(), borderColor: 'rgb(153, 102, 255)', backgroundColor: 'rgba(153, 102, 255, 0.5)', yAxisID: 'y2', } ], }; const chartOptions = { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false, }, stacked: false, plugins: { title: { display: true, text: 'Body Composition Trends', }, }, scales: { x: { title: { display: true, text: 'Date', }, }, y: { type: 'linear', display: true, position: 'left', title: { display: true, text: `Weight (${unitSystem === 'imperial' ? 'lbs' : 'kg'})`, }, }, y1: { type: 'linear', display: true, position: 'right', title: { display: true, text: 'Body Fat %', }, grid: { drawOnChartArea: false, // Only draw grid lines for the first axis }, }, y2: { type: 'linear', display: true, position: 'right', title: { display: true, text: 'Femininity %', }, grid: { drawOnChartArea: false, // Only draw grid lines for the first axis }, }, }, }; if (loading) { return (
Loading app...
); } return (

Inclusive Body Composition Tracker Version {appVersion}

{error && (
Error! {error}
)} {message && (
Success! {message}
)}

Your Measurements

setInputDate(e.target.value)} className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm" />
setWeight(e.target.value)} className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm" placeholder={unitSystem === 'imperial' ? 'e.g., 150' : 'e.g., 68'} />
setHeight(e.target.value)} className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm" placeholder={unitSystem === 'imperial' ? 'e.g., 68' : 'e.g., 173'} />
setAge(e.target.value)} className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm" placeholder="e.g., 30" />
setNeckCircumference(e.target.value)} className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm" placeholder={unitSystem === 'imperial' ? 'e.g., 15' : 'e.g., 38'} />
setWaistCircumference(e.target.value)} className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm" placeholder={unitSystem === 'imperial' ? 'e.g., 30' : 'e.g., 76'} />
setHipCircumference(e.target.value)} className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm" placeholder={unitSystem === 'imperial' ? 'e.g., 38' : 'e.g., 96'} />

Femininity Crossfade

setFemininityPercentage(e.target.value)} className="mt-1 w-full h-2 bg-purple-200 rounded-lg appearance-none cursor-pointer accent-purple-500" />
Masculine (0%) Feminine (100%)

Adjust this slider to blend between masculine and feminine body composition calculations. This helps visualize changes over time or find an average range.

Calculated Results

BMI: {calculatedBMI || 'N/A'}

Body Fat %: {calculatedBF || 'N/A'}

Lean Mass ({unitSystem === 'imperial' ? 'lbs' : 'kg'}): {calculatedLeanMass || 'N/A'}

Fat Mass ({unitSystem === 'imperial' ? 'lbs' : 'kg'}): {calculatedFatMass || 'N/A'}

Your Progress Over Time

Your User ID: {userId || 'N/A'}

{history.length > 0 ? ( ) : (

No data yet. Save your first entry to see your progress!

)}

Historical Entries

{history.length === 0 ? (

No historical data available.

) : (
{history.map((entry) => ( ))}
Date Weight ({unitSystem === 'imperial' ? 'lbs' : 'kg'}) Height ({unitSystem === 'imperial' ? 'in' : 'cm'}) BF % Femininity % Notes Units
{entry.date} {entry.weight} {entry.height} {entry.calculatedBF} {entry.femininityPercentage} {entry.notes || 'N/A'} {entry.unitSystem || 'imperial'}
)}
); } export default App;