From b891d077a4c9cfb30e1435021b939f6b50b52a03 Mon Sep 17 00:00:00 2001 From: Ricardo Campos Date: Thu, 4 Sep 2025 17:12:14 -0300 Subject: [PATCH] feat: add calories in the calendar --- index.html | 1 + src/.env.example | 3 +- src/appwrite.ts | 69 ++++++++++++++++++++++++- src/index.ts | 127 ++++++++++++++++++++++++++++++++++++++++------- src/types.ts | 6 +++ style.css | 4 ++ todo.md | 4 +- 7 files changed, 191 insertions(+), 23 deletions(-) diff --git a/index.html b/index.html index c4f5700..89b3cf4 100644 --- a/index.html +++ b/index.html @@ -164,6 +164,7 @@ +
diff --git a/src/.env.example b/src/.env.example index 0493e4a..ed5e221 100644 --- a/src/.env.example +++ b/src/.env.example @@ -1,4 +1,5 @@ VITE_APPWRITE_ENDPOINT=here VITE_APPWRITE_DBID=here VITE_APPWRITE_FOODENTRIESID=here -VITE_APPWRITE_USERSETTINGSID=here \ No newline at end of file +VITE_APPWRITE_USERSETTINGSID=here +VITE_APPWRITE_MONTLYCALORIESID=here \ No newline at end of file diff --git a/src/appwrite.ts b/src/appwrite.ts index 3dee252..707b38c 100644 --- a/src/appwrite.ts +++ b/src/appwrite.ts @@ -1,5 +1,5 @@ import { Client, Account, Databases, Query } from 'appwrite'; -import { FoodStorage, UserSettings } from './types'; +import { DailyTotalCalories, FoodStorage, UserSettings } from './types'; // Initialize Appwrite client const client = new Client(); @@ -16,6 +16,7 @@ export const databases = new Databases(client); export const DATABASE_ID = import.meta.env.VITE_APPWRITE_DBID export const FOOD_ENTRIES_COLLECTION_ID = import.meta.env.VITE_APPWRITE_FOODENTRIESID export const USER_SETTINGS_COLLECTION_ID = import.meta.env.VITE_APPWRITE_USERSETTINGSID +export const MONTHLY_CALORIES_COLLECTION_ID = import.meta.env.VITE_APPWRITE_MONTLYCALORIESID // Auth helper functions export class AppwriteAuth { @@ -160,6 +161,72 @@ export class AppwriteDB { } } + // Get all calories for a particular date + static async getMonthlyCalories(date: Date) { + try { + const user = await account.get(); + const localDateTime = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString(); + + const response = await databases.listDocuments( + DATABASE_ID, + MONTHLY_CALORIES_COLLECTION_ID, + [ + Query.equal('userId', user.$id), + Query.startsWith('date', localDateTime.substring(0, 7)), + ] + ); + console.debug('Food entries retrieved:', response); + return response.documents; + } catch (error) { + console.error('Get all calories for month error:', error); + throw error; + } + } + + static async createMonthlyCaloryForDay(date: Date, totalCalories: number) { + try { + const localDateTime = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString(); + + const response = await databases.createDocument( + DATABASE_ID, + MONTHLY_CALORIES_COLLECTION_ID , + 'unique()', + { + userId: (await account.get()).$id, + date: localDateTime.substring(0, 10), + totalCalories: totalCalories + } + ); + console.debug('User settings saved:', response); + return response; + } catch (error) { + console.error('Save user settings error:', error); + throw error; + } + } + + static async updateMonthlyCaloryForDay(id: string, date: Date, totalCalories: number) { + try { + const localDateTime = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString(); + + const response = await databases.updateDocument( + DATABASE_ID, + MONTHLY_CALORIES_COLLECTION_ID , + id, + { + userId: (await account.get()).$id, + date: localDateTime.substring(0, 10), + totalCalories: totalCalories + } + ); + console.debug('User settings saved:', response); + return response; + } catch (error) { + console.error('Save user settings error:', error); + throw error; + } + } + // get user's settings static async getUserSettings() { try { diff --git a/src/index.ts b/src/index.ts index cd94e58..4802743 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import { foodDatabase } from './foodDatabase.js'; import { getButtonById, getButtonListByClassName, getDivById, getInputById, showFoodPreview } from './HtmlUtil.ts'; -import { FoodItem, FoodStorage } from './types.js'; +import { DailyTotalCalories, FoodItem, FoodStorage } from './types.js'; import { AppwriteAuth, AppwriteDB } from './appwrite.js'; import swal from 'sweetalert'; import { Models } from 'appwrite'; @@ -12,6 +12,7 @@ let searchTimeout: number | null = null; let selectedFood: FoodItem | null = null; // review here let currentHighlightIndex: number = -1; let currentResults: FoodItem[] = []; +let calendarMonthlyCalories: DailyTotalCalories[] = []; // Authentication state let currentUser: any | null = null; @@ -45,7 +46,7 @@ async function initializeAuth() { if (isLoggedIn) { currentUser = await AppwriteAuth.getCurrentUser(); - showMainApp(); + await showMainApp(); selectDate(selectedDate); } else { showAuthForms(); @@ -87,7 +88,7 @@ async function handleRegister(e: SubmitEvent) { await AppwriteAuth.login(email, password); currentUser = await AppwriteAuth.getCurrentUser(); - showMainApp(); + await showMainApp(); selectDate(selectedDate); swal('Registration successful! Welcome to Food Tracker.'); @@ -111,7 +112,7 @@ async function handleLogin(e: SubmitEvent) { await AppwriteAuth.login(email, password); currentUser = await AppwriteAuth.getCurrentUser(); - showMainApp(); + await showMainApp(); selectDate(selectedDate); } catch (error) { @@ -300,7 +301,7 @@ function showAuthForms() { appContent?.classList.add('hidden'); } -function showMainApp() { +async function showMainApp() { authSection?.classList.add('hidden'); userInfo?.classList.remove('hidden'); appContent?.classList.remove('hidden'); @@ -312,6 +313,14 @@ function showMainApp() { // Update date display updateCurrentDate(); + + // Get entries with total + const entries = await AppwriteDB.getMonthlyCalories(selectedDate); + entries.forEach((entry) => { + const calories = parseInt(entry.totalCalories); + const day = parseInt(entry.date.substring(8)); + updateDocumentIdForDay(entry.$id, calories, day); + }); } // Show/hide loading overlay @@ -645,6 +654,7 @@ async function setFoodToEdit(foodId: string) { getButtonById('add-food-btn').innerHTML = 'Save Food'; getInputById('foodIdToUpdate').value = foodId; getInputById('foodTimeToUpdate').value = foodToEdit[0].time; + getInputById('foodCaloriesToUpdate').value = foodToEdit[0].calories; hideLoading(); } catch (error) { @@ -772,6 +782,19 @@ const updateFood = async () => { // Save to Appwrite await AppwriteDB.updateFoodEntry(foodId, entry); + + // 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); + } + delay(1); clearEditing(); selectDate(selectedDate); } catch (error) { @@ -782,6 +805,43 @@ const updateFood = async () => { } } +const getDocumentIdForToday = (): DailyTotalCalories | null => { + const today = selectedDate.getDate(); + const todayRecord = calendarMonthlyCalories.filter(x => x.day === today); + if (todayRecord.length > 0) { + return todayRecord[0]; + } + return null; +} + +const getDocumentIdForDay = (day: number): number => { + const todayRecord = calendarMonthlyCalories.filter(x => x.day === day); + return todayRecord.length > 0 ? todayRecord[0].totalCalories : 0; +} + +const updateDocumentIdForToday = (id: string, totalCalories: number): void => { + const today = selectedDate.getDate(); + updateDocumentIdForDay(id, totalCalories, today); +} + +const updateDocumentIdForDay = (id: string, totalCalories: number, day: number): void => { + const record = calendarMonthlyCalories.filter(x => x.day === day); + if (record.length === 0) { + calendarMonthlyCalories.push({ + documentId: id, + day: day, + totalCalories: totalCalories + }); + } else { + calendarMonthlyCalories.forEach((record) => { + if (record.day === day) { + record.documentId = id; + record.totalCalories = totalCalories; + } + }); + } +} + // Add food to the log const addFood = async () => { if (!currentUser) { @@ -827,6 +887,19 @@ const addFood = async () => { // Save to Appwrite const savedEntry = await AppwriteDB.saveFoodEntry(entry); + // 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); + } + // Add to HTML table with Appwrite document ID addFoodToTable(entry, savedEntry.$id); @@ -849,14 +922,13 @@ const addFood = async () => { carboDiv.textContent = (Math.round(totalCarbs * 10) / 10).toString(); fiberDiv.textContent = (Math.round(totalFiber * 10) / 10).toString(); - // Update alkaline level - const div = getDivById('alkaline-level'); - // Reset form selectedFood = null; getInputById('foodSearchInput').value = ''; gramAmount.value = '100'; showFoodPreview(false); + await delay(1); + renderCalendar(); } catch (error) { console.error('Add food error:', error); swal('Oh no!', 'Failed to add food entry: ' + error.message, 'error'); @@ -980,6 +1052,8 @@ async function loadFoodEntries(date: Date) { } } +const delay = (seconds: number) => new Promise(resolve => setTimeout(resolve, seconds * 1000)); + // Handle food deletion async function handleDeleteFood(documentId: string) { if (!documentId) return; @@ -1000,6 +1074,24 @@ async function handleDeleteFood(documentId: string) { try { // Delete from Appwrite await AppwriteDB.deleteFoodEntry(documentId); + + // 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); + await delay(1); + } selectDate(selectedDate); } catch (error) { @@ -1172,13 +1264,14 @@ const renderCalendar = async () => { for (let i = startingDayOfWeek - 1; i >= 0; i--) { const day = daysInPrevMonth - i; - const dayElement = createDayElement(day, true, year, month - 1); + const dayElement = createDayElement(day, true, year, month - 1, 0); grid.appendChild(dayElement); } // Add days of current month for (let day = 1; day <= daysInMonth; day++) { - const dayElement = createDayElement(day, false, year, month); + const dayCalories = getDocumentIdForDay(day); + const dayElement = createDayElement(day, false, year, month, dayCalories); grid.appendChild(dayElement); } @@ -1187,12 +1280,12 @@ const renderCalendar = async () => { const remainingCells = 42 - totalCells; for (let day = 1; day <= remainingCells; day++) { - const dayElement = createDayElement(day, true, year, month + 1); + const dayElement = createDayElement(day, true, year, month + 1, 0); grid.appendChild(dayElement); } } -function createDayElement(day: number, isOtherMonth: boolean, year: number, month: number) { +function createDayElement(day: number, isOtherMonth: boolean, year: number, month: number, calories: number) { const dayElement = document.createElement('div') as HTMLElement; dayElement.className = 'calendar-day'; @@ -1217,14 +1310,10 @@ function createDayElement(day: number, isOtherMonth: boolean, year: number, mont } // Check if this day has data - const dayData = []; // FIXME get from AppWrite - if (dayData.length > 0) { - dayElement.classList.add('has-data'); - const totalCalories = 0; // dayData.reduce((sum, entry) => sum + entry.calories, 0); - + if (calories > 0) { dayElement.innerHTML = ` -
${day}
-
${totalCalories} cal
+ ${day} + ${calories} `; } else { dayElement.textContent = day.toString(); diff --git a/src/types.ts b/src/types.ts index 1fe51b6..36b6d28 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,3 +27,9 @@ export type UserSettings = { carboGoal: number; fiberGoal: number; }; + +export type DailyTotalCalories = { + documentId: string; + day: number; + totalCalories: number; +}; diff --git a/style.css b/style.css index e530d25..8f5f69d 100644 --- a/style.css +++ b/style.css @@ -685,6 +685,10 @@ label { display: none; } +.muted { + color: #ccc; +} + @media (max-width: 768px) { .container { margin: 0px; diff --git a/todo.md b/todo.md index 1a06ad3..40d2c56 100644 --- a/todo.md +++ b/todo.md @@ -1,9 +1,9 @@ # TODO - [x] Allow search without special characters -- [ ] Get the calories from the last 7 days and display in the calendar +- [x] Get the calories from the last 7 days and display in the calendar - [x] Add food type: Alkaline or Acid - [x] Display the percentage of Alkaline daily -- [ ] Remove the table and create a list-like component +- [x] Remove the table and create a list-like component - [x] Add logo - [ ] Group food added by time and consider a meal