Add new utility function to generate daily meal plans based on user's calorie targets and food categories. Include auto-generate button in meal planner UI with confirmation prompt. Add new Filipino food items to seed data for better meal variety. Fix database column references to use capitalized field names consistently.
264 lines
12 KiB
Plaintext
264 lines
12 KiB
Plaintext
<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>
|
||
<% } %>
|
||
|
||
<!-- Buttons -->
|
||
<div class="mt-auto space-y-2">
|
||
<!-- 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">
|
||
+ Plan Meal
|
||
</button>
|
||
|
||
<!-- Auto Generate Button -->
|
||
<form action="/meal-planner/auto-generate" method="POST" onsubmit="return confirm('This will replace any existing meal plan for this date. Continue?');">
|
||
<input type="hidden" name="date" value="<%= dateStr %>">
|
||
<button type="submit" class="w-full py-2 bg-green-50 text-green-600 rounded-lg hover:bg-green-100 transition text-sm font-medium">
|
||
⚡ Auto Generate
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</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> |