2025-06-12 10:39:19 -03:00
|
|
|
import { foodDatabase } from './foodDatabase.js';
|
2025-09-07 16:33:56 -03:00
|
|
|
import { getButtonById, getButtonListByClassName, getDivById, getInputById, showFoodPreview } from './DomUtils.ts';
|
2025-12-13 16:00:31 -03:00
|
|
|
import { DailyTotalCalories, FoodItem, FoodStorage, MealGroup, MealPeriod } 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-07 16:33:56 -03:00
|
|
|
import { closeMobileMenu, delay, getCleanName, getIcon, handleMobileCalendarClick, hideLoading, hideSearchResults, navigateResultsKeyboard, QUICK_DELAY, scrollToCalendarView, showLoading, toggleCardHandler, toggleMobileMenu } from './Utils.ts';
|
2025-12-03 16:42:52 -03:00
|
|
|
import { showAuthForms, toggleAuthForms, showRegisterForm, showLoginForm, hideAuthForms, closeAuthModal } from './auth.ts';
|
2025-09-07 16:33:56 -03:00
|
|
|
import { appState } from "./state";
|
2025-12-10 12:21:17 -03:00
|
|
|
import { getNutritionInfo, NutritionInfo, clearNutritionCache, getCacheStats } from './claudeService';
|
2025-06-12 10:39:19 -03:00
|
|
|
|
2025-12-10 12:56:58 -03:00
|
|
|
// PWA Service Worker Registration
|
2026-02-12 11:13:51 -03:00
|
|
|
import { registerSW } from 'virtual:pwa-register';
|
2025-12-10 12:56:58 -03:00
|
|
|
|
|
|
|
|
const updateSW = registerSW({
|
|
|
|
|
onNeedRefresh() {
|
|
|
|
|
swal({
|
|
|
|
|
title: 'Update Available',
|
|
|
|
|
text: 'New content is available. Reload to update?',
|
|
|
|
|
icon: 'info',
|
|
|
|
|
buttons: {
|
|
|
|
|
cancel: 'Later',
|
|
|
|
|
confirm: 'Update Now'
|
|
|
|
|
}
|
|
|
|
|
}).then((willUpdate) => {
|
|
|
|
|
if (willUpdate) {
|
|
|
|
|
updateSW(true)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
onOfflineReady() {
|
|
|
|
|
console.log('App ready to work offline')
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
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 selectedFood: FoodItem | null = null; // review here
|
2025-06-12 18:30:11 -03:00
|
|
|
let currentUser: any | null = null;
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function initializeAuth() {
|
2025-10-30 13:18:01 -03:00
|
|
|
// Check if this is a shared view first
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
|
const shareId = urlParams.get('share');
|
|
|
|
|
|
|
|
|
|
if (shareId) {
|
|
|
|
|
await checkSharedView();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-12 18:30:11 -03:00
|
|
|
showLoading();
|
2025-10-30 13:18:01 -03:00
|
|
|
|
2025-06-12 18:30:11 -03:00
|
|
|
try {
|
|
|
|
|
const isLoggedIn = await AppwriteAuth.isLoggedIn();
|
2025-10-30 13:18:01 -03:00
|
|
|
|
2025-06-12 18:30:11 -03:00
|
|
|
if (isLoggedIn) {
|
|
|
|
|
currentUser = await AppwriteAuth.getCurrentUser();
|
2025-09-04 17:12:14 -03:00
|
|
|
await showMainApp();
|
2025-06-12 18:30:11 -03:00
|
|
|
selectDate(selectedDate);
|
|
|
|
|
} else {
|
|
|
|
|
showAuthForms();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Initialize app error:', error);
|
|
|
|
|
showAuthForms();
|
|
|
|
|
} finally {
|
|
|
|
|
hideLoading();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
2025-12-03 16:42:52 -03:00
|
|
|
|
2025-06-12 18:30:11 -03:00
|
|
|
// Auto login after registration
|
|
|
|
|
await AppwriteAuth.login(email, password);
|
|
|
|
|
currentUser = await AppwriteAuth.getCurrentUser();
|
2025-12-03 16:42:52 -03:00
|
|
|
|
|
|
|
|
closeAuthModal();
|
2025-09-04 17:12:14 -03:00
|
|
|
await showMainApp();
|
2025-06-12 18:30:11 -03:00
|
|
|
selectDate(selectedDate);
|
2025-12-03 16:42:52 -03:00
|
|
|
|
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-10-30 11:29:19 -03:00
|
|
|
if (error instanceof Error) {
|
|
|
|
|
swal('Oh no!', 'Registration failed: ' + error.message, 'error');
|
|
|
|
|
}
|
2025-06-12 18:30:11 -03:00
|
|
|
} finally {
|
|
|
|
|
hideLoading();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
2025-12-03 16:42:52 -03:00
|
|
|
|
|
|
|
|
closeAuthModal();
|
2025-09-04 17:12:14 -03:00
|
|
|
await showMainApp();
|
2025-06-12 18:30:11 -03:00
|
|
|
selectDate(selectedDate);
|
2025-12-03 16:42:52 -03:00
|
|
|
|
2025-06-12 18:30:11 -03:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Login error:', error);
|
2025-10-30 11:29:19 -03:00
|
|
|
if (error instanceof Error) {
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
console.debug('All requests completed:', results);
|
2025-06-13 19:18:31 -03:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('One or more requests failed:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 11:13:51 -03:00
|
|
|
function setupBMICalculator() {
|
|
|
|
|
const bodyWeight = getInputById('bodyWeight').value;
|
|
|
|
|
const height = getInputById('height').value;
|
|
|
|
|
|
|
|
|
|
if (!bodyWeight || !height || parseFloat(height) === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bmi = (parseFloat(bodyWeight) / ((parseFloat(height) / 100) ** 2)).toFixed(1);
|
|
|
|
|
getInputById('bmi').value = bmi;
|
|
|
|
|
const bmiResult = getInputById('bmiResult');
|
|
|
|
|
if (bmiResult) {
|
|
|
|
|
// BMI ranges
|
|
|
|
|
const bmiValue = parseFloat(bmi);
|
|
|
|
|
if (bmiValue < 18.5) {
|
|
|
|
|
bmiResult.value = 'Underweight';
|
|
|
|
|
} else if (bmiValue >= 18.5 && bmiValue < 24.9) {
|
|
|
|
|
bmiResult.value = 'Normal weight';
|
|
|
|
|
} else if (bmiValue >= 25 && bmiValue < 29.9) {
|
|
|
|
|
bmiResult.value = 'Overweight';
|
|
|
|
|
} else {
|
|
|
|
|
bmiResult.value = 'Obesity';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-13 19:18:31 -03:00
|
|
|
async function handleSaveSettings(e: SubmitEvent) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
showLoading();
|
|
|
|
|
|
|
|
|
|
try {
|
2026-02-12 11:13:51 -03:00
|
|
|
const bodyWeight = getInputById('bodyWeight').value;
|
|
|
|
|
const height = getInputById('height').value;
|
|
|
|
|
const bmi = getInputById('bmi').value;
|
|
|
|
|
const bmiResult = getInputById('bmiResult');
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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),
|
2026-02-12 11:13:51 -03:00
|
|
|
bodyWeight: bodyWeight ? parseFloat(bodyWeight) : undefined,
|
|
|
|
|
height: height ? parseFloat(height) : undefined,
|
|
|
|
|
bmi: bmi ? parseFloat(bmi) : undefined,
|
|
|
|
|
bmiResult: bmiResult ? bmiResult.value : undefined,
|
2025-06-13 19:18:31 -03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
hideLoading();
|
2025-09-07 16:33:56 -03:00
|
|
|
toggleSettingsView();
|
2025-06-13 19:18:31 -03:00
|
|
|
selectDate(selectedDate);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
hideLoading();
|
|
|
|
|
console.error('Saving error:', error);
|
2025-10-30 11:29:19 -03:00
|
|
|
if (error instanceof Error) {
|
|
|
|
|
swal('Oh no!', 'Saving failed: ' + error.message, 'error');
|
|
|
|
|
}
|
2025-06-13 19:18:31 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 12:21:17 -03:00
|
|
|
function updateCacheStats() {
|
|
|
|
|
try {
|
|
|
|
|
const stats = getCacheStats();
|
|
|
|
|
const cacheItemCountEl = document.getElementById('cacheItemCount');
|
|
|
|
|
const cacheSizeEl = document.getElementById('cacheSize');
|
|
|
|
|
|
|
|
|
|
if (cacheItemCountEl) {
|
|
|
|
|
cacheItemCountEl.textContent = stats.size.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cacheSizeEl) {
|
|
|
|
|
cacheSizeEl.textContent = `${stats.sizeInKB} KB`;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error updating cache stats:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleClearCache() {
|
|
|
|
|
try {
|
|
|
|
|
const result = await swal({
|
|
|
|
|
title: 'Clear Cache?',
|
|
|
|
|
text: 'This will remove all cached nutrition data. You may need to fetch this data again from the AI.',
|
|
|
|
|
icon: 'warning',
|
|
|
|
|
buttons: ['Cancel', 'Clear Cache'],
|
|
|
|
|
dangerMode: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (result) {
|
|
|
|
|
clearNutritionCache();
|
|
|
|
|
updateCacheStats();
|
|
|
|
|
swal('Success', 'Cache cleared successfully!', 'success');
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error clearing cache:', error);
|
|
|
|
|
swal('Error', 'Failed to clear cache', 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 12:27:08 -03:00
|
|
|
function handleAICheckboxChange(e: Event) {
|
|
|
|
|
const checkbox = e.target as HTMLInputElement;
|
|
|
|
|
|
|
|
|
|
if (checkbox.checked) {
|
|
|
|
|
// If a food is already selected, load its AI nutrition info
|
|
|
|
|
if (selectedFood) {
|
|
|
|
|
loadAINutritionInfo(selectedFood.name);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Hide the AI nutrition card
|
|
|
|
|
hideAINutritionCard();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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-10-30 11:29:19 -03:00
|
|
|
if (error instanceof Error) {
|
|
|
|
|
swal('Oh no!', 'Logout failed: ' + error.message, 'error');
|
|
|
|
|
}
|
2025-06-13 19:18:31 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-07 16:33:56 -03:00
|
|
|
async function toggleSettingsView() {
|
2025-09-04 13:58:30 -03:00
|
|
|
const showSettings = getButtonById('settingsBtn').textContent.includes('Settings');
|
2025-06-13 19:18:31 -03:00
|
|
|
|
|
|
|
|
if (showSettings) {
|
|
|
|
|
showLoading();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
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;
|
2026-02-12 11:13:51 -03:00
|
|
|
getInputById('bodyWeight').value = settings[index].bodyWeight ?? 0;
|
|
|
|
|
getInputById('height').value = settings[index].height ?? 0;
|
|
|
|
|
getInputById('bmi').value = settings[index].bmi ?? 0;
|
|
|
|
|
getInputById('bmiResult').value = settings[index].bmiResult ?? '';
|
2025-06-13 19:18:31 -03:00
|
|
|
} 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-09-04 13:58:30 -03:00
|
|
|
getButtonById('settingsBtn').textContent = '🔙 Back';
|
|
|
|
|
getButtonById('settingsBtnMobile').textContent = '🔙 Back';
|
2025-06-13 19:18:31 -03:00
|
|
|
|
2025-09-07 16:33:56 -03:00
|
|
|
getDivById('app-content').classList.add('hidden');
|
|
|
|
|
getDivById('settings-content').classList.remove('hidden');
|
2025-12-10 12:21:17 -03:00
|
|
|
|
|
|
|
|
// Update cache statistics display
|
|
|
|
|
updateCacheStats();
|
|
|
|
|
|
2025-06-13 19:18:31 -03:00
|
|
|
hideLoading();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
hideLoading();
|
|
|
|
|
console.error('Fetching settings failed:', error);
|
2025-10-30 11:29:19 -03:00
|
|
|
if (error instanceof Error) {
|
|
|
|
|
swal('Oh no!', 'Fetching settings failed: ' + error.message, 'error');
|
|
|
|
|
}
|
2025-06-13 19:18:31 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
2025-09-07 16:33:56 -03:00
|
|
|
getDivById('app-content').classList.remove('hidden');
|
|
|
|
|
getDivById('settings-content').classList.add('hidden');
|
2025-09-04 13:49:01 -03:00
|
|
|
getButtonById('settingsBtn').textContent = '⚙️ Settings';
|
2025-09-04 13:58:30 -03:00
|
|
|
getButtonById('settingsBtnMobile').textContent = '⚙️ Settings';
|
2025-09-04 13:49:01 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-07 16:33:56 -03:00
|
|
|
async function handleMobileSettingsClick() {
|
2025-09-04 13:49:01 -03:00
|
|
|
closeMobileMenu();
|
2025-09-07 16:33:56 -03:00
|
|
|
await toggleSettingsView();
|
2025-06-12 18:30:11 -03:00
|
|
|
}
|
|
|
|
|
|
2025-09-04 17:12:14 -03:00
|
|
|
async function showMainApp() {
|
2025-12-03 16:42:52 -03:00
|
|
|
hideAuthForms();
|
2025-09-07 16:33:56 -03:00
|
|
|
getDivById('user-info').classList.remove('hidden');
|
|
|
|
|
getDivById('app-content').classList.remove('hidden');
|
2025-12-03 16:42:52 -03:00
|
|
|
|
2025-06-12 18:30:11 -03:00
|
|
|
if (currentUser) {
|
|
|
|
|
getDivById('userName').textContent = `Welcome, ${currentUser.name}!`;
|
|
|
|
|
}
|
2025-12-03 16:42:52 -03:00
|
|
|
|
2025-06-12 18:30:11 -03:00
|
|
|
updateCurrentDate();
|
2025-09-04 17:12:14 -03:00
|
|
|
|
2025-09-07 16:33:56 -03:00
|
|
|
// Get daily entries with total from Appwrite to display in calendar
|
2025-11-03 11:25:39 -03:00
|
|
|
fetchCaloriesCurrentMonth();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fetchCaloriesCurrentMonth() {
|
2025-09-04 17:12:14 -03:00
|
|
|
const entries = await AppwriteDB.getMonthlyCalories(selectedDate);
|
|
|
|
|
entries.forEach((entry) => {
|
|
|
|
|
const calories = parseInt(entry.totalCalories);
|
2025-10-30 11:29:19 -03:00
|
|
|
let day = parseInt(entry.date.substring(8));
|
2025-10-30 11:30:06 -03:00
|
|
|
// New rule starting in October 31st, 2025
|
|
|
|
|
if (selectedDate >= new Date('2025-10-31')) {
|
2025-10-30 11:29:19 -03:00
|
|
|
day = parseInt(entry.date.substring(8)) + parseInt(entry.date.substring(5,7)) + parseInt(entry.date.substring(0,4));
|
|
|
|
|
}
|
2025-09-04 17:12:14 -03:00
|
|
|
updateDocumentIdForDay(entry.$id, calories, day);
|
|
|
|
|
});
|
2025-06-12 18:30:11 -03:00
|
|
|
}
|
|
|
|
|
|
2025-09-04 12:51:43 -03:00
|
|
|
function clearEditing() {
|
2025-09-07 16:33:56 -03:00
|
|
|
getInputById('foodSearchInput').value = '';
|
2025-09-04 12:51:43 -03:00
|
|
|
getInputById('gramAmount').value = '100';
|
|
|
|
|
showFoodPreview(false);
|
2025-12-03 17:38:29 -03:00
|
|
|
hideAINutritionCard();
|
2025-09-04 17:33:16 -03:00
|
|
|
getDivById('add-foot-title').innerHTML = 'Add Food Entry';
|
2025-09-04 12:51:43 -03:00
|
|
|
getButtonById('add-food-btn').innerHTML = 'Add Food';
|
2025-09-13 23:49:29 +00:00
|
|
|
getButtonById('cancel-food-btn').style.display = 'none';
|
2025-12-03 17:38:29 -03:00
|
|
|
|
2025-09-13 23:49:29 +00:00
|
|
|
// Clear edit-specific hidden inputs
|
|
|
|
|
getInputById('foodIdToUpdate').value = '';
|
|
|
|
|
getInputById('foodTimeToUpdate').value = '';
|
|
|
|
|
getInputById('foodCaloriesToUpdate').value = '';
|
2025-12-03 17:38:29 -03:00
|
|
|
|
2025-09-04 12:51:43 -03:00
|
|
|
selectedFood = null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-12 18:30:11 -03:00
|
|
|
// 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-04 12:51:43 -03:00
|
|
|
getButtonById('cancel-food-btn').style.display = 'none';
|
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) {
|
2025-09-04 12:51:43 -03:00
|
|
|
throw new Error(`Food not found for name ${foodName}`);
|
2025-06-12 10:39:19 -03:00
|
|
|
}
|
|
|
|
|
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-12-03 17:38:29 -03:00
|
|
|
hideAINutritionCard();
|
2025-06-16 17:33:52 -03:00
|
|
|
}
|
2025-06-12 10:39:19 -03:00
|
|
|
});
|
|
|
|
|
|
2025-12-10 12:01:08 -03:00
|
|
|
getInputById('gramAmount').addEventListener('change', () => {
|
|
|
|
|
previewCalories();
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-12 10:39:19 -03:00
|
|
|
getButtonById('add-food-btn').addEventListener('click', () => {
|
2025-09-04 12:51:43 -03:00
|
|
|
if (getButtonById('add-food-btn').innerHTML === 'Add Food') {
|
|
|
|
|
addFood();
|
|
|
|
|
} else {
|
|
|
|
|
updateFood();
|
|
|
|
|
}
|
2025-06-12 10:39:19 -03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-04 12:51:43 -03:00
|
|
|
getButtonById('cancel-food-btn').addEventListener('click', () => {
|
|
|
|
|
clearEditing();
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-12 18:30:11 -03:00
|
|
|
// Auth forms
|
|
|
|
|
document.getElementById('loginForm')?.addEventListener('submit', handleLogin);
|
|
|
|
|
document.getElementById('registerForm')?.addEventListener('submit', handleRegister);
|
2025-09-04 13:49:01 -03:00
|
|
|
|
|
|
|
|
// Desktop header buttons
|
2025-12-10 12:21:17 -03:00
|
|
|
getButtonById('logoutBtn').addEventListener('click', handleLogout);
|
|
|
|
|
getButtonById('settingsBtn').addEventListener('click', toggleSettingsView);
|
|
|
|
|
getButtonById('calendarBtn').addEventListener('click', scrollToCalendarView);
|
2025-09-04 13:49:01 -03:00
|
|
|
|
|
|
|
|
// Mobile header buttons
|
2025-12-10 12:21:17 -03:00
|
|
|
getButtonById('logoutBtnMobile').addEventListener('click', handleLogout);
|
|
|
|
|
getButtonById('settingsBtnMobile').addEventListener('click', handleMobileSettingsClick);
|
|
|
|
|
getButtonById('calendarBtnMobile').addEventListener('click', handleMobileCalendarClick);
|
2025-09-04 13:49:01 -03:00
|
|
|
|
|
|
|
|
// Mobile menu toggle
|
|
|
|
|
document.getElementById('mobileMenuToggle')?.addEventListener('click', toggleMobileMenu);
|
|
|
|
|
|
|
|
|
|
// Settings form
|
2025-06-13 19:18:31 -03:00
|
|
|
document.getElementById('settingsForm')?.addEventListener('submit', handleSaveSettings);
|
2025-06-16 17:33:52 -03:00
|
|
|
|
2026-02-12 11:13:51 -03:00
|
|
|
// BMI calculator - recalculate on input change
|
|
|
|
|
getInputById('bodyWeight').addEventListener('change', () => {
|
|
|
|
|
setupBMICalculator();
|
|
|
|
|
});
|
|
|
|
|
getInputById('height').addEventListener('change', () => {
|
|
|
|
|
setupBMICalculator();
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-10 12:21:17 -03:00
|
|
|
// Clear cache button
|
|
|
|
|
getButtonById('clearCacheBtn')?.addEventListener('click', handleClearCache);
|
|
|
|
|
|
2025-12-10 12:27:08 -03:00
|
|
|
// AI nutrition checkbox
|
|
|
|
|
document.getElementById('enableAINutrition')?.addEventListener('change', handleAICheckboxChange);
|
|
|
|
|
|
2025-10-30 13:18:01 -03:00
|
|
|
// Share button event listeners
|
2025-12-10 12:21:17 -03:00
|
|
|
getButtonById('shareBtn').addEventListener('click', handleShareClick);
|
|
|
|
|
getButtonById('shareBtnMobile').addEventListener('click', handleShareClick);
|
|
|
|
|
getButtonById('close-share-modal').addEventListener('click', closeShareModal);
|
2025-10-30 13:18:01 -03:00
|
|
|
document.getElementById('copy-share-link')?.addEventListener('click', copyShareLink);
|
|
|
|
|
|
2025-12-03 16:42:52 -03:00
|
|
|
// Landing page CTA button event listeners
|
|
|
|
|
document.getElementById('cta-register-btn')?.addEventListener('click', () => {
|
|
|
|
|
showRegisterForm();
|
|
|
|
|
});
|
|
|
|
|
document.getElementById('cta-login-btn')?.addEventListener('click', () => {
|
|
|
|
|
showLoginForm();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Header auth button event listeners
|
|
|
|
|
document.getElementById('open-login-btn')?.addEventListener('click', () => {
|
|
|
|
|
showLoginForm();
|
|
|
|
|
});
|
|
|
|
|
document.getElementById('open-register-btn')?.addEventListener('click', () => {
|
|
|
|
|
showRegisterForm();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Close auth modal
|
|
|
|
|
document.getElementById('close-auth-modal')?.addEventListener('click', () => {
|
|
|
|
|
closeAuthModal();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Close modal when clicking outside
|
|
|
|
|
document.getElementById('auth-modal')?.addEventListener('click', (e) => {
|
|
|
|
|
if (e.target === document.getElementById('auth-modal')) {
|
|
|
|
|
closeAuthModal();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-16 17:33:52 -03:00
|
|
|
// Search functionality
|
2025-09-07 16:33:56 -03:00
|
|
|
getInputById('foodSearchInput').addEventListener('input', function(e: Event) {
|
2025-06-16 17:33:52 -03:00
|
|
|
const target = e.target as HTMLInputElement;
|
|
|
|
|
const query = target.value.trim();
|
|
|
|
|
|
|
|
|
|
if (query.length === 0) {
|
|
|
|
|
hideSearchResults();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show loading
|
2025-09-07 16:33:56 -03:00
|
|
|
getDivById('searchLoading').classList.add('show');
|
2025-06-16 17:33:52 -03:00
|
|
|
|
|
|
|
|
// Clear previous timeout
|
2025-09-07 16:33:56 -03:00
|
|
|
if (appState.searchTimeout) {
|
|
|
|
|
clearTimeout(appState.searchTimeout);
|
2025-06-16 17:33:52 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Debounce search
|
2025-09-07 16:33:56 -03:00
|
|
|
appState.searchTimeout = setTimeout(() => {
|
2025-06-16 17:33:52 -03:00
|
|
|
performSearch(query);
|
|
|
|
|
}, 300);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Keyboard navigation
|
2025-09-07 16:33:56 -03:00
|
|
|
getInputById('foodSearchInput').addEventListener('keydown', function(e) {
|
|
|
|
|
if (!getDivById('searchResults').classList.contains('show')) return;
|
2025-06-16 17:33:52 -03:00
|
|
|
|
|
|
|
|
switch(e.key) {
|
|
|
|
|
case 'ArrowDown':
|
|
|
|
|
e.preventDefault();
|
2025-09-07 16:33:56 -03:00
|
|
|
navigateResultsKeyboard(1);
|
2025-06-16 17:33:52 -03:00
|
|
|
break;
|
|
|
|
|
case 'ArrowUp':
|
|
|
|
|
e.preventDefault();
|
2025-09-07 16:33:56 -03:00
|
|
|
navigateResultsKeyboard(-1);
|
2025-06-16 17:33:52 -03:00
|
|
|
break;
|
|
|
|
|
case 'Enter':
|
|
|
|
|
e.preventDefault();
|
2025-09-07 16:33:56 -03:00
|
|
|
if (appState.currentHighlightIndex >= 0 && appState.searchResults[appState.currentHighlightIndex]) {
|
|
|
|
|
selectFood(appState.searchResults[appState.currentHighlightIndex]);
|
2025-06-16 17:33:52 -03:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'Escape':
|
|
|
|
|
hideSearchResults();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-04 13:49:01 -03:00
|
|
|
// Click outside to close search results
|
2025-06-16 17:33:52 -03:00
|
|
|
document.addEventListener('click', function(e: Event) {
|
|
|
|
|
const target = e.target as HTMLElement;
|
2025-09-07 16:33:56 -03:00
|
|
|
if (!getDivById('searchResults').contains(target) && !getInputById('foodSearchInput').contains(target)) {
|
2025-06-16 17:33:52 -03:00
|
|
|
hideSearchResults();
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-09-04 13:49:01 -03:00
|
|
|
|
|
|
|
|
// Click outside to close mobile menu
|
|
|
|
|
document.addEventListener('click', function(e: Event) {
|
|
|
|
|
const target = e.target as HTMLElement;
|
|
|
|
|
const mobileMenu = document.getElementById('mobileMenu');
|
|
|
|
|
const mobileMenuToggle = document.getElementById('mobileMenuToggle');
|
|
|
|
|
|
|
|
|
|
if (mobileMenu && mobileMenuToggle &&
|
|
|
|
|
!mobileMenu.contains(target) &&
|
|
|
|
|
!mobileMenuToggle.contains(target)) {
|
|
|
|
|
closeMobileMenu();
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-06-16 17:33:52 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function performSearch(query: string) {
|
2025-06-18 11:55:11 -03:00
|
|
|
const results: FoodItem[] = foodDatabase.filter(food => {
|
|
|
|
|
const cleanName = getCleanName(food.name);
|
2025-12-01 18:15:09 -03:00
|
|
|
const cleanNameEn = getCleanName(food.nameEn);
|
2025-06-18 11:55:11 -03:00
|
|
|
const category = food.info.category.toLowerCase();
|
2025-12-01 18:15:09 -03:00
|
|
|
return cleanName.includes(query.toLowerCase()) || cleanNameEn.includes(query.toLowerCase()) || category.includes(query.toLowerCase())
|
2025-06-18 11:55:11 -03:00
|
|
|
}).slice(0, 5); // Limit to 5 results
|
2025-06-16 17:33:52 -03:00
|
|
|
|
2025-09-07 16:33:56 -03:00
|
|
|
appState.searchResults = results;
|
2025-06-16 17:33:52 -03:00
|
|
|
displaySearchResults(results);
|
2025-09-07 16:33:56 -03:00
|
|
|
getDivById('searchLoading').classList.remove('show');
|
2025-06-16 17:33:52 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function displaySearchResults(results: FoodItem[]) {
|
|
|
|
|
if (results.length === 0) {
|
2025-09-07 16:33:56 -03:00
|
|
|
getDivById('searchResults').innerHTML = '<div class="no-results">No foods found. Try a different search term.</div>';
|
2025-06-16 17:33:52 -03:00
|
|
|
} else {
|
2025-09-07 16:33:56 -03:00
|
|
|
getDivById('searchResults').innerHTML = results.map((food, index) => `
|
2025-06-16 17:33:52 -03:00
|
|
|
<div class="search-result-item" data-index="${index}" onclick="selectFood(${JSON.stringify(food).replace(/"/g, '"')})">
|
|
|
|
|
<div class="food-info">
|
2025-09-07 16:33:56 -03:00
|
|
|
<div class="food-name">${getIcon(food.info.category)} ${food.name}</div>
|
2025-06-16 17:33:52 -03:00
|
|
|
<div class="food-details">${food.info.category} • 100 g</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="food-calories">${food.info.calories} cal</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-07 16:33:56 -03:00
|
|
|
getDivById('searchResults').classList.add('show');
|
|
|
|
|
appState.currentHighlightIndex = -1;
|
2025-06-16 17:33:52 -03:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 17:38:29 -03:00
|
|
|
// AI Nutrition Card Functions
|
|
|
|
|
function showAINutritionCard() {
|
|
|
|
|
const card = getDivById('ai-nutrition-card');
|
|
|
|
|
card.classList.remove('display-none');
|
|
|
|
|
card.classList.add('display-block');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hideAINutritionCard() {
|
|
|
|
|
const card = getDivById('ai-nutrition-card');
|
|
|
|
|
card.classList.add('display-none');
|
|
|
|
|
card.classList.remove('display-block');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showAILoading() {
|
|
|
|
|
getDivById('ai-nutrition-loading').classList.remove('hidden');
|
|
|
|
|
getDivById('ai-nutrition-error').classList.add('hidden');
|
|
|
|
|
getDivById('ai-nutrition-content').classList.add('hidden');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showAIError() {
|
|
|
|
|
getDivById('ai-nutrition-loading').classList.add('hidden');
|
|
|
|
|
getDivById('ai-nutrition-error').classList.remove('hidden');
|
|
|
|
|
getDivById('ai-nutrition-content').classList.add('hidden');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showAIContent(nutritionInfo: NutritionInfo) {
|
|
|
|
|
getDivById('ai-nutrition-loading').classList.add('hidden');
|
|
|
|
|
getDivById('ai-nutrition-error').classList.add('hidden');
|
|
|
|
|
getDivById('ai-nutrition-content').classList.remove('hidden');
|
|
|
|
|
|
|
|
|
|
// Populate vitamins
|
|
|
|
|
const vitaminsList = document.getElementById('ai-vitamins-list');
|
|
|
|
|
if (vitaminsList) {
|
|
|
|
|
vitaminsList.innerHTML = nutritionInfo.vitamins.map(v => `<li>${v}</li>`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Populate minerals
|
|
|
|
|
const mineralsList = document.getElementById('ai-minerals-list');
|
|
|
|
|
if (mineralsList) {
|
|
|
|
|
mineralsList.innerHTML = nutritionInfo.minerals.map(m => `<li>${m}</li>`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Populate benefits
|
|
|
|
|
const benefitsList = document.getElementById('ai-benefits-list');
|
|
|
|
|
if (benefitsList) {
|
|
|
|
|
benefitsList.innerHTML = nutritionInfo.benefits.map(b => `<li>${b}</li>`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Populate notes
|
|
|
|
|
const notesText = document.getElementById('ai-notes-text');
|
|
|
|
|
const notesSection = document.getElementById('ai-notes-section');
|
|
|
|
|
if (notesText && notesSection && nutritionInfo.notes) {
|
|
|
|
|
notesText.textContent = nutritionInfo.notes;
|
|
|
|
|
notesSection.classList.remove('hidden');
|
|
|
|
|
} else if (notesSection) {
|
|
|
|
|
notesSection.classList.add('hidden');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 11:57:41 -03:00
|
|
|
async function loadAINutritionInfo(foodName: string) {
|
2025-12-03 17:38:29 -03:00
|
|
|
showAINutritionCard();
|
|
|
|
|
showAILoading();
|
|
|
|
|
|
|
|
|
|
try {
|
2025-12-10 11:57:41 -03:00
|
|
|
const nutritionInfo = await getNutritionInfo(foodName);
|
2025-12-03 17:38:29 -03:00
|
|
|
showAIContent(nutritionInfo);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error loading AI nutrition info:', error);
|
|
|
|
|
showAIError();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-16 17:33:52 -03:00
|
|
|
function selectFood(food: FoodItem) {
|
|
|
|
|
selectedFood = food;
|
2025-12-03 17:38:29 -03:00
|
|
|
|
2025-09-07 16:33:56 -03:00
|
|
|
getInputById('foodSearchInput').value = food.name;
|
2025-12-03 17:38:29 -03:00
|
|
|
hideSearchResults();
|
2025-06-16 17:33:52 -03:00
|
|
|
getInputById('gramAmount').focus();
|
|
|
|
|
previewCalories(food);
|
2025-12-03 17:38:29 -03:00
|
|
|
|
|
|
|
|
// Show cancel button so user can clear selection
|
|
|
|
|
getButtonById('cancel-food-btn').style.display = 'inline-block';
|
|
|
|
|
|
2025-12-10 12:27:08 -03:00
|
|
|
// Load AI nutrition info only if checkbox is checked
|
|
|
|
|
const aiCheckbox = document.getElementById('enableAINutrition') as HTMLInputElement;
|
|
|
|
|
if (aiCheckbox && aiCheckbox.checked) {
|
|
|
|
|
loadAINutritionInfo(food.name);
|
|
|
|
|
} else {
|
|
|
|
|
hideAINutritionCard();
|
|
|
|
|
}
|
2025-06-16 17:33:52 -03:00
|
|
|
}
|
|
|
|
|
|
2025-09-04 12:51:43 -03:00
|
|
|
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;
|
2025-09-07 16:33:56 -03:00
|
|
|
getInputById('foodSearchInput').value = foodToEdit[0].name;
|
2025-09-04 12:51:43 -03:00
|
|
|
getInputById('gramAmount').value = foodToEdit[0].grams;
|
|
|
|
|
previewCalories(foodData)
|
|
|
|
|
|
2025-09-04 17:33:16 -03:00
|
|
|
getDivById('add-foot-title').innerHTML = 'Edit Food Entry';
|
2025-09-04 12:51:43 -03:00
|
|
|
getButtonById('add-food-btn').innerHTML = 'Save Food';
|
2025-09-13 23:49:29 +00:00
|
|
|
getButtonById('cancel-food-btn').style.display = 'inline-block';
|
2025-09-04 12:51:43 -03:00
|
|
|
getInputById('foodIdToUpdate').value = foodId;
|
|
|
|
|
getInputById('foodTimeToUpdate').value = foodToEdit[0].time;
|
2025-09-04 17:12:14 -03:00
|
|
|
getInputById('foodCaloriesToUpdate').value = foodToEdit[0].calories;
|
2025-09-04 12:51:43 -03:00
|
|
|
|
2025-09-04 17:33:16 -03:00
|
|
|
// Scroll to top
|
|
|
|
|
const titleDiv = getDivById('add-foot-title') as HTMLElement;
|
|
|
|
|
if (titleDiv) {
|
|
|
|
|
titleDiv.scrollIntoView({
|
|
|
|
|
behavior: 'smooth',
|
|
|
|
|
block: 'start'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 12:51:43 -03:00
|
|
|
hideLoading();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Set food to edit error:', error);
|
2025-10-30 11:29:19 -03:00
|
|
|
if (error instanceof Error) {
|
|
|
|
|
swal('Oh no!', 'Failed to load food entry: ' + error.message, 'error');
|
|
|
|
|
}
|
2025-09-04 12:51:43 -03:00
|
|
|
} finally {
|
|
|
|
|
hideLoading();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-13 23:49:29 +00:00
|
|
|
async function setFoodToCopy(foodId: string) {
|
|
|
|
|
showLoading();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// get food item
|
|
|
|
|
const entries = await AppwriteDB.getFoodEntries(selectedDate);
|
|
|
|
|
const foodToCopy = entries.filter(entry => entry.$id === foodId);
|
|
|
|
|
if (foodToCopy.length === 0) {
|
|
|
|
|
swal('Hey!', 'Unable to get food entry from server', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// set the selected food name and quantity (same as original)
|
|
|
|
|
const foodData: FoodItem = getFoodItemByName(foodToCopy[0].name);
|
|
|
|
|
selectedFood = foodData;
|
|
|
|
|
getInputById('foodSearchInput').value = foodToCopy[0].name;
|
|
|
|
|
getInputById('gramAmount').value = foodToCopy[0].grams;
|
|
|
|
|
previewCalories(foodData)
|
|
|
|
|
|
|
|
|
|
// Set UI to copy mode (not edit mode)
|
|
|
|
|
getDivById('add-foot-title').innerHTML = 'Copy Food Entry';
|
|
|
|
|
getButtonById('add-food-btn').innerHTML = 'Add Food';
|
|
|
|
|
getButtonById('cancel-food-btn').style.display = 'inline-block';
|
|
|
|
|
|
|
|
|
|
// Clear edit-specific hidden inputs to ensure we're in add mode
|
|
|
|
|
getInputById('foodIdToUpdate').value = '';
|
|
|
|
|
getInputById('foodTimeToUpdate').value = '';
|
|
|
|
|
getInputById('foodCaloriesToUpdate').value = '';
|
|
|
|
|
|
|
|
|
|
// Scroll to top
|
|
|
|
|
const titleDiv = getDivById('add-foot-title') as HTMLElement;
|
|
|
|
|
if (titleDiv) {
|
|
|
|
|
titleDiv.scrollIntoView({
|
|
|
|
|
behavior: 'smooth',
|
|
|
|
|
block: 'start'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hideLoading();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Set food to copy error:', error);
|
2025-10-30 11:29:19 -03:00
|
|
|
if (error instanceof Error) {
|
|
|
|
|
swal('Oh no!', 'Failed to load food entry: ' + error.message, 'error');
|
|
|
|
|
}
|
2025-09-13 23:49:29 +00:00
|
|
|
} finally {
|
|
|
|
|
hideLoading();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-03 21:03:46 -03:00
|
|
|
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) {
|
2025-09-04 12:51:43 -03:00
|
|
|
setFoodToEdit(foodId);
|
2025-09-03 21:03:46 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-13 23:49:29 +00:00
|
|
|
function copyCardHandler(e: Event) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
|
|
const card = e.currentTarget as HTMLElement;
|
|
|
|
|
const foodId = card.getAttribute('data-food-id');
|
|
|
|
|
|
|
|
|
|
if (foodId) {
|
|
|
|
|
setFoodToCopy(foodId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-07 16:33:56 -03:00
|
|
|
const setupEditAndDeleteEvents = () => {
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-13 23:49:29 +00:00
|
|
|
// Setup copy card events
|
|
|
|
|
const copyCards = getButtonListByClassName('btn-copy');
|
|
|
|
|
Array.from(copyCards).forEach((card: HTMLElement) => {
|
|
|
|
|
card.removeEventListener('click', copyCardHandler);
|
|
|
|
|
card.addEventListener('click', copyCardHandler);
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-03 21:03:46 -03:00
|
|
|
// 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
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updateCurrentDate = () => {
|
|
|
|
|
const date = new Date(selectedDate);
|
|
|
|
|
getDivById('currentDate').textContent = date.toLocaleDateString('en-US', {
|
|
|
|
|
weekday: 'long',
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: 'long',
|
|
|
|
|
day: 'numeric'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 12:51:43 -03:00
|
|
|
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);
|
2025-09-04 17:12:14 -03:00
|
|
|
|
|
|
|
|
// Save or update monthly total calories
|
|
|
|
|
const monthlyDocumentId = getDocumentIdForToday();
|
|
|
|
|
if (!monthlyDocumentId?.documentId) {
|
|
|
|
|
const monthlyCreated = await AppwriteDB.createMonthlyCaloryForDay(selectedDate, entry.calories);
|
|
|
|
|
updateDocumentIdForToday(monthlyCreated.$id, monthlyCreated.totalCalories);
|
|
|
|
|
} else {
|
|
|
|
|
const gramsToRemove = parseInt(getInputById('foodCaloriesToUpdate').value);
|
|
|
|
|
const totalCalories = monthlyDocumentId?.totalCalories + entry.calories - gramsToRemove;
|
|
|
|
|
const monthlyUpdated = await AppwriteDB.updateMonthlyCaloryForDay(monthlyDocumentId?.documentId, selectedDate, totalCalories);
|
|
|
|
|
updateDocumentIdForToday(monthlyUpdated.$id, monthlyUpdated.totalCalories);
|
|
|
|
|
}
|
2025-09-07 16:33:56 -03:00
|
|
|
delay(QUICK_DELAY);
|
2025-09-04 12:51:43 -03:00
|
|
|
clearEditing();
|
|
|
|
|
selectDate(selectedDate);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Update food error:', error);
|
2025-10-30 11:29:19 -03:00
|
|
|
if (error instanceof Error) {
|
|
|
|
|
swal('Oh no!', 'Failed to update food entry: ' + error.message, 'error');
|
|
|
|
|
}
|
2025-09-04 12:51:43 -03:00
|
|
|
} finally {
|
|
|
|
|
hideLoading();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 17:12:14 -03:00
|
|
|
const getDocumentIdForToday = (): DailyTotalCalories | null => {
|
2025-10-30 11:30:06 -03:00
|
|
|
let today = selectedDate.getDate();
|
|
|
|
|
// New rule: today number will be the sum of the day, month, and year
|
|
|
|
|
if (selectedDate >= new Date('2025-10-31')) {
|
|
|
|
|
// New rule: today number will be the sum of the day, month, and year
|
|
|
|
|
today = selectedDate.getDate() + selectedDate.getMonth() + 1 + selectedDate.getFullYear();
|
|
|
|
|
}
|
2025-09-07 16:33:56 -03:00
|
|
|
const todayRecord = appState.calendarMonthlyCalories.filter(x => x.day === today);
|
2025-09-04 17:12:14 -03:00
|
|
|
if (todayRecord.length > 0) {
|
|
|
|
|
return todayRecord[0];
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 11:30:06 -03:00
|
|
|
/**
|
|
|
|
|
* Get the document ID for a specific day.
|
|
|
|
|
* The day parameter is the day of the month (1-31) or the sum of day, month, and year for dates after October 30th, 2025.
|
|
|
|
|
*
|
|
|
|
|
* @param day - The day of the month (1-31) or the sum of day, month, and year for dates after October 30th, 2025.
|
|
|
|
|
* @returns The document ID for the day or 0 if not found.
|
|
|
|
|
*/
|
2025-09-04 17:12:14 -03:00
|
|
|
const getDocumentIdForDay = (day: number): number => {
|
2025-09-07 16:33:56 -03:00
|
|
|
const todayRecord = appState.calendarMonthlyCalories.filter(x => x.day === day);
|
2025-09-04 17:12:14 -03:00
|
|
|
return todayRecord.length > 0 ? todayRecord[0].totalCalories : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updateDocumentIdForToday = (id: string, totalCalories: number): void => {
|
2025-10-30 11:29:19 -03:00
|
|
|
let today = selectedDate.getDate();
|
2025-10-30 11:30:06 -03:00
|
|
|
// Adopt new rule starting in October 31st, 2025
|
|
|
|
|
if (selectedDate >= new Date('2025-10-31')) {
|
2025-10-30 11:29:19 -03:00
|
|
|
// New rule: today number will be the sum of the day, month, and year
|
|
|
|
|
today = selectedDate.getDate() + selectedDate.getMonth() + 1 + selectedDate.getFullYear();
|
|
|
|
|
}
|
2025-09-04 17:12:14 -03:00
|
|
|
updateDocumentIdForDay(id, totalCalories, today);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 11:29:19 -03:00
|
|
|
/**
|
|
|
|
|
* Update or add the document ID for a specific day in the app state. The day is based on the date selected.
|
|
|
|
|
* The day parameter is the day of the month (1-31) or the sum of day, month, and year for dates after October 30th, 2025.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} id - The document ID to set.
|
|
|
|
|
* @param {number} totalCalories - The total calories for the day.
|
|
|
|
|
* @param {number} day - The day of the month (1-31) or the sum of day, month, and year.
|
|
|
|
|
*/
|
2025-09-04 17:12:14 -03:00
|
|
|
const updateDocumentIdForDay = (id: string, totalCalories: number, day: number): void => {
|
2025-09-07 16:33:56 -03:00
|
|
|
const record = appState.calendarMonthlyCalories.filter(x => x.day === day);
|
2025-09-04 17:12:14 -03:00
|
|
|
if (record.length === 0) {
|
2025-09-07 16:33:56 -03:00
|
|
|
appState.calendarMonthlyCalories.push({
|
2025-09-04 17:12:14 -03:00
|
|
|
documentId: id,
|
|
|
|
|
day: day,
|
|
|
|
|
totalCalories: totalCalories
|
|
|
|
|
});
|
2025-09-07 16:33:56 -03:00
|
|
|
} else {
|
|
|
|
|
appState.calendarMonthlyCalories.forEach((record) => {
|
2025-09-04 17:12:14 -03:00
|
|
|
if (record.day === day) {
|
|
|
|
|
record.documentId = id;
|
|
|
|
|
record.totalCalories = totalCalories;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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-09-04 12:51:43 -03:00
|
|
|
swal('Hey!', 'Please select a food item and enter the 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);
|
|
|
|
|
|
2025-09-04 17:12:14 -03:00
|
|
|
// Save or update monthly total calories
|
|
|
|
|
const monthlyDocumentId = getDocumentIdForToday();
|
|
|
|
|
if (!monthlyDocumentId?.documentId) {
|
|
|
|
|
// create
|
|
|
|
|
const monthlyCreated = await AppwriteDB.createMonthlyCaloryForDay(selectedDate, entry.calories);
|
|
|
|
|
updateDocumentIdForToday(monthlyCreated.$id, monthlyCreated.totalCalories);
|
|
|
|
|
} else {
|
|
|
|
|
// update
|
|
|
|
|
const totalCalories = monthlyDocumentId?.totalCalories + entry.calories;
|
|
|
|
|
const monthlyCreated = await AppwriteDB.updateMonthlyCaloryForDay(monthlyDocumentId?.documentId, selectedDate, totalCalories);
|
|
|
|
|
updateDocumentIdForToday(monthlyCreated.$id, monthlyCreated.totalCalories);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 16:00:31 -03:00
|
|
|
// Reload all entries to properly group them (this also updates totals and macros)
|
|
|
|
|
await loadFoodEntries(selectedDate);
|
2025-06-13 19:18:31 -03:00
|
|
|
|
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);
|
2025-12-10 12:21:17 -03:00
|
|
|
hideAINutritionCard();
|
2025-09-07 16:33:56 -03:00
|
|
|
await delay(QUICK_DELAY);
|
2025-09-04 17:12:14 -03:00
|
|
|
renderCalendar();
|
2025-06-12 18:30:11 -03:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Add food error:', error);
|
2025-10-30 11:29:19 -03:00
|
|
|
if (error instanceof Error) {
|
|
|
|
|
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-12-13 16:00:31 -03:00
|
|
|
// Meal grouping utility functions
|
|
|
|
|
function getMealPeriod(time: string): MealPeriod {
|
|
|
|
|
// Parse HH:MM format (24-hour time)
|
|
|
|
|
const [hours, minutes] = time.split(':').map(Number);
|
|
|
|
|
const totalMinutes = hours * 60 + minutes;
|
|
|
|
|
|
|
|
|
|
// Before 7 AM (0:00 - 6:59) = 0 - 419 minutes
|
|
|
|
|
if (totalMinutes < 420) return 'pre-workout';
|
|
|
|
|
|
|
|
|
|
// 7 AM - 9 AM (7:00 - 8:59) = 420 - 539 minutes
|
|
|
|
|
if (totalMinutes < 540) return 'breakfast';
|
|
|
|
|
|
|
|
|
|
// 9 AM - 12 PM (9:00 - 11:59) = 540 - 719 minutes
|
|
|
|
|
if (totalMinutes < 720) return 'second-breakfast';
|
|
|
|
|
|
|
|
|
|
// 12 PM - 3 PM (12:00 - 14:59) = 720 - 899 minutes
|
|
|
|
|
if (totalMinutes < 900) return 'lunch';
|
|
|
|
|
|
|
|
|
|
// 3 PM - 6 PM (15:00 - 17:59) = 900 - 1079 minutes
|
|
|
|
|
if (totalMinutes < 1080) return 'snacks';
|
|
|
|
|
|
|
|
|
|
// 6 PM - 8 PM (18:00 - 19:59) = 1080 - 1199 minutes
|
|
|
|
|
if (totalMinutes < 1200) return 'dinner';
|
|
|
|
|
|
|
|
|
|
// After 8 PM (20:00 - 23:59) = 1200+ minutes
|
|
|
|
|
return 'night-snacks';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getMealLabel(period: MealPeriod): string {
|
|
|
|
|
const labels: Record<MealPeriod, string> = {
|
|
|
|
|
'pre-workout': '🏋️ Pre-Workout',
|
|
|
|
|
'breakfast': '🌅 Breakfast',
|
|
|
|
|
'second-breakfast': '☕ Second Breakfast',
|
|
|
|
|
'lunch': '🍽️ Lunch',
|
|
|
|
|
'snacks': '🍪 Snacks',
|
|
|
|
|
'dinner': '🌙 Dinner',
|
|
|
|
|
'night-snacks': '🌃 Night Snacks'
|
|
|
|
|
};
|
|
|
|
|
return labels[period];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function groupFoodEntriesByMeal(entries: FoodStorage[]): MealGroup[] {
|
|
|
|
|
// Initialize all meal groups
|
|
|
|
|
const groups: Record<MealPeriod, MealGroup> = {
|
|
|
|
|
'pre-workout': {
|
|
|
|
|
period: 'pre-workout',
|
|
|
|
|
label: getMealLabel('pre-workout'),
|
|
|
|
|
entries: [],
|
|
|
|
|
totalCalories: 0,
|
|
|
|
|
totalProtein: 0,
|
|
|
|
|
totalFat: 0,
|
|
|
|
|
totalCarbs: 0,
|
|
|
|
|
isExpanded: true
|
|
|
|
|
},
|
|
|
|
|
'breakfast': {
|
|
|
|
|
period: 'breakfast',
|
|
|
|
|
label: getMealLabel('breakfast'),
|
|
|
|
|
entries: [],
|
|
|
|
|
totalCalories: 0,
|
|
|
|
|
totalProtein: 0,
|
|
|
|
|
totalFat: 0,
|
|
|
|
|
totalCarbs: 0,
|
|
|
|
|
isExpanded: true
|
|
|
|
|
},
|
|
|
|
|
'second-breakfast': {
|
|
|
|
|
period: 'second-breakfast',
|
|
|
|
|
label: getMealLabel('second-breakfast'),
|
|
|
|
|
entries: [],
|
|
|
|
|
totalCalories: 0,
|
|
|
|
|
totalProtein: 0,
|
|
|
|
|
totalFat: 0,
|
|
|
|
|
totalCarbs: 0,
|
|
|
|
|
isExpanded: true
|
|
|
|
|
},
|
|
|
|
|
'lunch': {
|
|
|
|
|
period: 'lunch',
|
|
|
|
|
label: getMealLabel('lunch'),
|
|
|
|
|
entries: [],
|
|
|
|
|
totalCalories: 0,
|
|
|
|
|
totalProtein: 0,
|
|
|
|
|
totalFat: 0,
|
|
|
|
|
totalCarbs: 0,
|
|
|
|
|
isExpanded: true
|
|
|
|
|
},
|
|
|
|
|
'snacks': {
|
|
|
|
|
period: 'snacks',
|
|
|
|
|
label: getMealLabel('snacks'),
|
|
|
|
|
entries: [],
|
|
|
|
|
totalCalories: 0,
|
|
|
|
|
totalProtein: 0,
|
|
|
|
|
totalFat: 0,
|
|
|
|
|
totalCarbs: 0,
|
|
|
|
|
isExpanded: true
|
|
|
|
|
},
|
|
|
|
|
'dinner': {
|
|
|
|
|
period: 'dinner',
|
|
|
|
|
label: getMealLabel('dinner'),
|
|
|
|
|
entries: [],
|
|
|
|
|
totalCalories: 0,
|
|
|
|
|
totalProtein: 0,
|
|
|
|
|
totalFat: 0,
|
|
|
|
|
totalCarbs: 0,
|
|
|
|
|
isExpanded: true
|
|
|
|
|
},
|
|
|
|
|
'night-snacks': {
|
|
|
|
|
period: 'night-snacks',
|
|
|
|
|
label: getMealLabel('night-snacks'),
|
|
|
|
|
entries: [],
|
|
|
|
|
totalCalories: 0,
|
|
|
|
|
totalProtein: 0,
|
|
|
|
|
totalFat: 0,
|
|
|
|
|
totalCarbs: 0,
|
|
|
|
|
isExpanded: true
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Group entries and calculate totals
|
|
|
|
|
entries.forEach(entry => {
|
|
|
|
|
const period = getMealPeriod(entry.time);
|
|
|
|
|
groups[period].entries.push(entry);
|
|
|
|
|
groups[period].totalCalories += entry.calories;
|
|
|
|
|
groups[period].totalProtein += entry.protein;
|
|
|
|
|
groups[period].totalFat += entry.fat;
|
|
|
|
|
groups[period].totalCarbs += entry.carbs;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Return only groups that have entries, in chronological order
|
|
|
|
|
const orderedPeriods: MealPeriod[] = [
|
2025-12-15 05:17:05 -03:00
|
|
|
'pre-workout',
|
2025-12-13 16:00:31 -03:00
|
|
|
'breakfast', 'second-breakfast', 'lunch', 'snacks', 'dinner', 'night-snacks'
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return orderedPeriods
|
|
|
|
|
.map(period => groups[period])
|
|
|
|
|
.filter(group => group.entries.length > 0);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-07 16:33:56 -03:00
|
|
|
// Load food entries from Appwrite for a given date
|
2025-06-12 18:30:11 -03:00
|
|
|
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);
|
2025-12-13 16:00:31 -03:00
|
|
|
|
|
|
|
|
const container = getDivById('foodCardsContainer');
|
|
|
|
|
container.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
if (entries.length === 0) {
|
|
|
|
|
container.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>
|
|
|
|
|
`;
|
|
|
|
|
} else {
|
|
|
|
|
// Convert entries to FoodStorage format
|
|
|
|
|
const foodEntries: FoodStorage[] = entries.map(entry => ({
|
2025-06-12 18:30:11 -03:00
|
|
|
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-12-13 16:00:31 -03:00
|
|
|
}));
|
2025-09-07 16:33:56 -03:00
|
|
|
|
2025-12-13 16:00:31 -03:00
|
|
|
// Group by meal period
|
|
|
|
|
const mealGroups = groupFoodEntriesByMeal(foodEntries);
|
2025-06-12 10:39:19 -03:00
|
|
|
|
2025-12-13 16:00:31 -03:00
|
|
|
// Render each meal group
|
|
|
|
|
mealGroups.forEach(group => {
|
|
|
|
|
renderMealGroup(group);
|
|
|
|
|
});
|
2025-09-02 18:21:59 -03:00
|
|
|
|
2025-12-13 16:00:31 -03:00
|
|
|
setupEditAndDeleteEvents();
|
|
|
|
|
updateTotalCalories();
|
|
|
|
|
}
|
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;
|
2026-02-12 11:13:51 -03:00
|
|
|
const bodyWeight = parseInt(settings[index].bodyWeight);
|
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) {
|
2026-02-12 11:13:51 -03:00
|
|
|
let percent = '';
|
|
|
|
|
if (bodyWeight > 0) {
|
|
|
|
|
const proteinPerKg = Math.round((parseInt(settings[index].proteinGoal) / bodyWeight) * 10) / 10;
|
|
|
|
|
percent = ` (${proteinPerKg} g/kg)`;
|
|
|
|
|
}
|
|
|
|
|
getDivById('proteinGoalText').textContent = `of ${settings[index].proteinGoal}${percent}`;
|
2025-06-13 19:18:31 -03:00
|
|
|
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) {
|
2026-02-12 11:13:51 -03:00
|
|
|
let percent = '';
|
|
|
|
|
if (bodyWeight > 0) {
|
|
|
|
|
const carboPerKg = Math.round((parseInt(settings[index].carboGoal) / bodyWeight) * 10) / 10;
|
|
|
|
|
percent = ` (${carboPerKg} g/kg)`;
|
|
|
|
|
}
|
|
|
|
|
getDivById('carboGoalText').textContent = `of ${settings[index].carboGoal}${percent}`;
|
2025-06-13 19:18:31 -03:00
|
|
|
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-10-30 11:29:19 -03:00
|
|
|
if (error instanceof Error) {
|
|
|
|
|
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-09-04 17:12:14 -03:00
|
|
|
|
|
|
|
|
// Delete from monthly total
|
|
|
|
|
const monthlyDocumentId = getDocumentIdForToday();
|
|
|
|
|
if (monthlyDocumentId?.documentId) {
|
|
|
|
|
// find calories from document
|
|
|
|
|
const targetDiv = document.querySelector(`.food-card[data-id="${documentId}"]`);
|
|
|
|
|
let existingToDelete = 0;
|
|
|
|
|
if (targetDiv) {
|
|
|
|
|
const divCalories = targetDiv.querySelectorAll('.calories-display');
|
|
|
|
|
if (divCalories && divCalories.length > 0) {
|
|
|
|
|
existingToDelete = parseInt(divCalories[0].innerHTML.replace(' cal', ''));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const totalCalories = monthlyDocumentId?.totalCalories - existingToDelete;
|
|
|
|
|
const monthlyCreated = await AppwriteDB.updateMonthlyCaloryForDay(monthlyDocumentId?.documentId, selectedDate, totalCalories);
|
|
|
|
|
updateDocumentIdForToday(monthlyCreated.$id, monthlyCreated.totalCalories);
|
2025-09-07 16:33:56 -03:00
|
|
|
await delay(QUICK_DELAY);
|
2025-09-04 17:12:14 -03:00
|
|
|
}
|
2025-06-12 18:30:11 -03:00
|
|
|
|
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-10-30 11:29:19 -03:00
|
|
|
if (error instanceof Error) {
|
|
|
|
|
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-12-13 16:00:31 -03:00
|
|
|
// Create a food card element (without appending it to the DOM)
|
|
|
|
|
function createFoodCard(foodData: FoodStorage): HTMLElement {
|
2025-09-02 18:21:59 -03:00
|
|
|
const card = document.createElement('div');
|
|
|
|
|
card.className = 'food-card';
|
2025-12-13 16:00:31 -03:00
|
|
|
card.setAttribute('data-id', foodData.id || '');
|
2025-09-03 21:03:46 -03:00
|
|
|
|
2025-09-07 16:33:56 -03:00
|
|
|
const foodFromDatabase = getFoodItemByName(foodData.name);
|
2025-10-30 11:33:11 -03:00
|
|
|
const isAlkalineStr = foodFromDatabase.info.alkaline ? ' (Alk)' : ' (Not Alk)';
|
2025-09-07 16:33:56 -03:00
|
|
|
|
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>
|
2025-09-04 15:02:43 -03:00
|
|
|
<div class="food-is-alkaline hidden">${foodData.alkaline ? 'alkaline' : ''}</div>
|
2025-09-02 18:21:59 -03:00
|
|
|
</div>
|
|
|
|
|
<div class="card-actions">
|
2025-10-30 11:33:11 -03:00
|
|
|
<span class="muted">${foodFromDatabase.info.category} ${isAlkalineStr}</span>
|
2025-09-07 16:33:56 -03:00
|
|
|
<div>
|
2025-12-13 16:00:31 -03:00
|
|
|
<button class="btn btn-copy" data-food-id="${foodData.id}">Copy</button>
|
|
|
|
|
<button class="btn btn-edit" data-food-id="${foodData.id}">Edit</button>
|
|
|
|
|
<button class="btn btn-delete" data-food-id="${foodData.id}">Delete</button>
|
2025-09-07 16:33:56 -03:00
|
|
|
</div>
|
2025-09-02 18:21:59 -03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
2025-12-13 16:00:31 -03:00
|
|
|
return card;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render a meal group with header and food items
|
|
|
|
|
function renderMealGroup(group: MealGroup) {
|
|
|
|
|
const container = getDivById('foodCardsContainer');
|
|
|
|
|
|
|
|
|
|
// Create meal group container
|
|
|
|
|
const groupDiv = document.createElement('div');
|
|
|
|
|
groupDiv.className = 'meal-group';
|
|
|
|
|
groupDiv.setAttribute('data-meal-period', group.period);
|
|
|
|
|
|
|
|
|
|
// Create meal group header (collapsible)
|
|
|
|
|
const headerDiv = document.createElement('div');
|
|
|
|
|
headerDiv.className = 'meal-group-header';
|
|
|
|
|
|
|
|
|
|
// Round nutritional values to 1 decimal place
|
|
|
|
|
const roundedProtein = Math.round(group.totalProtein * 10) / 10;
|
|
|
|
|
const roundedFat = Math.round(group.totalFat * 10) / 10;
|
|
|
|
|
const roundedCarbs = Math.round(group.totalCarbs * 10) / 10;
|
|
|
|
|
|
|
|
|
|
headerDiv.innerHTML = `
|
|
|
|
|
<div class="meal-group-info">
|
|
|
|
|
<div class="meal-group-label">${group.label}</div>
|
|
|
|
|
<div class="meal-group-summary">
|
|
|
|
|
<span class="meal-summary-item">${group.totalCalories} cal</span>
|
|
|
|
|
<span class="meal-summary-divider">•</span>
|
|
|
|
|
<span class="meal-summary-item">${roundedProtein}g protein</span>
|
|
|
|
|
<span class="meal-summary-divider">•</span>
|
|
|
|
|
<span class="meal-summary-item">${roundedFat}g fat</span>
|
|
|
|
|
<span class="meal-summary-divider">•</span>
|
|
|
|
|
<span class="meal-summary-item">${roundedCarbs}g carbs</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="meal-group-toggle">▼</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// Create food items container (collapsible)
|
|
|
|
|
const itemsDiv = document.createElement('div');
|
|
|
|
|
itemsDiv.className = 'meal-group-items';
|
|
|
|
|
if (!group.isExpanded) {
|
|
|
|
|
itemsDiv.classList.add('collapsed');
|
|
|
|
|
headerDiv.classList.add('collapsed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add each food entry to the group
|
|
|
|
|
group.entries.forEach(entry => {
|
|
|
|
|
const card = createFoodCard(entry);
|
|
|
|
|
itemsDiv.appendChild(card);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add click handler for collapsing/expanding
|
|
|
|
|
headerDiv.addEventListener('click', () => {
|
|
|
|
|
itemsDiv.classList.toggle('collapsed');
|
|
|
|
|
headerDiv.classList.toggle('collapsed');
|
2025-09-03 21:03:46 -03:00
|
|
|
|
2025-12-13 16:00:31 -03:00
|
|
|
// Rotate the toggle icon
|
|
|
|
|
const toggleIcon = headerDiv.querySelector('.meal-group-toggle');
|
|
|
|
|
if (toggleIcon) {
|
|
|
|
|
toggleIcon.classList.toggle('rotated');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
groupDiv.appendChild(headerDiv);
|
|
|
|
|
groupDiv.appendChild(itemsDiv);
|
|
|
|
|
container.appendChild(groupDiv);
|
2025-06-12 10:39:19 -03:00
|
|
|
}
|
|
|
|
|
|
2025-09-07 16:33:56 -03:00
|
|
|
// Update total calories and display it in the counter
|
|
|
|
|
// Also updates alkaline level percentage
|
|
|
|
|
function updateTotalCalories() {
|
2025-09-04 15:02:43 -03:00
|
|
|
const caloriesDiv = document.querySelectorAll('.calories-display');
|
|
|
|
|
let total = 0;
|
2025-09-03 21:03:46 -03:00
|
|
|
|
2025-09-04 15:02:43 -03:00
|
|
|
caloriesDiv.forEach((innerDiv: Element) => {
|
|
|
|
|
total += parseInt(innerDiv.innerHTML.replace(' cal', '')) || 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-06-18 16:56:11 -03:00
|
|
|
// Update alkaline level
|
2025-09-04 15:02:43 -03:00
|
|
|
const gramsDivs = document.querySelectorAll('.food-time');
|
|
|
|
|
let totalGrams = 0;
|
2025-06-18 16:56:11 -03:00
|
|
|
let indexMap: {index: number, grams: number}[] = [];
|
2025-09-04 15:02:43 -03:00
|
|
|
gramsDivs.forEach((innerDiv: Element, key: number) => {
|
|
|
|
|
const totalGramDiv = parseInt(innerDiv.innerHTML.split(' ')[2].replace('g', ''));
|
|
|
|
|
totalGrams += totalGramDiv || 0;
|
2025-06-18 16:56:11 -03:00
|
|
|
indexMap.push({
|
|
|
|
|
index: key,
|
2025-09-04 15:02:43 -03:00
|
|
|
grams: totalGramDiv || 0
|
2025-06-18 16:56:11 -03:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-04 15:02:43 -03:00
|
|
|
const alkalineDivs = document.querySelectorAll('.food-is-alkaline');
|
|
|
|
|
let totalAlkaline = 0;
|
|
|
|
|
alkalineDivs.forEach((alkaDiv: Element, key: number) => {
|
|
|
|
|
if (alkaDiv.innerHTML === '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;
|
|
|
|
|
updateCurrentDate();
|
2025-06-12 18:30:11 -03:00
|
|
|
await loadFoodEntries(date);
|
2025-11-03 11:25:39 -03:00
|
|
|
await fetchCaloriesCurrentMonth();
|
2025-06-12 10:39:19 -03:00
|
|
|
renderCalendar();
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 15:02:43 -03:00
|
|
|
const renderCalendar = async () => {
|
2025-06-12 10:39:19 -03:00
|
|
|
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;
|
2025-09-04 17:12:14 -03:00
|
|
|
const dayElement = createDayElement(day, true, year, month - 1, 0);
|
2025-06-12 10:39:19 -03:00
|
|
|
grid.appendChild(dayElement);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add days of current month
|
|
|
|
|
for (let day = 1; day <= daysInMonth; day++) {
|
2025-10-30 11:30:06 -03:00
|
|
|
// New rule starting October 31st, 2025
|
2025-10-30 11:29:19 -03:00
|
|
|
let adjustedDay = day;
|
2025-10-30 11:30:06 -03:00
|
|
|
if (currentViewDate >= new Date('2025-10-31')) {
|
2025-10-30 11:29:19 -03:00
|
|
|
adjustedDay = day + (month + 1) + year;
|
|
|
|
|
}
|
|
|
|
|
const dayCalories = getDocumentIdForDay(adjustedDay);
|
2025-09-04 17:12:14 -03:00
|
|
|
const dayElement = createDayElement(day, false, year, month, dayCalories);
|
2025-06-12 10:39:19 -03:00
|
|
|
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++) {
|
2025-09-04 17:12:14 -03:00
|
|
|
const dayElement = createDayElement(day, true, year, month + 1, 0);
|
2025-06-12 10:39:19 -03:00
|
|
|
grid.appendChild(dayElement);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 17:12:14 -03:00
|
|
|
function createDayElement(day: number, isOtherMonth: boolean, year: number, month: number, calories: number) {
|
2025-06-12 10:39:19 -03:00
|
|
|
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-09-04 17:12:14 -03:00
|
|
|
if (calories > 0) {
|
2025-06-12 10:39:19 -03:00
|
|
|
dayElement.innerHTML = `
|
2025-09-04 17:12:14 -03:00
|
|
|
${day}
|
|
|
|
|
<small class="muted">${calories}</small>
|
2025-06-12 10:39:19 -03:00
|
|
|
`;
|
|
|
|
|
} else {
|
|
|
|
|
dayElement.textContent = day.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dayElement;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 13:18:01 -03:00
|
|
|
// Share functionality
|
|
|
|
|
async function handleShareClick() {
|
|
|
|
|
if (!currentUser) {
|
|
|
|
|
swal('Hey!', 'Please log in to share your food log.', 'info');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (appState.isSharedView) {
|
|
|
|
|
swal('Info', 'You cannot share a shared view.', 'info');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
closeMobileMenu();
|
|
|
|
|
showLoading();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Get current day's food entries
|
|
|
|
|
const entries = await AppwriteDB.getFoodEntries(selectedDate);
|
|
|
|
|
|
|
|
|
|
if (entries.length === 0) {
|
|
|
|
|
hideLoading();
|
|
|
|
|
swal('Info', 'No food entries to share for this day.', 'info');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert entries to FoodStorage format
|
|
|
|
|
const foodEntries: FoodStorage[] = entries.map(entry => ({
|
|
|
|
|
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,
|
|
|
|
|
alkaline: entry.alkaline,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Create shared day snapshot
|
|
|
|
|
const sharedDay = await AppwriteDB.createSharedDay(selectedDate, foodEntries);
|
|
|
|
|
|
|
|
|
|
// Generate share URL
|
|
|
|
|
const shareUrl = `${window.location.origin}${window.location.pathname}?share=${sharedDay.shareId}`;
|
|
|
|
|
|
|
|
|
|
// Show modal with link
|
|
|
|
|
getInputById('share-link-input').value = shareUrl;
|
|
|
|
|
getDivById('share-modal').classList.remove('hidden');
|
|
|
|
|
|
|
|
|
|
hideLoading();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
hideLoading();
|
|
|
|
|
console.error('Share error:', error);
|
|
|
|
|
if (error instanceof Error) {
|
|
|
|
|
swal('Oh no!', 'Failed to create share link: ' + error.message, 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeShareModal() {
|
|
|
|
|
getDivById('share-modal').classList.add('hidden');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function copyShareLink() {
|
|
|
|
|
const shareInput = getInputById('share-link-input');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await navigator.clipboard.writeText(shareInput.value);
|
|
|
|
|
const copyBtn = document.getElementById('copy-share-link');
|
|
|
|
|
if (copyBtn) {
|
|
|
|
|
const originalText = copyBtn.textContent;
|
|
|
|
|
copyBtn.textContent = 'Copied!';
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
copyBtn.textContent = originalText;
|
|
|
|
|
}, 2000);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Copy error:', error);
|
|
|
|
|
swal('Oh no!', 'Failed to copy link to clipboard', 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for share parameter on page load
|
|
|
|
|
async function checkSharedView() {
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
|
const shareId = urlParams.get('share');
|
|
|
|
|
|
|
|
|
|
if (shareId) {
|
|
|
|
|
showLoading();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const sharedDay = await AppwriteDB.getSharedDay(shareId);
|
|
|
|
|
|
|
|
|
|
// Set shared view mode
|
|
|
|
|
appState.isSharedView = true;
|
|
|
|
|
appState.sharedData = {
|
|
|
|
|
shareId: sharedDay.shareId,
|
|
|
|
|
userId: sharedDay.userId,
|
|
|
|
|
userName: sharedDay.userName,
|
|
|
|
|
date: sharedDay.date,
|
|
|
|
|
foodEntries: sharedDay.foodEntries,
|
|
|
|
|
createdAt: sharedDay.createdAt
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Parse the date and set as selected date (as local date, not UTC)
|
|
|
|
|
const [year, month, day] = sharedDay.date.split('-').map(Number);
|
|
|
|
|
selectedDate = new Date(year, month - 1, day);
|
|
|
|
|
currentViewDate = new Date(year, month - 1, day);
|
|
|
|
|
|
|
|
|
|
// Show shared view banner
|
|
|
|
|
const banner = getDivById('shared-view-banner');
|
|
|
|
|
banner.classList.remove('hidden');
|
|
|
|
|
getDivById('shared-user-name').textContent = sharedDay.userName;
|
|
|
|
|
getDivById('shared-date').textContent = selectedDate.toLocaleDateString('en-US', {
|
|
|
|
|
weekday: 'long',
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: 'long',
|
|
|
|
|
day: 'numeric'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Hide auth section and show app content
|
2025-12-03 16:42:52 -03:00
|
|
|
getDivById('auth-buttons').classList.add('hidden');
|
2025-10-30 13:18:01 -03:00
|
|
|
getDivById('user-info').classList.add('hidden');
|
|
|
|
|
getDivById('app-content').classList.remove('hidden');
|
|
|
|
|
|
|
|
|
|
// Render shared day view
|
|
|
|
|
renderSharedDayView();
|
|
|
|
|
|
|
|
|
|
// Apply read-only mode
|
|
|
|
|
applyReadOnlyMode();
|
|
|
|
|
|
|
|
|
|
hideLoading();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
hideLoading();
|
|
|
|
|
console.error('Load shared day error:', error);
|
|
|
|
|
swal('Oh no!', 'Failed to load shared day: ' + (error instanceof Error ? error.message : 'Unknown error'), 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderSharedDayView() {
|
|
|
|
|
if (!appState.sharedData) return;
|
|
|
|
|
|
|
|
|
|
updateCurrentDate();
|
|
|
|
|
|
|
|
|
|
// Parse food entries
|
|
|
|
|
const foodEntries: FoodStorage[] = JSON.parse(appState.sharedData.foodEntries);
|
|
|
|
|
|
|
|
|
|
// Clear container
|
2025-12-13 16:00:31 -03:00
|
|
|
const container = getDivById('foodCardsContainer');
|
|
|
|
|
container.innerHTML = '';
|
2025-10-30 13:18:01 -03:00
|
|
|
|
2025-12-13 16:00:31 -03:00
|
|
|
if (foodEntries.length === 0) {
|
|
|
|
|
container.innerHTML = `
|
|
|
|
|
<div class="empty-state">
|
|
|
|
|
<div class="empty-state-icon">🍽️</div>
|
|
|
|
|
<div>No food items logged yet.</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
} else {
|
|
|
|
|
// Group by meal period
|
|
|
|
|
const mealGroups = groupFoodEntriesByMeal(foodEntries);
|
|
|
|
|
|
|
|
|
|
// Render each meal group
|
|
|
|
|
mealGroups.forEach(group => {
|
|
|
|
|
renderMealGroup(group);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setupEditAndDeleteEvents();
|
|
|
|
|
updateTotalCalories();
|
|
|
|
|
}
|
2025-10-30 13:18:01 -03:00
|
|
|
|
|
|
|
|
// Calculate and display totals
|
|
|
|
|
let totalCalories = 0;
|
|
|
|
|
let totalProtein = 0;
|
|
|
|
|
let totalFat = 0;
|
|
|
|
|
let totalCarbs = 0;
|
|
|
|
|
let totalFiber = 0;
|
|
|
|
|
|
|
|
|
|
foodEntries.forEach(entry => {
|
|
|
|
|
totalCalories += entry.calories;
|
|
|
|
|
totalProtein += entry.protein;
|
|
|
|
|
totalFat += entry.fat;
|
|
|
|
|
totalCarbs += entry.carbs;
|
|
|
|
|
totalFiber += entry.fiber;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
getDivById('caloriesCounter').textContent = totalCalories.toString();
|
|
|
|
|
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();
|
|
|
|
|
getDivById('fiberValue').textContent = (Math.round(totalFiber * 10) / 10).toString();
|
|
|
|
|
|
|
|
|
|
// Update alkaline level
|
|
|
|
|
updateTotalCalories();
|
|
|
|
|
|
|
|
|
|
renderCalendar();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyReadOnlyMode() {
|
|
|
|
|
// Hide add food section
|
|
|
|
|
const addFoodSection = document.querySelector('.add-food-section');
|
|
|
|
|
if (addFoodSection) {
|
|
|
|
|
addFoodSection.classList.add('read-only-disabled');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Hide edit/delete/copy buttons
|
|
|
|
|
const actionButtons = document.querySelectorAll('.card-actions button');
|
|
|
|
|
actionButtons.forEach(button => {
|
|
|
|
|
(button as HTMLElement).style.display = 'none';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Hide calendar navigation
|
|
|
|
|
const calendarSection = document.querySelector('.calendar-section');
|
|
|
|
|
if (calendarSection) {
|
|
|
|
|
calendarSection.classList.add('read-only-disabled');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-16 17:33:52 -03:00
|
|
|
(window as any).selectFood = selectFood;
|