From 7f7c6e873dfc16bb871bceb0a6344322e3b6d2c1 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 21 Feb 2026 16:37:23 +0000 Subject: [PATCH] Autosave: 20260221-163723 --- api/ai_analyze.php | 51 +++++ api/analysis.php | 30 +++ api/logs.php | 89 +++++++++ api/water.php | 42 +++++ api/weight.php | 28 +++ assets/css/custom.css | 422 ++++++++++++++---------------------------- assets/js/main.js | 406 ++++++++++++++++++++++++++++++++++++---- index.php | 407 ++++++++++++++++++++++++++-------------- 8 files changed, 1030 insertions(+), 445 deletions(-) create mode 100644 api/ai_analyze.php create mode 100644 api/analysis.php create mode 100644 api/logs.php create mode 100644 api/water.php create mode 100644 api/weight.php diff --git a/api/ai_analyze.php b/api/ai_analyze.php new file mode 100644 index 0000000..4d52db5 --- /dev/null +++ b/api/ai_analyze.php @@ -0,0 +1,51 @@ + 'No text provided']); + http_response_code(400); + exit; +} + +$prompt = "You are a nutrition expert. Analyze the following food description and provide the estimated nutritional information. +The user is from Bulgaria, so understand Bulgarian food names (e.g., 'banitsa', 'shopska salad', 'lyutenitsa', 'kebapche', 'tarator') and language. +If the description is in Bulgarian, translate the 'entry_name' to English but use the Bulgarian context for nutritional estimation. +Return only a JSON object with the following keys: +- 'entry_name': A short, descriptive name of the food (in English). +- 'calories': Estimated total calories (integer). +- 'protein': Estimated protein in grams (integer). +- 'creatine': Estimated creatine in grams (float, usually 0 unless it's a supplement). + +If the user mentions a supplement like 'creatine', ensure to include it. +If multiple items are mentioned, provide the total for all of them. + +Food description: \"$text\""; + +$response = LocalAIApi::createResponse([ + 'input' => [ + ['role' => 'system', 'content' => 'You are a nutrition expert that returns only JSON. You understand Bulgarian and international cuisine.'], + ['role' => 'user', 'content' => $prompt], + ], +]); + +if (!empty($response['success'])) { + $decoded = LocalAIApi::decodeJsonFromResponse($response); + if ($decoded) { + echo json_encode($decoded); + } else { + $rawText = LocalAIApi::extractText($response); + if (preg_match('/{.*}/s', $rawText, $matches)) { + echo $matches[0]; + } else { + echo json_encode(['error' => 'Failed to parse AI response', 'raw' => $rawText]); + http_response_code(500); + } + } +} else { + echo json_encode(['error' => $response['error'] ?? 'AI request failed']); + http_response_code(500); +} \ No newline at end of file diff --git a/api/analysis.php b/api/analysis.php new file mode 100644 index 0000000..b05e2b7 --- /dev/null +++ b/api/analysis.php @@ -0,0 +1,30 @@ + [], + 'weight' => [], + 'water' => [] +]; + +// Get nutrition (last X days) +$stmt = $pdo->prepare("SELECT DATE(created_at) as log_date, SUM(calories) as total_calories, SUM(protein) as total_protein, SUM(creatine) as total_creatine FROM nutrition_logs WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY) GROUP BY log_date ORDER BY log_date ASC"); +$stmt->execute([$days]); +$data['nutrition'] = $stmt->fetchAll(PDO::FETCH_ASSOC); + +// Get weight (last X days) +$stmt = $pdo->prepare("SELECT weight, logged_at FROM weight_logs WHERE logged_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY) ORDER BY logged_at ASC"); +$stmt->execute([$days]); +$data['weight'] = $stmt->fetchAll(PDO::FETCH_ASSOC); + +// Get water (last X days) +$stmt = $pdo->prepare("SELECT amount, logged_at FROM water_logs WHERE logged_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY) ORDER BY logged_at ASC"); +$stmt->execute([$days]); +$data['water'] = $stmt->fetchAll(PDO::FETCH_ASSOC); + +echo json_encode($data); diff --git a/api/logs.php b/api/logs.php new file mode 100644 index 0000000..8aecdc5 --- /dev/null +++ b/api/logs.php @@ -0,0 +1,89 @@ +query("SELECT goal_key, goal_value FROM user_goals"); + $goals = []; + while ($row = $stmt->fetch()) { + $goals[$row['goal_key']] = (float)$row['goal_value']; + } + + // Get today's nutrition logs + $stmt = $pdo->prepare("SELECT SUM(calories) as calories, SUM(protein) as protein, SUM(creatine) as creatine FROM nutrition_logs WHERE DATE(created_at) = ?"); + $stmt->execute([$today]); + $stats = $stmt->fetch(); + + // Get today's water + $stmtW = $pdo->prepare("SELECT SUM(amount) as amount FROM water_logs WHERE logged_at = ?"); + $stmtW->execute([$today]); + $waterStats = $stmtW->fetch(); + + $consumed = [ + 'calories' => (int)($stats['calories'] ?? 0), + 'protein' => (int)($stats['protein'] ?? 0), + 'creatine' => (float)($stats['creatine'] ?? 0), + 'water' => (float)($waterStats['amount'] ?? 0) + ]; + + echo json_encode([ + 'goals' => $goals, + 'consumed' => $consumed, + 'remaining' => [ + 'calories' => max(0, ($goals['calories'] ?? 0) - $consumed['calories']), + 'protein' => max(0, ($goals['protein'] ?? 0) - $consumed['protein']), + 'creatine' => max(0, ($goals['creatine'] ?? 0) - $consumed['creatine']), + 'water' => max(0, ($goals['water'] ?? 0) - $consumed['water']) + ] + ]); + exit; +} + +if ($action === 'add_log') { + $input = json_decode(file_get_contents('php://input'), true); + + if (empty($input['entry_name'])) { + echo json_encode(['error' => 'Name is required']); + http_response_code(400); + exit; + } + + $stmt = $pdo->prepare("INSERT INTO nutrition_logs (entry_name, calories, protein, creatine) VALUES (?, ?, ?, ?)"); + $stmt->execute([ + $input['entry_name'], + (int)($input['calories'] ?? 0), + (int)($input['protein'] ?? 0), + (float)($input['creatine'] ?? 0) + ]); + + echo json_encode(['success' => true]); + exit; +} + +if ($action === 'get_recent') { + $stmt = $pdo->query("SELECT * FROM nutrition_logs ORDER BY created_at DESC LIMIT 10"); + echo json_encode($stmt->fetchAll()); + exit; +} + +if ($action === 'update_goals') { + $input = json_decode(file_get_contents('php://input'), true); + + $stmt = $pdo->prepare("INSERT INTO user_goals (goal_key, goal_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE goal_value = VALUES(goal_value)"); + + $allowedKeys = ['calories', 'protein', 'creatine', 'water', 'weight']; + foreach ($input as $key => $value) { + if (in_array($key, $allowedKeys)) { + $stmt->execute([$key, (float)$value]); + } + } + + echo json_encode(['success' => true]); + exit; +} \ No newline at end of file diff --git a/api/water.php b/api/water.php new file mode 100644 index 0000000..4adcbdd --- /dev/null +++ b/api/water.php @@ -0,0 +1,42 @@ + false, 'error' => 'Invalid amount']); + exit; + } + + try { + // Find existing record for today + $stmt = $pdo->prepare("SELECT id, amount FROM water_logs WHERE logged_at = ?"); + $stmt->execute([$date]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($row) { + $newAmount = $row['amount'] + $amount; + $stmtUpdate = $pdo->prepare("UPDATE water_logs SET amount = ? WHERE id = ?"); + $stmtUpdate->execute([$newAmount, $row['id']]); + } else { + $stmtInsert = $pdo->prepare("INSERT INTO water_logs (amount, logged_at) VALUES (?, ?)"); + $stmtInsert->execute([$amount, $date]); + } + echo json_encode(['success' => true]); + } catch (PDOException $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); + } +} elseif ($method === 'GET') { + $date = $_GET['date'] ?? date('Y-m-d'); + $stmt = $pdo->prepare("SELECT amount FROM water_logs WHERE logged_at = ?"); + $stmt->execute([$date]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + echo json_encode(['amount' => $row ? $row['amount'] : 0]); +} diff --git a/api/weight.php b/api/weight.php new file mode 100644 index 0000000..fefa8dd --- /dev/null +++ b/api/weight.php @@ -0,0 +1,28 @@ + false, 'error' => 'Invalid weight']); + exit; + } + + try { + $stmt = $pdo->prepare("INSERT INTO weight_logs (weight, logged_at) VALUES (?, ?) ON DUPLICATE KEY UPDATE weight = VALUES(weight)"); + $stmt->execute([$weight, $date]); + echo json_encode(['success' => true]); + } catch (PDOException $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); + } +} elseif ($method === 'GET') { + $stmt = $pdo->query("SELECT weight, logged_at FROM weight_logs ORDER BY logged_at DESC LIMIT 30"); + echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC)); +} diff --git a/assets/css/custom.css b/assets/css/custom.css index 50e0502..1dd9c4d 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,302 +1,168 @@ +:root { + --primary: #3b82f6; + --primary-hover: #2563eb; + --bg: #0f172a; + --surface: #1e293b; + --border: #334155; + --text: #f8fafc; + --text-muted: #94a3b8; + --success: #10b981; + --info: #06b6d4; + --warning: #f59e0b; + --radius: 12px; + --card-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); +} + body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; + font-family: 'Inter', system-ui, -apple-system, sans-serif; + background-color: var(--bg); + color: var(--text); margin: 0; - min-height: 100vh; + padding: 0; + -webkit-font-smoothing: antialiased; } -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - width: 100%; +.container-mobile { + max-width: 480px; + margin: 0 auto; + padding: 24px 16px; + padding-bottom: 80px; /* footer space */ +} + +header { + margin-bottom: 24px; +} + +.card-stat { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; + margin-bottom: 16px; + box-shadow: var(--card-shadow); + transition: transform 0.2s ease; } -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } +.stat-label { + font-size: 0.7rem; + color: var(--text-muted); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; } -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; -} - -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); +.stat-value { + font-size: 1.75rem; font-weight: 700; - font-size: 1.1rem; + margin: 4px 0; + color: var(--text); +} + +.progress-thin { + height: 6px; + background-color: var(--border); + border-radius: 4px; + overflow: hidden; + margin-top: 8px; +} + +.progress-bar-inner { + height: 100%; + background-color: var(--primary); + transition: width 0.6s cubic-bezier(0.34, 1.56, 0.64, 1), background-color 0.3s ease; +} + +.btn-primary-custom { + background-color: var(--primary); + color: white; + border: none; + border-radius: var(--radius); + padding: 14px 24px; + font-weight: 600; + width: 100%; + transition: all 0.2s ease; +} + +.btn-primary-custom:hover { + background-color: var(--primary-hover); +} + +.log-item { display: flex; justify-content: space-between; align-items: center; + padding: 14px; + background: var(--surface); + border-radius: var(--radius); + margin-bottom: 8px; + border: 1px solid var(--border); } -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; -} - -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); -} - -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; -} - -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; -} - -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.chat-input-area form { - display: flex; - gap: 0.75rem; -} - -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; -} - -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; -} - -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); -} - -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; -} - -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); -} - -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); -} - -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.admin-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.admin-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; -} - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; -} - -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; - text-transform: uppercase; +.log-details { font-size: 0.75rem; - letter-spacing: 1px; + color: var(--text-muted); } -.table td { - background: #fff; - padding: 1rem; - border: none; -} - -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - font-size: 0.9rem; +.modal-content { + background-color: var(--surface); + color: var(--text); + border-radius: 20px; + border: 1px solid var(--border); } .form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; + background-color: var(--bg); + border-radius: var(--radius); + border: 1px solid var(--border); + padding: 12px 16px; + color: var(--text); } .form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); -} \ No newline at end of file + background-color: var(--bg); + border-color: var(--primary); + color: var(--text); + box-shadow: none; +} + +/* Tabs styling */ +.nav-pills { + background: var(--surface); + padding: 4px; + border-radius: var(--radius); + border: 1px solid var(--border); +} + +.nav-pills .nav-link { + color: var(--text-muted); + border-radius: calc(var(--radius) - 4px); + font-weight: 600; + transition: all 0.2s; + font-size: 0.85rem; +} + +.nav-pills .nav-link.active { + background-color: var(--bg); + color: var(--primary); + box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); +} + +.btn-outline-secondary { border-color: var(--border); color: var(--text-muted); } +.btn-outline-secondary:hover { background-color: var(--border); color: var(--text); } + +.bg-success-subtle { background-color: rgba(16, 185, 129, 0.1) !important; } +.bg-secondary-subtle { background-color: rgba(51, 65, 85, 0.3) !important; } + +#reminders-row::-webkit-scrollbar { display: none; } + +.btn-xs { padding: 0.25rem 0.5rem; font-size: 0.7rem; } + +/* Custom colors for stats */ +.text-info { color: var(--info) !important; } +.text-warning { color: var(--warning) !important; } +.bg-info { background-color: var(--info) !important; } +.bg-warning { background-color: var(--warning) !important; } + +canvas { + max-width: 100% !important; +} diff --git a/assets/js/main.js b/assets/js/main.js index d349598..0b027fc 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,383 @@ -document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); +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'); + + // 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 periodWeekly = document.getElementById('periodWeekly'); + const periodMonthly = document.getElementById('periodMonthly'); - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; + let weightChart = null; + let caloriesChart = null; - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; + // Initial fetch + refreshAll(); - appendMessage(message, 'visitor'); - chatInput.value = ''; + function refreshAll() { + fetchStats(); + fetchRecentLogs(); + fetchWater(); + fetchWeight(); + } - try { - const response = await fetch('api/chat.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) + // --- 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); + 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 + 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'; + } + + // 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); }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); + } + + 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 + '%'; + 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) { + 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 = '

