feat: replace the food table by a better ui

This commit is contained in:
2025-09-03 21:03:46 -03:00
parent 5b62fd989f
commit 71564b1c80
5 changed files with 138 additions and 109 deletions
+4 -27
View File
@@ -183,33 +183,10 @@
</div> </div>
</div> </div>
<div class="food-table-section"> <div class="section-title">Food Log</div>
<div class="table-title">Food Log</div>
<div class="table-wrapper"> <div class="food-cards-container" id="foodCardsContainer">
<table class="food-table"> <!-- Food cards will be added here -->
<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="food-cards-container" id="foodCardsContainer">
<!-- Food cards will be added here -->
</div>
</div> </div>
<div class="calendar-section"> <div class="calendar-section">
+8 -8
View File
@@ -9,12 +9,12 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"appwrite": "^18.2.0", "appwrite": "^19.0.0",
"sweetalert": "^2.1.2" "sweetalert": "^2.1.2"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.9.2", "typescript": "^5.9.2",
"vite": "^7.1.3" "vite": "^7.1.4"
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
@@ -747,9 +747,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/appwrite": { "node_modules/appwrite": {
"version": "18.2.0", "version": "19.0.0",
"resolved": "https://registry.npmjs.org/appwrite/-/appwrite-18.2.0.tgz", "resolved": "https://registry.npmjs.org/appwrite/-/appwrite-19.0.0.tgz",
"integrity": "sha512-g7pQpsxqR7+amEIaQLXMN4XzdQKenTHnGdA4s7UUJdZufhlHdJby8895h8z893+S0XipeHZhi0wpxYA2An95Rg==", "integrity": "sha512-guamzST6Fvn/lvuBMLJGzJGL+N+o313j53qyfKM1ZHfcDd1ADnSONgm9DZihRFYBaWtU7s1bwkphkBJWSUwurQ==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/es6-object-assign": { "node_modules/es6-object-assign": {
@@ -999,9 +999,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "7.1.3", "version": "7.1.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.4.tgz",
"integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==", "integrity": "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
+2 -2
View File
@@ -13,10 +13,10 @@
"description": "", "description": "",
"devDependencies": { "devDependencies": {
"typescript": "^5.9.2", "typescript": "^5.9.2",
"vite": "^7.1.3" "vite": "^7.1.4"
}, },
"dependencies": { "dependencies": {
"appwrite": "^18.2.0", "appwrite": "^19.0.0",
"sweetalert": "^2.1.2" "sweetalert": "^2.1.2"
} }
} }
+108 -61
View File
@@ -3,6 +3,7 @@ import { getButtonById, getButtonListByClassName, getDivById, getInputById, show
import { FoodItem, FoodStorage } from './types.js'; import { FoodItem, FoodStorage } from './types.js';
import { AppwriteAuth, AppwriteDB } from './appwrite.js'; import { AppwriteAuth, AppwriteDB } from './appwrite.js';
import swal from 'sweetalert'; import swal from 'sweetalert';
import { Models } from 'appwrite';
// App state // App state
let selectedDate = new Date(); let selectedDate = new Date();
@@ -11,6 +12,9 @@ let searchTimeout: number | null = null;
let selectedFood: FoodItem | null = null; // review here let selectedFood: FoodItem | null = null; // review here
let currentHighlightIndex: number = -1; let currentHighlightIndex: number = -1;
let currentResults: FoodItem[] = []; let currentResults: FoodItem[] = [];
let totalCaloriesToday: number = 0;
let totalGramsToday: number = 0;
let totalAlkalineToday: number = 0;
// Authentication state // Authentication state
let currentUser: any | null = null; let currentUser: any | null = null;
@@ -283,11 +287,20 @@ function hideLoading() {
// Clear application data // Clear application data
function clearAppData() { function clearAppData() {
getDivById('foodTableBody').innerHTML = '';
getDivById('caloriesCounter').textContent = '0'; getDivById('caloriesCounter').textContent = '0';
getInputById('foodSearchInput').value = ''; getInputById('foodSearchInput').value = '';
getInputById('gramAmount').value = '100'; 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; selectedFood = null;
totalCaloriesToday = 0;
totalGramsToday = 0;
totalAlkalineToday = 0;
} }
const getFoodData = (grams: number, foodData: FoodItem): FoodItem => { const getFoodData = (grams: number, foodData: FoodItem): FoodItem => {
@@ -537,17 +550,65 @@ function hideSearchResults() {
currentHighlightIndex = -1; 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) {
// FIXME: Implement edit functionality
// handleEditFood(foodId);
window.alert('Edit functionality is not implemented yet.');
}
}
const addDeleteEvents = () => { const addDeleteEvents = () => {
const elements = getButtonListByClassName('delete-food-entry'); // Setup toggle show cards events
Array.from(elements).forEach((el: HTMLButtonElement) => { const cards = getButtonListByClassName('card-header-toggle');
el.addEventListener('click', (e) => { Array.from(cards).forEach((card: HTMLElement) => {
const target = e.target as HTMLElement; card.removeEventListener('click', toggleCardHandler);
const row = target.closest('tr'); card.addEventListener('click', toggleCardHandler);
const entryId = row?.querySelector('.hidden-column')?.textContent; });
if (entryId && row) {
handleDeleteFood(entryId); // 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);
}); });
} }
@@ -611,7 +672,7 @@ const addFood = async () => {
addFoodToTable(entry, savedEntry.$id); addFoodToTable(entry, savedEntry.$id);
// Update totals // Update totals
updateTotalCalories(); updateTotalCalories([savedEntry]);
// Update total macros // Update total macros
const proteinDiv: HTMLElement = getDivById('proteinValue'); const proteinDiv: HTMLElement = getDivById('proteinValue');
@@ -655,7 +716,6 @@ async function loadFoodEntries(date: Date) {
const entries = await AppwriteDB.getFoodEntries(date); const entries = await AppwriteDB.getFoodEntries(date);
// Clear current table // Clear current table
getDivById('foodTableBody').innerHTML = '';
getDivById('foodCardsContainer').innerHTML = ''; getDivById('foodCardsContainer').innerHTML = '';
// Add each entry to table // Add each entry to table
@@ -691,7 +751,7 @@ async function loadFoodEntries(date: Date) {
addDeleteEvents(); addDeleteEvents();
// Update total calories // Update total calories
updateTotalCalories(); updateTotalCalories(entries);
// Update counters // Update counters
let totalCalories = 0; let totalCalories = 0;
@@ -783,7 +843,6 @@ async function handleDeleteFood(documentId: string) {
await AppwriteDB.deleteFoodEntry(documentId); await AppwriteDB.deleteFoodEntry(documentId);
selectDate(selectedDate); selectDate(selectedDate);
} catch (error) { } catch (error) {
console.error('Delete food error:', error); console.error('Delete food error:', error);
swal('Oh no!', 'Failed to delete food entry: ' + error.message, 'error'); swal('Oh no!', 'Failed to delete food entry: ' + error.message, 'error');
@@ -794,36 +853,18 @@ async function handleDeleteFood(documentId: string) {
// Add food to table (existing function - update to use document ID) // Add food to table (existing function - update to use document ID)
function addFoodToTable(foodData: FoodStorage, documentId: string) { 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>
`;
tableBody?.appendChild(row);
// Setup Delete events
addDeleteEvents();
// New Food card // New Food card
const container = document.getElementById('foodCardsContainer'); const container = getDivById('foodCardsContainer');
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'food-card'; card.className = 'food-card';
card.setAttribute('data-id', documentId); card.setAttribute('data-id', documentId);
if (container.innerHTML.includes('empty-state')) {
container.innerHTML = '';
}
card.innerHTML = ` card.innerHTML = `
<div class="card-header" onclick="toggleCard(${documentId})"> <div class="card-header card-header-toggle">
<div class="card-main-info"> <div class="card-main-info">
<div class="food-name-time"> <div class="food-name-time">
<div class="food-name">${foodData.name}</div> <div class="food-name">${foodData.name}</div>
@@ -854,42 +895,45 @@ function addFoodToTable(foodData: FoodStorage, documentId: string) {
</div> </div>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<button class="btn btn-edit" onclick="editFood(${documentId})">Edit</button> <button class="btn btn-edit" data-food-id="${documentId}">Edit</button>
<button class="btn btn-delete" onclick="deleteFood(${documentId})">Delete</button> <button class="btn btn-delete" data-food-id="${documentId}">Delete</button>
</div> </div>
</div> </div>
</div> </div>
`; `;
container?.appendChild(card); container?.appendChild(card);
// Setup Delete events
addDeleteEvents();
} }
// Update total calories // Update total calories
function updateTotalCalories() { function updateTotalCalories(entries: Models.DefaultDocument[]) {
const caloriesCells = document.querySelectorAll('#foodTableBody td:nth-child(5)'); // 5th column is calories let total = totalCaloriesToday;
let total = 0;
entries.forEach((entry: Models.DefaultDocument) => {
caloriesCells.forEach((cell: Element) => { total += entry.calories || 0;
total += parseInt(cell.innerHTML) || 0;
}); });
getDivById('caloriesCounter').textContent = total.toString(); getDivById('caloriesCounter').textContent = total.toString();
totalCaloriesToday = total;
// FIX ME: alkaline count
// Update alkaline level // Update alkaline level
const gramsCell = document.querySelectorAll('#foodTableBody td:nth-child(4)'); // 4th is grams let totalGrams = totalGramsToday;
let totalGrams = 0;
let indexMap: {index: number, grams: number}[] = []; let indexMap: {index: number, grams: number}[] = [];
gramsCell.forEach((cell: Element, key: number) => { entries.forEach((entry: Models.DefaultDocument, key: number) => {
totalGrams += parseInt(cell.innerHTML) || 0; totalGrams += entry.grams || 0;
indexMap.push({ indexMap.push({
index: key, 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 = totalAlkalineToday;
let totalAlkaline = 0; entries.forEach((entry: Models.DefaultDocument, key: number) => {
alkalineCell.forEach((cell: Element, key: number) => { if (entry.alkaline) {
if (cell.innerHTML === 'true') {
for (let ob of indexMap) { for (let ob of indexMap) {
if (ob.index === key) { if (ob.index === key) {
totalAlkaline += ob.grams; totalAlkaline += ob.grams;
@@ -916,6 +960,9 @@ const nextMonth = () => {
const selectDate = async (date: Date) => { const selectDate = async (date: Date) => {
selectedDate = date; selectedDate = date;
totalCaloriesToday = 0;
totalGramsToday = 0;
totalAlkalineToday = 0;
updateCurrentDate(); updateCurrentDate();
await loadFoodEntries(date); await loadFoodEntries(date);
renderCalendar(); renderCalendar();
@@ -1028,16 +1075,16 @@ function createDayElement(day: number, isOtherMonth: boolean, year: number, mont
return dayElement; return dayElement;
} }
function toggleCard(id) { function toggleCard(id: string) {
const card = document.querySelector(`[data-id="${id}"]`); const card = document.querySelector(`[data-id="${id}"]`);
card.classList.toggle('expanded'); card?.classList.toggle('expanded');
} }
function deleteFood(id) { function deleteFood(id: string) {
handleDeleteFood(id); handleDeleteFood(id);
} }
function editFood(id) { function editFood(id: string) {
alert(`Edit functionality for food item ${id} would be implemented here`); alert(`Edit functionality for food item ${id} would be implemented here`);
} }
+16 -11
View File
@@ -232,6 +232,10 @@ h1 {
margin-bottom: 4px; margin-bottom: 4px;
} }
.card-header .food-name {
color: #fff;
}
.food-time { .food-time {
font-size: 13px; font-size: 13px;
opacity: 0.9; opacity: 0.9;
@@ -251,19 +255,20 @@ h1 {
opacity: 0.8; opacity: 0.8;
} }
.food-card.expanded .expand-icon {
transform: rotate(180deg);
}
.card-details { .card-details {
max-height: 0; display: none;
overflow: hidden;
transition: max-height 0.3s ease;
background: #fafafa;
} }
.food-card.expanded .card-details { .card-details.expanded {
max-height: 300px; display: block;
}
.card-header-toggle .expand-icon {
transition: transform 0.3s ease;
}
.card-header-toggle.expanded .expand-icon {
transform: rotate(180deg);
} }
.details-content { .details-content {
@@ -288,7 +293,6 @@ h1 {
.nutrition-label { .nutrition-label {
font-size: 12px; font-size: 12px;
color: #666; color: #666;
text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
margin-bottom: 4px; margin-bottom: 4px;
} }
@@ -530,6 +534,7 @@ label {
border-radius: 6px; border-radius: 6px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1); box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
overflow: hidden; overflow: hidden;
margin-top: 30px;
} }
.calendar-header { .calendar-header {