Bladeren bron

Task #56: Implement Save Meal button and naming modal UI

FerRo988 3 dagen geleden
bovenliggende
commit
25d37de5ab
3 gewijzigde bestanden met toevoegingen van 228 en 0 verwijderingen
  1. 20 0
      static/index.html
  2. 87 0
      static/script.js
  3. 121 0
      static/style.css

+ 20 - 0
static/index.html

@@ -128,6 +128,26 @@
         </footer>
     </div>
     
+    <!-- Save Meal Modal (Sprint 8) -->
+    <div id="save-meal-modal" class="modal" style="display: none;">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h3>Save Current Meal</h3>
+                <button class="close-btn" id="cancel-save-btn">&times;</button>
+            </div>
+            <div class="modal-body">
+                <p>Give this meal a name to save it to your dashboard.</p>
+                <div class="input-group">
+                    <input type="text" id="meal-name-input" placeholder="e.g., Healthy Monday Lunch" autofocus>
+                </div>
+                <div id="save-meal-error" class="error-text"></div>
+            </div>
+            <div class="modal-footer">
+                <button id="confirm-save-btn" class="primary-btn">Save to Dashboard</button>
+            </div>
+        </div>
+    </div>
+    
     <!-- Authentication Gateway -->
     <div class="auth-container" id="auth-screen">
         <div class="auth-header">

+ 87 - 0
static/script.js

@@ -516,6 +516,9 @@ document.addEventListener('DOMContentLoaded', () => {
             if (totalProEl) totalProEl.textContent = '0';
             if (totalFatEl) totalFatEl.textContent = '0';
             if (totalCarbEl) totalCarbEl.textContent = '0';
+            
+            const saveBtn = document.getElementById('save-meal-btn');
+            if (saveBtn) saveBtn.style.display = 'none';
             return;
         }
 
@@ -554,6 +557,10 @@ document.addEventListener('DOMContentLoaded', () => {
                 if (totalProEl) animateValue(totalProEl, data.macros.protein_g);
                 if (totalFatEl) animateValue(totalFatEl, data.macros.fat_g);
                 if (totalCarbEl) animateValue(totalCarbEl, data.macros.carbs_g);
+
+                // Show Save button (Sprint 8)
+                const saveBtn = document.getElementById('save-meal-btn');
+                if (saveBtn) saveBtn.style.display = 'flex';
             } else {
                 if (totalsBanner) totalsBanner.classList.add('has-error');
             }
@@ -876,4 +883,84 @@ document.addEventListener('DOMContentLoaded', () => {
 
     // Initialize state
     sendBtn.disabled = true;
+
+    // --- Saved Meals Logic (Sprint 8) ---
+    const saveMealModal = document.getElementById('save-meal-modal');
+    const openSaveModalBtn = document.getElementById('save-meal-btn');
+    const cancelSaveBtn = document.getElementById('cancel-save-btn');
+    const confirmSaveBtn = document.getElementById('confirm-save-btn');
+    const mealNameInput = document.getElementById('meal-name-input');
+    const saveMealError = document.getElementById('save-meal-error');
+
+    if (openSaveModalBtn) {
+        openSaveModalBtn.addEventListener('click', (e) => {
+            e.stopPropagation();
+            if (currentMealItems.length === 0) return;
+            saveMealModal.style.display = 'flex';
+            mealNameInput.value = '';
+            saveMealError.textContent = '';
+            mealNameInput.focus();
+        });
+    }
+
+    const closeSaveModal = () => {
+        saveMealModal.style.display = 'none';
+    };
+
+    if (cancelSaveBtn) cancelSaveBtn.addEventListener('click', closeSaveModal);
+
+    if (confirmSaveBtn) {
+        confirmSaveBtn.addEventListener('click', async () => {
+            const name = mealNameInput.value.trim();
+            if (!name) {
+                saveMealError.textContent = 'Please enter a name for your meal.';
+                return;
+            }
+
+            confirmSaveBtn.disabled = true;
+            confirmSaveBtn.textContent = 'Saving...';
+
+            try {
+                const token = localStorage.getItem('localFoodToken');
+                const response = await fetch('/api/meals', {
+                    method: 'POST',
+                    headers: {
+                        'Content-Type': 'application/json',
+                        'Authorization': `Bearer ${token}`
+                    },
+                    body: JSON.stringify({
+                        name: name,
+                        items: currentMealItems.map(item => ({
+                            food_id: item.id,
+                            amount_g: item.amount
+                        }))
+                    })
+                });
+
+                if (response.ok) {
+                    closeSaveModal();
+                    addMessage('system', `✅ Successfully saved "${name}" to your dashboard!`);
+                    
+                    // Trigger a refresh of the dashboard if it's open
+                    if (window.refreshDashboard) window.refreshDashboard();
+                } else {
+                    const data = await response.json();
+                    saveMealError.textContent = data.detail || 'Failed to save meal.';
+                }
+            } catch (err) {
+                console.error('Save meal error:', err);
+                saveMealError.textContent = 'Server error. Please try again.';
+            } finally {
+                confirmSaveBtn.disabled = false;
+                confirmSaveBtn.textContent = 'Save to Dashboard';
+            }
+        });
+    }
+
+    // Close modal on outside click
+    window.addEventListener('click', (e) => {
+        if (e.target === saveMealModal) {
+            closeSaveModal();
+        }
+    });
 });

+ 121 - 0
static/style.css

@@ -1148,3 +1148,124 @@ textarea::placeholder {
     70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); }
     100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
 }
