initial commit

This commit is contained in:
Jp
2026-01-30 23:32:43 +08:00
commit 3df16ee995
20 changed files with 6405 additions and 0 deletions

237
views/add_meal.ejs Normal file
View 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, '&quot;');
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>