Autosave: 20260221-163723

This commit is contained in:
Flatlogic Bot 2026-02-21 16:37:23 +00:00
parent c604dcf2e1
commit 7f7c6e873d
8 changed files with 1030 additions and 445 deletions

51
api/ai_analyze.php Normal file
View File

@ -0,0 +1,51 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../ai/LocalAIApi.php';
$input = json_decode(file_get_contents('php://input'), true);
$text = $input['text'] ?? '';
if (empty($text)) {
echo json_encode(['error' => '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);
}

30
api/analysis.php Normal file
View File

@ -0,0 +1,30 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$type = $_GET['type'] ?? 'weekly';
$days = ($type === 'monthly') ? 30 : 7;
$data = [
'nutrition' => [],
'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);

89
api/logs.php Normal file
View File

@ -0,0 +1,89 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$action = $_GET['action'] ?? '';
if ($action === 'get_stats') {
$today = date('Y-m-d');
// Get goals
$stmt = $pdo->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;
}

42
api/water.php Normal file
View File

@ -0,0 +1,42 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
$amount = filter_var($input['amount'] ?? 0, FILTER_VALIDATE_FLOAT); // e.g. 0.25 for 250ml
$date = $input['date'] ?? date('Y-m-d');
if ($amount <= 0) {
echo json_encode(['success' => 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]);
}

28
api/weight.php Normal file
View File

@ -0,0 +1,28 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
$weight = filter_var($input['weight'] ?? 0, FILTER_VALIDATE_FLOAT);
$date = $input['date'] ?? date('Y-m-d');
if ($weight <= 0) {
echo json_encode(['success' => 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));
}

View File

@ -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);
}
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;
}

View File

