Spaces:
Running
Running
| import { initializeApp } from "https://www.gstatic.com/firebasejs/10.8.0/firebase-app.js"; | |
| import { getAuth, createUserWithEmailAndPassword, signInWithEmailAndPassword, signOut, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/10.8.0/firebase-auth.js"; | |
| import { getFirestore, collection, addDoc, deleteDoc, doc, onSnapshot, query, orderBy, where, setDoc, getDoc, limit } from "https://www.gstatic.com/firebasejs/10.8.0/firebase-firestore.js"; | |
| // --- CONFIGURATION --- | |
| const firebaseConfig = { | |
| apiKey: "AIzaSyACa6exkcvxegYEsudPpw9puJt7NcZQYrY", | |
| authDomain: "calories-75d65.firebaseapp.com", | |
| projectId: "calories-75d65", | |
| storageBucket: "calories-75d65.firebasestorage.app", | |
| messagingSenderId: "875037676758", | |
| appId: "1:875037676758:web:3db8c1bf501f08d740e577", | |
| measurementId: "G-G9H9Q84MEG" | |
| }; | |
| const app = initializeApp(firebaseConfig); | |
| const auth = getAuth(app); | |
| const db = getFirestore(app); | |
| const MEALS_COL = "meals"; | |
| const PREFS_COL = "user_settings"; | |
| // State | |
| let currentUser = null; | |
| let unsubscribeMeals = null; | |
| let PROFILE = { | |
| calories: 2500, protein: 180, carbs: 250, fat: 80, | |
| weight: 180, tdee: 2500 | |
| }; | |
| // UI Refs | |
| const authView = document.getElementById('auth-view'); | |
| const appView = document.getElementById('app-view'); | |
| const errorMsg = document.getElementById('auth-error'); | |
| const modal = document.getElementById('settings-modal'); | |
| // Display & Bar Refs | |
| const displays = { | |
| cals: document.getElementById('total-cals'), | |
| prot: document.getElementById('total-prot'), | |
| carb: document.getElementById('total-carb'), | |
| fat: document.getElementById('total-fat'), | |
| predWeight: document.getElementById('pred-weight'), | |
| predDiff: document.getElementById('pred-diff'), | |
| calPercent: document.getElementById('cal-percent'), | |
| calBarFill: document.getElementById('cal-bar-fill'), | |
| protBar: document.getElementById('prot-bar'), | |
| carbBar: document.getElementById('carb-bar'), | |
| fatBar: document.getElementById('fat-bar') | |
| }; | |
| const ring = document.getElementById('ring-cals'); | |
| const circumference = 2 * Math.PI * 42; | |
| // Initialize ring | |
| ring.style.strokeDasharray = `${circumference} ${circumference}`; | |
| ring.style.strokeDashoffset = circumference; | |
| // Set greeting based on time | |
| function setGreeting() { | |
| const hour = new Date().getHours(); | |
| const greeting = document.getElementById('greeting'); | |
| if (hour < 12) greeting.textContent = 'Good morning'; | |
| else if (hour < 18) greeting.textContent = 'Good afternoon'; | |
| else greeting.textContent = 'Good evening'; | |
| } | |
| // --- AUTH HANDLERS --- | |
| document.getElementById('login-btn').addEventListener('click', () => { | |
| const e = document.getElementById('email').value; | |
| const p = document.getElementById('password').value; | |
| errorMsg.innerText = "Signing in..."; | |
| signInWithEmailAndPassword(auth, e, p).catch(err => showError(err)); | |
| }); | |
| document.getElementById('signup-btn').addEventListener('click', () => { | |
| const e = document.getElementById('email').value; | |
| const p = document.getElementById('password').value; | |
| errorMsg.innerText = "Creating account..."; | |
| createUserWithEmailAndPassword(auth, e, p).catch(err => showError(err)); | |
| }); | |
| document.getElementById('logout-btn').addEventListener('click', () => signOut(auth)); | |
| onAuthStateChanged(auth, (user) => { | |
| if (user) { | |
| currentUser = user; | |
| authView.classList.add('hidden'); | |
| appView.classList.remove('hidden'); | |
| setGreeting(); | |
| loadSettings(); | |
| loadMeals(); | |
| } else { | |
| currentUser = null; | |
| authView.classList.remove('hidden'); | |
| appView.classList.add('hidden'); | |
| if(unsubscribeMeals) unsubscribeMeals(); | |
| errorMsg.innerText = ""; | |
| } | |
| }); | |
| function showError(error) { | |
| let msg = error.code.replace("auth/", "").replace(/-/g, " "); | |
| errorMsg.innerText = "Error: " + msg; | |
| } | |
| // --- SETTINGS HANDLERS --- | |
| document.getElementById('settings-btn').addEventListener('click', () => { | |
| document.getElementById('set-cals').value = PROFILE.calories; | |
| document.getElementById('set-prot').value = PROFILE.protein; | |
| document.getElementById('set-carb').value = PROFILE.carbs; | |
| document.getElementById('set-fat').value = PROFILE.fat; | |
| document.getElementById('set-weight').value = PROFILE.weight || 180; | |
| document.getElementById('set-tdee').value = PROFILE.tdee || 2000; | |
| modal.style.display = 'flex'; | |
| }); | |
| document.getElementById('close-settings').addEventListener('click', () => modal.style.display = 'none'); | |
| modal.addEventListener('click', (e) => { | |
| if (e.target === modal) modal.style.display = 'none'; | |
| }); | |
| document.getElementById('settings-form').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const newProfile = { | |
| calories: parseInt(document.getElementById('set-cals').value) || 2000, | |
| protein: parseInt(document.getElementById('set-prot').value) || 150, | |
| carbs: parseInt(document.getElementById('set-carb').value) || 200, | |
| fat: parseInt(document.getElementById('set-fat').value) || 60, | |
| weight: parseFloat(document.getElementById('set-weight').value) || 180, | |
| tdee: parseInt(document.getElementById('set-tdee').value) || 2000, | |
| }; | |
| try { | |
| await setDoc(doc(db, PREFS_COL, currentUser.uid), newProfile); | |
| PROFILE = newProfile; | |
| updateGoalsUI(); | |
| modal.style.display = 'none'; | |
| location.reload(); | |
| } catch (e) { | |
| alert("Error saving settings: " + e.message); | |
| } | |
| }); | |
| async function loadSettings() { | |
| const docSnap = await getDoc(doc(db, PREFS_COL, currentUser.uid)); | |
| if (docSnap.exists()) PROFILE = docSnap.data(); | |
| updateGoalsUI(); | |
| } | |
| function updateGoalsUI() { | |
| document.getElementById('goal-label').innerText = `of ${PROFILE.calories} kcal`; | |
| } | |
| // --- DATA LOGIC --- | |
| function loadMeals() { | |
| const q = query( | |
| collection(db, MEALS_COL), | |
| where("uid", "==", currentUser.uid), | |
| orderBy("timestamp", "desc"), | |
| limit(50) | |
| ); | |
| unsubscribeMeals = onSnapshot(q, | |
| (snap) => { | |
| const entries = []; | |
| snap.forEach(doc => entries.push({ ...doc.data(), id: doc.id })); | |
| render(entries); | |
| }, | |
| (error) => { | |
| console.error(error); | |
| if(error.message.includes("index")) { | |
| alert("Database Index Missing. Check console for link."); | |
| } | |
| } | |
| ); | |
| } | |
| document.getElementById('tracker-form').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| if(!currentUser) return; | |
| const btn = document.getElementById('submit-btn'); | |
| const btnText = btn.querySelector('span'); | |
| btnText.innerText = "Adding..."; | |
| try { | |
| await addDoc(collection(db, MEALS_COL), { | |
| uid: currentUser.uid, | |
| name: document.getElementById('in-name').value, | |
| cals: parseInt(document.getElementById('in-cals').value) || 0, | |
| prot: parseInt(document.getElementById('in-prot').value) || 0, | |
| carb: parseInt(document.getElementById('in-carb').value) || 0, | |
| fat: parseInt(document.getElementById('in-fat').value) || 0, | |
| timestamp: Date.now() | |
| }); | |
| e.target.reset(); | |
| document.getElementById('in-name').focus(); | |
| } catch (e) { | |
| alert("Error adding meal: " + e.message); | |
| } | |
| btnText.innerText = "Add Entry"; | |
| }); | |
| window.deleteEntry = async (id) => { | |
| if(confirm("Delete this entry?")) await deleteDoc(doc(db, MEALS_COL, id)); | |
| } | |
| // --- RENDER UI & PREDICTION --- | |
| function render(entries) { | |
| const todayStr = new Date().toDateString(); | |
| const todaysEntries = entries.filter(item => { | |
| const itemDate = new Date(item.timestamp).toDateString(); | |
| return itemDate === todayStr; | |
| }); | |
| const totals = todaysEntries.reduce((acc, item) => { | |
| acc.cals += item.cals; acc.prot += item.prot; acc.carb += item.carb; acc.fat += item.fat; | |
| return acc; | |
| }, { cals: 0, prot: 0, carb: 0, fat: 0 }); | |
| // Update Main Stats | |
| displays.cals.innerText = totals.cals.toLocaleString(); | |
| displays.prot.innerText = totals.prot + 'g'; | |
| displays.carb.innerText = totals.carb + 'g'; | |
| displays.fat.innerText = totals.fat + 'g'; | |
| // Calculate percentages | |
| const calPercent = Math.min((totals.cals / PROFILE.calories) * 100, 100); | |
| const protPercent = Math.min((totals.prot / PROFILE.protein) * 100, 100); | |
| const carbPercent = Math.min((totals.carb / PROFILE.carbs) * 100, 100); | |
| const fatPercent = Math.min((totals.fat / PROFILE.fat) * 100, 100); | |
| // Update percentage display | |
| displays.calPercent.innerText = Math.round(calPercent) + '%'; | |
| // Update ring | |
| const offset = circumference - (calPercent / 100) * circumference; | |
| ring.style.strokeDashoffset = offset; | |
| // Update bars | |
| displays.calBarFill.style.width = calPercent + '%'; | |
| displays.protBar.style.width = protPercent + '%'; | |
| displays.carbBar.style.width = carbPercent + '%'; | |
| displays.fatBar.style.width = fatPercent + '%'; | |
| // Over limit styling | |
| const goalLabel = document.getElementById('goal-label'); | |
| const isOver = totals.cals > PROFILE.calories; | |
| displays.cals.classList.toggle('over', isOver); | |
| ring.classList.toggle('over', isOver); | |
| displays.calBarFill.classList.toggle('over', isOver); | |
| goalLabel.classList.toggle('over', isOver); | |
| if (isOver) { | |
| goalLabel.innerText = `Over by ${(totals.cals - PROFILE.calories).toLocaleString()} kcal`; | |
| } else { | |
| goalLabel.innerText = `of ${PROFILE.calories.toLocaleString()} kcal`; | |
| } | |
| // --- WEIGHT PREDICTION --- | |
| const dailyDeficit = totals.cals - (PROFILE.tdee || 2000); | |
| const totalChangeCals = dailyDeficit * 28; | |
| const lbsChange = totalChangeCals / 3500; | |
| const futureWeight = (PROFILE.weight || 180) + lbsChange; | |
| displays.predWeight.innerText = futureWeight.toFixed(1) + " lbs"; | |
| displays.predWeight.classList.toggle('gain', lbsChange > 0); | |
| const changeIndicator = document.querySelector('.change-indicator'); | |
| const changeText = document.querySelector('.change-text'); | |
| changeIndicator.classList.toggle('gain', lbsChange > 0); | |
| if (lbsChange > 0) { | |
| changeText.innerText = `+${lbsChange.toFixed(1)} lbs gain`; | |
| } else { | |
| changeText.innerText = `${lbsChange.toFixed(1)} lbs loss`; | |
| } | |
| // --- LIST RENDER --- | |
| const list = document.getElementById('log-list'); | |
| list.innerHTML = ''; | |
| entries.forEach(item => { | |
| const itemDateStr = new Date(item.timestamp).toDateString(); | |
| const isToday = itemDateStr === todayStr; | |
| const timeLabel = new Date(item.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); | |
| const dateLabel = isToday ? timeLabel : new Date(item.timestamp).toLocaleDateString([], {month:'short', day:'numeric'}); | |
| const li = document.createElement('li'); | |
| li.className = `log-item${isToday ? '' : ' old'}`; | |
| li.innerHTML = ` | |
| <div class="item-details"> | |
| <h4>${item.name}</h4> | |
| <span class="item-time">${dateLabel}</span> | |
| <div class="item-macros"> | |
| <span class="p">${item.prot}p</span> | |
| <span class="c">${item.carb}c</span> | |
| <span class="f">${item.fat}f</span> | |
| </div> | |
| </div> | |
| <div class="item-cals">${item.cals}</div> | |
| <button class="del-btn" onclick="deleteEntry('${item.id}')"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"> | |
| <line x1="18" y1="6" x2="6" y2="18"/> | |
| <line x1="6" y1="6" x2="18" y2="18"/> | |
| </svg> | |
| </button> | |
| `; | |
| list.appendChild(li); | |
| }); | |
| } |