253 lines
11 KiB
Plaintext
253 lines
11 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>
|
||
<% } %>
|
||
|
||
<!-- 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> |