|
|
@@ -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,237 @@ 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();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // --- Dashboard Logic (Sprint 8) ---
|
|
|
+ const dashboardOverlay = document.getElementById('dashboard-overlay');
|
|
|
+ const openDashboardBtn = document.getElementById('nav-dashboard-btn');
|
|
|
+ const closeDashboardBtn = document.getElementById('close-dashboard-btn');
|
|
|
+ const dashboardGrid = document.getElementById('dashboard-grid');
|
|
|
+ const dashboardEmptyMsg = document.getElementById('dashboard-empty-msg');
|
|
|
+
|
|
|
+ const toggleDashboard = (show) => {
|
|
|
+ if (show) {
|
|
|
+ dashboardOverlay.style.display = 'flex';
|
|
|
+ loadSavedMeals();
|
|
|
+ } else {
|
|
|
+ dashboardOverlay.style.display = 'none';
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ if (openDashboardBtn) openDashboardBtn.addEventListener('click', () => toggleDashboard(true));
|
|
|
+ if (closeDashboardBtn) closeDashboardBtn.addEventListener('click', () => toggleDashboard(false));
|
|
|
+
|
|
|
+ async function loadSavedMeals() {
|
|
|
+ dashboardGrid.innerHTML = '<div class="search-loading">Loading your meals...</div>';
|
|
|
+ dashboardEmptyMsg.style.display = 'none';
|
|
|
+
|
|
|
+ try {
|
|
|
+ const token = localStorage.getItem('localFoodToken');
|
|
|
+ const response = await fetch('/api/meals', {
|
|
|
+ headers: { 'Authorization': `Bearer ${token}` }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ const data = await response.json();
|
|
|
+ renderDashboard(data.meals);
|
|
|
+ } else {
|
|
|
+ dashboardGrid.innerHTML = '<div class="error-text">Failed to load meals.</div>';
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Dashboard load error:', err);
|
|
|
+ dashboardGrid.innerHTML = '<div class="error-text">Connection error.</div>';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderDashboard(meals) {
|
|
|
+ dashboardGrid.innerHTML = '';
|
|
|
+ if (!meals || meals.length === 0) {
|
|
|
+ dashboardEmptyMsg.style.display = 'block';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ meals.forEach((meal, index) => {
|
|
|
+ const card = document.createElement('div');
|
|
|
+ card.className = 'meal-card';
|
|
|
+ card.style.animationDelay = `${index * 0.07}s`;
|
|
|
+
|
|
|
+ const dateStr = new Date(meal.created_at).toLocaleDateString(undefined, {
|
|
|
+ month: 'short', day: 'numeric', year: 'numeric'
|
|
|
+ });
|
|
|
+
|
|
|
+ card.innerHTML = `
|
|
|
+ <div class="meal-card-header">
|
|
|
+ <span class="meal-card-name">${meal.name}</span>
|
|
|
+ <span class="meal-card-date">${dateStr}</span>
|
|
|
+ </div>
|
|
|
+ <div class="meal-card-macros">
|
|
|
+ <div class="card-macro kcal">
|
|
|
+ <span class="card-macro-val">${Math.round(meal.total_calories)}</span>
|
|
|
+ <span class="card-macro-lbl">kcal</span>
|
|
|
+ </div>
|
|
|
+ <div class="card-macro protein">
|
|
|
+ <span class="card-macro-val">${Math.round(meal.total_protein)}g</span>
|
|
|
+ <span class="card-macro-lbl">Protein</span>
|
|
|
+ </div>
|
|
|
+ <div class="card-macro carbs">
|
|
|
+ <span class="card-macro-val">${Math.round(meal.total_carbs)}g</span>
|
|
|
+ <span class="card-macro-lbl">Carbs</span>
|
|
|
+ </div>
|
|
|
+ <div class="card-macro fat">
|
|
|
+ <span class="card-macro-val">${Math.round(meal.total_fat)}g</span>
|
|
|
+ <span class="card-macro-lbl">Fat</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="meal-card-actions">
|
|
|
+ <button class="edit-meal-btn" data-id="${meal.id}" data-name="${meal.name}" title="Rename Meal">
|
|
|
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
|
|
|
+ </button>
|
|
|
+ <button class="delete-meal-btn" data-id="${meal.id}" title="Delete Meal">
|
|
|
+ <svg width="14" height="14" 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>
|
|
|
+ `;
|
|
|
+
|
|
|
+ // Attach listeners to new buttons
|
|
|
+ card.querySelector('.edit-meal-btn').addEventListener('click', (e) => {
|
|
|
+ e.stopPropagation();
|
|
|
+ const newName = prompt('Enter new name for this meal:', meal.name);
|
|
|
+ if (newName && newName.trim() && newName !== meal.name) {
|
|
|
+ renameMeal(meal.id, newName.trim());
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ card.querySelector('.delete-meal-btn').addEventListener('click', (e) => {
|
|
|
+ e.stopPropagation();
|
|
|
+ if (confirm(`Are you sure you want to delete "${meal.name}"?`)) {
|
|
|
+ deleteMeal(meal.id);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ dashboardGrid.appendChild(card);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ async function renameMeal(id, newName) {
|
|
|
+ try {
|
|
|
+ const token = localStorage.getItem('localFoodToken');
|
|
|
+ const response = await fetch(`/api/meals/${id}`, {
|
|
|
+ method: 'PUT',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ 'Authorization': `Bearer ${token}`
|
|
|
+ },
|
|
|
+ body: JSON.stringify({ name: newName })
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ loadSavedMeals(); // Refresh
|
|
|
+ } else {
|
|
|
+ alert('Failed to rename meal.');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Rename error:', err);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function deleteMeal(id) {
|
|
|
+ try {
|
|
|
+ const token = localStorage.getItem('localFoodToken');
|
|
|
+ const response = await fetch(`/api/meals/${id}`, {
|
|
|
+ method: 'DELETE',
|
|
|
+ headers: { 'Authorization': `Bearer ${token}` }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ loadSavedMeals(); // Refresh
|
|
|
+ } else {
|
|
|
+ alert('Failed to delete meal.');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Delete error:', err);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Attach refresh helper
|
|
|
+ window.refreshDashboard = loadSavedMeals;
|
|
|
});
|