calorie / script.js
yetarnished's picture
Update script.js
0163c16 verified
Raw
History Blame Contribute Delete
11.9 kB
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);
});
}