Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cb0064488 | ||
|
|
bb9eef0fb8 | ||
|
|
7f7c6e873d |
51
api/ai_analyze.php
Normal file
51
api/ai_analyze.php
Normal 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
30
api/analysis.php
Normal 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
89
api/logs.php
Normal 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
30
api/photos.php
Normal 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
40
api/supplements.php
Normal 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
42
api/water.php
Normal 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
28
api/weight.php
Normal 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));
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
413
index.php
@ -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
17
manifest.json
Normal 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
26
sw.js
Normal 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);
|
||||
})
|
||||
);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user