Files
calories-tracker/src/index.ts
T

1095 lines
33 KiB
TypeScript
Raw Normal View History

2025-06-12 10:39:19 -03:00
import { foodDatabase } from './foodDatabase.js';
2025-06-12 16:06:02 -03:00
import { getButtonById, getButtonListByClassName, getDivById, getInputById, showFoodPreview } from './HtmlUtil.ts';
2025-06-12 10:39:19 -03:00
import { FoodItem, FoodStorage } from './types.js';
2025-06-12 18:30:11 -03:00
import { AppwriteAuth, AppwriteDB } from './appwrite.js';
2025-06-12 18:52:44 -03:00
import swal from 'sweetalert';
2025-09-03 21:03:46 -03:00
import { Models } from 'appwrite';
2025-06-12 10:39:19 -03:00
// App state
2025-06-12 18:30:11 -03:00
let selectedDate = new Date();
2025-06-12 10:39:19 -03:00
let currentViewDate = new Date();
2025-06-16 17:33:52 -03:00
let searchTimeout: number | null = null;
let selectedFood: FoodItem | null = null; // review here
let currentHighlightIndex: number = -1;
let currentResults: FoodItem[] = [];
2025-09-03 21:03:46 -03:00
let totalCaloriesToday: number = 0;
let totalGramsToday: number = 0;
let totalAlkalineToday: number = 0;
2025-06-12 10:39:19 -03:00
2025-06-12 18:30:11 -03:00
// Authentication state
let currentUser: any | null = null;
// DOM Elements
const authSection = document.getElementById('auth-section');
const userInfo = document.getElementById('user-info');
const appContent = document.getElementById('app-content');
2025-06-13 19:18:31 -03:00
const settingsContent = document.getElementById('settings-content');
2025-06-12 18:30:11 -03:00
const loadingOverlay = getDivById('loading-overlay');
const loginForm = document.getElementById('login-form');
const registerForm = document.getElementById('register-form');
2025-06-16 17:33:52 -03:00
const searchInput = getInputById('foodSearchInput');
const searchLoading = document.getElementById('searchLoading');
const searchResults = getDivById('searchResults');
2025-06-12 18:30:11 -03:00
// Initialize when page loads
document.addEventListener('DOMContentLoaded', async function() {
await initializeAuth();
2025-06-12 10:39:19 -03:00
updateCurrentDate();
2025-06-12 18:30:11 -03:00
setupEventListeners();
});
// Initialize application
async function initializeAuth() {
showLoading();
try {
// Check if user is already logged in
const isLoggedIn = await AppwriteAuth.isLoggedIn();
if (isLoggedIn) {
currentUser = await AppwriteAuth.getCurrentUser();
showMainApp();
selectDate(selectedDate);
} else {
showAuthForms();
}
} catch (error) {
console.error('Initialize app error:', error);
showAuthForms();
} finally {
hideLoading();
}
}
// Toggle between login and register forms
function toggleAuthForms() {
loginForm?.classList.toggle('hidden');
registerForm?.classList.toggle('hidden');
// Clear form data
const logForm = document.getElementById('loginForm') as HTMLFormElement;
logForm.reset();
const resetForm = document.getElementById('registerForm') as HTMLFormElement;
resetForm.reset();
}
// Handle user registration
async function handleRegister(e: SubmitEvent) {
e.preventDefault();
showLoading();
const name = getInputById('registerName').value;
const email = getInputById('registerEmail').value;
const password = getInputById('registerPassword').value;
try {
await AppwriteAuth.register(email, password, name);
// Auto login after registration
await AppwriteAuth.login(email, password);
currentUser = await AppwriteAuth.getCurrentUser();
showMainApp();
selectDate(selectedDate);
2025-06-12 18:52:44 -03:00
swal('Registration successful! Welcome to Food Tracker.');
2025-06-12 18:30:11 -03:00
} catch (error) {
console.error('Registration error:', error);
2025-06-12 18:52:44 -03:00
swal('Oh no!', 'Registration failed: ' + error.message, 'error');
2025-06-12 18:30:11 -03:00
} finally {
hideLoading();
}
}
// Handle user login
async function handleLogin(e: SubmitEvent) {
e.preventDefault();
showLoading();
const email = getInputById('loginEmail').value;
const password = getInputById('loginPassword').value;
try {
await AppwriteAuth.login(email, password);
currentUser = await AppwriteAuth.getCurrentUser();
showMainApp();
selectDate(selectedDate);
} catch (error) {
console.error('Login error:', error);
2025-06-12 18:52:44 -03:00
swal('Oh no!', 'Login failed: ' + error.message, 'error');
2025-06-12 18:30:11 -03:00
} finally {
hideLoading();
}
}
2025-06-13 19:18:31 -03:00
async function processResponseItems<T, R>(
response: T[],
processItem: (item: T) => Promise<R>
): Promise<R[]> {
// Create promises for each item (but don't execute yet)
const promises = response.map(item => processItem(item));
// Execute all promises concurrently and wait for completion
const results = await Promise.all(promises);
return results;
}
2025-06-13 21:03:36 -03:00
async function fetchUserSettings(documentId: string): Promise<any> {
return AppwriteDB.deleteUserSettings(documentId);
2025-06-13 19:18:31 -03:00
}
// Main function that processes the response
2025-06-13 21:03:36 -03:00
async function handleBulkDelete(idsToDelete: string[]): Promise<void> {
2025-06-13 19:18:31 -03:00
try {
// This creates and executes all promises concurrently
2025-06-13 21:03:36 -03:00
const results = await processResponseItems(idsToDelete, fetchUserSettings);
2025-06-13 19:18:31 -03:00
2025-06-13 21:03:36 -03:00
console.debug('All requests completed:', results);
2025-06-13 19:18:31 -03:00
} catch (error) {
console.error('One or more requests failed:', error);
}
}
async function handleSaveSettings(e: SubmitEvent) {
e.preventDefault();
showLoading();
try {
2025-06-23 18:24:37 -03:00
const caloriesGoal = getInputById('caloriesGoal').value;
2025-06-13 19:18:31 -03:00
const proteinGoal = getInputById('proteinGoal').value;
const fatGoal = getInputById('fatGoal').value;
const carboGoal = getInputById('carboGoal').value;
const fiberGoal = getInputById('fiberGoal').value;
// Delete existing settings - keep only the new one
const documents = await AppwriteDB.getUserSettings();
2025-06-13 21:03:36 -03:00
const documentsToDelete: string[] = [];
2025-06-13 19:18:31 -03:00
for (let i=0; i<documents.length; i++) {
2025-06-13 21:03:36 -03:00
documentsToDelete.push(documents[i].$id);
}
if (documentsToDelete) {
await handleBulkDelete(documentsToDelete);
2025-06-13 19:18:31 -03:00
}
// Save to Appwrite
await AppwriteDB.saveUserSettings({
2025-06-23 18:24:37 -03:00
caloriesGoal: parseInt(caloriesGoal),
2025-06-13 19:18:31 -03:00
proteinGoal: parseInt(proteinGoal),
fatGoal: parseInt(fatGoal),
carboGoal: parseInt(carboGoal),
fiberGoal: parseInt(fiberGoal),
});
hideLoading();
handleSettings();
selectDate(selectedDate);
} catch (error) {
hideLoading();
console.error('Saving error:', error);
swal('Oh no!', 'Saving failed: ' + error.message, 'error');
}
}
2025-06-12 18:30:11 -03:00
async function handleLogout() {
showLoading();
try {
await AppwriteAuth.logout();
currentUser = null;
showAuthForms();
clearAppData();
2025-06-13 19:18:31 -03:00
hideLoading();
2025-06-12 18:30:11 -03:00
} catch (error) {
2025-06-13 19:18:31 -03:00
hideLoading();
2025-06-12 18:30:11 -03:00
console.error('Logout error:', error);
2025-06-12 18:52:44 -03:00
swal('Oh no!', 'Logout failed: ' + error.message, 'error');
2025-06-13 19:18:31 -03:00
}
}
async function handleSettings() {
const showSettings = getButtonById('settingsBtn').textContent === 'Settings';
if (showSettings) {
showLoading();
try {
// Fetch goals from settings
const settings = await AppwriteDB.getUserSettings();
const hasGoals = Array.isArray(settings) && settings.length > 0;
if (hasGoals) {
const index = settings.length - 1;
2025-06-23 18:24:37 -03:00
getInputById('caloriesGoal').value = settings[index].caloriesGoal;
2025-06-13 19:18:31 -03:00
getInputById('proteinGoal').value = settings[index].proteinGoal;
getInputById('fatGoal').value = settings[index].fatGoal;
getInputById('carboGoal').value = settings[index].carboGoal;
getInputById('fiberGoal').value = settings[index].fiberGoal;
} else {
2025-06-23 18:24:37 -03:00
getInputById('caloriesGoal').value = '';
2025-06-13 19:18:31 -03:00
getInputById('proteinGoal').value = '';
getInputById('fatGoal').value = '';
getInputById('carboGoal').value = '';
getInputById('fiberGoal').value = '';
}
2025-06-16 17:33:52 -03:00
getButtonById('settingsBtn').textContent = 'Back';
2025-06-13 19:18:31 -03:00
appContent?.classList.add('hidden');
settingsContent?.classList.remove('hidden');
hideLoading();
} catch (error) {
hideLoading();
console.error('Fetching settings failed:', error);
swal('Oh no!', 'Fetching settings failed: ' + error.message, 'error');
}
}
else {
appContent?.classList.remove('hidden');
settingsContent?.classList.add('hidden');
getButtonById('settingsBtn').textContent = 'Settings';
2025-06-12 18:30:11 -03:00
}
}
// Show authentication forms
function showAuthForms() {
authSection?.classList.remove('hidden');
userInfo?.classList.add('hidden');
appContent?.classList.add('hidden');
}
function showMainApp() {
authSection?.classList.add('hidden');
userInfo?.classList.remove('hidden');
appContent?.classList.remove('hidden');
// Update user info display
if (currentUser) {
getDivById('userName').textContent = `Welcome, ${currentUser.name}!`;
}
// Update date display
updateCurrentDate();
}
// Show/hide loading overlay
function showLoading() {
loadingOverlay.classList.remove('hidden');
}
function hideLoading() {
loadingOverlay.classList.add('hidden');
}
// Clear application data
function clearAppData() {
getDivById('caloriesCounter').textContent = '0';
2025-06-16 17:33:52 -03:00
getInputById('foodSearchInput').value = '';
2025-06-12 18:30:11 -03:00
getInputById('gramAmount').value = '100';
2025-09-03 21:03:46 -03:00
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>
`;
2025-06-16 17:33:52 -03:00
selectedFood = null;
2025-09-03 21:03:46 -03:00
totalCaloriesToday = 0;
totalGramsToday = 0;
totalAlkalineToday = 0;
2025-06-12 10:39:19 -03:00
}
2025-06-12 18:30:11 -03:00
const getFoodData = (grams: number, foodData: FoodItem): FoodItem => {
2025-06-12 10:39:19 -03:00
const multiplier = grams / 100; // Database values are per 100g
return {
...foodData,
info: {
calories: Math.round(foodData.info.calories * multiplier),
protein: Math.round(foodData.info.protein * multiplier * 10) / 10,
fat: Math.round(foodData.info.fat * multiplier * 10) / 10,
carbs: Math.round(foodData.info.carbs * multiplier * 10) / 10,
2025-06-16 17:33:52 -03:00
fiber: Math.round(foodData.info.fiber * multiplier * 10) / 10,
2025-06-18 16:56:11 -03:00
category: foodData.info.category,
alkaline: foodData.info.alkaline
2025-06-12 10:39:19 -03:00
}
};
}
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}`);
}
return foodDataSearch[0];
}
2025-06-16 17:33:52 -03:00
const previewCalories = (foodSelected?: FoodItem) => {
const foodToPreview = foodSelected ?? selectedFood;
2025-06-12 10:39:19 -03:00
const gramAmount = getInputById('gramAmount');
2025-06-16 17:33:52 -03:00
if (!foodToPreview) {
2025-06-12 10:39:19 -03:00
showFoodPreview(false);
return;
}
let grams = 100;
if (gramAmount && gramAmount.value && parseFloat(gramAmount.value) > 0) {
grams = parseFloat(gramAmount.value);
}
2025-06-16 17:33:52 -03:00
const foodData: FoodItem = getFoodItemByName(foodToPreview.name);
2025-06-12 18:30:11 -03:00
const proportion = getFoodData(grams, foodData);
2025-06-12 10:39:19 -03:00
2025-06-12 11:02:03 -03:00
getDivById('calorieValuePreview').textContent = proportion.info.calories.toString();
getDivById('proteinValuePreview').textContent = (Math.round(proportion.info.protein * 10) / 10).toString ();
getDivById('fatValuePreview').textContent = (Math.round(proportion.info.fat * 10) / 10).toString ();
getDivById('carboValuePreview').textContent = (Math.round(proportion.info.carbs * 10) / 10).toString ();
getDivById('fiberValuePreview').textContent = (Math.round(proportion.info.fiber * 10) / 10).toString ();
2025-06-12 10:39:19 -03:00
showFoodPreview(true);
}
2025-06-12 18:30:11 -03:00
const setupEventListeners = () => {
2025-06-16 17:33:52 -03:00
getInputById('foodSearchInput').addEventListener('change', (e: Event) => {
const target = e.target as HTMLInputElement;
if (target.value === ''){
showFoodPreview(false);
}
2025-06-12 10:39:19 -03:00
});
2025-06-12 11:02:03 -03:00
getInputById('gramAmount').addEventListener('change', () => {
2025-06-12 10:39:19 -03:00
previewCalories();
});
getButtonById('add-food-btn').addEventListener('click', () => {
addFood();
});
getButtonById('next-month-btn').addEventListener('click', () => {
nextMonth();
});
getButtonById('previous-month-btn').addEventListener('click', () => {
previousMonth();
});
2025-06-12 18:30:11 -03:00
// Form switching
document.getElementById('showRegister')?.addEventListener('click', (e) => {
e.preventDefault();
toggleAuthForms();
});
document.getElementById('showLogin')?.addEventListener('click', (e) => {
e.preventDefault();
toggleAuthForms();
});
// Auth forms
document.getElementById('loginForm')?.addEventListener('submit', handleLogin);
document.getElementById('registerForm')?.addEventListener('submit', handleRegister);
document.getElementById('logoutBtn')?.addEventListener('click', handleLogout);
2025-06-13 19:18:31 -03:00
document.getElementById('settingsBtn')?.addEventListener('click', handleSettings);
document.getElementById('settingsForm')?.addEventListener('submit', handleSaveSettings);
2025-06-16 17:33:52 -03:00
// Search functionality
searchInput.addEventListener('input', function(e: Event) {
const target = e.target as HTMLInputElement;
const query = target.value.trim();
if (query.length === 0) {
hideSearchResults();
return;
}
// Show loading
searchLoading?.classList.add('show');
// Clear previous timeout
if (searchTimeout) {
clearTimeout(searchTimeout);
}
// Debounce search
searchTimeout = setTimeout(() => {
performSearch(query);
}, 300);
});
// Keyboard navigation
searchInput?.addEventListener('keydown', function(e) {
if (!searchResults?.classList.contains('show')) return;
switch(e.key) {
case 'ArrowDown':
e.preventDefault();
navigateResults(1);
break;
case 'ArrowUp':
e.preventDefault();
navigateResults(-1);
break;
case 'Enter':
e.preventDefault();
if (currentHighlightIndex >= 0 && currentResults[currentHighlightIndex]) {
selectFood(currentResults[currentHighlightIndex]);
}
break;
case 'Escape':
hideSearchResults();
break;
}
});
// Click outside to close
document.addEventListener('click', function(e: Event) {
const target = e.target as HTMLElement;
if (!searchResults.contains(target) && !searchInput.contains(target)) {
hideSearchResults();
}
});
}
const getCleanName = (foodName: string): string => {
const newName = foodName
.replace(/á/g, 'a')
.replace(/ã/g, 'a')
.replace(/â/g, 'a')
.replace(/ê/g, 'e')
.replace(/é/g, 'e')
.replace(/ó/g, 'o')
.replace(/ú/g, 'u')
.replace(/ç/g, 'c');
return newName.toLowerCase();
}
2025-06-16 17:33:52 -03:00
// Perform search
function performSearch(query: string) {
// Simulate API call - replace with your actual database search
const results: FoodItem[] = foodDatabase.filter(food => {
const cleanName = getCleanName(food.name);
const category = food.info.category.toLowerCase();
return cleanName.includes(query.toLowerCase()) || category.includes(query.toLowerCase())
}).slice(0, 5); // Limit to 5 results
2025-06-16 17:33:52 -03:00
currentResults = results;
displaySearchResults(results);
searchLoading?.classList.remove('show');
}
// Display search results
function displaySearchResults(results: FoodItem[]) {
if (results.length === 0) {
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)': '🍍'
};
searchResults.innerHTML = results.map((food, index) => `
<div class="search-result-item" data-index="${index}" onclick="selectFood(${JSON.stringify(food).replace(/"/g, '&quot;')})">
<div class="food-info">
<div class="food-name">${icons[food.info.category]} ${food.name}</div>
<div class="food-details">${food.info.category} • 100 g</div>
</div>
<div class="food-calories">${food.info.calories} cal</div>
</div>
`).join('');
}
searchResults.classList.add('show');
currentHighlightIndex = -1;
}
// Navigate results with keyboard
function navigateResults(direction: number) {
const items = searchResults.querySelectorAll('.search-result-item');
if (items.length === 0) return;
// Remove current highlight
if (currentHighlightIndex >= 0) {
items[currentHighlightIndex].classList.remove('highlighted');
}
// Calculate new index
currentHighlightIndex += direction;
if (currentHighlightIndex < 0) currentHighlightIndex = items.length - 1;
if (currentHighlightIndex >= items.length) currentHighlightIndex = 0;
// Add new highlight
items[currentHighlightIndex].classList.add('highlighted');
items[currentHighlightIndex].scrollIntoView({ block: 'nearest' });
}
// Select food
function selectFood(food: FoodItem) {
selectedFood = food;
// Clear search and hide results
searchInput.value = food.name;
hideSearchResults();
// Focus on quantity input
getInputById('gramAmount').focus();
// Update calories
previewCalories(food);
}
// Hide search results
function hideSearchResults() {
searchResults.classList.remove('show');
currentHighlightIndex = -1;
2025-06-12 10:39:19 -03:00
}
2025-09-03 21:03:46 -03:00
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.');
}
}
2025-06-12 10:39:19 -03:00
const addDeleteEvents = () => {
2025-09-03 21:03:46 -03:00
// 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);
2025-06-12 10:39:19 -03:00
});
}
// Update current date display
const updateCurrentDate = () => {
const date = new Date(selectedDate);
getDivById('currentDate').textContent = date.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
// Add food to the log
2025-06-12 18:30:11 -03:00
const addFood = async () => {
if (!currentUser) {
2025-06-12 18:52:44 -03:00
swal('Hey!', 'Please log in to add food entries.', 'info');
2025-06-12 18:30:11 -03:00
return;
}
2025-06-12 10:39:19 -03:00
const gramAmount = getInputById('gramAmount');
2025-06-16 17:33:52 -03:00
if (!selectedFood || !gramAmount.value) {
2025-06-12 18:52:44 -03:00
swal('Hey!', 'Please select a food item and enter amount', 'error');
2025-06-12 10:39:19 -03:00
return;
}
2025-06-12 18:30:11 -03:00
showLoading();
try {
const grams: number = parseFloat(gramAmount.value);
2025-06-16 17:33:52 -03:00
const foodData: FoodItem = getFoodItemByName(selectedFood.name);
// Current date, with timezone
const date = new Date(selectedDate.getTime() - (selectedDate.getTimezoneOffset() * 60000)).toISOString();
2025-06-12 18:30:11 -03:00
// Calculate nutrition for the amount
const proportion: FoodItem = getFoodData(grams, foodData);
const entry: FoodStorage = {
2025-06-16 17:33:52 -03:00
name: selectedFood.name,
2025-06-12 18:30:11 -03:00
grams: grams,
calories: proportion.info.calories,
protein: proportion.info.protein,
fat: proportion.info.fat,
carbs: proportion.info.carbs,
fiber: proportion.info.fiber,
2025-06-16 17:33:52 -03:00
date: date.split('T')[0],
2025-06-18 16:56:11 -03:00
alkaline: selectedFood.info.alkaline,
2025-06-12 18:30:11 -03:00
time: new Date().toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit'
})
};
// Save to Appwrite
const savedEntry = await AppwriteDB.saveFoodEntry(entry);
// Add to HTML table with Appwrite document ID
addFoodToTable(entry, savedEntry.$id);
// Update totals
2025-09-03 21:03:46 -03:00
updateTotalCalories([savedEntry]);
2025-06-12 18:30:11 -03:00
2025-06-13 19:18:31 -03:00
// Update total macros
const proteinDiv: HTMLElement = getDivById('proteinValue');
const fatDiv: HTMLElement = getDivById('fatValue');
const carboDiv: HTMLElement = getDivById('carboValue');
const fiberDiv: HTMLElement = getDivById('fiberValue');
const totalProtein: number = parseInt(proteinDiv.textContent ?? '0') + proportion.info.protein;
const totalFat: number = parseInt(fatDiv.textContent ?? '0') + proportion.info.fat;
const totalCarbs: number = parseInt(carboDiv.textContent ?? '0') + proportion.info.carbs;
const totalFiber: number = parseInt(fiberDiv.textContent ?? '0') + proportion.info.fiber;
proteinDiv.textContent = (Math.round(totalProtein * 10) / 10).toString();
fatDiv.textContent = (Math.round(totalFat * 10) / 10).toString();
carboDiv.textContent = (Math.round(totalCarbs * 10) / 10).toString();
fiberDiv.textContent = (Math.round(totalFiber * 10) / 10).toString();
2025-06-18 16:56:11 -03:00
// Update alkaline level
const div = getDivById('alkaline-level');
2025-06-12 18:30:11 -03:00
// Reset form
2025-06-16 17:33:52 -03:00
selectedFood = null;
getInputById('foodSearchInput').value = '';
2025-06-12 18:30:11 -03:00
gramAmount.value = '100';
showFoodPreview(false);
} catch (error) {
console.error('Add food error:', error);
2025-06-12 18:52:44 -03:00
swal('Oh no!', 'Failed to add food entry: ' + error.message, 'error');
2025-06-12 18:30:11 -03:00
} finally {
hideLoading();
}
}
2025-06-12 10:39:19 -03:00
2025-06-12 18:30:11 -03:00
// Load food entries from Appwrite
async function loadFoodEntries(date: Date) {
if (!currentUser) return;
2025-06-12 10:39:19 -03:00
2025-06-12 18:30:11 -03:00
showLoading();
try {
const entries = await AppwriteDB.getFoodEntries(date);
// Clear current table
2025-09-02 18:21:59 -03:00
getDivById('foodCardsContainer').innerHTML = '';
2025-06-12 18:30:11 -03:00
// Add each entry to table
entries.forEach(entry => {
const foodData: FoodStorage = {
id: entry.$id,
name: entry.name,
grams: entry.grams,
calories: entry.calories,
protein: entry.protein,
fat: entry.fat,
carbs: entry.carbs,
fiber: entry.fiber,
time: entry.time,
date: entry.date,
2025-06-18 16:56:11 -03:00
alkaline: entry.alkaline,
2025-06-12 18:30:11 -03:00
};
addFoodToTable(foodData, entry.$id);
});
2025-06-12 10:39:19 -03:00
2025-09-02 18:21:59 -03:00
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>
`;
}
2025-06-12 18:30:11 -03:00
// Setup Delete events
addDeleteEvents();
// Update total calories
2025-09-03 21:03:46 -03:00
updateTotalCalories(entries);
2025-06-13 08:46:26 -03:00
// Update counters
let totalCalories = 0;
let totalProtein = 0;
let totalFat = 0;
let totalCarbs = 0;
let totalFiber = 0;
entries.forEach(entry => {
totalCalories += entry.calories;
totalProtein += entry.protein;
totalFat += entry.fat;
totalCarbs += entry.carbs;
totalFiber += entry.fiber;
});
getDivById('proteinValue').textContent = (Math.round(totalProtein * 10) / 10).toString();
getDivById('fatValue').textContent = (Math.round(totalFat * 10) / 10).toString();
getDivById('carboValue').textContent = (Math.round(totalCarbs * 10) / 10).toString();
2025-06-13 19:18:31 -03:00
getDivById('fiberValue').textContent = (Math.round(totalFiber * 10) / 10).toString();
// Update goals
const settings = await AppwriteDB.getUserSettings();
const hasGoals = Array.isArray(settings) && settings.length > 0;
if (hasGoals) {
const index = settings.length - 1;
2025-06-23 18:24:37 -03:00
getDivById('caloriesGoalText').classList.add('hidden');
if (parseInt(settings[index].caloriesGoal) > 0) {
getDivById('caloriesGoalText').textContent = `of ${settings[index].caloriesGoal}`;
getDivById('caloriesGoalText').classList.remove('hidden');
}
2025-06-13 19:18:31 -03:00
getDivById('proteinGoalText').classList.add('hidden');
if (parseInt(settings[index].proteinGoal) > 0) {
getDivById('proteinGoalText').textContent = `of ${settings[index].proteinGoal}`;
getDivById('proteinGoalText').classList.remove('hidden');
}
getDivById('fatGoalText').classList.add('hidden');
if (parseInt(settings[index].fatGoal) > 0) {
getDivById('fatGoalText').textContent = `of ${settings[index].fatGoal}`;
getDivById('fatGoalText').classList.remove('hidden');
}
getDivById('carboGoalText').classList.add('hidden');
if (parseInt(settings[index].carboGoal) > 0) {
getDivById('carboGoalText').textContent = `of ${settings[index].carboGoal}`;
getDivById('carboGoalText').classList.remove('hidden');
}
getDivById('fiberGoalText').classList.add('hidden');
if (parseInt(settings[index].fiberGoal) > 0) {
getDivById('fiberGoalText').textContent = `of ${settings[index].fiberGoal}`;
getDivById('fiberGoalText').classList.remove('hidden');
}
} else {
getDivById('proteinGoalText').classList.add('hidden');
getDivById('fatGoalText').classList.add('hidden');
getDivById('carboGoalText').classList.add('hidden');
getDivById('fiberGoalText').classList.add('hidden');
}
2025-06-12 18:30:11 -03:00
} catch (error) {
console.error('Load food entries error:', error);
2025-06-12 18:52:44 -03:00
swal('Oh no!', 'Failed to load food entries: ' + error.message, 'error');
2025-06-12 18:30:11 -03:00
} finally {
hideLoading();
2025-06-12 10:39:19 -03:00
}
}
2025-06-12 18:30:11 -03:00
// Handle food deletion
2025-09-02 18:21:59 -03:00
async function handleDeleteFood(documentId: string) {
2025-06-12 18:30:11 -03:00
if (!documentId) return;
2025-06-12 18:52:44 -03:00
const willDelete = await swal({
title: "Are you sure?",
text: 'You want to delete this food entry?',
icon: 'warning',
dangerMode: true,
closeOnEsc: true,
buttons:["No, abort", "Yes, Do it!"],
});
2025-06-12 18:30:11 -03:00
2025-06-12 18:52:44 -03:00
if (!willDelete) return;
2025-06-12 18:30:11 -03:00
showLoading();
try {
// Delete from Appwrite
await AppwriteDB.deleteFoodEntry(documentId);
2025-06-13 19:18:31 -03:00
selectDate(selectedDate);
2025-06-12 18:30:11 -03:00
} catch (error) {
console.error('Delete food error:', error);
2025-06-12 18:52:44 -03:00
swal('Oh no!', 'Failed to delete food entry: ' + error.message, 'error');
2025-06-12 18:30:11 -03:00
} finally {
hideLoading();
}
2025-06-12 10:39:19 -03:00
}
2025-06-12 18:30:11 -03:00
// Add food to table (existing function - update to use document ID)
function addFoodToTable(foodData: FoodStorage, documentId: string) {
2025-09-02 18:21:59 -03:00
// New Food card
2025-09-03 21:03:46 -03:00
const container = getDivById('foodCardsContainer');
2025-09-02 18:21:59 -03:00
const card = document.createElement('div');
card.className = 'food-card';
card.setAttribute('data-id', documentId);
2025-09-03 21:03:46 -03:00
if (container.innerHTML.includes('empty-state')) {
container.innerHTML = '';
}
2025-09-02 18:21:59 -03:00
card.innerHTML = `
2025-09-03 21:03:46 -03:00
<div class="card-header card-header-toggle">
2025-09-02 18:21:59 -03:00
<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">
2025-09-03 21:03:46 -03:00
<button class="btn btn-edit" data-food-id="${documentId}">Edit</button>
<button class="btn btn-delete" data-food-id="${documentId}">Delete</button>
2025-09-02 18:21:59 -03:00
</div>
</div>
</div>
`;
container?.appendChild(card);
2025-09-03 21:03:46 -03:00
// Setup Delete events
addDeleteEvents();
2025-06-12 10:39:19 -03:00
}
2025-06-12 18:30:11 -03:00
// Update total calories
2025-09-03 21:03:46 -03:00
function updateTotalCalories(entries: Models.DefaultDocument[]) {
let total = totalCaloriesToday;
entries.forEach((entry: Models.DefaultDocument) => {
total += entry.calories || 0;
2025-06-12 18:30:11 -03:00
});
2025-09-03 21:03:46 -03:00
2025-06-12 18:30:11 -03:00
getDivById('caloriesCounter').textContent = total.toString();
2025-09-03 21:03:46 -03:00
totalCaloriesToday = total;
// FIX ME: alkaline count
2025-06-18 16:56:11 -03:00
// Update alkaline level
2025-09-03 21:03:46 -03:00
let totalGrams = totalGramsToday;
2025-06-18 16:56:11 -03:00
let indexMap: {index: number, grams: number}[] = [];
2025-09-03 21:03:46 -03:00
entries.forEach((entry: Models.DefaultDocument, key: number) => {
totalGrams += entry.grams || 0;
2025-06-18 16:56:11 -03:00
indexMap.push({
index: key,
2025-09-03 21:03:46 -03:00
grams: entry.grams || 0
2025-06-18 16:56:11 -03:00
});
});
2025-09-03 21:03:46 -03:00
let totalAlkaline = totalAlkalineToday;
entries.forEach((entry: Models.DefaultDocument, key: number) => {
if (entry.alkaline) {
2025-06-18 16:56:11 -03:00
for (let ob of indexMap) {
if (ob.index === key) {
totalAlkaline += ob.grams;
break;
}
}
}
});
const alkalinePercent = totalAlkaline / totalGrams * 100;
getDivById('alkaline-level').textContent = (Math.round(alkalinePercent * 10) / 10).toString() + '% Alkaline';
2025-06-12 10:39:19 -03:00
}
// Calendar functions
const previousMonth = () => {
currentViewDate.setMonth(currentViewDate.getMonth() - 1);
renderCalendar();
}
const nextMonth = () => {
currentViewDate.setMonth(currentViewDate.getMonth() + 1);
renderCalendar();
}
2025-06-12 18:30:11 -03:00
const selectDate = async (date: Date) => {
2025-06-12 10:39:19 -03:00
selectedDate = date;
2025-09-03 21:03:46 -03:00
totalCaloriesToday = 0;
totalGramsToday = 0;
totalAlkalineToday = 0;
2025-06-12 10:39:19 -03:00
updateCurrentDate();
2025-06-12 18:30:11 -03:00
await loadFoodEntries(date);
2025-06-12 10:39:19 -03:00
renderCalendar();
}
const renderCalendar = () => {
const year = currentViewDate.getFullYear();
const month = currentViewDate.getMonth();
// Update calendar title
const monthNames = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
getDivById('calendarTitle').textContent = `${monthNames[month]} ${year}`;
// Clear calendar
const grid = getDivById('calendarGrid');
grid.innerHTML = '';
// Add day headers
const dayHeaders = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
dayHeaders.forEach(day => {
const header = document.createElement('div');
header.className = 'calendar-day-header';
header.textContent = day;
grid.appendChild(header);
});
// Get first day of month and number of days
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const startingDayOfWeek = firstDay.getDay();
// Add empty cells for previous month
const prevMonth = new Date(year, month, 0);
const daysInPrevMonth = prevMonth.getDate();
for (let i = startingDayOfWeek - 1; i >= 0; i--) {
const day = daysInPrevMonth - i;
const dayElement = createDayElement(day, true, year, month - 1);
grid.appendChild(dayElement);
}
// Add days of current month
for (let day = 1; day <= daysInMonth; day++) {
const dayElement = createDayElement(day, false, year, month);
grid.appendChild(dayElement);
}
// Add days from next month to fill the grid
const totalCells = grid.children.length - 7;
const remainingCells = 42 - totalCells;
for (let day = 1; day <= remainingCells; day++) {
const dayElement = createDayElement(day, true, year, month + 1);
grid.appendChild(dayElement);
}
}
function createDayElement(day: number, isOtherMonth: boolean, year: number, month: number) {
const dayElement = document.createElement('div') as HTMLElement;
dayElement.className = 'calendar-day';
const dayDate = new Date(year, month, day);
const dateString = dayDate.toISOString().split('T')[0];
if (isOtherMonth) {
dayElement.classList.add('other-month');
} else {
2025-06-12 18:30:11 -03:00
dayElement.onclick = () => selectDate(dayDate);
2025-06-12 10:39:19 -03:00
}
// Check if this is today
const today = new Date().toISOString().split('T')[0];
if (!isOtherMonth && dateString === today) {
dayElement.classList.add('today');
}
// Check if this is selected date
2025-06-12 18:30:11 -03:00
if (!isOtherMonth && dateString === selectedDate.toISOString().split('T')[0]) {
2025-06-12 10:39:19 -03:00
dayElement.classList.add('selected');
}
// Check if this day has data
2025-06-12 18:30:11 -03:00
const dayData = []; // FIXME get from AppWrite
2025-06-12 10:39:19 -03:00
if (dayData.length > 0) {
dayElement.classList.add('has-data');
2025-06-12 18:30:11 -03:00
const totalCalories = 0; // dayData.reduce((sum, entry) => sum + entry.calories, 0);
2025-06-12 10:39:19 -03:00
dayElement.innerHTML = `
<div>${day}</div>
<div class="day-calories">${totalCalories} cal</div>
`;
} else {
dayElement.textContent = day.toString();
}
return dayElement;
}
2025-09-03 21:03:46 -03:00
function toggleCard(id: string) {
2025-09-02 18:21:59 -03:00
const card = document.querySelector(`[data-id="${id}"]`);
2025-09-03 21:03:46 -03:00
card?.classList.toggle('expanded');
2025-09-02 18:21:59 -03:00
}
2025-09-03 21:03:46 -03:00
function deleteFood(id: string) {
2025-09-02 18:21:59 -03:00
handleDeleteFood(id);
}
2025-09-03 21:03:46 -03:00
function editFood(id: string) {
2025-09-02 18:21:59 -03:00
alert(`Edit functionality for food item ${id} would be implemented here`);
}
2025-06-16 17:33:52 -03:00
(window as any).selectFood = selectFood;
2025-09-02 18:21:59 -03:00
(window as any).toggleCard = toggleCard;
(window as any).editFood = editFood;
(window as any).deleteFood = deleteFood;