Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d43544a232 |
@@ -22,6 +22,8 @@ jobs:
|
|||||||
VITE_APPWRITE_USERSETTINGSID: ${{ secrets.VITE_APPWRITE_USERSETTINGSID }}
|
VITE_APPWRITE_USERSETTINGSID: ${{ secrets.VITE_APPWRITE_USERSETTINGSID }}
|
||||||
VITE_APPWRITE_MONTLYCALORIESID: ${{ secrets.VITE_APPWRITE_MONTLYCALORIESID }}
|
VITE_APPWRITE_MONTLYCALORIESID: ${{ secrets.VITE_APPWRITE_MONTLYCALORIESID }}
|
||||||
VITE_APPWRITE_SHAREDDAYSID: ${{ secrets.VITE_APPWRITE_SHAREDDAYSID }}
|
VITE_APPWRITE_SHAREDDAYSID: ${{ secrets.VITE_APPWRITE_SHAREDDAYSID }}
|
||||||
|
VITE_APPWRITE_MEALPLANTEMPLATESID: ${{ secrets.VITE_APPWRITE_MEALPLANTEMPLATESID }}
|
||||||
|
VITE_APPWRITE_PLANNEDFOODITEMSID: ${{ secrets.VITE_APPWRITE_PLANNEDFOODITEMSID }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"projectId": "684ac3bf000ee8950f5a",
|
||||||
|
"endpoint": "https://fra.cloud.appwrite.io/v1"
|
||||||
|
}
|
||||||
+176
-2
@@ -7,6 +7,7 @@
|
|||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<link rel="stylesheet" href="auth.css">
|
<link rel="stylesheet" href="auth.css">
|
||||||
<link rel="stylesheet" href="settings.css">
|
<link rel="stylesheet" href="settings.css">
|
||||||
|
<link rel="stylesheet" href="mealPlanner.css">
|
||||||
<link rel="apple-touch-icon" sizes="57x57" href="/assets/apple-icon-57x57.png">
|
<link rel="apple-touch-icon" sizes="57x57" href="/assets/apple-icon-57x57.png">
|
||||||
<link rel="apple-touch-icon" sizes="60x60" href="/assets/apple-icon-60x60.png">
|
<link rel="apple-touch-icon" sizes="60x60" href="/assets/apple-icon-60x60.png">
|
||||||
<link rel="apple-touch-icon" sizes="72x72" href="/assets/apple-icon-72x72.png">
|
<link rel="apple-touch-icon" sizes="72x72" href="/assets/apple-icon-72x72.png">
|
||||||
@@ -82,6 +83,7 @@
|
|||||||
<div class="header-buttons">
|
<div class="header-buttons">
|
||||||
<button id="shareBtn" class="header-btn">🔗 Share</button>
|
<button id="shareBtn" class="header-btn">🔗 Share</button>
|
||||||
<button id="calendarBtn" class="header-btn">📅 Calendar</button>
|
<button id="calendarBtn" class="header-btn">📅 Calendar</button>
|
||||||
|
<button id="mealPlannerBtn" class="header-btn">📋 Meal Planner</button>
|
||||||
<button id="settingsBtn" class="header-btn">⚙️ Settings</button>
|
<button id="settingsBtn" class="header-btn">⚙️ Settings</button>
|
||||||
<button id="logoutBtn" class="header-btn logout">🚪 Logout</button>
|
<button id="logoutBtn" class="header-btn logout">🚪 Logout</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,6 +94,7 @@
|
|||||||
<div id="mobileMenu" class="mobile-menu hidden">
|
<div id="mobileMenu" class="mobile-menu hidden">
|
||||||
<button id="shareBtnMobile" class="mobile-menu-item">🔗 Share</button>
|
<button id="shareBtnMobile" class="mobile-menu-item">🔗 Share</button>
|
||||||
<button id="calendarBtnMobile" class="mobile-menu-item">📅 Calendar</button>
|
<button id="calendarBtnMobile" class="mobile-menu-item">📅 Calendar</button>
|
||||||
|
<button id="mealPlannerBtnMobile" class="mobile-menu-item">📋 Meal Planner</button>
|
||||||
<button id="settingsBtnMobile" class="mobile-menu-item">⚙️ Settings</button>
|
<button id="settingsBtnMobile" class="mobile-menu-item">⚙️ Settings</button>
|
||||||
<button id="logoutBtnMobile" class="mobile-menu-item logout">🚪 Logout</button>
|
<button id="logoutBtnMobile" class="mobile-menu-item logout">🚪 Logout</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,6 +169,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Add from Meal Plan -->
|
||||||
|
<div class="quick-add-section hidden" id="quickAddSection">
|
||||||
|
<h2 class="section-title">📋 Quick Add from Meal Plan</h2>
|
||||||
|
<div class="quick-add-items" id="quickAddItemsContainer">
|
||||||
|
<!-- Quick add items will be populated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="add-food-section">
|
<div class="add-food-section">
|
||||||
<h2 class="add-food-title" id="add-foot-title">Add Food Entry</h2>
|
<h2 class="add-food-title" id="add-foot-title">Add Food Entry</h2>
|
||||||
<div class="add-food-form">
|
<div class="add-food-form">
|
||||||
@@ -248,9 +259,9 @@
|
|||||||
<section id="settings-content" class="hidden">
|
<section id="settings-content" class="hidden">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>⚙️ Settings</h1>
|
<h1>⚙️ Settings</h1>
|
||||||
|
|
||||||
<div class="date-display">Define your daily macros goal</div>
|
<div class="date-display">Define your daily macros goal</div>
|
||||||
|
|
||||||
<form id="settingsForm" class="settings-form">
|
<form id="settingsForm" class="settings-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="caloriesGoal">Daily calorie goal</label>
|
<label for="caloriesGoal">Daily calorie goal</label>
|
||||||
@@ -277,6 +288,169 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Meal Planner -->
|
||||||
|
<section id="meal-planner-content" class="hidden">
|
||||||
|
<div class="container">
|
||||||
|
<h1>📋 Meal Planner</h1>
|
||||||
|
|
||||||
|
<div class="date-display">Plan your meals and compare with your goals</div>
|
||||||
|
|
||||||
|
<!-- Template Management -->
|
||||||
|
<div class="template-management">
|
||||||
|
<div class="template-selector-container">
|
||||||
|
<label for="templateSelector">Current Plan:</label>
|
||||||
|
<select id="templateSelector" class="template-selector">
|
||||||
|
<option value="">-- Select a plan --</option>
|
||||||
|
</select>
|
||||||
|
<button id="newTemplateBtn" class="template-btn">+ New Plan</button>
|
||||||
|
<button id="deleteTemplateBtn" class="template-btn delete-btn">Delete Plan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Plan Progress Summary -->
|
||||||
|
<div class="plan-progress-container">
|
||||||
|
<div class="calories-banner">
|
||||||
|
<div class="calories-number" id="planCaloriesTotal">0</div>
|
||||||
|
<div class="calories-text">Total Calories</div>
|
||||||
|
<div class="calories-text-goal" id="planCaloriesGoalText">of 0</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nutrition-summary" id="planNutritionSummary">
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-label">Protein (g)</div>
|
||||||
|
<div class="nutrition-value" id="planProteinValue">0</div>
|
||||||
|
<div class="nutrition-progress-bar">
|
||||||
|
<div class="nutrition-progress-fill" id="planProteinProgress"></div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-label" id="planProteinGoalText">of 0</div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-label">Fat (g)</div>
|
||||||
|
<div class="nutrition-value" id="planFatValue">0</div>
|
||||||
|
<div class="nutrition-progress-bar">
|
||||||
|
<div class="nutrition-progress-fill" id="planFatProgress"></div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-label" id="planFatGoalText">of 0</div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-label">Carbs (g)</div>
|
||||||
|
<div class="nutrition-value" id="planCarboValue">0</div>
|
||||||
|
<div class="nutrition-progress-bar">
|
||||||
|
<div class="nutrition-progress-fill" id="planCarboProgress"></div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-label" id="planCarboGoalText">of 0</div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-label">Fiber (g)</div>
|
||||||
|
<div class="nutrition-value" id="planFiberValue">0</div>
|
||||||
|
<div class="nutrition-progress-bar">
|
||||||
|
<div class="nutrition-progress-fill" id="planFiberProgress"></div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-label" id="planFiberGoalText">of 0</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Meal Sections -->
|
||||||
|
<div class="meals-container">
|
||||||
|
<!-- Breakfast -->
|
||||||
|
<div class="meal-section" data-meal-type="breakfast">
|
||||||
|
<h2 class="meal-section-title">🌅 Breakfast</h2>
|
||||||
|
<button class="add-meal-item-btn" data-meal-type="breakfast">+ Add Food</button>
|
||||||
|
<div class="meal-items-container" id="breakfastItemsContainer">
|
||||||
|
<!-- Planned items will be added here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Lunch -->
|
||||||
|
<div class="meal-section" data-meal-type="lunch">
|
||||||
|
<h2 class="meal-section-title">☀️ Lunch</h2>
|
||||||
|
<button class="add-meal-item-btn" data-meal-type="lunch">+ Add Food</button>
|
||||||
|
<div class="meal-items-container" id="lunchItemsContainer">
|
||||||
|
<!-- Planned items will be added here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dinner -->
|
||||||
|
<div class="meal-section" data-meal-type="dinner">
|
||||||
|
<h2 class="meal-section-title">🌙 Dinner</h2>
|
||||||
|
<button class="add-meal-item-btn" data-meal-type="dinner">+ Add Food</button>
|
||||||
|
<div class="meal-items-container" id="dinnerItemsContainer">
|
||||||
|
<!-- Planned items will be added here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Snacks -->
|
||||||
|
<div class="meal-section" data-meal-type="snacks">
|
||||||
|
<h2 class="meal-section-title">🍿 Snacks</h2>
|
||||||
|
<button class="add-meal-item-btn" data-meal-type="snacks">+ Add Food</button>
|
||||||
|
<div class="meal-items-container" id="snacksItemsContainer">
|
||||||
|
<!-- Planned items will be added here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Food to Plan Form (initially hidden) -->
|
||||||
|
<div class="add-food-to-plan-section hidden" id="addFoodToPlanSection">
|
||||||
|
<h2 class="add-food-title" id="addFoodToPlanTitle">Add Food to <span id="currentMealType"></span></h2>
|
||||||
|
<div class="add-food-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="planFoodSearchInput">Food Item</label>
|
||||||
|
<div class="food-search-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="food-search-input"
|
||||||
|
id="planFoodSearchInput"
|
||||||
|
placeholder="Search food (e.g., apple, chicken breast, rice...)"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<div class="search-loading" id="planSearchLoading"></div>
|
||||||
|
|
||||||
|
<!-- Search Results Overlay -->
|
||||||
|
<div class="search-results" id="planSearchResults">
|
||||||
|
<!-- Results will be populated here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden input for current meal type -->
|
||||||
|
<input type="hidden" id="currentMealTypeInput">
|
||||||
|
<input type="hidden" id="planItemIdToUpdate">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="planGramAmount">Amount (grams)</label>
|
||||||
|
<input type="number" id="planGramAmount" min="1" step="1" value="100">
|
||||||
|
</div>
|
||||||
|
<button class="add-btn" id="addFoodToPlanBtn">Add Food</button>
|
||||||
|
<button class="cancel-btn" id="cancelFoodToPlanBtn">Cancel</button>
|
||||||
|
</div>
|
||||||
|
<div class="display-none" id="plan-food-preview">
|
||||||
|
<div class="nutrition-summary">
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-value" id="planCalorieValuePreview">0</div>
|
||||||
|
<div class="nutrition-label">Calories</div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-value" id="planProteinValuePreview">0</div>
|
||||||
|
<div class="nutrition-label">Protein (g)</div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-value" id="planFatValuePreview">0</div>
|
||||||
|
<div class="nutrition-label">Fat (g)</div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-value" id="planCarboValuePreview">0</div>
|
||||||
|
<div class="nutrition-label">Carbs (g)</div>
|
||||||
|
</div>
|
||||||
|
<div class="nutrition-item">
|
||||||
|
<div class="nutrition-value" id="planFiberValuePreview">0</div>
|
||||||
|
<div class="nutrition-label">Fiber (g)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<script type="module" src="src/index.ts"></script>
|
<script type="module" src="src/index.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+283
@@ -0,0 +1,283 @@
|
|||||||
|
/* Meal Planner Page Styles */
|
||||||
|
|
||||||
|
/* Template Management */
|
||||||
|
.template-management {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--card-bg, #1e1e1e);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-selector-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-selector-container label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color, #ffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-selector {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--input-bg, #2a2a2a);
|
||||||
|
color: var(--text-color, #ffffff);
|
||||||
|
border: 1px solid var(--border-color, #3a3a3a);
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: var(--primary-color, #4CAF50);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-btn:hover {
|
||||||
|
background: var(--primary-hover, #45a049);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-btn.delete-btn {
|
||||||
|
background: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-btn.delete-btn:hover {
|
||||||
|
background: #da190b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Plan Progress Container */
|
||||||
|
.plan-progress-container {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nutrition Progress Bars */
|
||||||
|
.nutrition-progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: var(--progress-bg, #2a2a2a);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nutrition-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: #4CAF50;
|
||||||
|
transition: width 0.3s ease, background-color 0.3s ease;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Meals Container */
|
||||||
|
.meals-container {
|
||||||
|
margin: 30px 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Meal Section */
|
||||||
|
.meal-section {
|
||||||
|
background: var(--card-bg, #1e1e1e);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid var(--border-color, #3a3a3a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-section-title {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: var(--text-color, #ffffff);
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-meal-item-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--primary-color, #4CAF50);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-meal-item-btn:hover {
|
||||||
|
background: var(--primary-hover, #45a049);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-items-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-items-message {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
padding: 20px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Planned Item Cards */
|
||||||
|
.planned-item-card {
|
||||||
|
background: var(--input-bg, #2a2a2a);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--border-color, #3a3a3a);
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planned-item-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.planned-item-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planned-item-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color, #ffffff);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planned-item-grams {
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planned-item-nutrition {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-planned-item-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px;
|
||||||
|
background: #f44336;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-planned-item-btn:hover {
|
||||||
|
background: #da190b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add Food to Plan Section */
|
||||||
|
.add-food-to-plan-section {
|
||||||
|
margin: 30px 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--card-bg, #1e1e1e);
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid var(--primary-color, #4CAF50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quick Add Section in Food Log */
|
||||||
|
.quick-add-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--card-bg, #1e1e1e);
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--border-color, #3a3a3a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-add-items {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-add-meal-header {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
margin: 10px 0 5px 0;
|
||||||
|
color: var(--text-color, #ffffff);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-add-btn {
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--input-bg, #2a2a2a);
|
||||||
|
border: 1px solid var(--border-color, #3a3a3a);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-add-btn:hover {
|
||||||
|
background: var(--primary-color, #4CAF50);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(76, 175, 80, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-add-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color, #ffffff);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-add-details {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-add-btn:hover .quick-add-name,
|
||||||
|
.quick-add-btn:hover .quick-add-details {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsiveness */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.template-selector-container {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-selector {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meals-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-add-items {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.planned-item-nutrition {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-add-btn {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
+214
-1
@@ -1,5 +1,5 @@
|
|||||||
import { Client, Account, Databases, Query, ID } from 'appwrite';
|
import { Client, Account, Databases, Query, ID } from 'appwrite';
|
||||||
import { DailyTotalCalories, FoodStorage, UserSettings, SharedDay } from './types';
|
import { DailyTotalCalories, FoodStorage, UserSettings, SharedDay, MealPlanTemplate, PlannedFoodItem, MealType } from './types';
|
||||||
|
|
||||||
// Initialize Appwrite client
|
// Initialize Appwrite client
|
||||||
const client = new Client();
|
const client = new Client();
|
||||||
@@ -18,6 +18,8 @@ export const FOOD_ENTRIES_COLLECTION_ID = import.meta.env.VITE_APPWRITE_FOODENTR
|
|||||||
export const USER_SETTINGS_COLLECTION_ID = import.meta.env.VITE_APPWRITE_USERSETTINGSID
|
export const USER_SETTINGS_COLLECTION_ID = import.meta.env.VITE_APPWRITE_USERSETTINGSID
|
||||||
export const MONTHLY_CALORIES_COLLECTION_ID = import.meta.env.VITE_APPWRITE_MONTLYCALORIESID
|
export const MONTHLY_CALORIES_COLLECTION_ID = import.meta.env.VITE_APPWRITE_MONTLYCALORIESID
|
||||||
export const SHARED_DAYS_COLLECTION_ID = import.meta.env.VITE_APPWRITE_SHAREDDAYSID
|
export const SHARED_DAYS_COLLECTION_ID = import.meta.env.VITE_APPWRITE_SHAREDDAYSID
|
||||||
|
export const MEAL_PLAN_TEMPLATES_COLLECTION_ID = import.meta.env.VITE_APPWRITE_MEALPLANTEMPLATESID
|
||||||
|
export const PLANNED_FOOD_ITEMS_COLLECTION_ID = import.meta.env.VITE_APPWRITE_PLANNEDFOODITEMSID
|
||||||
|
|
||||||
// Auth helper functions
|
// Auth helper functions
|
||||||
export class AppwriteAuth {
|
export class AppwriteAuth {
|
||||||
@@ -330,4 +332,215 @@ export class AppwriteDB {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Meal Plan Template methods
|
||||||
|
|
||||||
|
// Create meal plan template
|
||||||
|
static async createMealPlanTemplate(name: string) {
|
||||||
|
try {
|
||||||
|
const user = await account.get();
|
||||||
|
const response = await databases.createDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
MEAL_PLAN_TEMPLATES_COLLECTION_ID,
|
||||||
|
'unique()',
|
||||||
|
{
|
||||||
|
name: name,
|
||||||
|
userId: user.$id,
|
||||||
|
isActive: false,
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.debug('Meal plan template created:', response);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create meal plan template error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all meal plan templates for user
|
||||||
|
static async getMealPlanTemplates() {
|
||||||
|
try {
|
||||||
|
const user = await account.get();
|
||||||
|
const response = await databases.listDocuments(
|
||||||
|
DATABASE_ID,
|
||||||
|
MEAL_PLAN_TEMPLATES_COLLECTION_ID,
|
||||||
|
[
|
||||||
|
Query.equal('userId', user.$id),
|
||||||
|
Query.limit(50)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
console.debug('Meal plan templates retrieved:', response);
|
||||||
|
return response.documents;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get meal plan templates error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active meal plan template
|
||||||
|
static async getActiveMealPlanTemplate() {
|
||||||
|
try {
|
||||||
|
const user = await account.get();
|
||||||
|
const response = await databases.listDocuments(
|
||||||
|
DATABASE_ID,
|
||||||
|
MEAL_PLAN_TEMPLATES_COLLECTION_ID,
|
||||||
|
[
|
||||||
|
Query.equal('userId', user.$id),
|
||||||
|
Query.equal('isActive', true),
|
||||||
|
Query.limit(1)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
console.debug('Active meal plan template retrieved:', response);
|
||||||
|
return response.documents.length > 0 ? response.documents[0] : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get active meal plan template error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set active meal plan template
|
||||||
|
static async setActiveMealPlanTemplate(templateId: string) {
|
||||||
|
try {
|
||||||
|
const user = await account.get();
|
||||||
|
|
||||||
|
// First, deactivate all templates
|
||||||
|
const allTemplates = await this.getMealPlanTemplates();
|
||||||
|
const deactivatePromises = allTemplates.map(template =>
|
||||||
|
databases.updateDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
MEAL_PLAN_TEMPLATES_COLLECTION_ID,
|
||||||
|
template.$id,
|
||||||
|
{ isActive: false }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await Promise.all(deactivatePromises);
|
||||||
|
|
||||||
|
// Then activate the selected template
|
||||||
|
const response = await databases.updateDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
MEAL_PLAN_TEMPLATES_COLLECTION_ID,
|
||||||
|
templateId,
|
||||||
|
{ isActive: true }
|
||||||
|
);
|
||||||
|
console.debug('Meal plan template set as active:', response);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Set active meal plan template error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete meal plan template
|
||||||
|
static async deleteMealPlanTemplate(templateId: string) {
|
||||||
|
try {
|
||||||
|
// First delete all planned items for this template
|
||||||
|
const plannedItems = await this.getPlannedItemsByTemplate(templateId);
|
||||||
|
const deletePromises = plannedItems.map(item =>
|
||||||
|
this.deletePlannedFoodItem(item.$id)
|
||||||
|
);
|
||||||
|
await Promise.all(deletePromises);
|
||||||
|
|
||||||
|
// Then delete the template
|
||||||
|
await databases.deleteDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
MEAL_PLAN_TEMPLATES_COLLECTION_ID,
|
||||||
|
templateId
|
||||||
|
);
|
||||||
|
console.debug('Meal plan template deleted:', templateId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete meal plan template error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Planned Food Item methods
|
||||||
|
|
||||||
|
// Create planned food item
|
||||||
|
static async createPlannedFoodItem(item: Omit<PlannedFoodItem, 'id'>) {
|
||||||
|
try {
|
||||||
|
const response = await databases.createDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
PLANNED_FOOD_ITEMS_COLLECTION_ID,
|
||||||
|
'unique()',
|
||||||
|
item
|
||||||
|
);
|
||||||
|
console.debug('Planned food item created:', response);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create planned food item error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all planned items for a template
|
||||||
|
static async getPlannedItemsByTemplate(templateId: string) {
|
||||||
|
try {
|
||||||
|
const response = await databases.listDocuments(
|
||||||
|
DATABASE_ID,
|
||||||
|
PLANNED_FOOD_ITEMS_COLLECTION_ID,
|
||||||
|
[
|
||||||
|
Query.equal('templateId', templateId),
|
||||||
|
Query.limit(100)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
console.debug('Planned items retrieved for template:', response);
|
||||||
|
return response.documents;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get planned items by template error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get planned items by template and meal type
|
||||||
|
static async getPlannedItemsByMealType(templateId: string, mealType: MealType) {
|
||||||
|
try {
|
||||||
|
const response = await databases.listDocuments(
|
||||||
|
DATABASE_ID,
|
||||||
|
PLANNED_FOOD_ITEMS_COLLECTION_ID,
|
||||||
|
[
|
||||||
|
Query.equal('templateId', templateId),
|
||||||
|
Query.equal('mealType', mealType),
|
||||||
|
Query.limit(50)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
console.debug('Planned items retrieved for meal type:', response);
|
||||||
|
return response.documents;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get planned items by meal type error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update planned food item
|
||||||
|
static async updatePlannedFoodItem(itemId: string, updates: Partial<PlannedFoodItem>) {
|
||||||
|
try {
|
||||||
|
const response = await databases.updateDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
PLANNED_FOOD_ITEMS_COLLECTION_ID,
|
||||||
|
itemId,
|
||||||
|
updates
|
||||||
|
);
|
||||||
|
console.debug('Planned food item updated:', response);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update planned food item error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete planned food item
|
||||||
|
static async deletePlannedFoodItem(itemId: string) {
|
||||||
|
try {
|
||||||
|
await databases.deleteDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
PLANNED_FOOD_ITEMS_COLLECTION_ID,
|
||||||
|
itemId
|
||||||
|
);
|
||||||
|
console.debug('Planned food item deleted:', itemId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete planned food item error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-3
@@ -6,6 +6,7 @@ import swal from 'sweetalert';
|
|||||||
import { closeMobileMenu, delay, getCleanName, getIcon, handleMobileCalendarClick, hideLoading, hideSearchResults, navigateResultsKeyboard, QUICK_DELAY, scrollToCalendarView, showLoading, toggleCardHandler, toggleMobileMenu } from './Utils.ts';
|
import { closeMobileMenu, delay, getCleanName, getIcon, handleMobileCalendarClick, hideLoading, hideSearchResults, navigateResultsKeyboard, QUICK_DELAY, scrollToCalendarView, showLoading, toggleCardHandler, toggleMobileMenu } from './Utils.ts';
|
||||||
import { showAuthForms, toggleAuthForms } from './auth.ts';
|
import { showAuthForms, toggleAuthForms } from './auth.ts';
|
||||||
import { appState } from "./state";
|
import { appState } from "./state";
|
||||||
|
import { setupMealPlannerEventListeners, displayQuickAddItems } from './mealPlanner.js';
|
||||||
|
|
||||||
// App state
|
// App state
|
||||||
let selectedDate = new Date();
|
let selectedDate = new Date();
|
||||||
@@ -485,13 +486,21 @@ const setupEventListeners = () => {
|
|||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
const mobileMenu = document.getElementById('mobileMenu');
|
const mobileMenu = document.getElementById('mobileMenu');
|
||||||
const mobileMenuToggle = document.getElementById('mobileMenuToggle');
|
const mobileMenuToggle = document.getElementById('mobileMenuToggle');
|
||||||
|
|
||||||
if (mobileMenu && mobileMenuToggle &&
|
if (mobileMenu && mobileMenuToggle &&
|
||||||
!mobileMenu.contains(target) &&
|
!mobileMenu.contains(target) &&
|
||||||
!mobileMenuToggle.contains(target)) {
|
!mobileMenuToggle.contains(target)) {
|
||||||
closeMobileMenu();
|
closeMobileMenu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Setup meal planner event listeners
|
||||||
|
setupMealPlannerEventListeners();
|
||||||
|
|
||||||
|
// Listen for food added event from quick add
|
||||||
|
window.addEventListener('foodAdded', () => {
|
||||||
|
loadFoodEntries(selectedDate);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function performSearch(query: string) {
|
function performSearch(query: string) {
|
||||||
@@ -1028,6 +1037,9 @@ async function loadFoodEntries(date: Date) {
|
|||||||
getDivById('carboGoalText').classList.add('hidden');
|
getDivById('carboGoalText').classList.add('hidden');
|
||||||
getDivById('fiberGoalText').classList.add('hidden');
|
getDivById('fiberGoalText').classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load quick add items from meal plan
|
||||||
|
await displayQuickAddItems();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Load food entries error:', error);
|
console.error('Load food entries error:', error);
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
|||||||
@@ -0,0 +1,770 @@
|
|||||||
|
import { foodDatabase } from './foodDatabase.js';
|
||||||
|
import { getButtonById, getDivById, getInputById, showFoodPreview } from './DomUtils.ts';
|
||||||
|
import { FoodItem, MealType, PlannedFoodItem } from './types.js';
|
||||||
|
import { AppwriteDB } from './appwrite.js';
|
||||||
|
import swal from 'sweetalert';
|
||||||
|
import { hideLoading, showLoading, getCleanName, getIcon } from './Utils.ts';
|
||||||
|
import { appState } from "./state";
|
||||||
|
|
||||||
|
let selectedPlanFood: FoodItem | null = null;
|
||||||
|
let currentMealType: MealType | null = null;
|
||||||
|
|
||||||
|
// Navigation functions
|
||||||
|
export async function toggleMealPlannerView() {
|
||||||
|
const showMealPlanner = getButtonById('mealPlannerBtn').textContent?.includes('Meal Planner');
|
||||||
|
|
||||||
|
if (showMealPlanner) {
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load templates
|
||||||
|
await loadMealPlanTemplates();
|
||||||
|
|
||||||
|
// Load active template if exists
|
||||||
|
const activeTemplate = await AppwriteDB.getActiveMealPlanTemplate();
|
||||||
|
if (activeTemplate) {
|
||||||
|
appState.currentTemplate = activeTemplate as any;
|
||||||
|
await loadPlannedItemsForTemplate(activeTemplate.$id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show meal planner page
|
||||||
|
getDivById('app-content').classList.add('hidden');
|
||||||
|
getDivById('settings-content').classList.add('hidden');
|
||||||
|
getDivById('meal-planner-content').classList.remove('hidden');
|
||||||
|
getButtonById('mealPlannerBtn').textContent = '← Back';
|
||||||
|
getButtonById('mealPlannerBtnMobile').textContent = '← Back';
|
||||||
|
hideLoading();
|
||||||
|
} catch (error) {
|
||||||
|
hideLoading();
|
||||||
|
console.error('Show meal planner error:', error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
swal('Oh no!', 'Failed to load meal planner: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Go back to main app
|
||||||
|
getDivById('app-content').classList.remove('hidden');
|
||||||
|
getDivById('meal-planner-content').classList.add('hidden');
|
||||||
|
getButtonById('mealPlannerBtn').textContent = '📋 Meal Planner';
|
||||||
|
getButtonById('mealPlannerBtnMobile').textContent = '📋 Meal Planner';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleMobileMealPlannerClick() {
|
||||||
|
toggleMealPlannerView();
|
||||||
|
// Close mobile menu
|
||||||
|
const mobileMenu = document.getElementById('mobileMenu');
|
||||||
|
if (mobileMenu) {
|
||||||
|
mobileMenu.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template management functions
|
||||||
|
export async function loadMealPlanTemplates() {
|
||||||
|
try {
|
||||||
|
const templates = await AppwriteDB.getMealPlanTemplates();
|
||||||
|
const templateSelector = document.getElementById('templateSelector') as HTMLSelectElement;
|
||||||
|
|
||||||
|
if (!templateSelector) return;
|
||||||
|
|
||||||
|
// Clear existing options except the first one
|
||||||
|
templateSelector.innerHTML = '<option value="">-- Select a plan --</option>';
|
||||||
|
|
||||||
|
// Add templates
|
||||||
|
templates.forEach(template => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = template.$id;
|
||||||
|
option.textContent = template.name;
|
||||||
|
if (template.isActive) {
|
||||||
|
option.selected = true;
|
||||||
|
}
|
||||||
|
templateSelector.appendChild(option);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Load templates error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createNewTemplate() {
|
||||||
|
const templateName = await swal({
|
||||||
|
title: 'New Meal Plan',
|
||||||
|
text: 'Enter a name for your meal plan:',
|
||||||
|
content: {
|
||||||
|
element: 'input',
|
||||||
|
attributes: {
|
||||||
|
placeholder: 'e.g., Workout Day, Rest Day',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
cancel: true,
|
||||||
|
confirm: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!templateName || templateName.trim() === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newTemplate = await AppwriteDB.createMealPlanTemplate(templateName.trim());
|
||||||
|
await loadMealPlanTemplates();
|
||||||
|
|
||||||
|
// Set as active and load it
|
||||||
|
await AppwriteDB.setActiveMealPlanTemplate(newTemplate.$id);
|
||||||
|
appState.currentTemplate = newTemplate as any;
|
||||||
|
appState.plannedItems = [];
|
||||||
|
|
||||||
|
// Update selector
|
||||||
|
const templateSelector = document.getElementById('templateSelector') as HTMLSelectElement;
|
||||||
|
if (templateSelector) {
|
||||||
|
templateSelector.value = newTemplate.$id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh display
|
||||||
|
displayPlannedItems();
|
||||||
|
updatePlanTotals();
|
||||||
|
|
||||||
|
hideLoading();
|
||||||
|
swal('Success!', 'Meal plan created successfully!', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
hideLoading();
|
||||||
|
console.error('Create template error:', error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
swal('Oh no!', 'Failed to create meal plan: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCurrentTemplate() {
|
||||||
|
if (!appState.currentTemplate) {
|
||||||
|
swal('Oh no!', 'No meal plan selected', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirm = await swal({
|
||||||
|
title: 'Delete Meal Plan',
|
||||||
|
text: `Are you sure you want to delete "${appState.currentTemplate.name}"? This will also delete all planned items.`,
|
||||||
|
icon: 'warning',
|
||||||
|
buttons: {
|
||||||
|
cancel: true,
|
||||||
|
confirm: {
|
||||||
|
text: 'Delete',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dangerMode: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await AppwriteDB.deleteMealPlanTemplate(appState.currentTemplate.id!);
|
||||||
|
appState.currentTemplate = null;
|
||||||
|
appState.plannedItems = [];
|
||||||
|
|
||||||
|
await loadMealPlanTemplates();
|
||||||
|
displayPlannedItems();
|
||||||
|
updatePlanTotals();
|
||||||
|
|
||||||
|
hideLoading();
|
||||||
|
swal('Success!', 'Meal plan deleted successfully!', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
hideLoading();
|
||||||
|
console.error('Delete template error:', error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
swal('Oh no!', 'Failed to delete meal plan: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleTemplateChange(templateId: string) {
|
||||||
|
if (!templateId) {
|
||||||
|
appState.currentTemplate = null;
|
||||||
|
appState.plannedItems = [];
|
||||||
|
displayPlannedItems();
|
||||||
|
updatePlanTotals();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Set as active template
|
||||||
|
await AppwriteDB.setActiveMealPlanTemplate(templateId);
|
||||||
|
|
||||||
|
// Load template details
|
||||||
|
const templates = await AppwriteDB.getMealPlanTemplates();
|
||||||
|
const template = templates.find(t => t.$id === templateId);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
appState.currentTemplate = template as any;
|
||||||
|
await loadPlannedItemsForTemplate(templateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideLoading();
|
||||||
|
} catch (error) {
|
||||||
|
hideLoading();
|
||||||
|
console.error('Handle template change error:', error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
swal('Oh no!', 'Failed to load meal plan: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPlannedItemsForTemplate(templateId: string) {
|
||||||
|
try {
|
||||||
|
const items = await AppwriteDB.getPlannedItemsByTemplate(templateId);
|
||||||
|
appState.plannedItems = items as any[];
|
||||||
|
displayPlannedItems();
|
||||||
|
updatePlanTotals();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Load planned items error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Food search and add functions
|
||||||
|
export function setupPlanFoodSearch() {
|
||||||
|
const searchInput = document.getElementById('planFoodSearchInput') as HTMLInputElement;
|
||||||
|
const gramAmountInput = document.getElementById('planGramAmount') as HTMLInputElement;
|
||||||
|
|
||||||
|
if (!searchInput || !gramAmountInput) return;
|
||||||
|
|
||||||
|
searchInput.addEventListener('input', (e) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
const searchTerm = target.value.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (searchTerm === '') {
|
||||||
|
hidePlanSearchResults();
|
||||||
|
selectedPlanFood = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce search
|
||||||
|
if (appState.planSearchTimeout) {
|
||||||
|
clearTimeout(appState.planSearchTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
appState.planSearchTimeout = window.setTimeout(() => {
|
||||||
|
performPlanFoodSearch(searchTerm);
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
searchInput.addEventListener('keydown', (e) => {
|
||||||
|
navigatePlanResultsKeyboard(e as KeyboardEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
gramAmountInput.addEventListener('input', () => {
|
||||||
|
previewPlanCalories();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function performPlanFoodSearch(searchTerm: string) {
|
||||||
|
const results = foodDatabase.filter(food =>
|
||||||
|
food.name.toLowerCase().includes(searchTerm)
|
||||||
|
).slice(0, 10);
|
||||||
|
|
||||||
|
appState.planSearchResults = results;
|
||||||
|
appState.planCurrentHighlightIndex = -1;
|
||||||
|
displayPlanSearchResults(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayPlanSearchResults(results: FoodItem[]) {
|
||||||
|
const searchResults = document.getElementById('planSearchResults');
|
||||||
|
if (!searchResults) return;
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
searchResults.innerHTML = '<div class="no-results">No foods found</div>';
|
||||||
|
searchResults.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchResults.innerHTML = '';
|
||||||
|
results.forEach((food, index) => {
|
||||||
|
const resultItem = document.createElement('div');
|
||||||
|
resultItem.className = 'search-result-item';
|
||||||
|
resultItem.setAttribute('data-index', index.toString());
|
||||||
|
resultItem.innerHTML = `
|
||||||
|
${getIcon(food.info.category)}
|
||||||
|
<span class="food-name">${getCleanName(food.name)}</span>
|
||||||
|
<span class="food-calories">${food.info.calories} cal</span>
|
||||||
|
`;
|
||||||
|
resultItem.addEventListener('click', () => selectPlanFood(food));
|
||||||
|
searchResults.appendChild(resultItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
searchResults.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hidePlanSearchResults() {
|
||||||
|
const searchResults = document.getElementById('planSearchResults');
|
||||||
|
if (searchResults) {
|
||||||
|
searchResults.classList.add('hidden');
|
||||||
|
searchResults.innerHTML = '';
|
||||||
|
}
|
||||||
|
appState.planSearchResults = [];
|
||||||
|
appState.planCurrentHighlightIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigatePlanResultsKeyboard(e: KeyboardEvent) {
|
||||||
|
const results = appState.planSearchResults;
|
||||||
|
|
||||||
|
if (results.length === 0) return;
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
appState.planCurrentHighlightIndex = Math.min(appState.planCurrentHighlightIndex + 1, results.length - 1);
|
||||||
|
updatePlanResultsHighlight();
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
appState.planCurrentHighlightIndex = Math.max(appState.planCurrentHighlightIndex - 1, -1);
|
||||||
|
updatePlanResultsHighlight();
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (appState.planCurrentHighlightIndex >= 0) {
|
||||||
|
selectPlanFood(results[appState.planCurrentHighlightIndex]);
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
hidePlanSearchResults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePlanResultsHighlight() {
|
||||||
|
const searchResults = document.getElementById('planSearchResults');
|
||||||
|
if (!searchResults) return;
|
||||||
|
|
||||||
|
const items = searchResults.querySelectorAll('.search-result-item');
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
if (index === appState.planCurrentHighlightIndex) {
|
||||||
|
item.classList.add('highlighted');
|
||||||
|
} else {
|
||||||
|
item.classList.remove('highlighted');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPlanFood(food: FoodItem) {
|
||||||
|
selectedPlanFood = food;
|
||||||
|
const searchInput = document.getElementById('planFoodSearchInput') as HTMLInputElement;
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.value = getCleanName(food.name);
|
||||||
|
}
|
||||||
|
hidePlanSearchResults();
|
||||||
|
previewPlanCalories();
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewPlanCalories() {
|
||||||
|
const gramAmountInput = document.getElementById('planGramAmount') as HTMLInputElement;
|
||||||
|
const previewContainer = document.getElementById('plan-food-preview');
|
||||||
|
|
||||||
|
if (!selectedPlanFood || !gramAmountInput || !previewContainer) {
|
||||||
|
if (previewContainer) previewContainer.classList.add('display-none');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const grams = parseFloat(gramAmountInput.value) || 100;
|
||||||
|
const proportion = grams / 100;
|
||||||
|
|
||||||
|
const calories = Math.round(selectedPlanFood.info.calories * proportion);
|
||||||
|
const protein = Math.round(selectedPlanFood.info.protein * proportion * 10) / 10;
|
||||||
|
const fat = Math.round(selectedPlanFood.info.fat * proportion * 10) / 10;
|
||||||
|
const carbs = Math.round(selectedPlanFood.info.carbs * proportion * 10) / 10;
|
||||||
|
const fiber = Math.round(selectedPlanFood.info.fiber * proportion * 10) / 10;
|
||||||
|
|
||||||
|
const calorieEl = document.getElementById('planCalorieValuePreview');
|
||||||
|
const proteinEl = document.getElementById('planProteinValuePreview');
|
||||||
|
const fatEl = document.getElementById('planFatValuePreview');
|
||||||
|
const carbsEl = document.getElementById('planCarboValuePreview');
|
||||||
|
const fiberEl = document.getElementById('planFiberValuePreview');
|
||||||
|
|
||||||
|
if (calorieEl) calorieEl.textContent = calories.toString();
|
||||||
|
if (proteinEl) proteinEl.textContent = protein.toString();
|
||||||
|
if (fatEl) fatEl.textContent = fat.toString();
|
||||||
|
if (carbsEl) carbsEl.textContent = carbs.toString();
|
||||||
|
if (fiberEl) fiberEl.textContent = fiber.toString();
|
||||||
|
|
||||||
|
previewContainer.classList.remove('display-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showAddFoodToPlan(mealType: MealType) {
|
||||||
|
if (!appState.currentTemplate) {
|
||||||
|
swal('Oh no!', 'Please select or create a meal plan first', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentMealType = mealType;
|
||||||
|
const addSection = document.getElementById('addFoodToPlanSection');
|
||||||
|
const mealTypeSpan = document.getElementById('currentMealType');
|
||||||
|
const currentMealTypeInput = document.getElementById('currentMealTypeInput') as HTMLInputElement;
|
||||||
|
|
||||||
|
if (addSection) addSection.classList.remove('hidden');
|
||||||
|
if (mealTypeSpan) mealTypeSpan.textContent = mealType.charAt(0).toUpperCase() + mealType.slice(1);
|
||||||
|
if (currentMealTypeInput) currentMealTypeInput.value = mealType;
|
||||||
|
|
||||||
|
// Scroll to form
|
||||||
|
addSection?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hideAddFoodToPlan() {
|
||||||
|
const addSection = document.getElementById('addFoodToPlanSection');
|
||||||
|
const searchInput = document.getElementById('planFoodSearchInput') as HTMLInputElement;
|
||||||
|
const gramInput = document.getElementById('planGramAmount') as HTMLInputElement;
|
||||||
|
const previewContainer = document.getElementById('plan-food-preview');
|
||||||
|
|
||||||
|
if (addSection) addSection.classList.add('hidden');
|
||||||
|
if (searchInput) searchInput.value = '';
|
||||||
|
if (gramInput) gramInput.value = '100';
|
||||||
|
if (previewContainer) previewContainer.classList.add('display-none');
|
||||||
|
|
||||||
|
selectedPlanFood = null;
|
||||||
|
currentMealType = null;
|
||||||
|
hidePlanSearchResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addFoodToPlan() {
|
||||||
|
if (!selectedPlanFood || !currentMealType || !appState.currentTemplate) {
|
||||||
|
swal('Oh no!', 'Please select a food item', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gramAmountInput = document.getElementById('planGramAmount') as HTMLInputElement;
|
||||||
|
const grams = parseFloat(gramAmountInput.value);
|
||||||
|
|
||||||
|
if (!grams || grams <= 0) {
|
||||||
|
swal('Oh no!', 'Please enter a valid amount in grams', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const proportion = grams / 100;
|
||||||
|
const plannedItem: Omit<PlannedFoodItem, 'id'> = {
|
||||||
|
templateId: appState.currentTemplate.id!,
|
||||||
|
mealType: currentMealType,
|
||||||
|
name: selectedPlanFood.name,
|
||||||
|
grams: grams,
|
||||||
|
calories: Math.round(selectedPlanFood.info.calories * proportion),
|
||||||
|
protein: Math.round(selectedPlanFood.info.protein * proportion * 10) / 10,
|
||||||
|
fat: Math.round(selectedPlanFood.info.fat * proportion * 10) / 10,
|
||||||
|
carbs: Math.round(selectedPlanFood.info.carbs * proportion * 10) / 10,
|
||||||
|
fiber: Math.round(selectedPlanFood.info.fiber * proportion * 10) / 10,
|
||||||
|
alkaline: selectedPlanFood.info.alkaline
|
||||||
|
};
|
||||||
|
|
||||||
|
await AppwriteDB.createPlannedFoodItem(plannedItem);
|
||||||
|
await loadPlannedItemsForTemplate(appState.currentTemplate.id!);
|
||||||
|
|
||||||
|
hideAddFoodToPlan();
|
||||||
|
hideLoading();
|
||||||
|
swal('Success!', 'Food added to meal plan!', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
hideLoading();
|
||||||
|
console.error('Add food to plan error:', error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
swal('Oh no!', 'Failed to add food: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display functions
|
||||||
|
export function displayPlannedItems() {
|
||||||
|
const mealTypes: MealType[] = ['breakfast', 'lunch', 'dinner', 'snacks'];
|
||||||
|
|
||||||
|
mealTypes.forEach(mealType => {
|
||||||
|
const container = document.getElementById(`${mealType}ItemsContainer`);
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const items = appState.plannedItems.filter(item => item.mealType === mealType);
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
container.innerHTML = '<p class="no-items-message">No foods planned for this meal</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = '';
|
||||||
|
items.forEach(item => {
|
||||||
|
const card = createPlannedItemCard(item);
|
||||||
|
container.appendChild(card);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPlannedItemCard(item: PlannedFoodItem): HTMLElement {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'planned-item-card';
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="planned-item-header">
|
||||||
|
<span class="planned-item-name">${getCleanName(item.name)}</span>
|
||||||
|
<span class="planned-item-grams">${item.grams}g</span>
|
||||||
|
</div>
|
||||||
|
<div class="planned-item-nutrition">
|
||||||
|
<span>${item.calories} cal</span>
|
||||||
|
<span>P: ${item.protein}g</span>
|
||||||
|
<span>F: ${item.fat}g</span>
|
||||||
|
<span>C: ${item.carbs}g</span>
|
||||||
|
</div>
|
||||||
|
<button class="delete-planned-item-btn" data-item-id="${item.id}">Delete</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const deleteBtn = card.querySelector('.delete-planned-item-btn');
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', () => deletePlannedItem(item.id!));
|
||||||
|
}
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePlannedItem(itemId: string) {
|
||||||
|
const confirm = await swal({
|
||||||
|
title: 'Delete Item',
|
||||||
|
text: 'Are you sure you want to delete this item from your meal plan?',
|
||||||
|
icon: 'warning',
|
||||||
|
buttons: {
|
||||||
|
cancel: true,
|
||||||
|
confirm: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirm) return;
|
||||||
|
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await AppwriteDB.deletePlannedFoodItem(itemId);
|
||||||
|
if (appState.currentTemplate) {
|
||||||
|
await loadPlannedItemsForTemplate(appState.currentTemplate.id!);
|
||||||
|
}
|
||||||
|
hideLoading();
|
||||||
|
} catch (error) {
|
||||||
|
hideLoading();
|
||||||
|
console.error('Delete planned item error:', error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
swal('Oh no!', 'Failed to delete item: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatePlanTotals() {
|
||||||
|
const totals = appState.plannedItems.reduce((acc, item) => {
|
||||||
|
acc.calories += item.calories;
|
||||||
|
acc.protein += item.protein;
|
||||||
|
acc.fat += item.fat;
|
||||||
|
acc.carbs += item.carbs;
|
||||||
|
acc.fiber += item.fiber;
|
||||||
|
return acc;
|
||||||
|
}, { calories: 0, protein: 0, fat: 0, carbs: 0, fiber: 0 });
|
||||||
|
|
||||||
|
// Update totals display
|
||||||
|
const caloriesTotalEl = document.getElementById('planCaloriesTotal');
|
||||||
|
const proteinValueEl = document.getElementById('planProteinValue');
|
||||||
|
const fatValueEl = document.getElementById('planFatValue');
|
||||||
|
const carboValueEl = document.getElementById('planCarboValue');
|
||||||
|
const fiberValueEl = document.getElementById('planFiberValue');
|
||||||
|
|
||||||
|
if (caloriesTotalEl) caloriesTotalEl.textContent = totals.calories.toString();
|
||||||
|
if (proteinValueEl) proteinValueEl.textContent = Math.round(totals.protein * 10) / 10 + '';
|
||||||
|
if (fatValueEl) fatValueEl.textContent = Math.round(totals.fat * 10) / 10 + '';
|
||||||
|
if (carboValueEl) carboValueEl.textContent = Math.round(totals.carbs * 10) / 10 + '';
|
||||||
|
if (fiberValueEl) fiberValueEl.textContent = Math.round(totals.fiber * 10) / 10 + '';
|
||||||
|
|
||||||
|
// Update progress bars and goals
|
||||||
|
updatePlanProgressBars(totals);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePlanProgressBars(totals: { calories: number, protein: number, fat: number, carbs: number, fiber: number }) {
|
||||||
|
try {
|
||||||
|
const settings = await AppwriteDB.getUserSettings();
|
||||||
|
if (!settings || settings.length === 0) return;
|
||||||
|
|
||||||
|
const userSettings = settings[settings.length - 1];
|
||||||
|
|
||||||
|
// Update goal texts
|
||||||
|
const caloriesGoalEl = document.getElementById('planCaloriesGoalText');
|
||||||
|
const proteinGoalEl = document.getElementById('planProteinGoalText');
|
||||||
|
const fatGoalEl = document.getElementById('planFatGoalText');
|
||||||
|
const carboGoalEl = document.getElementById('planCarboGoalText');
|
||||||
|
const fiberGoalEl = document.getElementById('planFiberGoalText');
|
||||||
|
|
||||||
|
if (caloriesGoalEl) caloriesGoalEl.textContent = `of ${userSettings.caloriesGoal || 0}`;
|
||||||
|
if (proteinGoalEl) proteinGoalEl.textContent = `of ${userSettings.proteinGoal}`;
|
||||||
|
if (fatGoalEl) fatGoalEl.textContent = `of ${userSettings.fatGoal}`;
|
||||||
|
if (carboGoalEl) carboGoalEl.textContent = `of ${userSettings.carboGoal}`;
|
||||||
|
if (fiberGoalEl) fiberGoalEl.textContent = `of ${userSettings.fiberGoal}`;
|
||||||
|
|
||||||
|
// Update progress bars
|
||||||
|
updateProgressBar('planProteinProgress', totals.protein, userSettings.proteinGoal);
|
||||||
|
updateProgressBar('planFatProgress', totals.fat, userSettings.fatGoal);
|
||||||
|
updateProgressBar('planCarboProgress', totals.carbs, userSettings.carboGoal);
|
||||||
|
updateProgressBar('planFiberProgress', totals.fiber, userSettings.fiberGoal);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update progress bars error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgressBar(elementId: string, current: number, goal: number) {
|
||||||
|
const progressBar = document.getElementById(elementId);
|
||||||
|
if (!progressBar) return;
|
||||||
|
|
||||||
|
const percentage = Math.min((current / goal) * 100, 100);
|
||||||
|
progressBar.style.width = `${percentage}%`;
|
||||||
|
|
||||||
|
// Color coding: green when within range, yellow when getting close, red when over
|
||||||
|
if (percentage < 80) {
|
||||||
|
progressBar.style.backgroundColor = '#4CAF50'; // green
|
||||||
|
} else if (percentage < 100) {
|
||||||
|
progressBar.style.backgroundColor = '#FFC107'; // yellow
|
||||||
|
} else {
|
||||||
|
progressBar.style.backgroundColor = '#f44336'; // red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick add functions for food log
|
||||||
|
export async function displayQuickAddItems() {
|
||||||
|
const quickAddSection = document.getElementById('quickAddSection');
|
||||||
|
const quickAddContainer = document.getElementById('quickAddItemsContainer');
|
||||||
|
|
||||||
|
if (!quickAddSection || !quickAddContainer) return;
|
||||||
|
|
||||||
|
// Check if there's an active template
|
||||||
|
const activeTemplate = await AppwriteDB.getActiveMealPlanTemplate();
|
||||||
|
|
||||||
|
if (!activeTemplate) {
|
||||||
|
quickAddSection.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load planned items
|
||||||
|
const plannedItems = await AppwriteDB.getPlannedItemsByTemplate(activeTemplate.$id);
|
||||||
|
|
||||||
|
if (plannedItems.length === 0) {
|
||||||
|
quickAddSection.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show section and populate items
|
||||||
|
quickAddSection.classList.remove('hidden');
|
||||||
|
quickAddContainer.innerHTML = '';
|
||||||
|
|
||||||
|
// Group by meal type
|
||||||
|
const mealTypes: MealType[] = ['breakfast', 'lunch', 'dinner', 'snacks'];
|
||||||
|
const mealIcons = {
|
||||||
|
breakfast: '🌅',
|
||||||
|
lunch: '☀️',
|
||||||
|
dinner: '🌙',
|
||||||
|
snacks: '🍿'
|
||||||
|
};
|
||||||
|
|
||||||
|
mealTypes.forEach(mealType => {
|
||||||
|
const items = plannedItems.filter(item => item.mealType === mealType);
|
||||||
|
|
||||||
|
if (items.length > 0) {
|
||||||
|
// Add meal type header
|
||||||
|
const mealHeader = document.createElement('h3');
|
||||||
|
mealHeader.className = 'quick-add-meal-header';
|
||||||
|
mealHeader.textContent = `${mealIcons[mealType]} ${mealType.charAt(0).toUpperCase() + mealType.slice(1)}`;
|
||||||
|
quickAddContainer.appendChild(mealHeader);
|
||||||
|
|
||||||
|
// Add items
|
||||||
|
items.forEach(item => {
|
||||||
|
const quickAddItem = createQuickAddButton(item);
|
||||||
|
quickAddContainer.appendChild(quickAddItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createQuickAddButton(item: any): HTMLElement {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.className = 'quick-add-btn';
|
||||||
|
button.innerHTML = `
|
||||||
|
<span class="quick-add-name">${getCleanName(item.name)}</span>
|
||||||
|
<span class="quick-add-details">${item.grams}g • ${item.calories} cal</span>
|
||||||
|
`;
|
||||||
|
button.addEventListener('click', () => quickAddToFoodLog(item));
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function quickAddToFoodLog(plannedItem: any) {
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const now = new Date();
|
||||||
|
const time = now.toLocaleTimeString('en-US', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedDate = new Date(); // This should be the actual selected date from the app
|
||||||
|
const localDateTime = new Date(selectedDate.getTime() - (selectedDate.getTimezoneOffset() * 60000)).toISOString();
|
||||||
|
|
||||||
|
const foodEntry = {
|
||||||
|
name: plannedItem.name,
|
||||||
|
grams: plannedItem.grams,
|
||||||
|
calories: plannedItem.calories,
|
||||||
|
protein: plannedItem.protein,
|
||||||
|
fat: plannedItem.fat,
|
||||||
|
carbs: plannedItem.carbs,
|
||||||
|
fiber: plannedItem.fiber,
|
||||||
|
time: time,
|
||||||
|
date: localDateTime.split('T')[0],
|
||||||
|
alkaline: plannedItem.alkaline
|
||||||
|
};
|
||||||
|
|
||||||
|
await AppwriteDB.saveFoodEntry(foodEntry);
|
||||||
|
|
||||||
|
// Dispatch custom event to trigger food log reload
|
||||||
|
const event = new CustomEvent('foodAdded');
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
|
||||||
|
hideLoading();
|
||||||
|
swal('Success!', `${getCleanName(plannedItem.name)} added to food log!`, 'success');
|
||||||
|
} catch (error) {
|
||||||
|
hideLoading();
|
||||||
|
console.error('Quick add to food log error:', error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
swal('Oh no!', 'Failed to add food: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listener setup
|
||||||
|
export function setupMealPlannerEventListeners() {
|
||||||
|
// Navigation buttons
|
||||||
|
document.getElementById('mealPlannerBtn')?.addEventListener('click', toggleMealPlannerView);
|
||||||
|
document.getElementById('mealPlannerBtnMobile')?.addEventListener('click', handleMobileMealPlannerClick);
|
||||||
|
|
||||||
|
// Template management
|
||||||
|
document.getElementById('newTemplateBtn')?.addEventListener('click', createNewTemplate);
|
||||||
|
document.getElementById('deleteTemplateBtn')?.addEventListener('click', deleteCurrentTemplate);
|
||||||
|
document.getElementById('templateSelector')?.addEventListener('change', (e) => {
|
||||||
|
const target = e.target as HTMLSelectElement;
|
||||||
|
handleTemplateChange(target.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add food buttons for each meal
|
||||||
|
const addMealButtons = document.querySelectorAll('.add-meal-item-btn');
|
||||||
|
addMealButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const mealType = target.getAttribute('data-meal-type') as MealType;
|
||||||
|
showAddFoodToPlan(mealType);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add food to plan form
|
||||||
|
document.getElementById('addFoodToPlanBtn')?.addEventListener('click', addFoodToPlan);
|
||||||
|
document.getElementById('cancelFoodToPlanBtn')?.addEventListener('click', hideAddFoodToPlan);
|
||||||
|
|
||||||
|
// Setup food search
|
||||||
|
setupPlanFoodSearch();
|
||||||
|
}
|
||||||
+14
-2
@@ -1,4 +1,4 @@
|
|||||||
import { DailyTotalCalories, FoodItem, SharedDay } from "./types";
|
import { DailyTotalCalories, FoodItem, SharedDay, MealPlanTemplate, PlannedFoodItem } from "./types";
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
currentHighlightIndex: number;
|
currentHighlightIndex: number;
|
||||||
@@ -7,6 +7,12 @@ interface AppState {
|
|||||||
calendarMonthlyCalories: DailyTotalCalories[];
|
calendarMonthlyCalories: DailyTotalCalories[];
|
||||||
isSharedView: boolean;
|
isSharedView: boolean;
|
||||||
sharedData: SharedDay | null;
|
sharedData: SharedDay | null;
|
||||||
|
// Meal Planner state
|
||||||
|
currentTemplate: MealPlanTemplate | null;
|
||||||
|
plannedItems: PlannedFoodItem[];
|
||||||
|
planSearchResults: FoodItem[];
|
||||||
|
planCurrentHighlightIndex: number;
|
||||||
|
planSearchTimeout: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appState: AppState = {
|
export const appState: AppState = {
|
||||||
@@ -15,5 +21,11 @@ export const appState: AppState = {
|
|||||||
searchResults: [],
|
searchResults: [],
|
||||||
calendarMonthlyCalories: [],
|
calendarMonthlyCalories: [],
|
||||||
isSharedView: false,
|
isSharedView: false,
|
||||||
sharedData: null
|
sharedData: null,
|
||||||
|
// Meal Planner state
|
||||||
|
currentTemplate: null,
|
||||||
|
plannedItems: [],
|
||||||
|
planSearchResults: [],
|
||||||
|
planCurrentHighlightIndex: -1,
|
||||||
|
planSearchTimeout: null
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,3 +43,27 @@ export type SharedDay = {
|
|||||||
foodEntries: string; // JSON stringified FoodStorage[]
|
foodEntries: string; // JSON stringified FoodStorage[]
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MealType = 'breakfast' | 'lunch' | 'dinner' | 'snacks';
|
||||||
|
|
||||||
|
export type MealPlanTemplate = {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
userId: string;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlannedFoodItem = {
|
||||||
|
id?: string;
|
||||||
|
templateId: string;
|
||||||
|
mealType: MealType;
|
||||||
|
name: string;
|
||||||
|
grams: number;
|
||||||
|
calories: number;
|
||||||
|
protein: number;
|
||||||
|
fat: number;
|
||||||
|
carbs: number;
|
||||||
|
fiber: number;
|
||||||
|
alkaline: boolean | null;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user