v1 ft tracker

This commit is contained in:
Flatlogic Bot 2026-02-21 16:43:49 +00:00
parent 7f7c6e873d
commit bb9eef0fb8
5 changed files with 292 additions and 66 deletions

30
api/photos.php Normal file
View File

@ -0,0 +1,30 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$pdo = db();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Handle photo upload
if (isset($_FILES['photo'])) {
$file = $_FILES['photo'];
$weight = $_POST['weight'] ?? null;
$date = $_POST['date'] ?? date('Y-m-d');
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = uniqid('photo_') . '.' . $ext;
$target = '../assets/images/progress/' . $filename;
if (move_uploaded_file($file['tmp_name'], $target)) {
$stmt = $pdo->prepare("INSERT INTO progress_photos (photo_path, weight, logged_at) VALUES (?, ?, ?)");
$stmt->execute(['assets/images/progress/' . $filename, $weight, $date]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['error' => 'Failed to save file']);
}
exit;
}
}
// Default action: list photos
$stmt = $pdo->query("SELECT * FROM progress_photos ORDER BY logged_at DESC, created_at DESC");
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));

40
api/supplements.php Normal file
View File

@ -0,0 +1,40 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$action = $_GET['action'] ?? 'get_status';
$today = date('Y-m-d');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
if ($action === 'log') {
$id = $input['id'] ?? null;
$name = $input['name'] ?? '';
$stmt = $pdo->prepare("INSERT INTO supplement_logs (supplement_id, name, taken_at) VALUES (?, ?, ?)");
$stmt->execute([$id, $name, $today]);
echo json_encode(['success' => true]);
exit;
}
}
if ($action === 'get_status') {
// Get all supplements
$stmt = $pdo->query("SELECT * FROM supplement_list ORDER BY name ASC");
$list = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Get today's logs
$stmt = $pdo->prepare("SELECT name FROM supplement_logs WHERE taken_at = ?");
$stmt->execute([$today]);
$taken = $stmt->fetchAll(PDO::FETCH_COLUMN);
$results = [];
foreach ($list as $sup) {
$sup['taken'] = in_array($sup['name'], $taken);
$results[] = $sup;
}
echo json_encode($results);
exit;
}

View File

@ -166,3 +166,25 @@ header {
canvas {
max-width: 100% !important;
}
.x-small { font-size: 0.65rem; }
#gallery-container .card {
transition: transform 0.3s ease;
}
#gallery-container .card:hover {
transform: scale(1.02);
}
.supplement-toggle-btn {
transition: all 0.2s;
font-size: 0.8rem !important;
text-align: center;
}
.supplement-toggle-btn.btn-success {
background-color: var(--success);
border-color: var(--success) !important;
color: white;
}

View File