+
+/* =============================================
+   Sprint 8: Saved Meals & Dashboard UI
+   ============================================= */
+
+.secondary-btn-sm {
+    background: rgba(255, 255, 255, 0.05);
+    color: var(--text-main);
+    border: 1px solid var(--border-color);
+    border-radius: 8px;
+    padding: 8px 12px;
+    font-size: 0.85rem;
+    font-weight: 500;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    white-space: nowrap;
+}
+
+.secondary-btn-sm:hover {
+    background: rgba(255, 255, 255, 0.1);
+    border-color: var(--text-muted);
+}
+
+.modal {
+    position: fixed;
+    top: 0; left: 0; width: 100%; height: 100%;
+    background: rgba(0, 0, 0, 0.75);
+    backdrop-filter: blur(10px);
+    -webkit-backdrop-filter: blur(10px);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    z-index: 2000;
+}
+
+.modal-content {
+    background: #161b22;
+    border: 1px solid var(--border-color);
+    border-radius: 20px;
+    width: 90%;
+    max-width: 420px;
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.6);
+    animation: modalSlideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
+    overflow: hidden;
+}
+
+@keyframes modalSlideUp {
+    from { transform: translateY(30px); opacity: 0; }
+    to { transform: translateY(0); opacity: 1; }
+}
+
+.modal-header {
+    padding: 20px 24px;
+    border-bottom: 1px solid var(--border-color);
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background: rgba(255, 255, 255, 0.02);
+}
+
+.modal-header h3 {
+    font-size: 1.15rem;
+    font-weight: 600;
+    margin: 0;
+    color: #f0f6fc;
+}
+
+.close-btn {
+    background: none;
+    border: none;
+    color: var(--text-muted);
+    font-size: 1.8rem;
+    cursor: pointer;
+    line-height: 1;
+    padding: 0;
+    transition: color 0.2s;
+}
+
+.close-btn:hover {
+    color: #ef4444;
+}
+
+.modal-body {
+    padding: 24px;
+}
+
+.modal-body p {
+    font-size: 0.95rem;
+    color: var(--text-muted);
+    line-height: 1.5;
+    margin-bottom: 16px;
+}
+
+.modal-footer {
+    padding: 20px 24px;
+    border-top: 1px solid var(--border-color);
+    display: flex;
+    gap: 12px;
+    justify-content: flex-end;
+    background: rgba(255, 255, 255, 0.02);
+}
+
+.secondary-btn {
+    background: transparent;
+    border: 1px solid var(--border-color);
+    color: var(--text-main);
+    padding: 10px 20px;
+    border-radius: 10px;
+    font-size: 0.95rem;
+    font-weight: 500;
+    cursor: pointer;
+    transition: all 0.2s;
+}
+
+.secondary-btn:hover {
+    background: rgba(255, 255, 255, 0.05);
+    border-color: var(--text-muted);
+}