Merge pull request #3 from RMCampos/feat/improve-ui

feat: improve UI
This commit is contained in:
2025-09-04 12:53:26 -03:00
committed by GitHub
9 changed files with 596 additions and 108 deletions
+9 -24
View File
@@ -149,6 +149,10 @@
<div class="search-results" id="searchResults">
<!-- Results will be populated here -->
</div>
<!-- Hidden inputs for state -->
<input type="hidden" id="foodIdToUpdate">
<input type="hidden" id="foodTimeToUpdate">
</div>
</div>
<div class="form-group">
@@ -156,6 +160,7 @@
<input type="number" id="gramAmount" min="1" step="1" value="100">
</div>
<button class="add-btn" id="add-food-btn">Add Food</button>
<button class="cancel-btn" id="cancel-food-btn">Cancel</button>
</div>
<div class="display-none" id="food-preview">
<div class="nutrition-summary">
@@ -183,30 +188,10 @@
</div>
</div>
<div class="food-table-section">
<div class="table-title">Food Log</div>
<div class="table-wrapper">
<table class="food-table">
<thead>
<tr>
<th class="hidden-column">ID</th>
<th>Time</th>
<th>Food</th>
<th>Grams</th>
<th>Calories</th>
<th>Protein (g)</th>
<th>Fat (g)</th>
<th>Carbs (g)</th>
<th>Fiber (g)</th>
<th>Action</th>
<th class="hidden-column">Alkaline</th>
</tr>
</thead>
<tbody id="foodTableBody">
<!-- Food entries will be added here -->
</tbody>
</table>
</div>
<div class="section-title">Food Log</div>
<div class="food-cards-container" id="foodCardsContainer">
<!-- Food cards will be added here -->
</div>
<div class="calendar-section">
+8 -8
View File
@@ -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": {
+2 -2
View File
@@ -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"
}
}
+4
View File
@@ -0,0 +1,4 @@
VITE_APPWRITE_ENDPOINT=here
VITE_APPWRITE_DBID=here
VITE_APPWRITE_FOODENTRIESID=here
VITE_APPWRITE_USERSETTINGSID=here
+20
View File
@@ -98,6 +98,26 @@ export class AppwriteDB {
}
}
// Upadte food entry
static async updateFoodEntry(documentId: string, entryData: Partial<FoodStorage>) {
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 {
+42 -12
View File
@@ -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 } },
];
+274 -58
View File
@@ -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 = `
<div class="empty-state">
<div class="empty-state-icon">🍽️</div>
<div>No food items logged yet.</div>
<div style="margin-top: 8px; font-size: 14px; opacity: 0.7;">Add your first item to get started!</div>
</div>
`;
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 = '<div class="no-results">No foods found. Try a different search term.</div>';
} 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 = `
<div class="empty-state">
<div class="empty-state-icon">🍽️</div>
<div>No food items logged yet.</div>
<div style="margin-top: 8px; font-size: 14px; opacity: 0.7;">Add your first item to get started!</div>
</div>
`;
}
// 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 = `
<td class="hidden-column">${documentId}</td>
<td>${foodData.time || new Date().toLocaleTimeString()}</td>
<td>${foodData.name}</td>
<td>${foodData.grams}</td>
<td>${foodData.calories}</td>
<td>${foodData.protein}</td>
<td>${foodData.fat}</td>
<td>${foodData.carbs}</td>
<td>${foodData.fiber}</td>
<td><button class="delete-btn delete-food-entry">Delete</button></td>
<td class="hidden-column">${foodData.alkaline}</td>
// 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 = `
<div class="card-header card-header-toggle">
<div class="card-main-info">
<div class="food-name-time">
<div class="food-name">${foodData.name}</div>
<div class="food-time">${foodData.time || new Date().toLocaleTimeString()}${foodData.grams}g</div>
</div>
<div class="calories-display">${foodData.calories} cal</div>
</div>
<div class="expand-icon">▼</div>
</div>
<div class="card-details">
<div class="details-content">
<div class="nutrition-grid">
<div class="nutrition-item">
<div class="nutrition-label">Protein</div>
<div class="nutrition-value">${foodData.protein}g</div>
</div>
<div class="nutrition-item">
<div class="nutrition-label">Fat</div>
<div class="nutrition-value">${foodData.fat}g</div>
</div>
<div class="nutrition-item">
<div class="nutrition-label">Carbs</div>
<div class="nutrition-value">${foodData.carbs}g</div>
</div>
<div class="nutrition-item">
<div class="nutrition-label">Fiber</div>
<div class="nutrition-value">${foodData.fiber}g</div>
</div>
</div>
<div class="card-actions">
<button class="btn btn-edit" data-food-id="${documentId}">Edit</button>
<button class="btn btn-delete" data-food-id="${documentId}">Delete</button>
</div>
</div>
</div>
`;
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;
+1 -1
View File
@@ -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;
+236 -3
View File
@@ -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;
}
}