Jelajahi Sumber

TG-40: Implement expandable detailed nutritional view in search results

FerRo988 2 minggu lalu
induk
melakukan
f4ad888acb
2 mengubah file dengan 152 tambahan dan 1 penghapusan
  1. 80 1
      static/script.js
  2. 72 0
      static/style.css

+ 80 - 1
static/script.js

@@ -479,9 +479,17 @@ document.addEventListener('DOMContentLoaded', () => {
                             <div class="macro-tag">Fat: <span>${item.fat_g}g</span></div>
                             <div class="macro-tag">Carb: <span>${item.carbs_g}g</span></div>
                         </div>
+                        <div class="food-item-footer">
+                            <button class="details-btn" data-id="${item.id}">📊 Details</button>
+                        </div>
+                        <div class="details-panel" id="details-${item.id}">
+                            <!-- Populated on demand -->
+                        </div>
                     `;
                     
-                    foodEl.addEventListener('click', () => {
+                    // Click on the header/name area auto-fills the chat
+                    foodEl.querySelector('.food-item-header').addEventListener('click', (e) => {
+                        e.stopPropagation();
                         userInput.value = `Can you build a meal around ${item.name} (${item.calories} cal, ${item.protein_g}g protein)?`;
                         userInput.focus();
                         userInput.style.height = 'auto'; // Reset
@@ -490,6 +498,13 @@ document.addEventListener('DOMContentLoaded', () => {
                         searchInput.value = '';
                         clearSearchBtn.style.display = 'none';
                     });
+
+                    // Click on the details button toggles the panel
+                    foodEl.querySelector('.details-btn').addEventListener('click', (e) => {
+                        e.stopPropagation();
+                        const panel = document.getElementById(`details-${item.id}`);
+                        toggleFoodDetails(item.id, panel, e.target);
+                    });
                     
                     searchDropdown.appendChild(foodEl);
                 });
@@ -502,6 +517,70 @@ document.addEventListener('DOMContentLoaded', () => {
         }
     };
 
+    const toggleFoodDetails = async (foodId, panel, btn) => {
+        const isExpanded = panel.classList.contains('expanded');
+        
+        if (isExpanded) {
+            panel.classList.remove('expanded');
+            btn.textContent = '📊 Details';
+            return;
+        }
+
+        // If not loaded yet, fetch from API
+        if (panel.innerHTML.trim() === '') {
+            panel.innerHTML = '<div class="search-loading">Loading details...</div>';
+            panel.classList.add('expanded'); // Show loading state
+            
+            try {
+                const token = localStorage.getItem('localFoodToken');
+                const response = await fetch(`/api/food/${foodId}`, {
+                    headers: { 'Authorization': `Bearer ${token}` }
+                });
+                
+                if (!response.ok) throw new Error("Failed to fetch details");
+                
+                const data = await response.json();
+                renderFoodDetails(panel, data);
+            } catch (err) {
+                console.error(err);
+                panel.innerHTML = '<div class="search-empty">Error loading details.</div>';
+            }
+        } else {
+            panel.classList.add('expanded');
+        }
+        
+        btn.textContent = '✖ Close';
+    };
+
+    const renderFoodDetails = (panel, data) => {
+        panel.innerHTML = `
+            <div class="nutrient-section">
+                <div class="nutrient-section-title">Extended Nutrition</div>
+                <div class="nutrient-grid">
+                    <div class="nutrient-item"><span class="nutrient-label">Fiber</span><span class="nutrient-value">${data.extended.fiber_g}g</span></div>
+                    <div class="nutrient-item"><span class="nutrient-label">Sugar</span><span class="nutrient-value">${data.extended.sugar_g}g</span></div>
+                    <div class="nutrient-item"><span class="nutrient-label">Cholesterol</span><span class="nutrient-value">${data.extended.cholesterol_mg}mg</span></div>
+                </div>
+            </div>
+            <div class="nutrient-section">
+                <div class="nutrient-section-title">Vitamins</div>
+                <div class="nutrient-grid">
+                    <div class="nutrient-item"><span class="nutrient-label">Vitamin A</span><span class="nutrient-value">${data.vitamins.vitamin_a_iu}IU</span></div>
+                    <div class="nutrient-item"><span class="nutrient-label">Vitamin C</span><span class="nutrient-value">${data.vitamins.vitamin_c_mg}mg</span></div>
+                </div>
+            </div>
+            <div class="nutrient-section">
+                <div class="nutrient-section-title">Minerals</div>
+                <div class="nutrient-grid">
+                    <div class="nutrient-item"><span class="nutrient-label">Calcium</span><span class="nutrient-value">${data.minerals.calcium_mg}mg</span></div>
+                    <div class="nutrient-item"><span class="nutrient-label">Iron</span><span class="nutrient-value">${data.minerals.iron_mg}mg</span></div>
+                    <div class="nutrient-item"><span class="nutrient-label">Potassium</span><span class="nutrient-value">${data.minerals.potassium_mg}mg</span></div>
+                    <div class="nutrient-item"><span class="nutrient-label">Sodium</span><span class="nutrient-value">${data.minerals.sodium_mg}mg</span></div>
+                </div>
+            </div>
+        `;
+    };
+
     const handleSearchInput = debounce((e) => {
         const query = e.target.value.trim();
         if (query.length > 0) {

+ 72 - 0
static/style.css

@@ -628,6 +628,78 @@ textarea::placeholder {
     font-weight: 500;
 }
 
+/* --- Expandable Details (Task #40) --- */
+.food-item-footer {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-top: 10px;
+}
+
+.details-btn {
+    background: none;
+    border: 1px solid var(--border);
+    border-radius: 4px;
+    color: var(--primary);
+    font-size: 0.75rem;
+    padding: 2px 8px;
+    cursor: pointer;
+    transition: all 0.2s;
+}
+
+.details-btn:hover {
+    background: rgba(255, 255, 255, 0.05);
+    border-color: var(--primary);
+}
+
+.details-panel {
+    max-height: 0;
+    overflow: hidden;
+    transition: max-height 0.3s ease-out, margin-top 0.3s;
+}
+
+.details-panel.expanded {
+    max-height: 500px; /* Large enough for content */
+    margin-top: 12px;
+    border-top: 1px solid rgba(255, 255, 255, 0.05);
+    padding-top: 12px;
+}
+
+.nutrient-section {
+    margin-bottom: 12px;
+}
+
+.nutrient-section-title {
+    font-size: 0.7rem;
+    text-transform: uppercase;
+    letter-spacing: 0.05em;
+    color: var(--text-muted);
+    margin-bottom: 6px;
+    font-weight: 700;
+}
+
+.nutrient-grid {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    gap: 8px;
+}
+
+.nutrient-item {
+    display: flex;
+    justify-content: space-between;
+    font-size: 0.8rem;
+    padding: 4px 0;
+}
+
+.nutrient-label {
+    color: var(--text-muted);
+}
+
+.nutrient-value {
+    font-weight: 500;
+    color: var(--text);
+}
+
 .search-loading, .search-empty {
     padding: 20px;
     text-align: center;