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

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