Compare commits

...

3 Commits

Author SHA1 Message Date
Flatlogic Bot
1cb0064488 mb v1 2026-02-21 16:48:44 +00:00
Flatlogic Bot
bb9eef0fb8 v1 ft tracker 2026-02-21 16:43:49 +00:00
Flatlogic Bot
7f7c6e873d Autosave: 20260221-163723 2026-02-21 16:37:23 +00:00
12 changed files with 1309 additions and 447 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;
}

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

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,224 @@
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;
margin: 0;
min-height: 100vh;
:root {
--primary: #3b82f6;
--primary-hover: #2563eb;
--bg: #0f172a;
--surface: #1e293b;
--border: #334155;
--text: #f8fafc;
--text-muted: #94a3b8;
--success: #10b981;
--info: #06b6d4;
--warning: #f59e0b;
--radius: 16px;
--card-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}
.main-wrapper {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
width: 100%;
padding: 20px;
* {
-webkit-tap-highlight-color: transparent;
box-sizing: border-box;
position: relative;
z-index: 1;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: var(--bg);
color: var(--text);
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
/* Prevent rubber-banding on iOS if needed, but usually better to let it be */
}
.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;
/* App Shell Structure */
.app-shell {
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;
min-height: 100vh;
max-width: 480px;
margin: 0 auto;
background: var(--bg);
position: relative;
}
.chat-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background: rgba(255, 255, 255, 0.5);
.app-bar {
position: sticky;
top: 0;
z-index: 1000;
background: rgba(15, 23, 42, 0.8);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
padding-top: calc(8px + env(safe-area-inset-top));
}
.app-content {
flex: 1;
padding-bottom: calc(90px + env(safe-area-inset-bottom)) !important;
}
/* Bottom Navigation */
.bottom-nav {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 480px;
height: calc(70px + env(safe-area-inset-bottom));
background: rgba(30, 41, 59, 0.95);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-top: 1px solid var(--border);
display: flex;
justify-content: space-around;
align-items: center;
padding-bottom: env(safe-area-inset-bottom);
z-index: 1050;
}
.bottom-nav .nav-item {
display: flex;
flex-direction: column;
align-items: center;
text-decoration: none;
color: var(--text-muted);
font-size: 0.65rem;
font-weight: 500;
transition: all 0.2s;
flex: 1;
padding: 8px 0;
}
.bottom-nav .nav-item svg {
margin-bottom: 4px;
width: 24px;
height: 24px;
transition: transform 0.2s ease;
}
.bottom-nav .nav-item.active {
color: var(--primary);
}
.bottom-nav .nav-item.active svg {
transform: translateY(-2px);
stroke: var(--primary);
}
/* Cards & Stats */
.card-stat {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
margin-bottom: 16px;
box-shadow: var(--card-shadow);
}
.stat-label {
font-size: 0.7rem;
color: var(--text-muted);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.stat-value {
font-size: 1.75rem;
font-weight: 700;
font-size: 1.1rem;
margin: 4px 0;
color: var(--text);
}
.progress-thin {
height: 8px;
background-color: var(--border);
border-radius: 10px;
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);
}
.btn-primary-custom {
background-color: var(--primary);
color: white;
border: none;
border-radius: var(--radius);
padding: 16px 24px;
font-weight: 700;
width: 100%;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.btn-primary-custom:active {
transform: scale(0.98);
}
.log-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: var(--surface);
border-radius: var(--radius);
margin-bottom: 10px;
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);
.modal-content {
background-color: var(--surface);
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;
font-size: 0.75rem;
letter-spacing: 1px;
}
.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;
border: 1px solid var(--border);
}
.form-control {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid rgba(0, 0, 0, 0.1);
background-color: var(--bg);
border-radius: 12px;
background: #fff;
transition: all 0.3s ease;
box-sizing: border-box;
border: 1px solid var(--border);
padding: 14px 16px;
color: var(--text);
font-size: 1rem; /* Prevent zoom on iOS */
}
.form-control:focus {
outline: none;
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
/* Custom Utilities */
.x-small { font-size: 0.75rem; }
.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; }
#reminders-row::-webkit-scrollbar { display: none; }
/* Supplement buttons */
.supplement-toggle-btn {
border-radius: 12px !important;
font-weight: 600 !important;
padding: 8px 16px !important;
}
/* Gallery adjustments */
#gallery-container .card {
border-radius: var(--radius);
overflow: hidden;
border: 1px solid var(--border);
background: var(--surface);
}
/* Hide Bootstrap Tab Nav as we use Bottom Nav now */
#mainTab {
display: none !important;
}
@media (min-width: 481px) {
body {
background-color: #000;
}
.app-shell {
box-shadow: 0 0 40px rgba(0,0,0,0.5);
}
}

