485 lines
19 KiB
JavaScript
485 lines
19 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function() {
|
|
// Elements
|
|
const statsContainer = document.getElementById('stats-container');
|
|
const logsList = document.getElementById('recent-logs-list');
|
|
const addLogForm = document.getElementById('addLogForm');
|
|
const updateGoalsForm = document.getElementById('updateGoalsForm');
|
|
const weightForm = document.getElementById('weightForm');
|
|
const photoForm = document.getElementById('photoForm');
|
|
|
|
// AI Elements
|
|
const btnAnalyzeAI = document.getElementById('btnAnalyzeAI');
|
|
const aiInput = document.getElementById('aiInput');
|
|
const aiBtnText = document.getElementById('aiBtnText');
|
|
const aiBtnSpinner = document.getElementById('aiBtnSpinner');
|
|
|
|
// Tab Elements
|
|
const periodWeekly = document.getElementById('periodWeekly');
|
|
const periodMonthly = document.getElementById('periodMonthly');
|
|
|
|
let weightChart = null;
|
|
let caloriesChart = null;
|
|
|
|
// --- PWA SERVICE WORKER ---
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker.register('sw.js').then(() => {
|
|
console.log('Service Worker Registered');
|
|
});
|
|
}
|
|
|
|
// --- BOTTOM NAV & TAB HANDLING ---
|
|
const bottomNavItems = document.querySelectorAll('.bottom-nav .nav-item');
|
|
bottomNavItems.forEach(item => {
|
|
item.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
const tabId = this.dataset.tab;
|
|
|
|
// Update UI
|
|
bottomNavItems.forEach(i => i.classList.remove('active'));
|
|
this.classList.add('active');
|
|
|
|
// Show Tab
|
|
const tabTrigger = new bootstrap.Tab(document.querySelector(`#${tabId}-tab`));
|
|
tabTrigger.show();
|
|
|
|
// Special handling for specific tabs
|
|
if (tabId === 'gallery') fetchPhotos();
|
|
if (tabId === 'analysis') renderAnalysis();
|
|
|
|
// Scroll to top
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
});
|
|
});
|
|
|
|
// Initial fetch
|
|
refreshAll();
|
|
|
|
function refreshAll() {
|
|
fetchStats();
|
|
fetchRecentLogs();
|
|
fetchWater();
|
|
fetchWeight();
|
|
fetchSupplements();
|
|
}
|
|
|
|
// --- NUTRITION & STATS ---
|
|
function fetchStats() {
|
|
fetch('api/logs.php?action=get_stats')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
const { goals, consumed, remaining } = data;
|
|
|
|
// Update Goals (UI)
|
|
setText('cal-goal', goals.calories);
|
|
setText('pro-goal', goals.protein);
|
|
|
|
// Update Consumed (UI)
|
|
setText('cal-consumed', consumed.calories);
|
|
setText('pro-consumed', consumed.protein);
|
|
|
|
// Update Left (UI)
|
|
setText('cal-left', Math.max(0, remaining.calories));
|
|
setText('pro-left', Math.max(0, remaining.protein));
|
|
|
|
// Update Progress (UI)
|
|
updateProgress('cal-progress', consumed.calories, goals.calories);
|
|
updateProgress('pro-progress', consumed.protein, goals.protein);
|
|
|
|
// Creatine status in Health tab
|
|
const creGoal = parseFloat(goals.creatine || 0);
|
|
const creCons = parseFloat(consumed.creatine || 0);
|
|
updateProgress('cre-progress-health', creCons, creGoal);
|
|
const creBadge = document.getElementById('cre-status');
|
|
if (creBadge) {
|
|
if (creCons >= creGoal && creGoal > 0) {
|
|
creBadge.innerText = 'Taken';
|
|
creBadge.className = 'badge bg-success';
|
|
} else {
|
|
creBadge.innerText = 'Not taken';
|
|
creBadge.className = 'badge bg-danger';
|
|
}
|
|
}
|
|
|
|
// Pre-fill goal form
|
|
document.getElementById('goalCalories').value = goals.calories;
|
|
document.getElementById('goalProtein').value = goals.protein;
|
|
document.getElementById('goalCreatine').value = goals.creatine;
|
|
document.getElementById('goalWater').value = goals.water || 2.5;
|
|
|
|
updateReminders(consumed, goals);
|
|
});
|
|
}
|
|
|
|
function updateProgress(id, consumed, goal) {
|
|
const el = document.getElementById(id);
|
|
if (!el) return;
|
|
const percent = goal > 0 ? Math.min(100, (consumed / goal) * 100) : 0;
|
|
el.style.width = percent + '%';
|
|
}
|
|
|
|
function setText(id, val) {
|
|
const el = document.getElementById(id);
|
|
if (el) el.innerText = val;
|
|
}
|
|
|
|
function fetchRecentLogs() {
|
|
fetch('api/logs.php?action=get_recent')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.length === 0) {
|
|
logsList.innerHTML = '<div class="text-center py-4"><p class="text-muted small mb-0">No entries today yet.</p></div>';
|
|
return;
|
|
}
|
|
logsList.innerHTML = data.map(log => `
|
|
<div class="log-item">
|
|
<div>
|
|
<div class="fw-bold mb-1">${log.entry_name}</div>
|
|
<div class="log-details">${log.calories} kcal · ${log.protein}g protein ${log.creatine > 0 ? '· ' + log.creatine + 'g creatine' : ''}</div>
|
|
</div>
|
|
<div class="text-muted small">${new Date(log.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</div>
|
|
</div>
|
|
`).join('');
|
|
});
|
|
}
|
|
|
|
// --- WATER ---
|
|
function fetchWater() {
|
|
fetch('api/water.php')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
const amount = data.amount || 0;
|
|
fetch('api/logs.php?action=get_stats')
|
|
.then(res => res.json())
|
|
.then(stats => {
|
|
const goal = stats.goals.water || 2.5;
|
|
|
|
// Health Tab
|
|
setText('water-consumed', amount.toFixed(2));
|
|
setText('water-goal', goal);
|
|
updateProgress('water-progress', amount, goal);
|
|
|
|
// Today Tab
|
|
document.querySelectorAll('.today-water-consumed').forEach(el => el.innerText = amount.toFixed(2));
|
|
document.querySelectorAll('.today-water-goal').forEach(el => el.innerText = goal);
|
|
document.querySelectorAll('.today-water-progress-bar').forEach(el => {
|
|
const pct = goal > 0 ? Math.min(100, (amount / goal) * 100) : 0;
|
|
el.style.width = pct + '%';
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('.water-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const amount = parseFloat(btn.dataset.amount);
|
|
logWater(amount);
|
|
});
|
|
});
|
|
|
|
const btnAddWater = document.getElementById('btnAddWater');
|
|
if (btnAddWater) {
|
|
btnAddWater.addEventListener('click', () => {
|
|
logWater(0.25);
|
|
});
|
|
}
|
|
|
|
function logWater(amount) {
|
|
fetch('api/water.php', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ amount: amount })
|
|
}).then(() => fetchWater());
|
|
}
|
|
|
|
// --- SUPPLEMENTS ---
|
|
function fetchSupplements() {
|
|
fetch('api/supplements.php?action=get_status')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
const container = document.getElementById('today-supplements-list');
|
|
if (!container) return;
|
|
|
|
container.innerHTML = data.map(sup => `
|
|
<button class="btn btn-sm ${sup.taken ? 'btn-success' : 'btn-outline-secondary'} py-2 px-3 rounded-pill supplement-toggle-btn"
|
|
data-id="${sup.id}" data-name="${sup.name}" ${sup.taken ? 'disabled' : ''}>
|
|
${sup.name} ${sup.taken ? '✓' : ''}
|
|
<div class="x-small text-opacity-50">${sup.default_amount || ''}</div>
|
|
</button>
|
|
`).join('');
|
|
|
|
document.querySelectorAll('.supplement-toggle-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
logSupplement(btn.dataset.id, btn.dataset.name);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function logSupplement(id, name) {
|
|
fetch('api/supplements.php?action=log', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ id: id, name: name })
|
|
}).then(() => {
|
|
fetchSupplements();
|
|
fetchStats(); // Update reminders too
|
|
});
|
|
}
|
|
|
|
// --- WEIGHT ---
|
|
function fetchWeight() {
|
|
fetch('api/weight.php')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.length > 0) {
|
|
const last = data[0];
|
|
setText('weight-current', last.weight);
|
|
setText('weight-last-date', 'Last logged: ' + last.logged_at);
|
|
const weightInput = document.getElementById('weightInput');
|
|
if (weightInput) weightInput.value = last.weight;
|
|
const photoWeight = document.getElementById('photoWeight');
|
|
if (photoWeight) photoWeight.value = last.weight;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (weightForm) {
|
|
weightForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const weight = document.getElementById('weightInput').value;
|
|
fetch('api/weight.php', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ weight: weight })
|
|
}).then(res => res.json())
|
|
.then(res => {
|
|
if (res.success) {
|
|
bootstrap.Modal.getInstance(document.getElementById('weightModal')).hide();
|
|
fetchWeight();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- PHOTO GALLERY ---
|
|
function fetchPhotos() {
|
|
fetch('api/photos.php')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
const container = document.getElementById('gallery-container');
|
|
if (!container) return;
|
|
|
|
if (data.length === 0) {
|
|
container.innerHTML = '<div class="col-12 text-center py-5"><p class="text-muted small">No photos uploaded yet.</p></div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = data.map(photo => `
|
|
<div class="col-6">
|
|
<div class="card bg-dark border-secondary overflow-hidden h-100">
|
|
<img src="${photo.photo_path}" class="card-img-top" style="height: 160px; object-fit: cover;" alt="Progress">
|
|
<div class="card-body p-2">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<span class="x-small fw-bold">${photo.logged_at}</span>
|
|
${photo.weight ? `<span class="badge bg-secondary x-small">${photo.weight} kg</span>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
});
|
|
}
|
|
|
|
if (photoForm) {
|
|
photoForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData();
|
|
formData.append('photo', document.getElementById('photoInput').files[0]);
|
|
formData.append('weight', document.getElementById('photoWeight').value);
|
|
|
|
fetch('api/photos.php', {
|
|
method: 'POST',
|
|
body: formData
|
|
}).then(res => res.json())
|
|
.then(res => {
|
|
if (res.success) {
|
|
bootstrap.Modal.getInstance(document.getElementById('photoModal')).hide();
|
|
photoForm.reset();
|
|
fetchPhotos();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- REMINDERS ---
|
|
function updateReminders(consumed, goals) {
|
|
const row = document.getElementById('reminders-row');
|
|
if (!row) return;
|
|
row.innerHTML = '';
|
|
|
|
const items = [
|
|
{ label: 'Water', current: 0, goal: goals.water || 2.5, unit: 'L', color: 'info' },
|
|
{ label: 'Protein', current: consumed.protein, goal: goals.protein, unit: 'g', color: 'primary' }
|
|
];
|
|
|
|
fetch('api/water.php').then(res => res.json()).then(waterData => {
|
|
items[0].current = waterData.amount || 0;
|
|
|
|
items.forEach(item => {
|
|
const done = item.current >= item.goal && item.goal > 0;
|
|
const div = document.createElement('div');
|
|
div.className = `badge ${done ? 'bg-success-subtle text-success' : 'bg-secondary-subtle text-muted'} border border-opacity-10 px-3 py-2 rounded-pill flex-shrink-0 d-flex align-items-center gap-2`;
|
|
div.innerHTML = `
|
|
<span style="width:8px; height:8px;" class="rounded-circle bg-${done ? 'success' : item.color}"></span>
|
|
<span>${item.label}: ${done ? 'Done' : (item.goal - item.current).toFixed(1) + item.unit + ' left'}</span>
|
|
`;
|
|
row.appendChild(div);
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- ANALYSIS ---
|
|
periodWeekly.addEventListener('change', renderAnalysis);
|
|
periodMonthly.addEventListener('change', renderAnalysis);
|
|
|
|
function renderAnalysis() {
|
|
const type = periodMonthly.checked ? 'monthly' : 'weekly';
|
|
fetch(`api/analysis.php?type=${type}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
initWeightChart(data.weight);
|
|
initCaloriesChart(data.nutrition);
|
|
});
|
|
}
|
|
|
|
function initWeightChart(weightData) {
|
|
const ctx = document.getElementById('weightChart').getContext('2d');
|
|
if (weightChart) weightChart.destroy();
|
|
|
|
weightChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: weightData.map(d => d.logged_at),
|
|
datasets: [{
|
|
label: 'Weight (kg)',
|
|
data: weightData.map(d => d.weight),
|
|
borderColor: '#f59e0b',
|
|
backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
|
fill: true,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
y: { grid: { color: '#334155' }, ticks: { color: '#94a3b8' } },
|
|
x: { grid: { display: false }, ticks: { color: '#94a3b8' } }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function initCaloriesChart(nutritionData) {
|
|
const ctx = document.getElementById('caloriesChart').getContext('2d');
|
|
if (caloriesChart) caloriesChart.destroy();
|
|
|
|
fetch('api/logs.php?action=get_stats').then(res => res.json()).then(stats => {
|
|
const goal = stats.goals.calories;
|
|
caloriesChart = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: nutritionData.map(d => d.log_date),
|
|
datasets: [{
|
|
label: 'Consumed',
|
|
data: nutritionData.map(d => d.total_calories),
|
|
backgroundColor: nutritionData.map(d => d.total_calories >= goal ? '#10b981' : '#3b82f6'),
|
|
borderRadius: 4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
y: { grid: { color: '#334155' }, ticks: { color: '#94a3b8' } },
|
|
x: { grid: { display: false }, ticks: { color: '#94a3b8' } }
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- FORM HANDLERS ---
|
|
addLogForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const data = {
|
|
entry_name: document.getElementById('logName').value,
|
|
calories: document.getElementById('logCalories').value || 0,
|
|
protein: document.getElementById('logProtein').value || 0,
|
|
creatine: document.getElementById('logCreatine').value || 0
|
|
};
|
|
|
|
fetch('api/logs.php?action=add_log', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data)
|
|
}).then(res => res.json())
|
|
.then(res => {
|
|
if (res.success) {
|
|
bootstrap.Modal.getInstance(document.getElementById('addLogModal')).hide();
|
|
addLogForm.reset();
|
|
refreshAll();
|
|
}
|
|
});
|
|
});
|
|
|
|
btnAnalyzeAI.addEventListener('click', function() {
|
|
const text = aiInput.value.trim();
|
|
if (!text) return;
|
|
|
|
aiBtnText.textContent = 'Analyzing...';
|
|
aiBtnSpinner.classList.remove('d-none');
|
|
btnAnalyzeAI.disabled = true;
|
|
|
|
fetch('api/ai_analyze.php', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ text: text })
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
document.getElementById('logName').value = data.entry_name;
|
|
document.getElementById('logCalories').value = data.calories;
|
|
document.getElementById('logProtein').value = data.protein;
|
|
document.getElementById('logCreatine').value = data.creatine;
|
|
new bootstrap.Tab(document.getElementById('manual-tab')).show();
|
|
resetAIButton();
|
|
})
|
|
.catch(() => resetAIButton());
|
|
});
|
|
|
|
function resetAIButton() {
|
|
aiBtnText.textContent = 'Analyze with AI';
|
|
aiBtnSpinner.classList.add('d-none');
|
|
btnAnalyzeAI.disabled = false;
|
|
}
|
|
|
|
updateGoalsForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const data = {
|
|
calories: document.getElementById('goalCalories').value,
|
|
protein: document.getElementById('goalProtein').value,
|
|
creatine: document.getElementById('goalCreatine').value,
|
|
water: document.getElementById('goalWater').value,
|
|
weight: 75
|
|
};
|
|
|
|
fetch('api/logs.php?action=update_goals', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data)
|
|
}).then(res => res.json())
|
|
.then(res => {
|
|
if (res.success) {
|
|
bootstrap.Modal.getInstance(document.getElementById('goalsModal')).hide();
|
|
refreshAll();
|
|
}
|
|
});
|
|
});
|
|
}); |