|
@@ -423,6 +423,153 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
const searchInput = document.getElementById('food-search-input');
|
|
const searchInput = document.getElementById('food-search-input');
|
|
|
const searchDropdown = document.getElementById('search-results-dropdown');
|
|
const searchDropdown = document.getElementById('search-results-dropdown');
|
|
|
const clearSearchBtn = document.getElementById('clear-search-btn');
|
|
const clearSearchBtn = document.getElementById('clear-search-btn');
|
|
|
|
|
+
|
|
|
|
|
+ // --- Meal Builder State & UI (US-10 Task #46) ---
|
|
|
|
|
+ let currentMealItems = [];
|
|
|
|
|
+ const mealBuilder = document.getElementById('meal-builder');
|
|
|
|
|
+ const mealContent = document.getElementById('meal-content');
|
|
|
|
|
+ const mealItemsList = document.getElementById('meal-items-list');
|
|
|
|
|
+ const mealItemCount = document.getElementById('meal-item-count');
|
|
|
|
|
+ const toggleMealBtn = document.getElementById('toggle-meal-btn');
|
|
|
|
|
+ const emptyMealMsg = document.getElementById('empty-meal-msg');
|
|
|
|
|
+ const mealBuilderFooter = document.getElementById('meal-builder-footer');
|
|
|
|
|
+ const generateRecipeBtn = document.getElementById('generate-recipe-btn');
|
|
|
|
|
+
|
|
|
|
|
+ const toggleMealBuilder = () => {
|
|
|
|
|
+ const isCollapsed = mealContent.classList.contains('collapsed');
|
|
|
|
|
+ if (isCollapsed) {
|
|
|
|
|
+ mealContent.classList.remove('collapsed');
|
|
|
|
|
+ toggleMealBtn.style.transform = 'rotate(180deg)';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ mealContent.classList.add('collapsed');
|
|
|
|
|
+ toggleMealBtn.style.transform = 'rotate(0deg)';
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const addItemToMeal = (food) => {
|
|
|
|
|
+ // Duplicate Handling: Merge by updating grams
|
|
|
|
|
+ const existingItem = currentMealItems.find(item => item.id === food.id);
|
|
|
|
|
+ if (existingItem) {
|
|
|
|
|
+ existingItem.amount += 100;
|
|
|
|
|
+ // Directly update the DOM input for this item to avoid re-rendering the whole list
|
|
|
|
|
+ const inputEl = document.querySelector(`.meal-weight-input[data-id="${food.id}"]`);
|
|
|
|
|
+ if (inputEl) {
|
|
|
|
|
+ inputEl.value = existingItem.amount;
|
|
|
|
|
+ // Animate the row slightly to show it was updated
|
|
|
|
|
+ const rowEl = inputEl.closest('.meal-item-row');
|
|
|
|
|
+ if (rowEl) {
|
|
|
|
|
+ rowEl.style.transform = 'scale(1.02)';
|
|
|
|
|
+ rowEl.style.background = 'rgba(255, 255, 255, 0.1)';
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ rowEl.style.transform = '';
|
|
|
|
|
+ rowEl.style.background = 'rgba(255, 255, 255, 0.05)';
|
|
|
|
|
+ }, 200);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ currentMealItems.push({
|
|
|
|
|
+ id: food.id,
|
|
|
|
|
+ name: food.name,
|
|
|
|
|
+ amount: 100,
|
|
|
|
|
+ base_macros: {
|
|
|
|
|
+ calories: food.calories,
|
|
|
|
|
+ protein_g: food.protein_g,
|
|
|
|
|
+ fat_g: food.fat_g,
|
|
|
|
|
+ carbs_g: food.carbs_g
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ renderMealItems(); // Only re-render if it's a new item
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Ensure builder is expanded when adding an item
|
|
|
|
|
+ if (mealContent.classList.contains('collapsed')) {
|
|
|
|
|
+ toggleMealBuilder();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const removeItemFromMeal = (id) => {
|
|
|
|
|
+ currentMealItems = currentMealItems.filter(item => item.id !== id);
|
|
|
|
|
+ renderMealItems();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const updateItemAmount = (id, newAmount) => {
|
|
|
|
|
+ const item = currentMealItems.find(item => item.id === id);
|
|
|
|
|
+ if (item) {
|
|
|
|
|
+ item.amount = parseInt(newAmount) || 0;
|
|
|
|
|
+ // No need to re-render everything, just keep state in sync
|
|
|
|
|
+ // Task #50 will handle dynamic totals later
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const renderMealItems = () => {
|
|
|
|
|
+ mealItemsList.innerHTML = '';
|
|
|
|
|
+ mealItemCount.textContent = `${currentMealItems.length} item${currentMealItems.length !== 1 ? 's' : ''}`;
|
|
|
|
|
+
|
|
|
|
|
+ if (currentMealItems.length === 0) {
|
|
|
|
|
+ mealItemsList.appendChild(emptyMealMsg);
|
|
|
|
|
+ mealBuilderFooter.style.display = 'none';
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ mealBuilderFooter.style.display = 'flex';
|
|
|
|
|
+
|
|
|
|
|
+ currentMealItems.forEach(item => {
|
|
|
|
|
+ const row = document.createElement('div');
|
|
|
|
|
+ row.className = 'meal-item-row';
|
|
|
|
|
+ row.innerHTML = `
|
|
|
|
|
+ <div class="meal-item-name" title="${item.name}">${item.name}</div>
|
|
|
|
|
+ <div class="meal-item-controls">
|
|
|
|
|
+ <div class="weight-input-wrapper">
|
|
|
|
|
+ <input type="number" value="${item.amount}" min="0" max="5000" data-id="${item.id}" class="meal-weight-input">
|
|
|
|
|
+ <span class="weight-unit">g</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button class="remove-item-btn" data-id="${item.id}" title="Remove item">
|
|
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"></path></svg>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ // Event Listeners for rows
|
|
|
|
|
+ row.querySelector('.meal-weight-input').addEventListener('input', (e) => {
|
|
|
|
|
+ updateItemAmount(item.id, e.target.value);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ row.querySelector('.remove-item-btn').addEventListener('click', () => {
|
|
|
|
|
+ removeItemFromMeal(item.id);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ mealItemsList.appendChild(row);
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (toggleMealBtn) {
|
|
|
|
|
+ toggleMealBtn.addEventListener('click', toggleMealBuilder);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (generateRecipeBtn) {
|
|
|
|
|
+ generateRecipeBtn.addEventListener('click', () => {
|
|
|
|
|
+ if (currentMealItems.length === 0) return;
|
|
|
|
|
+
|
|
|
|
|
+ const itemNames = currentMealItems.map(item => item.name).join(', ');
|
|
|
|
|
+ userInput.value = `Give me a recipe that contains: ${itemNames}`;
|
|
|
|
|
+
|
|
|
|
|
+ // UI Feedback
|
|
|
|
|
+ userInput.focus();
|
|
|
|
|
+ userInput.style.height = 'auto';
|
|
|
|
|
+ userInput.style.height = userInput.scrollHeight + 'px';
|
|
|
|
|
+ sendBtn.disabled = false;
|
|
|
|
|
+
|
|
|
|
|
+ // Smoothly scroll to the bottom to see the chat input
|
|
|
|
|
+ window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Also toggle when clicking the header
|
|
|
|
|
+ document.querySelector('.meal-builder-header').addEventListener('click', (e) => {
|
|
|
|
|
+ if (!e.target.closest('#toggle-meal-btn')) {
|
|
|
|
|
+ toggleMealBuilder();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
function debounce(func, delay) {
|
|
function debounce(func, delay) {
|
|
|
let timeout;
|
|
let timeout;
|
|
@@ -480,6 +627,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
<div class="macro-tag">Carb: <span>${item.carbs_g}g</span></div>
|
|
<div class="macro-tag">Carb: <span>${item.carbs_g}g</span></div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="food-item-footer">
|
|
<div class="food-item-footer">
|
|
|
|
|
+ <button class="add-meal-btn" data-id="${item.id}">➕ Add to Meal</button>
|
|
|
<button class="details-btn" data-id="${item.id}">📊 Details</button>
|
|
<button class="details-btn" data-id="${item.id}">📊 Details</button>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="details-panel" id="details-${item.id}"></div>
|
|
<div class="details-panel" id="details-${item.id}"></div>
|
|
@@ -497,6 +645,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
clearSearchBtn.style.display = 'none';
|
|
clearSearchBtn.style.display = 'none';
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ // Click on the add to meal button
|
|
|
|
|
+ foodEl.querySelector('.add-meal-btn').addEventListener('click', (e) => {
|
|
|
|
|
+ e.stopPropagation();
|
|
|
|
|
+ addItemToMeal(item);
|
|
|
|
|
+ // Optional: provide visual feedback
|
|
|
|
|
+ const btn = e.target;
|
|
|
|
|
+ const originalText = btn.textContent;
|
|
|
|
|
+ btn.textContent = '✅ Added';
|
|
|
|
|
+ btn.style.background = '#22c55e';
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ btn.textContent = originalText;
|
|
|
|
|
+ btn.style.background = '';
|
|
|
|
|
+ }, 1000);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
// Click on the details button toggles the panel
|
|
// Click on the details button toggles the panel
|
|
|
foodEl.querySelector('.details-btn').addEventListener('click', (e) => {
|
|
foodEl.querySelector('.details-btn').addEventListener('click', (e) => {
|
|
|
e.stopPropagation();
|
|
e.stopPropagation();
|