No entries today yet.

'; + return; + } + logsList.innerHTML = data.map(log => ` +
+
+
${log.entry_name}
+
${log.calories} kcal · ${log.protein}g protein ${log.creatine > 0 ? '· ' + log.creatine + 'g creatine' : ''}
+
+
${new Date(log.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
+
+ `).join(''); + }); + } + + // --- WATER --- + function fetchWater() { + fetch('api/water.php') + .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; + setText('water-goal', goal); + updateProgress('water-progress', amount, goal); + }); + }); + } + + document.querySelectorAll('.water-btn').forEach(btn => { + btn.addEventListener('click', () => { + const amount = parseFloat(btn.dataset.amount); + logWater(amount); + }); + }); + + document.getElementById('btnAddWater').addEventListener('click', () => { + logWater(0.25); // Default add 250ml + }); + + function logWater(amount) { + fetch('api/water.php', { + method: 'POST', + body: JSON.stringify({ amount: amount }) + }).then(() => fetchWater()); + } + + // --- 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); + document.getElementById('weightInput').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(); + } + }); + }); + + // --- REMINDERS --- + function updateReminders(consumed, goals) { + const row = document.getElementById('reminders-row'); + 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' } + ]; + + // 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; + + 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 = ` + + ${item.label}: ${done ? 'Done' : (item.goal - item.current).toFixed(1) + item.unit + ' left'} + `; + row.appendChild(div); + }); + }); + } + + // --- ANALYSIS & CHARTS --- + analysisTab.addEventListener('shown.bs.tab', () => { + renderAnalysis(); + }); + + 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(); + + // 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, { + 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 }, + annotation: { + annotations: { + line1: { + type: 'line', + yMin: goal, + yMax: goal, + borderColor: 'rgba(255, 255, 255, 0.5)', + borderWidth: 2, + borderDash: [6, 6] + } + } + } + }, + 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(); + aiInput.value = ''; + 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 // placeholder or add input if needed + }; + + 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(); + } + }); }); }); diff --git a/index.php b/index.php index 7205f3d..0f7cfb5 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,285 @@ - - - - - - New Style -exec("INSERT IGNORE INTO user_goals (goal_key, goal_value) VALUES ('calories', 2500), ('protein', 130), ('creatine', 5), ('water', 2.5), ('weight', 75)"); + +$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Modern nutrition tracking for athletes.'; $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; ?> - - - - + + + + + + Nutrition Pulse + - - - - - - - - + + + + - - + + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… + +
+
+
+

Nutrition Pulse

+

+
+ +
+ + + +
+ +
+ +
+ +
+ + +
+ +
+
+ Calories + 0 kcal left +
+
0 / 0
+
+
+ + +
+
+ Protein + 0g left +
+
0g / 0g
+
+
+
+ + + + +
+

Today's History

+
+
+

No entries today yet.

+
+
+
+
+ + +
+ +
+
+ Water Intake + 0 / 0 L +
+
+
+
+
+ +
+
+ + +
+
+ + +
+
+ Current Weight + Last logged: -- +
+
+ 0 + kg +
+ +
+ + +
+ Supplements +
+
+ Creatine + Not taken +
+
+
+
+
+ + +
+
+

Performance Trends

+
+ + + + +
+
+ +
+ Weight (kg) +
+ +
+
+ +
+ Calories vs Goal +
+ +
+
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

-
- + + + + + + + + + + + - + \ No newline at end of file