@ -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 = '<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;
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 = `
<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 & 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();
}
});
});
});

407
index.php
View File

@ -1,150 +1,285 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once __DIR__ . '/db/config.php';
$pdo = db();
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
// Initialize goals
$pdo->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'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<!doctype html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<title>Nutrition Pulse</title>
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
<div class="container-mobile">
<header class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 fw-bold mb-1">Nutrition Pulse</h1>
<p class="text-muted small mb-0"><?= date('l, M d') ?></p>
</div>
<div class="dropdown">
<button class="btn btn-outline-secondary btn-sm rounded-circle p-2 border-0" type="button" data-bs-toggle="dropdown">
<svg width="20" height="20" fill="currentColor" viewBox="0 0 16 16"><path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/></svg>
</button>
<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><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>
</div>
</header>
<ul class="nav nav-pills mb-4 w-100 p-1" id="mainTab" role="tablist">
<li class="nav-item flex-fill" role="presentation">
<button class="nav-link active w-100" id="today-tab" data-bs-toggle="pill" data-bs-target="#today" type="button" role="tab">Today</button>
</li>
<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="analysis-tab" data-bs-toggle="pill" data-bs-target="#analysis" type="button" role="tab">Trends</button>
</li>
</ul>
<div class="tab-content" id="mainTabContent">
<!-- TODAY TAB -->
<div class="tab-pane fade show active" id="today" role="tabpanel">
<!-- Reminders / Quick Status -->
<div id="reminders-row" class="d-flex gap-2 mb-3 overflow-auto pb-2" style="scrollbar-width: none;">
<!-- Reminders filled by JS -->
</div>
<!-- Stats Section -->
<div id="stats-container">
<!-- Calories -->
<div class="card-stat">
<div class="d-flex justify-content-between align-items-center">
<span class="stat-label">Calories</span>
<span class="text-muted small"><span id="cal-left" class="fw-bold text-white">0</span> kcal left</span>
</div>
<div class="stat-value"><span id="cal-consumed">0</span> <span class="fs-6 fw-normal text-muted">/ <span id="cal-goal">0</span></span></div>
<div class="progress-thin"><div id="cal-progress" class="progress-bar-inner" style="width: 0%"></div></div>
</div>
<!-- Protein -->
<div class="card-stat">
<div class="d-flex justify-content-between align-items-center">
<span class="stat-label">Protein</span>
<span class="text-muted small"><span id="pro-left" class="fw-bold text-white">0</span>g left</span>
</div>
<div class="stat-value"><span id="pro-consumed">0</span>g <span class="fs-6 fw-normal text-muted">/ <span id="pro-goal">0</span>g</span></div>
<div class="progress-thin"><div id="pro-progress" class="progress-bar-inner" style="width: 0%"></div></div>
</div>
</div>
<button class="btn btn-primary-custom mb-4" data-bs-toggle="modal" data-bs-target="#addLogModal">Log Food / Supplement</button>
<!-- Recent History -->
<div class="recent-logs">
<h2 class="h6 fw-bold mb-3">Today's History</h2>
<div id="recent-logs-list">
<div class="text-center py-4">
<p class="text-muted small mb-0">No entries today yet.</p>
</div>
</div>
</div>
</div>
<!-- HEALTH TAB -->
<div class="tab-pane fade" id="health" role="tabpanel">
<!-- Water Intake -->
<div class="card-stat mb-4">
<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 id="water-consumed">0</span> / <span id="water-goal">0</span> L</span>
</div>
<div class="d-flex align-items-center gap-3">
<div class="flex-grow-1">
<div class="progress-thin"><div id="water-progress" class="progress-bar-inner bg-info" style="width: 0%"></div></div>
</div>
<button id="btnAddWater" class="btn btn-info btn-sm rounded-circle p-2 text-white" style="width:36px; height:36px;">+</button>
</div>
<div class="mt-3 d-flex gap-2">
<button class="btn btn-outline-info btn-xs py-1 px-2 small water-btn" data-amount="0.25">250ml</button>
<button class="btn btn-outline-info btn-xs py-1 px-2 small water-btn" data-amount="0.5">500ml</button>
</div>
</div>
<!-- Weight Tracking -->
<div class="card-stat mb-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="stat-label text-warning">Current Weight</span>
<span class="text-muted small" id="weight-last-date">Last logged: --</span>
</div>
<div class="d-flex align-items-baseline gap-2">
<span class="stat-value text-warning mb-0" id="weight-current">0</span>
<span class="text-muted">kg</span>
</div>
<button class="btn btn-outline-warning w-100 mt-3 btn-sm" data-bs-toggle="modal" data-bs-target="#weightModal">Update Weight</button>
</div>
<!-- Supplement Status -->
<div class="card-stat">
<span class="stat-label">Supplements</span>
<div class="mt-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="small">Creatine</span>
<span id="cre-status" class="badge bg-danger">Not taken</span>
</div>
<div class="progress-thin"><div id="cre-progress-health" class="progress-bar-inner" style="width: 0%"></div></div>
</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">
<h2 class="h6 fw-bold mb-0">Performance Trends</h2>
<div class="btn-group btn-group-sm">
<input type="radio" class="btn-check" name="analysisPeriod" id="periodWeekly" checked>
<label class="btn btn-outline-secondary" for="periodWeekly">Week</label>
<input type="radio" class="btn-check" name="analysisPeriod" id="periodMonthly">
<label class="btn btn-outline-secondary" for="periodMonthly">Month</label>
</div>
</div>
<div class="card-stat mb-4">
<span class="stat-label">Weight (kg)</span>
<div class="mt-2" style="height: 200px;">
<canvas id="weightChart"></canvas>
</div>
</div>
<div class="card-stat mb-4">
<span class="stat-label">Calories vs Goal</span>
<div class="mt-2" style="height: 200px;">
<canvas id="caloriesChart"></canvas>
</div>
</div>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
</div>
<!-- MODALS -->
<div class="modal fade" id="addLogModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="h5 fw-bold mb-0">New Log</h2>
<ul class="nav nav-pills small" id="logTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active py-1 px-3" id="manual-tab" data-bs-toggle="pill" data-bs-target="#manual-log" type="button" role="tab">Manual</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link py-1 px-3" id="ai-tab" data-bs-toggle="pill" data-bs-target="#ai-log" type="button" role="tab">AI </button>
</li>
</ul>
</div>
<div class="tab-content">
<div class="tab-pane fade show active" id="manual-log" role="tabpanel">
<form id="addLogForm">
<div class="mb-3">
<label class="form-label small fw-bold">Item Name</label>
<input type="text" id="logName" class="form-control" placeholder="e.g. Chicken Breast" required>
</div>
<div class="row g-3">
<div class="col-6 mb-3">
<label class="form-label small fw-bold">Calories (kcal)</label>
<input type="number" id="logCalories" class="form-control" placeholder="0">
</div>
<div class="col-6 mb-3">
<label class="form-label small fw-bold">Protein (g)</label>
<input type="number" id="logProtein" class="form-control" placeholder="0">
</div>
<div class="col-12 mb-3">
<label class="form-label small fw-bold">Creatine (g)</label>
<input type="number" step="0.1" id="logCreatine" class="form-control" placeholder="0">
</div>
</div>
<button type="submit" class="btn btn-primary-custom mt-3">Save Entry</button>
</form>
</div>
<div class="tab-pane fade" id="ai-log" role="tabpanel">
<div class="mb-3">
<label class="form-label small fw-bold">Describe your meal (Bulgarian OK)</label>
<textarea id="aiInput" class="form-control" rows="4" placeholder="e.g. Една баница и протеинов шейк"></textarea>
<div id="aiFeedback" class="mt-2 small d-none"></div>
</div>
<button id="btnAnalyzeAI" class="btn btn-primary-custom">
<span id="aiBtnText">Analyze with AI</span>
<span id="aiBtnSpinner" class="spinner-border spinner-border-sm ms-2 d-none" role="status"></span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="weightModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content p-4">
<h2 class="h5 fw-bold mb-4">Log Weight</h2>
<form id="weightForm">
<div class="mb-3">
<label class="form-label small fw-bold">Weight (kg)</label>
<input type="number" step="0.1" id="weightInput" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary-custom mt-3">Save Weight</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">
<h2 class="h5 fw-bold mb-4">Daily Goals</h2>
<form id="updateGoalsForm">
<div class="mb-3">
<label class="form-label small fw-bold">Calorie Target (kcal)</label>
<input type="number" id="goalCalories" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">Protein Target (g)</label>
<input type="number" id="goalProtein" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">Creatine Target (g)</label>
<input type="number" step="0.1" id="goalCreatine" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">Water Target (L)</label>
<input type="number" step="0.1" id="goalWater" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary-custom mt-3">Update Goals</button>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?= time() ?>"></script>
</body>
</html>
</html>