diff --git a/index.html b/index.html index 47c9c53..f3d4dd3 100644 --- a/index.html +++ b/index.html @@ -149,6 +149,10 @@
+ + + +
@@ -156,6 +160,7 @@
+ -
-
Food Log
-
- - - - - - - - - - - - - - - - - - - -
IDTimeFoodGramsCaloriesProtein (g)Fat (g)Carbs (g)Fiber (g)ActionAlkaline
-
+
Food Log
+ +
+
diff --git a/package-lock.json b/package-lock.json index abf198f..b3d4055 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,12 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "appwrite": "^18.2.0", + "appwrite": "^19.0.0", "sweetalert": "^2.1.2" }, "devDependencies": { "typescript": "^5.9.2", - "vite": "^7.1.3" + "vite": "^7.1.4" } }, "node_modules/@esbuild/aix-ppc64": { @@ -747,9 +747,9 @@ "license": "MIT" }, "node_modules/appwrite": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/appwrite/-/appwrite-18.2.0.tgz", - "integrity": "sha512-g7pQpsxqR7+amEIaQLXMN4XzdQKenTHnGdA4s7UUJdZufhlHdJby8895h8z893+S0XipeHZhi0wpxYA2An95Rg==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/appwrite/-/appwrite-19.0.0.tgz", + "integrity": "sha512-guamzST6Fvn/lvuBMLJGzJGL+N+o313j53qyfKM1ZHfcDd1ADnSONgm9DZihRFYBaWtU7s1bwkphkBJWSUwurQ==", "license": "BSD-3-Clause" }, "node_modules/es6-object-assign": { @@ -999,9 +999,9 @@ } }, "node_modules/vite": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz", - "integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.4.tgz", + "integrity": "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index a870824..4503707 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,10 @@ "description": "", "devDependencies": { "typescript": "^5.9.2", - "vite": "^7.1.3" + "vite": "^7.1.4" }, "dependencies": { - "appwrite": "^18.2.0", + "appwrite": "^19.0.0", "sweetalert": "^2.1.2" } } diff --git a/src/.env.example b/src/.env.example new file mode 100644 index 0000000..0493e4a --- /dev/null +++ b/src/.env.example @@ -0,0 +1,4 @@ +VITE_APPWRITE_ENDPOINT=here +VITE_APPWRITE_DBID=here +VITE_APPWRITE_FOODENTRIESID=here +VITE_APPWRITE_USERSETTINGSID=here \ No newline at end of file diff --git a/src/appwrite.ts b/src/appwrite.ts index d3a53b2..3dee252 100644 --- a/src/appwrite.ts +++ b/src/appwrite.ts @@ -98,6 +98,26 @@ export class AppwriteDB { } } + // Upadte food entry + static async updateFoodEntry(documentId: string, entryData: Partial) { + try { + const response = await databases.updateDocument( + DATABASE_ID, + FOOD_ENTRIES_COLLECTION_ID, + documentId, + { + ...entryData, + userId: (await account.get()).$id, + } + ); + console.debug('Food entry updated:', response); + return response; + } catch (error) { + console.error('Update food entry error:', error); + throw error; + } + } + // Save food entry static async saveUserSettings(userSettings: UserSettings) { try { diff --git a/src/foodDatabase.ts b/src/foodDatabase.ts index 9652aba..999acae 100644 --- a/src/foodDatabase.ts +++ b/src/foodDatabase.ts @@ -15,19 +15,29 @@ export const foodDatabase: FoodItem[] = [ { name: 'Semente de Abóbora', info: { calories: 446, protein: 18.55, fat: 19.4, carbs: 53.75, fiber: 6, category: 'fats', alkaline: true } }, // Proteínas - Proteins - { name: 'Atum Sólido, natural', info: { calories: 135, protein: 28.3, fat: 2.7, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } }, + { name: 'Atum, conserva em óleo', info: { calories: 166, protein: 26.2, fat: 6, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } }, { name: 'Filé de Tilápia, cozido', info: { calories: 128, protein: 26.1, fat: 2.6, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } }, - { name: 'Ovo inteiro, cozido', info: { calories: 146, protein: 13.3, fat: 9.5, carbs: 0.6, fiber: 0, category: 'proteins', alkaline: false } }, - { name: 'Ovo clara, cozido', info: { calories: 59, protein: 13.4, fat: 0.1, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } }, + { name: 'Ovo, de galinha, inteiro, cozido', info: { calories: 146, protein: 13.3, fat: 9.5, carbs: 0.6, fiber: 0, category: 'proteins', alkaline: false } }, + { name: 'Ovo, de galinha, gema, cozida', info: { calories: 353, protein: 15.9, fat: 30.8, carbs: 1.6, fiber: 0, category: 'proteins', alkaline: false } }, + { name: 'Ovo, de galinha, clara, cozida', info: { calories: 59, protein: 13.4, fat: 0.1, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } }, { name: 'PTS, crua', info: { calories: 288, protein: 52, fat: 0, carbs: 20, fiber: 14, category: 'proteins', alkaline: false } }, - { name: 'Patinho moído, grelhado', info: { calories: 219, protein: 35.9, fat: 7.3, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } }, + { name: 'Carne, bovina, patinho, sem gordura, grelhado', info: { calories: 219, protein: 35.9, fat: 7.3, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } }, { name: 'Soja em Pó Growth', info: { calories: 400, protein: 86.7, fat: 5, carbs: 3.3, fiber: 0, category: 'proteins', alkaline: false } }, { name: 'Sobrecoxa s/pele, cozida', info: { calories: 245, protein: 24.9, fat: 15.4, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } }, - { name: 'Tofu', info: { calories: 65, protein: 6.5, fat: 4, carbs: 2, fiber: 0.75, category: 'proteins', alkaline: false } }, + { name: 'Soja, queijo (tofu)', info: { calories: 64, protein: 6.6, fat: 4, carbs: 2.1, fiber: 0.8, category: 'proteins', alkaline: false } }, { name: 'Feijão preto, cozido', info: { calories: 77, protein: 4.5, fat: 0.5, carbs: 14, fiber: 8.4, category: 'proteins', alkaline: false } }, { name: 'Lentilha, cozida', info: { calories: 93, protein: 6.3, fat: 0.5, carbs: 16.3, fiber: 7.9, category: 'proteins', alkaline: false } }, { name: 'Frango, peito, sem pele, cozido', info: { calories: 163, protein: 31.5, fat: 3.2, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } }, { name: 'Frango, peito, sem pele, grelhado', info: { calories: 159, protein: 32, fat: 2.5, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } }, + { name: 'Carne, bovina, acém, moído, cozido', info: { calories: 212, protein: 26.7, fat: 10.9, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } }, + { name: 'Carne, bovina, contra-filé, sem gordura, grelhado', info: { calories: 194, protein: 35.9, fat: 4.5, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } }, + { name: 'Carne, bovina, costela, assada', info: { calories: 373, protein: 28.8, fat: 27.7, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } }, + { name: 'Carne, bovina, cupim, assado', info: { calories: 330, protein: 28.6, fat: 23, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } }, + { name: 'Carne, bovina, fígado, grelhado', info: { calories: 225, protein: 29.9, fat: 9, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } }, + { name: 'Carne, bovina, picanha, sem gordura, grelhada', info: { calories: 238, protein: 31.9, fat: 11.3, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } }, + { name: 'Apresuntado', info: { calories: 129, protein: 13.5, fat: 6.7, carbs: 2.9, fiber: 0, category: 'proteins', alkaline: false } }, + { name: 'Presunto, com capa de gordura', info: { calories: 128, protein: 14.4, fat: 6.8, carbs: 1.4, fiber: 0, category: 'proteins', alkaline: false } }, + { name: 'Presunto, sem capa de gordura', info: { calories: 94, protein: 14.3, fat: 2.7, carbs: 2.1, fiber: 0, category: 'proteins', alkaline: false } }, // Carboidratos, amiláceos - Carbs { name: 'Grão de Bico, cozido', info: { calories: 180, protein: 9.5, fat: 3, carbs: 30, fiber: 8.6, category: 'carbs (high)', alkaline: false } }, @@ -41,16 +51,16 @@ export const foodDatabase: FoodItem[] = [ { name: 'Batata inglesa, cozida', info: { calories: 52, protein: 1.2, fat: 0, carbs: 11.9, fiber: 1.3, category: 'carbs (high)', alkaline: true } }, { name: 'Batata inglesa, crua', info: { calories: 64, protein: 1.8, fat: 0, carbs: 14.7, fiber: 1.2, category: 'carbs (high)', alkaline: true } }, { name: 'Mandioca, cozida', info: { calories: 125, protein: 0.6, fat: 0.3, carbs: 30.1, fiber: 1.6, category: 'carbs (high)', alkaline: false } }, - { name: 'Pão Francês', info: { calories: 300, protein: 8, fat: 3.1, carbs: 58.6, fiber: 2.3, category: 'carbs (high)', alkaline: false } }, + { name: 'Pão, trigo, francês', info: { calories: 300, protein: 8, fat: 3.1, carbs: 58.6, fiber: 2.3, category: 'carbs (high)', alkaline: false } }, { name: 'Polenta, cozida', info: { calories: 192, protein: 4.8, fat: 1.2, carbs: 40, fiber: 3.8, category: 'carbs (high)', alkaline: false } }, - { name: 'Arroz branco, cozido', info: { calories: 128, protein: 2.5, fat: 0.2, carbs: 28.1, fiber: 1.6, category: 'carbs (high)', alkaline: false } }, - { name: 'Arroz integral, cozido', info: { calories: 124, protein: 2.6, fat: 1.0, carbs: 25.8, fiber: 2.7, category: 'carbs (high)', alkaline: false } }, + { name: 'Arroz, tipo 1 (branco), cozido', info: { calories: 128, protein: 2.5, fat: 0.2, carbs: 28.1, fiber: 1.6, category: 'carbs (high)', alkaline: false } }, + { name: 'Arroz, integral, cozido', info: { calories: 124, protein: 2.6, fat: 1.0, carbs: 25.8, fiber: 2.7, category: 'carbs (high)', alkaline: false } }, { name: 'Aveia', info: { calories: 394, protein: 13.9, fat: 8.5, carbs: 66.6, fiber: 9.1, category: 'carbs (high)', alkaline: false } }, { name: 'Ervilha seca, cozida', info: { calories: 88, protein: 7.5, fat: 0.5, carbs: 14.2, fiber: 9.7, category: 'carbs (high)', alkaline: true } }, { name: 'Ervilha congelada, cozida', info: { calories: 57, protein: 5, fat: 1, carbs: 7, fiber: 8, category: 'carbs (high)', alkaline: true } }, - { name: 'Pão integral de forma', info: { calories: 253, protein: 9.4, fat: 3.7, carbs: 49.9, fiber: 6.9, category: 'carbs (high)', alkaline: false } }, - { name: 'Pipoca c/óleo', info: { calories: 448, protein: 9.9, fat: 15.9, carbs: 70.3, fiber: 14.3, category: 'carbs (high)', alkaline: false } }, - { name: 'Pipoca s/óleo', info: { calories: 178, protein: 3.3, fat: 7.2, carbs: 25, fiber: 4.3, category: 'carbs (high)', alkaline: false } }, + { name: 'Pão, trigo, forma, integral', info: { calories: 253, protein: 9.4, fat: 3.7, carbs: 49.9, fiber: 6.9, category: 'carbs (high)', alkaline: false } }, + { name: 'Pipoca, com óleo de soja, sem sal', info: { calories: 448, protein: 9.9, fat: 15.9, carbs: 70.3, fiber: 14.3, category: 'carbs (high)', alkaline: false } }, + { name: 'Pipoca, sem óleo, sem sal', info: { calories: 178, protein: 3.3, fat: 7.2, carbs: 25, fiber: 4.3, category: 'carbs (high)', alkaline: false } }, { name: 'Quinoa, cozida', info: { calories: 158, protein: 5.55, fat: 2.5, carbs: 29, fiber: 2.5, category: 'carbs (high)', alkaline: true } }, { name: '70% Nibs Garoto', info: { calories: 544, protein: 8.4, fat: 40, carbs: 34, fiber: 0, category: 'carbs (high)', alkaline: false } }, { name: 'Barra de Proteína Vegana Growth', info: { calories: 400, protein: 25, fat: 22, carbs: 25, fiber: 8, category: 'carbs (high)', alkaline: false } }, @@ -59,6 +69,9 @@ export const foodDatabase: FoodItem[] = [ { name: 'Mel Silvestre', info: { calories: 230, protein: 0, fat: 0, carbs: 65, fiber: 0, category: 'carbs (high)', alkaline: false } }, { name: 'Melado', info: { calories: 297, protein: 0, fat: 0, carbs: 76.6, fiber: 0, category: 'carbs (high)', alkaline: false } }, { name: 'Tâmara desidratada', info: { calories: 230, protein: 0.3, fat: 33, carbs: 63.3, fiber: 7, category: 'carbs (high)', alkaline: false } }, + { name: 'Pão, de queijo, assado', info: { calories: 363, protein: 5.1, fat: 24.6, carbs: 34.2, fiber: 0.6, category: 'carbs (high)', alkaline: false } }, + { name: 'Cuscuz, de milho , cozido com sal', info: { calories: 113, protein: 2.2, fat: 0.7, carbs: 25.3, fiber: 2.1, category: 'carbs (high)', alkaline: false } }, + { name: 'Paçoca, amendoim', info: { calories: 487, protein: 16, fat: 26.1, carbs: 52.4, fiber: 7.3, category: 'carbs (high)', alkaline: false } }, // Folhas e verduras - Leaves { name: 'Agrião', info: { calories: 17, protein: 2.7, fat: 0.2, carbs: 2.3, fiber: 2.1, category: 'leaves', alkaline: true } }, @@ -75,6 +88,22 @@ export const foodDatabase: FoodItem[] = [ { name: 'Melão', info: { calories: 29, protein: 0.7, fat: 0, carbs: 7.5, fiber: 0.3, category: 'fruits', alkaline: true } }, { name: 'Uva', info: { calories: 53, protein: 0.7, fat: 0.2, carbs: 13.6, fiber: 0.9, category: 'fruits', alkaline: true } }, { name: 'Goiaba, branca, com casca, crua', info: { calories: 52, protein: 0.9, fat: 0.5, carbs: 12.4, fiber: 6.3, category: 'fruits', alkaline: true } }, + { name: 'Morango, cru', info: { calories: 30, protein: 0.9, fat: 0.3, carbs: 6.8, fiber: 1.7, category: 'fruits', alkaline: true } }, + + // Dairy + { name: 'Iogurte, natural, desnatado', info: { calories: 41, protein: 3.8, fat: 0.3, carbs: 5.8, fiber: 0, category: 'dairy', alkaline: true } }, + { name: 'Queijo, mozarela, mussarela, muçarela', info: { calories: 330, protein: 22.6, fat: 25.2, carbs: 3, fiber: 0, category: 'dairy', alkaline: true } }, + { name: 'Queijo, parmesão', info: { calories: 453, protein: 35.6, fat: 33.5, carbs: 1.7, fiber: 0, category: 'dairy', alkaline: true } }, + { name: 'Queijo, prato', info: { calories: 360, protein: 22.7, fat: 29.1, carbs: 1.9, fiber: 0, category: 'dairy', alkaline: true } }, + { name: 'Queijo, requeijão, cremoso', info: { calories: 257, protein: 9.6, fat: 23.4, carbs: 2.4, fiber: 0, category: 'dairy', alkaline: true } }, + { name: 'Requeijão light (Tirol)', info: { calories: 177, protein: 11, fat: 13.3, carbs: 3.3, fiber: 0, category: 'dairy', alkaline: true } }, + { name: 'Queijo, ricota', info: { calories: 140, protein: 12.6, fat: 8.1, carbs: 3.8, fiber: 0, category: 'dairy', alkaline: true } }, + { name: 'Creme de leite', info: { calories: 221, protein: 1.5, fat: 22.5, carbs: 4.6, fiber: 0, category: 'dairy', alkaline: true } }, + { name: 'Leite, condensado', info: { calories: 313, protein: 7.7, fat: 6.7, carbs: 57, fiber: 0, category: 'dairy', alkaline: true } }, + { name: 'Leite, de vaca, desnatado, pó', info: { calories: 362, protein: 34.7, fat: 0.9, carbs: 53, fiber: 0, category: 'dairy', alkaline: true } }, + { name: 'Leite, de vaca, desnatado, UTH (Tirol)', info: { calories: 30.5, protein: 3.15, fat: 0, carbs: 4.5, fiber: 0, category: 'dairy', alkaline: true } }, + { name: 'Leite, de vaca, integral, UTH (Tirol)', info: { calories: 57.5, protein: 3.15, fat: 3, carbs: 4.5, fiber: 0, category: 'dairy', alkaline: true } }, + { name: 'Leite, de vaca, integral, pó', info: { calories: 496, protein: 26, fat: 26.4, carbs: 38.4, fiber: 0, category: 'dairy', alkaline: true } }, // Low carb { name: 'Abóbrinha, italinaa, cozida', info: { calories: 15, protein: 1.1, fat: 0.2, carbs: 3.0, fiber: 1.6, category: 'carbs (low)', alkaline: true } }, @@ -89,5 +118,6 @@ export const foodDatabase: FoodItem[] = [ { name: 'Couve flor, cozida', info: { calories: 19, protein: 1.2, fat: 0.3, carbs: 3.9, fiber: 2.1, category: 'carbs (low)', alkaline: true } }, { name: 'Pepino', info: { calories: 10, protein: 0.9, fat: 0, carbs: 2, fiber: 1.1, category: 'carbs (low)', alkaline: true } }, { name: 'Chuchu, cozido', info: { calories: 19, protein: 0.4, fat: 0, carbs: 4.9, fiber: 1.0, category: 'carbs (low)', alkaline: true } }, - { name: 'Cogumelo Paris, in natura', info: { calories: 17, protein: 4.9, fat: 0, carbs: 1, fiber: 1.7, category: 'proteins', alkaline: true } }, + { name: 'Cogumelo Paris, in natura', info: { calories: 17, protein: 4.9, fat: 0, carbs: 1, fiber: 1.7, category: 'carbs (low)', alkaline: true } }, + { name: 'Milho, verde, enlatado, drenado', info: { calories: 98, protein: 3.2, fat: 2.4, carbs: 17.1, fiber: 4.6, category: 'carbs (low)', alkaline: true } }, ]; diff --git a/src/index.ts b/src/index.ts index 9a1efc6..749caf5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import { getButtonById, getButtonListByClassName, getDivById, getInputById, show import { FoodItem, FoodStorage } from './types.js'; import { AppwriteAuth, AppwriteDB } from './appwrite.js'; import swal from 'sweetalert'; +import { Models } from 'appwrite'; // App state let selectedDate = new Date(); @@ -11,6 +12,9 @@ let searchTimeout: number | null = null; let selectedFood: FoodItem | null = null; // review here let currentHighlightIndex: number = -1; let currentResults: FoodItem[] = []; +let totalCaloriesToday: number = 0; +let totalGramsToday: number = 0; +let totalAlkalineToday: number = 0; // Authentication state let currentUser: any | null = null; @@ -281,13 +285,31 @@ function hideLoading() { loadingOverlay.classList.add('hidden'); } +function clearEditing() { + searchInput.value = ''; + getInputById('gramAmount').value = '100'; + showFoodPreview(false); + getButtonById('add-food-btn').innerHTML = 'Add Food'; + selectedFood = null; +} + // Clear application data function clearAppData() { - getDivById('foodTableBody').innerHTML = ''; getDivById('caloriesCounter').textContent = '0'; getInputById('foodSearchInput').value = ''; getInputById('gramAmount').value = '100'; + getDivById('foodCardsContainer').innerHTML = ` +
+
🍽️
+
No food items logged yet.
+
Add your first item to get started!
+
+ `; selectedFood = null; + totalCaloriesToday = 0; + totalGramsToday = 0; + totalAlkalineToday = 0; + getButtonById('cancel-food-btn').style.display = 'none'; } const getFoodData = (grams: number, foodData: FoodItem): FoodItem => { @@ -309,7 +331,7 @@ const getFoodData = (grams: number, foodData: FoodItem): FoodItem => { const getFoodItemByName = (foodName: string): FoodItem => { const foodDataSearch: FoodItem[] = foodDatabase.filter(f => f.name === foodName); if (foodDataSearch.length === 0) { - throw new Error(`Food not found for name ${name}`); + throw new Error(`Food not found for name ${foodName}`); } return foodDataSearch[0]; } @@ -353,7 +375,11 @@ const setupEventListeners = () => { }); getButtonById('add-food-btn').addEventListener('click', () => { - addFood(); + if (getButtonById('add-food-btn').innerHTML === 'Add Food') { + addFood(); + } else { + updateFood(); + } }); getButtonById('next-month-btn').addEventListener('click', () => { @@ -375,6 +401,10 @@ const setupEventListeners = () => { toggleAuthForms(); }); + getButtonById('cancel-food-btn').addEventListener('click', () => { + clearEditing(); + }); + // Auth forms document.getElementById('loginForm')?.addEventListener('submit', handleLogin); document.getElementById('registerForm')?.addEventListener('submit', handleRegister); @@ -473,12 +503,13 @@ function displaySearchResults(results: FoodItem[]) { searchResults.innerHTML = '
No foods found. Try a different search term.
'; } else { const icons = { - 'fats': '🥑', - 'proteins': '🫘', - 'carbs (high)': '🍞', - 'leaves': '🥬', - 'fruits': '🍊', - 'carbs (low)': '🍍' + 'fats': '🥑 🍳 🍟', + 'proteins': '🫘 🥩 🥚', + 'carbs (high)': '🍞 🥔 🍠', + 'leaves': '🥬 🥗 🌿', + 'fruits': '🍊 🍇 🍎', + 'carbs (low)': '🥦 🍅 🍓', + 'dairy': '🧀 🧈 🥛' }; searchResults.innerHTML = results.map((food, index) => ` @@ -531,23 +562,101 @@ function selectFood(food: FoodItem) { previewCalories(food); } +async function setFoodToEdit(foodId: string) { + showLoading(); + + try { + // get food item + const entries = await AppwriteDB.getFoodEntries(selectedDate); + const foodToEdit = entries.filter(entry => entry.$id === foodId); + if (foodToEdit.length === 0) { + swal('Hey!', 'Unable to get food entry from server', 'error'); + return; + } + + // set the selected food name and quantity + const foodData: FoodItem = getFoodItemByName(foodToEdit[0].name); + selectedFood = foodData; + searchInput.value = foodToEdit[0].name; + getInputById('gramAmount').value = foodToEdit[0].grams; + previewCalories(foodData) + + getButtonById('add-food-btn').innerHTML = 'Save Food'; + getInputById('foodIdToUpdate').value = foodId; + getInputById('foodTimeToUpdate').value = foodToEdit[0].time; + + hideLoading(); + } catch (error) { + console.error('Set food to edit error:', error); + swal('Oh no!', 'Failed to load food entry: ' + error.message, 'error'); + } finally { + hideLoading(); + } +} + // Hide search results function hideSearchResults() { searchResults.classList.remove('show'); currentHighlightIndex = -1; } +function toggleCardHandler(e: Event) { + e.preventDefault(); + e.stopPropagation(); + + const card = e.currentTarget as HTMLElement; + card.classList.toggle('expanded'); + + const cardDetails = card.nextElementSibling as HTMLElement; + if (cardDetails && cardDetails.classList.contains('card-details')) { + cardDetails.classList.toggle('expanded'); + } +} + +function deleteCardHandler(e: Event) { + e.preventDefault(); + e.stopPropagation(); + + const card = e.currentTarget as HTMLElement; + const foodId = card.getAttribute('data-food-id'); + + if (foodId) { + handleDeleteFood(foodId); + } +} + +function editCardHandler(e: Event) { + e.preventDefault(); + e.stopPropagation(); + + const card = e.currentTarget as HTMLElement; + const foodId = card.getAttribute('data-food-id'); + + if (foodId) { + setFoodToEdit(foodId); + } +} + const addDeleteEvents = () => { - const elements = getButtonListByClassName('delete-food-entry'); - Array.from(elements).forEach((el: HTMLButtonElement) => { - el.addEventListener('click', (e) => { - const target = e.target as HTMLElement; - const row = target.closest('tr'); - const entryId = row?.querySelector('.hidden-column')?.textContent; - if (entryId && row) { - handleDeleteFood(entryId, row); - } - }); + // Setup toggle show cards events + const cards = getButtonListByClassName('card-header-toggle'); + Array.from(cards).forEach((card: HTMLElement) => { + card.removeEventListener('click', toggleCardHandler); + card.addEventListener('click', toggleCardHandler); + }); + + // Setup delete card events + const deleteCards = getButtonListByClassName('btn-delete'); + Array.from(deleteCards).forEach((card: HTMLElement) => { + card.removeEventListener('click', deleteCardHandler); + card.addEventListener('click', deleteCardHandler); + }); + + // Setup edit card events + const editCards = getButtonListByClassName('btn-edit'); + Array.from(editCards).forEach((card: HTMLElement) => { + card.removeEventListener('click', editCardHandler); + card.addEventListener('click', editCardHandler); }); } @@ -562,6 +671,56 @@ const updateCurrentDate = () => { }); } +const updateFood = async () => { + if (!currentUser) { + swal('Hey!', 'Please log in to add food entries.', 'info'); + return; + } + + const gramAmount = getInputById('gramAmount'); + + if (!selectedFood || !gramAmount.value) { + swal('Hey!', 'Please select a food item and enter the amount', 'error'); + return; + } + + showLoading(); + + try { + const foodId = getInputById('foodIdToUpdate').value; + const grams: number = parseFloat(gramAmount.value); + const foodData: FoodItem = getFoodItemByName(selectedFood.name); + + // Current date, with timezone + const date = new Date(selectedDate.getTime() - (selectedDate.getTimezoneOffset() * 60000)).toISOString(); + + // Calculate nutrition for the amount + const proportion: FoodItem = getFoodData(grams, foodData); + const entry: FoodStorage = { + name: selectedFood.name, + grams: grams, + calories: proportion.info.calories, + protein: proportion.info.protein, + fat: proportion.info.fat, + carbs: proportion.info.carbs, + fiber: proportion.info.fiber, + date: date.split('T')[0], + alkaline: selectedFood.info.alkaline, + time: getInputById('foodTimeToUpdate').value, + }; + + // Save to Appwrite + await AppwriteDB.updateFoodEntry(foodId, entry); + clearEditing(); + selectDate(selectedDate); + } catch (error) { + console.error('Update food error:', error); + swal('Oh no!', 'Failed to update food entry: ' + error.message, 'error'); + } finally { + hideLoading(); + } +} + // Add food to the log const addFood = async () => { if (!currentUser) { @@ -572,7 +731,7 @@ const addFood = async () => { const gramAmount = getInputById('gramAmount'); if (!selectedFood || !gramAmount.value) { - swal('Hey!', 'Please select a food item and enter amount', 'error'); + swal('Hey!', 'Please select a food item and enter the amount', 'error'); return; } @@ -611,7 +770,7 @@ const addFood = async () => { addFoodToTable(entry, savedEntry.$id); // Update totals - updateTotalCalories(); + updateTotalCalories([savedEntry]); // Update total macros const proteinDiv: HTMLElement = getDivById('proteinValue'); @@ -655,7 +814,7 @@ async function loadFoodEntries(date: Date) { const entries = await AppwriteDB.getFoodEntries(date); // Clear current table - getDivById('foodTableBody').innerHTML = ''; + getDivById('foodCardsContainer').innerHTML = ''; // Add each entry to table entries.forEach(entry => { @@ -676,11 +835,21 @@ async function loadFoodEntries(date: Date) { addFoodToTable(foodData, entry.$id); }); + if (entries.length === 0) { + getDivById('foodCardsContainer').innerHTML = ` +
+
🍽️
+
No food items logged yet.
+
Add your first item to get started!
+
+ `; + } + // Setup Delete events addDeleteEvents(); // Update total calories - updateTotalCalories(); + updateTotalCalories(entries); // Update counters let totalCalories = 0; @@ -751,7 +920,7 @@ async function loadFoodEntries(date: Date) { } // Handle food deletion -async function handleDeleteFood(documentId: string, row: HTMLTableRowElement) { +async function handleDeleteFood(documentId: string) { if (!documentId) return; const willDelete = await swal({ @@ -772,7 +941,6 @@ async function handleDeleteFood(documentId: string, row: HTMLTableRowElement) { await AppwriteDB.deleteFoodEntry(documentId); selectDate(selectedDate); - } catch (error) { console.error('Delete food error:', error); swal('Oh no!', 'Failed to delete food entry: ' + error.message, 'error'); @@ -783,55 +951,87 @@ async function handleDeleteFood(documentId: string, row: HTMLTableRowElement) { // Add food to table (existing function - update to use document ID) function addFoodToTable(foodData: FoodStorage, documentId: string) { - const tableBody = document.getElementById('foodTableBody'); - const row = document.createElement('tr'); - - row.innerHTML = ` - ${documentId} - ${foodData.time || new Date().toLocaleTimeString()} - ${foodData.name} - ${foodData.grams} - ${foodData.calories} - ${foodData.protein} - ${foodData.fat} - ${foodData.carbs} - ${foodData.fiber} - - ${foodData.alkaline} + // New Food card + const container = getDivById('foodCardsContainer'); + const card = document.createElement('div'); + card.className = 'food-card'; + card.setAttribute('data-id', documentId); + + if (container.innerHTML.includes('empty-state')) { + container.innerHTML = ''; + } + + card.innerHTML = ` +
+
+
+
${foodData.name}
+
${foodData.time || new Date().toLocaleTimeString()} • ${foodData.grams}g
+
+
${foodData.calories} cal
+
+
+
+
+
+
+
+
Protein
+
${foodData.protein}g
+
+
+
Fat
+
${foodData.fat}g
+
+
+
Carbs
+
${foodData.carbs}g
+
+
+
Fiber
+
${foodData.fiber}g
+
+
+
+ + +
+
+
`; - - tableBody?.appendChild(row); + + container?.appendChild(card); // Setup Delete events addDeleteEvents(); } // Update total calories -function updateTotalCalories() { - const caloriesCells = document.querySelectorAll('#foodTableBody td:nth-child(5)'); // 5th column is calories - let total = 0; - - caloriesCells.forEach((cell: Element) => { - total += parseInt(cell.innerHTML) || 0; +function updateTotalCalories(entries: Models.DefaultDocument[]) { + let total = totalCaloriesToday; + + entries.forEach((entry: Models.DefaultDocument) => { + total += entry.calories || 0; }); - + getDivById('caloriesCounter').textContent = total.toString(); + totalCaloriesToday = total; + + // FIX ME: alkaline count // Update alkaline level - const gramsCell = document.querySelectorAll('#foodTableBody td:nth-child(4)'); // 4th is grams - let totalGrams = 0; + let totalGrams = totalGramsToday; let indexMap: {index: number, grams: number}[] = []; - gramsCell.forEach((cell: Element, key: number) => { - totalGrams += parseInt(cell.innerHTML) || 0; + entries.forEach((entry: Models.DefaultDocument, key: number) => { + totalGrams += entry.grams || 0; indexMap.push({ index: key, - grams: parseInt(cell.innerHTML) || 0 + grams: entry.grams || 0 }); }); - const alkalineCell = document.querySelectorAll('#foodTableBody td:nth-child(11)'); // 11th is alkaline - let totalAlkaline = 0; - alkalineCell.forEach((cell: Element, key: number) => { - if (cell.innerHTML === 'true') { + let totalAlkaline = totalAlkalineToday; + entries.forEach((entry: Models.DefaultDocument, key: number) => { + if (entry.alkaline) { for (let ob of indexMap) { if (ob.index === key) { totalAlkaline += ob.grams; @@ -858,6 +1058,9 @@ const nextMonth = () => { const selectDate = async (date: Date) => { selectedDate = date; + totalCaloriesToday = 0; + totalGramsToday = 0; + totalAlkalineToday = 0; updateCurrentDate(); await loadFoodEntries(date); renderCalendar(); @@ -970,4 +1173,17 @@ function createDayElement(day: number, isOtherMonth: boolean, year: number, mont return dayElement; } +function toggleCard(id: string) { + const card = document.querySelector(`[data-id="${id}"]`); + card?.classList.toggle('expanded'); +} + +function deleteFood(id: string) { + handleDeleteFood(id); +} + +function editFood(id: string) { + alert(`Edit functionality for food item ${id} would be implemented here`); +} + (window as any).selectFood = selectFood; diff --git a/src/types.ts b/src/types.ts index 0722cf0..1fe51b6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -type FoodCategory = 'fats' | 'proteins' | 'carbs (high)' | 'leaves' | 'fruits' | 'carbs (low)'; +type FoodCategory = 'fats' | 'proteins' | 'carbs (high)' | 'leaves' | 'fruits' | 'carbs (low)' | 'dairy'; export type FoodItem = { name: string; diff --git a/style.css b/style.css index 0c69910..bb2345c 100644 --- a/style.css +++ b/style.css @@ -167,11 +167,204 @@ h1 { background-color: #f8f9ff; } +.food-log-section { + max-width: 800px; + margin: 0 auto; +} -.food-name { +.section-title { + font-size: 24px; font-weight: 600; color: #333; - margin-bottom: 2px; + margin-bottom: 20px; + text-align: center; +} + +.food-cards-container { + display: flex; + flex-direction: column; + gap: 12px; +} + +.food-card { + background: white; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + overflow: hidden; + transition: all 0.3s ease; +} + +.food-card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.card-header { + padding: 16px; + cursor: pointer; + display: flex; + justify-content: between; + align-items: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + position: relative; +} + +.card-header:hover { + background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%); +} + +.card-main-info { + flex: 1; + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; +} + +.food-name-time { + flex: 1; +} + +.food-name { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 4px; +} + +.card-header .food-name { + color: #fff; +} + +.food-time { + font-size: 13px; + opacity: 0.9; +} + +.calories-display { + font-size: 18px; + font-weight: 700; + color: #fff; + text-align: right; +} + +.expand-icon { + margin-left: 12px; + font-size: 14px; + transition: transform 0.3s ease; + opacity: 0.8; +} + +.card-details { + display: none; +} + +.card-details.expanded { + display: block; +} + +.card-header-toggle .expand-icon { + transition: transform 0.3s ease; +} + +.card-header-toggle.expanded .expand-icon { + transform: rotate(180deg); +} + +.details-content { + padding: 20px; +} + +.nutrition-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 16px; + margin-bottom: 20px; +} + +.nutrition-item { + text-align: center; + padding: 12px; + background: white; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.nutrition-label { + font-size: 12px; + color: #666; + letter-spacing: 0.5px; + margin-bottom: 4px; +} + +.nutrition-value { + font-size: 16px; + font-weight: 600; + color: #333; +} + +.card-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + padding-top: 16px; + border-top: 1px solid #e0e0e0; +} + +.btn { + padding: 8px 16px; + border: none; + border-radius: 6px; + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-delete { + background: #ff4757; + color: white; +} + +.btn-delete:hover { + background: #ff3838; +} + +.btn-edit { + background: #5352ed; + color: white; +} + +.btn-edit:hover { + background: #4834d4; +} + +.empty-state { + text-align: center; + padding: 60px 20px; + color: #666; +} + +.empty-state-icon { + font-size: 48px; + margin-bottom: 16px; + opacity: 0.5; +} + +.add-food-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + margin-bottom: 20px; + transition: all 0.2s ease; +} + +.add-food-btn:hover { + background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%); + transform: translateY(-1px); } .food-details { @@ -209,7 +402,7 @@ h1 { .add-food-form { display: grid; - grid-template-columns: 2fr 1fr auto; + grid-template-columns: 2fr 1fr 1fr auto; gap: 15px; align-items: end; } @@ -274,6 +467,24 @@ label { box-shadow: 0 5px 15px rgba(76, 175, 80, 0.3); } +.cancel-btn { + padding: 17px 25px; + background: linear-gradient(135deg, #757575, #616161); + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 1rem; + font-weight: 600; + transition: all 0.3s ease; + height: fit-content; +} + +.cancel-btn:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(76, 175, 80, 0.3); +} + .food-table-section { background: white; border-radius: 6px; @@ -341,6 +552,7 @@ label { border-radius: 6px; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1); overflow: hidden; + margin-top: 30px; } .calendar-header { @@ -522,4 +734,25 @@ label { h1 { font-size: 2rem; } + + .card-header { + padding: 14px; + } + + .food-name { + font-size: 15px; + } + + .calories-display { + font-size: 16px; + } + + .nutrition-grid { + grid-template-columns: repeat(2, 1fr); + gap: 12px; + } + + .details-content { + padding: 16px; + } }