@ -5,17 +5,17 @@ document.addEventListener('DOMContentLoaded', function() {
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 aiFeedback = document.getElementById('aiFeedback');
const aiBtnText = document.getElementById('aiBtnText');
const aiBtnSpinner = document.getElementById('aiBtnSpinner');
const manualTab = document.getElementById('manual-tab');
// Tab Elements
const analysisTab = document.getElementById('analysis-tab');
const galleryTab = document.getElementById('gallery-tab');
const periodWeekly = document.getElementById('periodWeekly');
const periodMonthly = document.getElementById('periodMonthly');
@ -30,6 +30,7 @@ document.addEventListener('DOMContentLoaded', function() {
fetchRecentLogs();
fetchWater();
fetchWeight();
fetchSupplements();
}
// --- NUTRITION & STATS ---
@ -42,33 +43,32 @@ document.addEventListener('DOMContentLoaded', function() {
// Update Goals (UI)
setText('cal-goal', goals.calories);
setText('pro-goal', goals.protein);
setText('cre-goal', goals.creatine || 0);
// Update Consumed (UI)
setText('cal-consumed', consumed.calories);
setText('pro-consumed', consumed.protein);
setText('cre-consumed', consumed.creatine || 0);
// Update Left (UI)
setText('cal-left', Math.max(0, remaining.calories));
setText('pro-left', Math.max(0, remaining.protein));
// setText('cre-left', Math.max(0, (goals.creatine || 0) - (consumed.creatine || 0)));
// Update Progress (UI)
updateProgress('cal-progress', consumed.calories, goals.calories);
updateProgress('pro-progress', consumed.protein, goals.protein);
// Supplements status
// 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 (creCons >= creGoal && creGoal > 0) {
creBadge.innerText = 'Taken';
creBadge.className = 'badge bg-success';
} else {
creBadge.innerText = 'Not taken';
creBadge.className = 'badge bg-danger';
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
@ -86,14 +86,6 @@ document.addEventListener('DOMContentLoaded', function() {
if (!el) return;
const percent = goal > 0 ? Math.min(100, (consumed / goal) * 100) : 0;
el.style.width = percent + '%';
if (percent >= 100) {
el.style.backgroundColor = 'var(--success)';
} else {
// Keep original if not success (some bars have specific colors)
if (!el.classList.contains('bg-info')) {
el.style.backgroundColor = 'var(--primary)';
}
}
}
function setText(id, val) {
@ -127,13 +119,23 @@ document.addEventListener('DOMContentLoaded', function() {
.then(res => res.json())
.then(data => {
const amount = data.amount || 0;
setText('water-consumed', amount.toFixed(2));
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 + '%';
});
});
});
}
@ -145,9 +147,12 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
document.getElementById('btnAddWater').addEventListener('click', () => {
logWater(0.25); // Default add 250ml
});
const btnAddWater = document.getElementById('btnAddWater');
if (btnAddWater) {
btnAddWater.addEventListener('click', () => {
logWater(0.25);
});
}
function logWater(amount) {
fetch('api/water.php', {
@ -156,6 +161,40 @@ document.addEventListener('DOMContentLoaded', function() {
}).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')
@ -165,38 +204,92 @@ document.addEventListener('DOMContentLoaded', function() {
const last = data[0];
setText('weight-current', last.weight);
setText('weight-last-date', 'Last logged: ' + last.logged_at);
document.getElementById('weightInput').value = last.weight;
const weightInput = document.getElementById('weightInput');
if (weightInput) weightInput.value = last.weight;
const photoWeight = document.getElementById('photoWeight');
if (photoWeight) photoWeight.value = last.weight;
}
});
}
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();
}
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 col-md-4">
<div class="card bg-dark border-secondary overflow-hidden h-100">
<img src="${photo.photo_path}" class="card-img-top" style="height: 180px; 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: consumed.water || 0, goal: goals.water || 2.5, unit: 'L', color: 'info' },
{ label: 'Protein', current: consumed.protein, goal: goals.protein, unit: 'g', color: 'primary' },
{ label: 'Creatine', current: consumed.creatine || 0, goal: goals.creatine || 5, unit: 'g', color: 'warning' }
{ 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 water separately for reminders since it's not in get_stats usually
fetch('api/water.php').then(res => res.json()).then(waterData => {
items[0].current = waterData.amount || 0;
@ -213,10 +306,9 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// --- ANALYSIS & CHARTS ---
analysisTab.addEventListener('shown.bs.tab', () => {
renderAnalysis();
});
// --- TABS SWITCHING ---
analysisTab.addEventListener('shown.bs.tab', renderAnalysis);
galleryTab.addEventListener('shown.bs.tab', fetchPhotos);
periodWeekly.addEventListener('change', renderAnalysis);
periodMonthly.addEventListener('change', renderAnalysis);
@ -264,7 +356,6 @@ document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('caloriesChart').getContext('2d');
if (caloriesChart) caloriesChart.destroy();
// Get goal from stats
fetch('api/logs.php?action=get_stats').then(res => res.json()).then(stats => {
const goal = stats.goals.calories;
caloriesChart = new Chart(ctx, {
@ -281,21 +372,7 @@ document.addEventListener('DOMContentLoaded', function() {
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
annotation: {
annotations: {
line1: {
type: 'line',
yMin: goal,
yMax: goal,
borderColor: 'rgba(255, 255, 255, 0.5)',
borderWidth: 2,
borderDash: [6, 6]
}
}
}
},
plugins: { legend: { display: false } },
scales: {
y: { grid: { color: '#334155' }, ticks: { color: '#94a3b8' } },
x: { grid: { display: false }, ticks: { color: '#94a3b8' } }
@ -323,7 +400,6 @@ document.addEventListener('DOMContentLoaded', function() {
if (res.success) {
bootstrap.Modal.getInstance(document.getElementById('addLogModal')).hide();
addLogForm.reset();
aiInput.value = '';
refreshAll();
}
});
@ -366,7 +442,7 @@ document.addEventListener('DOMContentLoaded', function() {
protein: document.getElementById('goalProtein').value,
creatine: document.getElementById('goalCreatine').value,
water: document.getElementById('goalWater').value,
weight: 75 // placeholder or add input if needed
weight: 75
};
fetch('api/logs.php?action=update_goals', {
@ -380,4 +456,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
});
});
});

View File

@ -44,6 +44,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<ul class="dropdown-menu dropdown-menu-end shadow-lg border-0">
<li><a class="dropdown-item small py-2" href="#" data-bs-toggle="modal" data-bs-target="#goalsModal">Daily Goals</a></li>
<li><a class="dropdown-item small py-2" href="#" data-bs-toggle="modal" data-bs-target="#weightModal">Log Weight</a></li>
<li><a class="dropdown-item small py-2" href="#" data-bs-toggle="modal" data-bs-target="#photoModal">Upload Progress Photo</a></li>
<li><hr class="dropdown-divider"></li>
<li><p class="dropdown-item-text text-muted mb-0" style="font-size: 0.7rem;">Bulgarian AI support enabled</p></li>
</ul>
@ -57,6 +58,9 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<li class="nav-item flex-fill" role="presentation">
<button class="nav-link w-100" id="health-tab" data-bs-toggle="pill" data-bs-target="#health" type="button" role="tab">Health</button>
</li>
<li class="nav-item flex-fill" role="presentation">
<button class="nav-link w-100" id="gallery-tab" data-bs-toggle="pill" data-bs-target="#gallery" type="button" role="tab">Gallery</button>
</li>
<li class="nav-item flex-fill" role="presentation">
<button class="nav-link w-100" id="analysis-tab" data-bs-toggle="pill" data-bs-target="#analysis" type="button" role="tab">Trends</button>
</li>
@ -93,6 +97,27 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
</div>
<!-- Water Intake (Quick Access) -->
<div class="card-stat mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="stat-label text-info">Water Intake</span>
<span class="text-muted small"><span class="today-water-consumed fw-bold text-white">0</span> / <span class="today-water-goal">0</span> L</span>
</div>
<div class="progress-thin mb-3"><div class="today-water-progress-bar progress-bar-inner bg-info" style="width: 0%"></div></div>
<div class="d-flex gap-2">
<button class="btn btn-outline-info flex-fill py-2 small water-btn" data-amount="0.25">+250ml</button>
<button class="btn btn-outline-info flex-fill py-2 small water-btn" data-amount="0.5">+500ml</button>
</div>
</div>
<!-- Supplements (Quick Check) -->
<div class="card-stat mb-4">
<span class="stat-label mb-3 d-block">Supplements</span>
<div id="today-supplements-list" class="d-flex flex-wrap gap-2">
<!-- Filled by JS -->
</div>
</div>
<button class="btn btn-primary-custom mb-4" data-bs-toggle="modal" data-bs-target="#addLogModal">Log Food / Supplement</button>
<!-- Recent History -->
@ -141,7 +166,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<!-- Supplement Status -->
<div class="card-stat">
<span class="stat-label">Supplements</span>
<span class="stat-label">Creatine Status</span>
<div class="mt-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="small">Creatine</span>
@ -152,6 +177,20 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
</div>
<!-- GALLERY TAB -->
<div class="tab-pane fade" id="gallery" role="tabpanel">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="h6 fw-bold mb-0">Progress Gallery</h2>
<button class="btn btn-primary-custom btn-sm" data-bs-toggle="modal" data-bs-target="#photoModal">+ Add Photo</button>
</div>
<div id="gallery-container" class="row g-3">
<!-- Photos filled by JS -->
<div class="col-12 text-center py-5">
<p class="text-muted small">No photos uploaded yet.</p>
</div>
</div>
</div>
<!-- ANALYSIS TAB -->
<div class="tab-pane fade" id="analysis" role="tabpanel">
<div class="d-flex justify-content-between align-items-center mb-4">
@ -252,6 +291,25 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
</div>
<div class="modal fade" id="photoModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content p-4">
<h2 class="h5 fw-bold mb-4">Progress Photo</h2>
<form id="photoForm">
<div class="mb-3">
<label class="form-label small fw-bold">Select Photo</label>
<input type="file" id="photoInput" class="form-control" accept="image/*" required>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">Current Weight (kg - optional)</label>
<input type="number" step="0.1" id="photoWeight" class="form-control">
</div>
<button type="submit" class="btn btn-primary-custom mt-3">Upload Photo</button>
</form>
</div>
</div>
</div>
<div class="modal fade" id="goalsModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content p-4">