View File

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

413
index.php
View File

@ -1,150 +1,291 @@
<?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, viewport-fit=cover" />
<title>Nutrition Pulse</title>
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Mobile / PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Pulse">
<meta name="theme-color" content="#0f172a">
<link rel="manifest" href="manifest.json">
<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="app-shell">
<header class="app-bar px-3 py-2">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h5 fw-bold mb-0">Nutrition Pulse</h1>
<p class="text-muted x-small mb-0"><?= date('l, F d') ?></p>
</div>
<div class="dropdown">
<button class="btn btn-outline-secondary btn-sm rounded-circle p-1 border-0" type="button" data-bs-toggle="dropdown">
<svg width="24" height="24" 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><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>
</div>
</div>
</header>
<!-- Hidden tab triggers for Bootstrap API -->
<ul class="nav nav-pills d-none" id="mainTab" role="tablist">
<li class="nav-item" role="presentation"><button class="nav-link active" id="today-tab" data-bs-toggle="pill" data-bs-target="#today" type="button" role="tab"></button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="health-tab" data-bs-toggle="pill" data-bs-target="#health" type="button" role="tab"></button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="gallery-tab" data-bs-toggle="pill" data-bs-target="#gallery" type="button" role="tab"></button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="analysis-tab" data-bs-toggle="pill" data-bs-target="#analysis" type="button" role="tab"></button></li>
</ul>
<main class="app-content px-3 pt-3 pb-5">
<div class="tab-content" id="mainTabContent">
<!-- TODAY TAB -->
<div class="tab-pane fade show active" id="today" role="tabpanel">
<div id="reminders-row" class="d-flex gap-2 mb-3 overflow-auto pb-2" style="scrollbar-width: none;"></div>
<div id="stats-container">
<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>
<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>
<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>
<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"></div>
</div>
<button class="btn btn-primary-custom mb-4 shadow-sm" data-bs-toggle="modal" data-bs-target="#addLogModal">Log Food / Supplement</button>
<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">
<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>
<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>
<div class="card-stat">
<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>
<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>
<!-- 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 w-auto py-2 px-3" data-bs-toggle="modal" data-bs-target="#photoModal">+ Add Photo</button>
</div>
<div id="gallery-container" class="row g-3"></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>
</div>
</main>
<nav class="bottom-nav">
<a href="#" class="nav-item active" data-tab="today">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<span>Today</span>
</a>
<a href="#" class="nav-item" data-tab="health">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"></path></svg>
<span>Health</span>
</a>
<a href="#" class="nav-item" data-tab="gallery">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
<span>Gallery</span>
</a>
<a href="#" class="nav-item" data-tab="analysis">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>
<span>Trends</span>
</a>
</nav>
</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="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>
<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>
<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>
<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="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">
<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>

17
manifest.json Normal file
View File

@ -0,0 +1,17 @@
{
"name": "Nutrition Pulse",
"short_name": "Pulse",
"description": "Modern nutrition and fitness tracking.",
"start_url": "/index.php",
"display": "standalone",
"background_color": "#0f172a",
"theme_color": "#3b82f6",
"icons": [
{
"src": "https://cdn-icons-png.flaticon.com/512/3069/3069172.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}

26
sw.js Normal file
View File

@ -0,0 +1,26 @@
const CACHE_NAME = 'pulse-v1';
const ASSETS = [
'/',
'/index.php',
'/assets/css/custom.css',
'/assets/js/main.js',
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css',
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js',
'https://cdn.jsdelivr.net/npm/chart.js'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(ASSETS);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});