initial commit
This commit is contained in:
237
views/add_meal.ejs
Normal file
237
views/add_meal.ejs
Normal file
@@ -0,0 +1,237 @@
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h1 class="text-3xl font-bold text-gray-800 mb-6">Add Meal</h1>
|
||||
|
||||
<form method="POST" id="mealForm" class="glass rounded-xl p-8 shadow-lg">
|
||||
<!-- Meal Details -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Date</label>
|
||||
<input type="date" name="date" value="<%= today %>" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Meal Type</label>
|
||||
<select name="meal_type" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary" required>
|
||||
<option value="breakfast">🌅 Breakfast</option>
|
||||
<option value="lunch">🌞 Lunch</option>
|
||||
<option value="dinner">🌙 Dinner</option>
|
||||
<option value="snack">🍪 Snack</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Time (optional)</label>
|
||||
<input type="time" name="time" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Food Search -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Search Foods</label>
|
||||
<input type="text" id="foodSearch" placeholder="Search Filipino or international foods..." class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary">
|
||||
<div id="searchResults" class="mt-2 max-h-64 overflow-y-auto hidden bg-white border rounded-lg shadow-lg z-10 relative"></div>
|
||||
</div>
|
||||
|
||||
<!-- Selected Foods -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-bold mb-3">Selected Foods</h3>
|
||||
<div id="selectedFoods" class="space-y-3">
|
||||
<p class="text-gray-500 text-center py-8" id="emptyMessage">No foods added yet. Search and add foods above.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="bg-blue-50 border-l-4 border-blue-500 p-4 rounded mb-6">
|
||||
<h4 class="font-bold text-blue-900 mb-2">Meal Summary</h4>
|
||||
<div class="grid grid-cols-4 gap-4 text-center">
|
||||
<div>
|
||||
<p class="text-sm text-blue-700">Calories</p>
|
||||
<p class="text-xl font-bold text-blue-900" id="totalCalories">0</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-blue-700">Protein</p>
|
||||
<p class="text-xl font-bold text-blue-900" id="totalProtein">0g</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-blue-700">Carbs</p>
|
||||
<p class="text-xl font-bold text-blue-900" id="totalCarbs">0g</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-blue-700">Fat</p>
|
||||
<p class="text-xl font-bold text-blue-900" id="totalFat">0g</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-between">
|
||||
<a href="/dashboard" class="px-6 py-3 border border-gray-300 rounded-lg hover:bg-gray-100 transition">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="bg-primary text-white px-8 py-3 rounded-lg hover:bg-red-700 transition">
|
||||
Save Meal
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let selectedFoods = [];
|
||||
let searchTimeout;
|
||||
|
||||
// Food Search
|
||||
document.getElementById('foodSearch').addEventListener('input', function(e) {
|
||||
clearTimeout(searchTimeout);
|
||||
const query = e.target.value;
|
||||
|
||||
if (query.length < 2) {
|
||||
document.getElementById('searchResults').classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
searchTimeout = setTimeout(() => {
|
||||
fetch(`/api/search-food?q=${encodeURIComponent(query)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => displaySearchResults(data));
|
||||
}, 300);
|
||||
});
|
||||
|
||||
function displaySearchResults(results) {
|
||||
const container = document.getElementById('searchResults');
|
||||
|
||||
if (results.length === 0) {
|
||||
container.innerHTML = '<p class="p-4 text-gray-500">No foods found</p>';
|
||||
container.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = results.map(food => {
|
||||
// Safe stringify for onclick attribute
|
||||
const foodJson = JSON.stringify(food).replace(/"/g, '"');
|
||||
|
||||
return `
|
||||
<div class="p-3 border-b hover:bg-gray-50 cursor-pointer transition-colors" onclick="addFood(${foodJson})">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<p class="font-semibold">${food.name}</p>
|
||||
${food.name_tagalog ? `<p class="text-sm text-gray-600">${food.name_tagalog}</p>` : ''}
|
||||
<p class="text-xs text-gray-500">${food.serving_description || '1 serving'}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="font-bold text-primary">${Math.round(food.calories)} cal</p>
|
||||
<p class="text-xs text-gray-600">P:${Math.round(food.protein_g)}g C:${Math.round(food.carbs_g)}g F:${Math.round(food.fat_g)}g</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`}).join('');
|
||||
|
||||
container.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function addFood(food) {
|
||||
// Save to database if from API
|
||||
if (food.source === 'api_ninjas' && !food.id) {
|
||||
fetch('/api/add-food', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(food)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
food.id = data.food_id;
|
||||
addFoodToList(food);
|
||||
} else {
|
||||
alert('Error adding food');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
addFoodToList(food);
|
||||
}
|
||||
|
||||
// Clear search
|
||||
document.getElementById('foodSearch').value = '';
|
||||
document.getElementById('searchResults').classList.add('hidden');
|
||||
}
|
||||
|
||||
function addFoodToList(food) {
|
||||
selectedFoods.push({...food, quantity: 1});
|
||||
renderSelectedFoods();
|
||||
}
|
||||
|
||||
function removeFood(index) {
|
||||
selectedFoods.splice(index, 1);
|
||||
renderSelectedFoods();
|
||||
}
|
||||
|
||||
function updateQuantity(index, quantity) {
|
||||
selectedFoods[index].quantity = parseFloat(quantity);
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
function renderSelectedFoods() {
|
||||
const container = document.getElementById('selectedFoods');
|
||||
const emptyMessage = document.getElementById('emptyMessage');
|
||||
|
||||
if (selectedFoods.length === 0) {
|
||||
emptyMessage.classList.remove('hidden');
|
||||
container.innerHTML = ''; // Clear container but keep empty message logic
|
||||
container.appendChild(emptyMessage);
|
||||
updateSummary();
|
||||
return;
|
||||
}
|
||||
|
||||
emptyMessage.classList.add('hidden');
|
||||
|
||||
// Rebuild list
|
||||
const listHtml = selectedFoods.map((food, index) => `
|
||||
<div class="flex items-center justify-between p-4 border rounded-lg bg-white shadow-sm">
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold">${food.name}</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
${Math.round(food.calories * food.quantity)} cal |
|
||||
P:${Math.round(food.protein_g * food.quantity)}g
|
||||
C:${Math.round(food.carbs_g * food.quantity)}g
|
||||
F:${Math.round(food.fat_g * food.quantity)}g
|
||||
</p>
|
||||
<input type="hidden" name="food_id[]" value="${food.id}">
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<label class="text-sm text-gray-600">Servings:</label>
|
||||
<input type="number" step="0.1" min="0.1" name="quantity[]" value="${food.quantity}"
|
||||
class="w-20 px-2 py-1 border rounded focus:ring-primary focus:border-primary"
|
||||
onchange="updateQuantity(${index}, this.value)">
|
||||
<button type="button" onclick="removeFood(${index})" class="text-red-600 hover:text-red-800 p-1 rounded hover:bg-red-50 transition">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
container.innerHTML = listHtml;
|
||||
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
function updateSummary() {
|
||||
const totals = selectedFoods.reduce((acc, food) => ({
|
||||
calories: acc.calories + (food.calories * food.quantity),
|
||||
protein: acc.protein + (food.protein_g * food.quantity),
|
||||
carbs: acc.carbs + (food.carbs_g * food.quantity),
|
||||
fat: acc.fat + (food.fat_g * food.quantity)
|
||||
}), {calories: 0, protein: 0, carbs: 0, fat: 0});
|
||||
|
||||
document.getElementById('totalCalories').textContent = Math.round(totals.calories);
|
||||
document.getElementById('totalProtein').textContent = Math.round(totals.protein) + 'g';
|
||||
document.getElementById('totalCarbs').textContent = Math.round(totals.carbs) + 'g';
|
||||
document.getElementById('totalFat').textContent = Math.round(totals.fat) + 'g';
|
||||
}
|
||||
|
||||
// Prevent form submission if no foods
|
||||
document.getElementById('mealForm').addEventListener('submit', function(e) {
|
||||
if (selectedFoods.length === 0) {
|
||||
e.preventDefault();
|
||||
alert('Please add at least one food item');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
294
views/dashboard.ejs
Normal file
294
views/dashboard.ejs
Normal file
@@ -0,0 +1,294 @@
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-800">Today's Summary</h1>
|
||||
<p class="text-gray-600"><%= new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) %></p>
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<a href="/add-meal" class="bg-primary text-white px-6 py-3 rounded-lg hover:bg-red-700 transition inline-block">
|
||||
➕ Add Meal
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Cards Row -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
|
||||
<!-- Calories Card -->
|
||||
<div class="glass rounded-xl p-6 shadow-lg">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Calories</p>
|
||||
<h2 class="text-3xl font-bold text-primary"><%= Math.round(nutrition.calories) %></h2>
|
||||
<p class="text-sm text-gray-500">/ <%= current_user.target_daily_calories %></p>
|
||||
</div>
|
||||
<span class="text-4xl">🔥</span>
|
||||
</div>
|
||||
<!-- Progress Bar -->
|
||||
<div class="w-full bg-gray-200 rounded-full h-3">
|
||||
<% let cal_percent = current_user.target_daily_calories > 0 ? Math.round(nutrition.calories / current_user.target_daily_calories * 100) : 0; %>
|
||||
<div class="bg-primary h-3 rounded-full transition-all" style="width: <%= Math.min(cal_percent, 100) %>%"></div>
|
||||
</div>
|
||||
<p class="text-sm mt-2 <%= remaining.calories >= 0 ? 'text-success' : 'text-red-600' %>">
|
||||
<%= Math.round(remaining.calories) %> remaining
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Protein Card -->
|
||||
<div class="glass rounded-xl p-6 shadow-lg">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm">Protein</p>
|
||||
<h2 class="text-3xl font-bold text-secondary"><%= Math.round(nutrition.protein) %>g</h2>
|
||||
<p class="text-sm text-gray-500">/ <%= goals.target_protein_g %>g</p>
|
||||
</div>
|
||||
<span class="text-4xl">💪</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-3">
|
||||
<% let prot_percent = goals.target_protein_g > 0 ? Math.round(nutrition.protein / goals.target_protein_g * 100) : 0; %>
|
||||
<div class="bg-secondary h-3 rounded-full" style="width: <%= Math.min(prot_percent, 100) %>%"></div>
|
||||
</div>
|
||||
<p class="text-sm mt-2 <%= remaining.protein >= 0 ? 'text-success' : 'text-red-600' %>">
|
||||
<%= Math.round(remaining.protein) %>g remaining
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Carbs & Fat Card -->
|
||||
<div class="glass rounded-xl p-6 shadow-lg">
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600 text-sm">Carbs</span>
|
||||
<span class="font-bold text-warning"><%= Math.round(nutrition.carbs) %>g / <%= goals.target_carbs_g %>g</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2 mt-1">
|
||||
<% let carb_percent = goals.target_carbs_g > 0 ? Math.round(nutrition.carbs / goals.target_carbs_g * 100) : 0; %>
|
||||
<div class="bg-warning h-2 rounded-full" style="width: <%= Math.min(carb_percent, 100) %>%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600 text-sm">Fat</span>
|
||||
<span class="font-bold text-orange-600"><%= Math.round(nutrition.fat) %>g / <%= goals.target_fat_g %>g</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2 mt-1">
|
||||
<% let fat_percent = goals.target_fat_g > 0 ? Math.round(nutrition.fat / goals.target_fat_g * 100) : 0; %>
|
||||
<div class="bg-orange-600 h-2 rounded-full" style="width: <%= Math.min(fat_percent, 100) %>%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Water & Weight Card -->
|
||||
<div class="glass rounded-xl p-6 shadow-lg">
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600 text-sm">Water</span>
|
||||
<span class="font-bold text-blue-600"><%= water.total_ml %>ml / <%= goals.target_water_ml %>ml</span>
|
||||
</div>
|
||||
<div class="flex space-x-1 mt-2">
|
||||
<% let glasses_filled = Math.floor(water.total_ml / 250); %>
|
||||
<% for (let i = 0; i < 8; i++) { %>
|
||||
<span class="<%= i < glasses_filled ? 'text-blue-500' : 'text-gray-300' %>">💧</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<!-- Quick add water buttons -->
|
||||
<form method="POST" action="/add-water" class="flex space-x-1 mt-2">
|
||||
<button type="submit" name="amount_ml" value="250" class="bg-blue-100 text-blue-700 px-2 py-1 rounded text-xs hover:bg-blue-200">+250ml</button>
|
||||
<button type="submit" name="amount_ml" value="500" class="bg-blue-100 text-blue-700 px-2 py-1 rounded text-xs hover:bg-blue-200">+500ml</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="pt-3 border-t">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600 text-sm">Weight</span>
|
||||
<% if (typeof weight_today !== 'undefined' && weight_today) { %>
|
||||
<div class="text-right">
|
||||
<span class="font-bold"><%= weight_today.weight_kg %>kg</span>
|
||||
<% if (typeof weight_change !== 'undefined' && weight_change !== null) { %>
|
||||
<span class="text-xs <%= weight_change < 0 ? 'text-success' : 'text-red-600' %>">
|
||||
<%= weight_change.toFixed(1) %>kg
|
||||
</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<form method="POST" action="/add-weight" class="flex items-center space-x-2">
|
||||
<input type="number" step="0.1" name="weight_kg" placeholder="kg" class="w-20 px-2 py-1 border rounded text-sm" required>
|
||||
<button type="submit" class="bg-green-500 text-white px-2 py-1 rounded text-xs hover:bg-green-600">Log</button>
|
||||
</form>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Macro Distribution Pie Chart -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
<div class="glass rounded-xl p-6 shadow-lg">
|
||||
<h3 class="text-lg font-bold mb-4">Macro Distribution</h3>
|
||||
<canvas id="macroChart" height="200"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Suggestions -->
|
||||
<div class="glass rounded-xl p-6 shadow-lg lg:col-span-2">
|
||||
<h3 class="text-lg font-bold mb-4">💡 Smart Suggestions</h3>
|
||||
<% if (suggestions && suggestions.length > 0) { %>
|
||||
<div class="space-y-3">
|
||||
<% suggestions.forEach(function(suggestion) { %>
|
||||
<div class="bg-blue-50 border-l-4 border-blue-500 p-3 rounded">
|
||||
<p class="font-semibold text-blue-900"><%= suggestion.category %></p>
|
||||
<p class="text-sm text-blue-700">Try: <%= suggestion.examples.join(', ') %></p>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<p class="text-gray-600">You're on track! Keep up the good work! 🎉</p>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Today's Meals -->
|
||||
<div class="glass rounded-xl p-6 shadow-lg mb-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-xl font-bold">Today's Meals</h3>
|
||||
<a href="/add-meal" class="text-primary hover:underline">+ Add Meal</a>
|
||||
</div>
|
||||
|
||||
<% if (nutrition.meals && nutrition.meals.length > 0) { %>
|
||||
<div class="space-y-4">
|
||||
<% nutrition.meals.forEach(function(meal) { %>
|
||||
<div class="border-l-4 <%= meal.type == 'breakfast' ? 'border-yellow-500' : (meal.type == 'lunch' ? 'border-green-500' : (meal.type == 'dinner' ? 'border-blue-500' : 'border-purple-500')) %> pl-4 py-2">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-2xl">
|
||||
<% if (meal.type == 'breakfast') { %>🌅<% } else if (meal.type == 'lunch') { %>🌞<% } else if (meal.type == 'dinner') { %>🌙<% } else { %>🍪<% } %>
|
||||
</span>
|
||||
<h4 class="font-bold text-lg capitalize"><%= meal.type %></h4>
|
||||
<% if (meal.time) { %>
|
||||
<span class="text-sm text-gray-500"><%= meal.time %></span>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="mt-2 space-y-1">
|
||||
<% meal.foods.forEach(function(food) { %>
|
||||
<p class="text-sm text-gray-700">• <%= food.name %> (<%= food.quantity %>x) - <%= Math.round(food.calories) %> cal</p>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-2xl font-bold text-primary"><%= Math.round(meal.totals.calories) %></p>
|
||||
<p class="text-xs text-gray-500">calories</p>
|
||||
<p class="text-xs text-gray-600 mt-1">
|
||||
P: <%= Math.round(meal.totals.protein) %>g |
|
||||
C: <%= Math.round(meal.totals.carbs) %>g |
|
||||
F: <%= Math.round(meal.totals.fat) %>g
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="text-center py-12 text-gray-500">
|
||||
<p class="text-4xl mb-4">🍽️</p>
|
||||
<p>No meals logged yet today.</p>
|
||||
<a href="/add-meal" class="text-primary hover:underline mt-2 inline-block">Add your first meal</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<!-- Weekly Trends -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Calorie Trend -->
|
||||
<div class="glass rounded-xl p-6 shadow-lg">
|
||||
<h3 class="text-lg font-bold mb-4">📈 Calorie Trend (7 Days)</h3>
|
||||
<canvas id="calorieChart" height="200"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Weight Trend -->
|
||||
<div class="glass rounded-xl p-6 shadow-lg">
|
||||
<h3 class="text-lg font-bold mb-4">⚖️ Weight Trend (7 Days)</h3>
|
||||
<canvas id="weightChart" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Macro Distribution Chart
|
||||
const macroCtx = document.getElementById('macroChart').getContext('2d');
|
||||
new Chart(macroCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Protein', 'Carbs', 'Fat'],
|
||||
datasets: [{
|
||||
data: [<%= macro_percentages.protein %>, <%= macro_percentages.carbs %>, <%= macro_percentages.fat %>],
|
||||
backgroundColor: ['#003F87', '#FFB703', '#FF6B35']
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Calorie Trend Chart
|
||||
const calorieCtx = document.getElementById('calorieChart').getContext('2d');
|
||||
new Chart(calorieCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: <%- JSON.stringify(calorie_trend.map(d => d.date)) %>,
|
||||
datasets: [{
|
||||
label: 'Calories',
|
||||
data: <%- JSON.stringify(calorie_trend.map(d => d.calories)) %>,
|
||||
borderColor: '#D62828',
|
||||
backgroundColor: 'rgba(214, 40, 40, 0.1)',
|
||||
tension: 0.4
|
||||
}, {
|
||||
label: 'Target',
|
||||
data: Array(7).fill(<%= current_user.target_daily_calories %>),
|
||||
borderColor: '#06D6A0',
|
||||
borderDash: [5, 5],
|
||||
pointRadius: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Weight Trend Chart
|
||||
<% if (weight_trend && weight_trend.length > 0) { %>
|
||||
const weightCtx = document.getElementById('weightChart').getContext('2d');
|
||||
new Chart(weightCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: <%- JSON.stringify(weight_trend.map(d => d.date)) %>,
|
||||
datasets: [{
|
||||
label: 'Weight (kg)',
|
||||
data: <%- JSON.stringify(weight_trend.map(d => d.weight_kg)) %>,
|
||||
borderColor: '#003F87',
|
||||
backgroundColor: 'rgba(0, 63, 135, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
<% } %>
|
||||
</script>
|
||||
104
views/foods.ejs
Normal file
104
views/foods.ejs
Normal file
@@ -0,0 +1,104 @@
|
||||
<h1 class="text-3xl font-bold mb-6">Food Database</h1>
|
||||
|
||||
<!-- Search and Filter -->
|
||||
<div class="glass rounded-xl p-6 mb-8">
|
||||
<form method="GET" action="/foods" class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="md:col-span-3">
|
||||
<input type="text" name="q" value="<%= search_query %>" placeholder="Search for foods (English or Tagalog)..." class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
</div>
|
||||
<div>
|
||||
<select name="category" onchange="this.form.submit()" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
<% categories.forEach(cat => { %>
|
||||
<option value="<%= cat %>" <%= current_category === cat ? 'selected' : '' %>><%= cat.charAt(0).toUpperCase() + cat.slice(1) %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold mb-4 flex items-center">
|
||||
<span class="mr-2">🇵🇭</span> Filipino Foods
|
||||
</h2>
|
||||
|
||||
<% if (filipino_foods.length === 0) { %>
|
||||
<p class="text-gray-500 italic">No Filipino foods found matching your criteria.</p>
|
||||
<% } else { %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<% filipino_foods.forEach(food => { %>
|
||||
<div class="glass rounded-xl p-4 hover:shadow-lg transition">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<h3 class="font-bold text-lg"><%= food.name %></h3>
|
||||
<% if (food.name_tagalog) { %>
|
||||
<p class="text-sm text-gray-600 italic"><%= food.name_tagalog %></p>
|
||||
<% } %>
|
||||
</div>
|
||||
<span class="bg-primary text-white text-xs px-2 py-1 rounded-full"><%= food.category %></span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 gap-2 text-center text-sm mt-3">
|
||||
<div class="bg-red-50 rounded p-1">
|
||||
<span class="block font-bold text-primary"><%= Math.round(food.calories) %></span>
|
||||
<span class="text-xs text-gray-500">cal</span>
|
||||
</div>
|
||||
<div class="bg-blue-50 rounded p-1">
|
||||
<span class="block font-bold text-blue-600"><%= Math.round(food.protein_g) %>g</span>
|
||||
<span class="text-xs text-gray-500">prot</span>
|
||||
</div>
|
||||
<div class="bg-yellow-50 rounded p-1">
|
||||
<span class="block font-bold text-yellow-600"><%= Math.round(food.carbs_g) %>g</span>
|
||||
<span class="text-xs text-gray-500">carb</span>
|
||||
</div>
|
||||
<div class="bg-orange-50 rounded p-1">
|
||||
<span class="block font-bold text-orange-600"><%= Math.round(food.fat_g) %>g</span>
|
||||
<span class="text-xs text-gray-500">fat</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-xs text-gray-500 text-center">
|
||||
Per <%= food.serving_description || 'serving' %> (<%= food.serving_size_g %>g)
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold mb-4">Other Foods</h2>
|
||||
|
||||
<% if (other_foods.length === 0) { %>
|
||||
<p class="text-gray-500 italic">No other foods found.</p>
|
||||
<% } else { %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<% other_foods.forEach(food => { %>
|
||||
<div class="glass rounded-xl p-4 hover:shadow-lg transition opacity-90">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<h3 class="font-bold text-lg"><%= food.name %></h3>
|
||||
<span class="bg-gray-200 text-gray-700 text-xs px-2 py-1 rounded-full">Global</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 gap-2 text-center text-sm mt-3">
|
||||
<div class="bg-red-50 rounded p-1">
|
||||
<span class="block font-bold text-primary"><%= Math.round(food.calories) %></span>
|
||||
<span class="text-xs text-gray-500">cal</span>
|
||||
</div>
|
||||
<div class="bg-blue-50 rounded p-1">
|
||||
<span class="block font-bold text-blue-600"><%= Math.round(food.protein_g) %>g</span>
|
||||
<span class="text-xs text-gray-500">prot</span>
|
||||
</div>
|
||||
<div class="bg-yellow-50 rounded p-1">
|
||||
<span class="block font-bold text-yellow-600"><%= Math.round(food.carbs_g) %>g</span>
|
||||
<span class="text-xs text-gray-500">carb</span>
|
||||
</div>
|
||||
<div class="bg-orange-50 rounded p-1">
|
||||
<span class="block font-bold text-orange-600"><%= Math.round(food.fat_g) %>g</span>
|
||||
<span class="text-xs text-gray-500">fat</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
117
views/goals.ejs
Normal file
117
views/goals.ejs
Normal file
@@ -0,0 +1,117 @@
|
||||
<h1 class="text-3xl font-bold mb-6">Goals & Settings</h1>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Personal Info Form -->
|
||||
<div class="glass rounded-xl p-6">
|
||||
<h2 class="text-xl font-semibold mb-4 text-primary">Personal Information</h2>
|
||||
<form method="POST" action="/goals">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Age</label>
|
||||
<input type="number" name="age" value="<%= current_user.age || 25 %>" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Gender</label>
|
||||
<select name="gender" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent" required>
|
||||
<option value="male" <%= current_user.gender === 'male' ? 'selected' : '' %>>Male</option>
|
||||
<option value="female" <%= current_user.gender === 'female' ? 'selected' : '' %>>Female</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Height (cm)</label>
|
||||
<input type="number" step="0.1" name="height_cm" value="<%= current_user.height_cm || 170 %>" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Weight (kg)</label>
|
||||
<input type="number" step="0.1" name="weight_kg" value="<%= current_user.weight_kg || 70 %>" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2">Activity Level</label>
|
||||
<select name="activity_level" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent" required>
|
||||
<option value="sedentary" <%= current_user.activity_level === 'sedentary' ? 'selected' : '' %>>Sedentary (Office job)</option>
|
||||
<option value="light" <%= current_user.activity_level === 'light' ? 'selected' : '' %>>Light (Exercise 1-3 days/week)</option>
|
||||
<option value="moderate" <%= current_user.activity_level === 'moderate' ? 'selected' : '' %>>Moderate (Exercise 3-5 days/week)</option>
|
||||
<option value="active" <%= current_user.activity_level === 'active' ? 'selected' : '' %>>Active (Exercise 6-7 days/week)</option>
|
||||
<option value="very_active" <%= current_user.activity_level === 'very_active' ? 'selected' : '' %>>Very Active (Physical job/training)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<hr class="my-6 border-gray-200">
|
||||
|
||||
<h2 class="text-xl font-semibold mb-4 text-primary">Goals</h2>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2">Goal Type</label>
|
||||
<select name="goal_type" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent" required>
|
||||
<option value="weight_loss" <%= goals && goals.goal_type === 'weight_loss' ? 'selected' : '' %>>Weight Loss</option>
|
||||
<option value="recomp" <%= (!goals || goals.goal_type === 'recomp') ? 'selected' : '' %>>Maintain / Recomposition</option>
|
||||
<option value="muscle_gain" <%= goals && goals.goal_type === 'muscle_gain' ? 'selected' : '' %>>Muscle Gain</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Target Weight (kg)</label>
|
||||
<input type="number" step="0.1" name="target_weight_kg" value="<%= goals ? goals.target_weight_kg : 70 %>" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Target Water (ml)</label>
|
||||
<input type="number" name="target_water_ml" value="<%= goals ? goals.target_water_ml : 2000 %>" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full bg-primary text-white font-bold py-3 px-4 rounded-lg hover:bg-red-700 transition duration-300">
|
||||
Update Goals & Recalculate Macros
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Calculated Stats -->
|
||||
<div class="space-y-6">
|
||||
<div class="glass rounded-xl p-6">
|
||||
<h2 class="text-xl font-semibold mb-4 text-primary">Your Stats</h2>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="p-4 bg-red-50 rounded-lg">
|
||||
<p class="text-sm text-gray-500 mb-1">BMR</p>
|
||||
<p class="text-2xl font-bold text-gray-800"><%= bmr || '-' %></p>
|
||||
<p class="text-xs text-gray-400">Calories burned at rest</p>
|
||||
</div>
|
||||
<div class="p-4 bg-green-50 rounded-lg">
|
||||
<p class="text-sm text-gray-500 mb-1">TDEE</p>
|
||||
<p class="text-2xl font-bold text-gray-800"><%= tdee || '-' %></p>
|
||||
<p class="text-xs text-gray-400">Total daily energy expenditure</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if (goals) { %>
|
||||
<div class="glass rounded-xl p-6">
|
||||
<h2 class="text-xl font-semibold mb-4 text-primary">Daily Targets</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center p-3 border-b">
|
||||
<span class="text-gray-600">Calories</span>
|
||||
<span class="font-bold text-lg"><%= current_user.target_daily_calories %></span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center p-3 border-b">
|
||||
<span class="text-gray-600">Protein</span>
|
||||
<span class="font-bold"><%= goals.target_protein_g %>g</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center p-3 border-b">
|
||||
<span class="text-gray-600">Carbs</span>
|
||||
<span class="font-bold"><%= goals.target_carbs_g %>g</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center p-3 border-b">
|
||||
<span class="text-gray-600">Fat</span>
|
||||
<span class="font-bold"><%= goals.target_fat_g %>g</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center p-3">
|
||||
<span class="text-gray-600">Water</span>
|
||||
<span class="font-bold"><%= goals.target_water_ml %>ml</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
104
views/layout.ejs
Normal file
104
views/layout.ejs
Normal file
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Calorie Tracker</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#D62828',
|
||||
secondary: '#003F87',
|
||||
success: '#06D6A0',
|
||||
warning: '#FFB703',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<% if (current_user) { %>
|
||||
<!-- Navigation -->
|
||||
<nav class="bg-primary text-white shadow-lg">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center py-4">
|
||||
<div class="flex items-center space-x-8">
|
||||
<a href="/" class="text-2xl font-bold">🍽️ Calorie Tracker</a>
|
||||
<div class="hidden md:flex space-x-6">
|
||||
<a href="/dashboard" class="hover:text-gray-200 transition <%= path === '/dashboard' ? 'font-bold' : '' %>">
|
||||
🏠 Dashboard
|
||||
</a>
|
||||
<a href="/meal-planner" class="hover:text-gray-200 transition <%= path === '/meal-planner' ? 'font-bold' : '' %>">
|
||||
📅 Meal Planner
|
||||
</a>
|
||||
<a href="/foods" class="hover:text-gray-200 transition <%= path === '/foods' ? 'font-bold' : '' %>">
|
||||
🍛 Foods
|
||||
</a>
|
||||
<a href="/progress" class="hover:text-gray-200 transition <%= path === '/progress' ? 'font-bold' : '' %>">
|
||||
📊 Progress
|
||||
</a>
|
||||
<a href="/goals" class="hover:text-gray-200 transition <%= path === '/goals' ? 'font-bold' : '' %>">
|
||||
🎯 Goals
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="hidden md:inline">👤 <%= current_user.name || current_user.username %></span>
|
||||
<a href="/logout" class="bg-white text-primary px-4 py-2 rounded hover:bg-gray-100 transition">
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<% } %>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
<% if (success_msg.length > 0) { %>
|
||||
<div class="container mx-auto px-4 mt-4">
|
||||
<div class="bg-green-100 border-green-400 text-green-700 border px-4 py-3 rounded relative mb-4" role="alert">
|
||||
<span class="block sm:inline"><%= success_msg %></span>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (error_msg.length > 0) { %>
|
||||
<div class="container mx-auto px-4 mt-4">
|
||||
<div class="bg-red-100 border-red-400 text-red-700 border px-4 py-3 rounded relative mb-4" role="alert">
|
||||
<span class="block sm:inline"><%= error_msg %></span>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (error.length > 0) { %>
|
||||
<div class="container mx-auto px-4 mt-4">
|
||||
<div class="bg-red-100 border-red-400 text-red-700 border px-4 py-3 rounded relative mb-4" role="alert">
|
||||
<span class="block sm:inline"><%= error %></span>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="container mx-auto px-4 py-8">
|
||||
<%- body %>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gray-800 text-white text-center py-4 mt-12">
|
||||
<p>© 2026 Calorie Tracker - Filipino Food Edition</p>
|
||||
</footer>
|
||||
|
||||
<%- defineContent('scripts') %>
|
||||
</body>
|
||||
</html>
|
||||
26
views/login.ejs
Normal file
26
views/login.ejs
Normal file
@@ -0,0 +1,26 @@
|
||||
<div class="max-w-md mx-auto">
|
||||
<div class="glass rounded-xl p-8 shadow-lg">
|
||||
<h1 class="text-3xl font-bold text-center text-gray-800 mb-6">🍽️ Calorie Tracker</h1>
|
||||
<p class="text-center text-gray-600 mb-8">Filipino Food Edition</p>
|
||||
|
||||
<form method="POST" action="/login">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Username</label>
|
||||
<input type="text" name="username" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Password</label>
|
||||
<input type="password" name="password" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full bg-primary text-white py-3 rounded-lg hover:bg-red-700 transition font-semibold">
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="text-center mt-6 text-gray-600">
|
||||
Don't have an account? <a href="/register" class="text-primary hover:underline font-semibold">Register</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
253
views/meal_planner.ejs
Normal file
253
views/meal_planner.ejs
Normal file
@@ -0,0 +1,253 @@
|
||||
<h1 class="text-3xl font-bold mb-6">Weekly Meal Planner</h1>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-7 gap-4 overflow-x-auto pb-4">
|
||||
<% dates.forEach(dateStr => { %>
|
||||
<% const dateObj = new Date(dateStr); %>
|
||||
<% const isToday = dateStr === today; %>
|
||||
|
||||
<div class="min-w-[250px] md:min-w-0 flex flex-col h-full">
|
||||
<!-- Date Header -->
|
||||
<div class="p-3 rounded-t-xl text-center <%= isToday ? 'bg-primary text-white' : 'bg-white border-t border-x' %>">
|
||||
<p class="text-xs font-bold uppercase"><%= dateObj.toLocaleDateString('en-US', { weekday: 'short' }) %></p>
|
||||
<p class="text-lg font-bold"><%= dateObj.getDate() %></p>
|
||||
</div>
|
||||
|
||||
<!-- Meal Column -->
|
||||
<div class="bg-gray-50 border-x border-b rounded-b-xl flex-1 p-2 space-y-3 min-h-[500px]">
|
||||
<% if (meal_plans[dateStr] && meal_plans[dateStr].length > 0) { %>
|
||||
<% meal_plans[dateStr].forEach(plan => { %>
|
||||
<div class="bg-white p-3 rounded-lg shadow-sm border border-gray-100 hover:shadow-md transition relative group <%= plan.is_completed ? 'opacity-60' : '' %>">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-xs font-bold uppercase text-gray-500"><%= plan.meal_type %></span>
|
||||
<div class="flex space-x-1">
|
||||
<!-- Complete Toggle -->
|
||||
<form action="/meal-planner/complete/<%= plan.id %>" method="POST" class="inline">
|
||||
<button type="submit" class="text-gray-400 hover:text-green-500 transition" title="<%= plan.is_completed ? 'Mark Incomplete' : 'Mark Complete' %>">
|
||||
<% if (plan.is_completed) { %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-500" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<% } else { %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<% } %>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Delete -->
|
||||
<form action="/meal-planner/delete/<%= plan.id %>" method="POST" class="inline" onsubmit="return confirm('Delete this meal plan?');">
|
||||
<button type="submit" class="text-gray-400 hover:text-red-500 transition opacity-0 group-hover:opacity-100">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="text-sm space-y-1 mb-2">
|
||||
<% plan.foods.forEach(food => { %>
|
||||
<li class="truncate <%= plan.is_completed ? 'line-through text-gray-400' : '' %>">• <%= food.name %></li>
|
||||
<% }); %>
|
||||
</ul>
|
||||
|
||||
<div class="text-xs text-gray-400 flex justify-between pt-2 border-t">
|
||||
<span><%= Math.round(plan.totals.calories) %> cal</span>
|
||||
<span>P:<%= Math.round(plan.totals.protein) %></span>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
<% } else { %>
|
||||
<div class="text-center py-8 text-gray-400 text-sm">
|
||||
No meals planned
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Add Button -->
|
||||
<button onclick="openPlanModal('<%= dateStr %>')" class="w-full py-2 border-2 border-dashed border-gray-300 rounded-lg text-gray-400 hover:border-primary hover:text-primary transition text-sm mt-auto">
|
||||
+ Plan Meal
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
|
||||
<!-- Plan Meal Modal -->
|
||||
<div id="planModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50 p-4">
|
||||
<div class="bg-white rounded-xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div class="p-6 border-b flex justify-between items-center">
|
||||
<h3 class="text-xl font-bold">Plan a Meal</h3>
|
||||
<button onclick="closePlanModal()" class="text-gray-500 hover:text-gray-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form action="/meal-planner/add" method="POST" id="planForm" class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Date</label>
|
||||
<input type="date" name="date" id="planDate" class="w-full px-4 py-2 border rounded-lg bg-gray-50" readonly>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Meal Type</label>
|
||||
<select name="meal_type" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary" required>
|
||||
<option value="breakfast">🌅 Breakfast</option>
|
||||
<option value="lunch">🌞 Lunch</option>
|
||||
<option value="dinner">🌙 Dinner</option>
|
||||
<option value="snack">🍪 Snack</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Food Search (Simplified from add_meal) -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Add Foods</label>
|
||||
<div class="relative">
|
||||
<input type="text" id="foodSearch" placeholder="Search foods..." class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary">
|
||||
<div id="searchResults" class="absolute left-0 right-0 mt-1 max-h-64 overflow-y-auto hidden bg-white border rounded-lg shadow-lg z-10"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected Foods List -->
|
||||
<div class="mb-6 bg-gray-50 rounded-lg p-4">
|
||||
<h4 class="font-bold text-sm mb-2 text-gray-700">Selected Foods:</h4>
|
||||
<div id="selectedFoods" class="space-y-2">
|
||||
<p class="text-sm text-gray-400 text-center py-2" id="emptyMessage">No foods added</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" onclick="closePlanModal()" class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg">Cancel</button>
|
||||
<button type="submit" class="bg-primary text-white px-6 py-2 rounded-lg hover:bg-red-700">Save Plan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let selectedFoods = [];
|
||||
let searchTimeout;
|
||||
|
||||
function openPlanModal(date) {
|
||||
document.getElementById('planDate').value = date;
|
||||
document.getElementById('planModal').classList.remove('hidden');
|
||||
document.getElementById('planModal').classList.add('flex');
|
||||
}
|
||||
|
||||
function closePlanModal() {
|
||||
document.getElementById('planModal').classList.add('hidden');
|
||||
document.getElementById('planModal').classList.remove('flex');
|
||||
selectedFoods = [];
|
||||
renderSelectedFoods();
|
||||
document.getElementById('foodSearch').value = '';
|
||||
}
|
||||
|
||||
// Food Search Logic (Copied & Simplified)
|
||||
document.getElementById('foodSearch').addEventListener('input', function(e) {
|
||||
clearTimeout(searchTimeout);
|
||||
const query = e.target.value;
|
||||
|
||||
if (query.length < 2) {
|
||||
document.getElementById('searchResults').classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
searchTimeout = setTimeout(() => {
|
||||
fetch(`/api/search-food?q=${encodeURIComponent(query)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => displaySearchResults(data));
|
||||
}, 300);
|
||||
});
|
||||
|
||||
function displaySearchResults(results) {
|
||||
const container = document.getElementById('searchResults');
|
||||
|
||||
if (results.length === 0) {
|
||||
container.innerHTML = '<p class="p-4 text-gray-500">No foods found</p>';
|
||||
container.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = results.map(food => {
|
||||
const foodJson = JSON.stringify(food).replace(/"/g, '"');
|
||||
return `
|
||||
<div class="p-3 border-b hover:bg-gray-50 cursor-pointer transition-colors" onclick="addFood(${foodJson})">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">${food.name}</span>
|
||||
<span class="text-primary text-sm">${Math.round(food.calories)} cal</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">${food.serving_description || '1 serving'}</p>
|
||||
</div>
|
||||
`}).join('');
|
||||
|
||||
container.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function addFood(food) {
|
||||
// If from API and new, save it first (simplified reuse of logic)
|
||||
if (food.source === 'api_ninjas' && !food.id) {
|
||||
fetch('/api/add-food', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(food)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
food.id = data.food_id;
|
||||
addFoodToList(food);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
addFoodToList(food);
|
||||
}
|
||||
|
||||
document.getElementById('foodSearch').value = '';
|
||||
document.getElementById('searchResults').classList.add('hidden');
|
||||
}
|
||||
|
||||
function addFoodToList(food) {
|
||||
selectedFoods.push({...food, quantity: 1});
|
||||
renderSelectedFoods();
|
||||
}
|
||||
|
||||
function removeFood(index) {
|
||||
selectedFoods.splice(index, 1);
|
||||
renderSelectedFoods();
|
||||
}
|
||||
|
||||
function renderSelectedFoods() {
|
||||
const container = document.getElementById('selectedFoods');
|
||||
const emptyMessage = document.getElementById('emptyMessage');
|
||||
|
||||
if (selectedFoods.length === 0) {
|
||||
emptyMessage.classList.remove('hidden');
|
||||
container.innerHTML = '';
|
||||
container.appendChild(emptyMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
emptyMessage.classList.add('hidden');
|
||||
|
||||
container.innerHTML = selectedFoods.map((food, index) => `
|
||||
<div class="flex items-center justify-between p-2 bg-white rounded border text-sm">
|
||||
<span class="font-medium">${food.name}</span>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="number" step="0.1" name="quantity[]" value="${food.quantity}" class="w-16 px-1 border rounded" onchange="selectedFoods[${index}].quantity=this.value">
|
||||
<input type="hidden" name="food_id[]" value="${food.id}">
|
||||
<button type="button" onclick="removeFood(${index})" class="text-red-500 hover:text-red-700">×</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
document.getElementById('planForm').addEventListener('submit', function(e) {
|
||||
if (selectedFoods.length === 0) {
|
||||
e.preventDefault();
|
||||
alert('Please add at least one food item');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
117
views/progress.ejs
Normal file
117
views/progress.ejs
Normal file
@@ -0,0 +1,117 @@
|
||||
<h1 class="text-3xl font-bold mb-6">Progress & Trends</h1>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div class="mb-6 flex justify-end">
|
||||
<form method="GET" action="/progress" class="bg-white rounded-lg shadow-sm border p-1 inline-flex">
|
||||
<button type="submit" name="days" value="7" class="px-4 py-2 rounded-md text-sm font-medium transition <%= days === 7 ? 'bg-primary text-white' : 'text-gray-600 hover:text-primary' %>">7 Days</button>
|
||||
<button type="submit" name="days" value="30" class="px-4 py-2 rounded-md text-sm font-medium transition <%= days === 30 ? 'bg-primary text-white' : 'text-gray-600 hover:text-primary' %>">30 Days</button>
|
||||
<button type="submit" name="days" value="90" class="px-4 py-2 rounded-md text-sm font-medium transition <%= days === 90 ? 'bg-primary text-white' : 'text-gray-600 hover:text-primary' %>">90 Days</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<div class="glass rounded-xl p-4 text-center">
|
||||
<p class="text-gray-500 text-xs uppercase tracking-wide">Avg Calories</p>
|
||||
<p class="text-2xl font-bold text-primary"><%= avg_calories %></p>
|
||||
</div>
|
||||
<div class="glass rounded-xl p-4 text-center">
|
||||
<p class="text-gray-500 text-xs uppercase tracking-wide">Avg Protein</p>
|
||||
<p class="text-2xl font-bold text-blue-600"><%= avg_protein %>g</p>
|
||||
</div>
|
||||
<div class="glass rounded-xl p-4 text-center">
|
||||
<p class="text-gray-500 text-xs uppercase tracking-wide">Avg Carbs</p>
|
||||
<p class="text-2xl font-bold text-yellow-600"><%= avg_carbs %>g</p>
|
||||
</div>
|
||||
<div class="glass rounded-xl p-4 text-center">
|
||||
<p class="text-gray-500 text-xs uppercase tracking-wide">Avg Fat</p>
|
||||
<p class="text-2xl font-bold text-orange-600"><%= avg_fat %>g</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||
<!-- Weight Chart -->
|
||||
<div class="glass rounded-xl p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-bold text-lg">Weight Trend</h3>
|
||||
<% if (weight_change !== null) { %>
|
||||
<span class="text-sm font-medium <%= weight_change <= 0 ? 'text-green-600' : 'text-red-600' %>">
|
||||
<%= weight_change > 0 ? '+' : '' %><%= weight_change.toFixed(1) %> kg
|
||||
</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="h-64">
|
||||
<canvas id="weightChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calories Chart -->
|
||||
<div class="glass rounded-xl p-6">
|
||||
<h3 class="font-bold text-lg mb-4">Calorie Intake</h3>
|
||||
<div class="h-64">
|
||||
<canvas id="calorieChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Weight Chart
|
||||
const weightCtx = document.getElementById('weightChart').getContext('2d');
|
||||
const weightData = <%- JSON.stringify(weight_trend) %>;
|
||||
|
||||
new Chart(weightCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: weightData.map(d => new Date(d.date).toLocaleDateString(undefined, {month:'short', day:'numeric'})),
|
||||
datasets: [{
|
||||
label: 'Weight (kg)',
|
||||
data: weightData.map(d => d.weight_kg),
|
||||
borderColor: 'rgb(220, 38, 38)', // primary red
|
||||
backgroundColor: 'rgba(220, 38, 38, 0.1)',
|
||||
tension: 0.3,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Calorie Chart
|
||||
const calorieCtx = document.getElementById('calorieChart').getContext('2d');
|
||||
const calorieData = <%- JSON.stringify(calorie_trend) %>;
|
||||
|
||||
new Chart(calorieCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: calorieData.map(d => new Date(d.date).toLocaleDateString(undefined, {month:'short', day:'numeric'})),
|
||||
datasets: [{
|
||||
label: 'Calories',
|
||||
data: calorieData.map(d => d.calories),
|
||||
backgroundColor: 'rgba(220, 38, 38, 0.7)',
|
||||
borderRadius: 4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
30
views/register.ejs
Normal file
30
views/register.ejs
Normal file
@@ -0,0 +1,30 @@
|
||||
<div class="max-w-md mx-auto">
|
||||
<div class="glass rounded-xl p-8 shadow-lg">
|
||||
<h1 class="text-3xl font-bold text-center text-gray-800 mb-6">Create Account</h1>
|
||||
|
||||
<form method="POST" action="/register">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Name</label>
|
||||
<input type="text" name="name" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Username</label>
|
||||
<input type="text" name="username" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Password</label>
|
||||
<input type="password" name="password" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full bg-primary text-white py-3 rounded-lg hover:bg-red-700 transition font-semibold">
|
||||
Register
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="text-center mt-6 text-gray-600">
|
||||
Already have an account? <a href="/login" class="text-primary hover:underline font-semibold">Login</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user