feat: add calories in the calendar

This commit is contained in:
2025-09-04 17:12:14 -03:00
parent f8440ac3e5
commit b891d077a4
7 changed files with 191 additions and 23 deletions
+1
View File
@@ -164,6 +164,7 @@
<!-- Hidden inputs for state --> <!-- Hidden inputs for state -->
<input type="hidden" id="foodIdToUpdate"> <input type="hidden" id="foodIdToUpdate">
<input type="hidden" id="foodTimeToUpdate"> <input type="hidden" id="foodTimeToUpdate">
<input type="hidden" id="foodCaloriesToUpdate">
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
+2 -1
View File
@@ -1,4 +1,5 @@
VITE_APPWRITE_ENDPOINT=here VITE_APPWRITE_ENDPOINT=here
VITE_APPWRITE_DBID=here VITE_APPWRITE_DBID=here
VITE_APPWRITE_FOODENTRIESID=here VITE_APPWRITE_FOODENTRIESID=here
VITE_APPWRITE_USERSETTINGSID=here VITE_APPWRITE_USERSETTINGSID=here
VITE_APPWRITE_MONTLYCALORIESID=here
+68 -1
View File
@@ -1,5 +1,5 @@
import { Client, Account, Databases, Query } from 'appwrite'; import { Client, Account, Databases, Query } from 'appwrite';
import { FoodStorage, UserSettings } from './types'; import { DailyTotalCalories, FoodStorage, UserSettings } from './types';
// Initialize Appwrite client // Initialize Appwrite client
const client = new 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 DATABASE_ID = import.meta.env.VITE_APPWRITE_DBID
export const FOOD_ENTRIES_COLLECTION_ID = import.meta.env.VITE_APPWRITE_FOODENTRIESID 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 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 // Auth helper functions
export class AppwriteAuth { 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 // get user's settings
static async getUserSettings() { static async getUserSettings() {
try { try {
+108 -19
View File
@@ -1,6 +1,6 @@
import { foodDatabase } from './foodDatabase.js'; import { foodDatabase } from './foodDatabase.js';
import { getButtonById, getButtonListByClassName, getDivById, getInputById, showFoodPreview } from './HtmlUtil.ts'; 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 { AppwriteAuth, AppwriteDB } from './appwrite.js';
import swal from 'sweetalert'; import swal from 'sweetalert';
import { Models } from 'appwrite'; import { Models } from 'appwrite';
@@ -12,6 +12,7 @@ let searchTimeout: number | null = null;
let selectedFood: FoodItem | null = null; // review here let selectedFood: FoodItem | null = null; // review here
let currentHighlightIndex: number = -1; let currentHighlightIndex: number = -1;
let currentResults: FoodItem[] = []; let currentResults: FoodItem[] = [];
let calendarMonthlyCalories: DailyTotalCalories[] = [];
// Authentication state // Authentication state
let currentUser: any | null = null; let currentUser: any | null = null;
@@ -45,7 +46,7 @@ async function initializeAuth() {
if (isLoggedIn) { if (isLoggedIn) {
currentUser = await AppwriteAuth.getCurrentUser(); currentUser = await AppwriteAuth.getCurrentUser();
showMainApp(); await showMainApp();
selectDate(selectedDate); selectDate(selectedDate);
} else { } else {
showAuthForms(); showAuthForms();
@@ -87,7 +88,7 @@ async function handleRegister(e: SubmitEvent) {
await AppwriteAuth.login(email, password); await AppwriteAuth.login(email, password);
currentUser = await AppwriteAuth.getCurrentUser(); currentUser = await AppwriteAuth.getCurrentUser();
showMainApp(); await showMainApp();
selectDate(selectedDate); selectDate(selectedDate);
swal('Registration successful! Welcome to Food Tracker.'); swal('Registration successful! Welcome to Food Tracker.');
@@ -111,7 +112,7 @@ async function handleLogin(e: SubmitEvent) {
await AppwriteAuth.login(email, password); await AppwriteAuth.login(email, password);
currentUser = await AppwriteAuth.getCurrentUser(); currentUser = await AppwriteAuth.getCurrentUser();
showMainApp(); await showMainApp();
selectDate(selectedDate); selectDate(selectedDate);
} catch (error) { } catch (error) {
@@ -300,7 +301,7 @@ function showAuthForms() {
appContent?.classList.add('hidden'); appContent?.classList.add('hidden');
} }
function showMainApp() { async function showMainApp() {
authSection?.classList.add('hidden'); authSection?.classList.add('hidden');
userInfo?.classList.remove('hidden'); userInfo?.classList.remove('hidden');
appContent?.classList.remove('hidden'); appContent?.classList.remove('hidden');
@@ -312,6 +313,14 @@ function showMainApp() {
// Update date display // Update date display
updateCurrentDate(); 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 // Show/hide loading overlay
@@ -645,6 +654,7 @@ async function setFoodToEdit(foodId: string) {
getButtonById('add-food-btn').innerHTML = 'Save Food'; getButtonById('add-food-btn').innerHTML = 'Save Food';
getInputById('foodIdToUpdate').value = foodId; getInputById('foodIdToUpdate').value = foodId;
getInputById('foodTimeToUpdate').value = foodToEdit[0].time; getInputById('foodTimeToUpdate').value = foodToEdit[0].time;
getInputById('foodCaloriesToUpdate').value = foodToEdit[0].calories;
hideLoading(); hideLoading();
} catch (error) { } catch (error) {
@@ -772,6 +782,19 @@ const updateFood = async () => {
// Save to Appwrite // Save to Appwrite
await AppwriteDB.updateFoodEntry(foodId, entry); 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(); clearEditing();
selectDate(selectedDate); selectDate(selectedDate);
} catch (error) { } 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 // Add food to the log
const addFood = async () => { const addFood = async () => {
if (!currentUser) { if (!currentUser) {
@@ -827,6 +887,19 @@ const addFood = async () => {
// Save to Appwrite // Save to Appwrite
const savedEntry = await AppwriteDB.saveFoodEntry(entry); 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 // Add to HTML table with Appwrite document ID
addFoodToTable(entry, savedEntry.$id); addFoodToTable(entry, savedEntry.$id);
@@ -849,14 +922,13 @@ const addFood = async () => {
carboDiv.textContent = (Math.round(totalCarbs * 10) / 10).toString(); carboDiv.textContent = (Math.round(totalCarbs * 10) / 10).toString();
fiberDiv.textContent = (Math.round(totalFiber * 10) / 10).toString(); fiberDiv.textContent = (Math.round(totalFiber * 10) / 10).toString();
// Update alkaline level
const div = getDivById('alkaline-level');
// Reset form // Reset form
selectedFood = null; selectedFood = null;
getInputById('foodSearchInput').value = ''; getInputById('foodSearchInput').value = '';
gramAmount.value = '100'; gramAmount.value = '100';
showFoodPreview(false); showFoodPreview(false);
await delay(1);
renderCalendar();
} catch (error) { } catch (error) {
console.error('Add food error:', error); console.error('Add food error:', error);
swal('Oh no!', 'Failed to add food entry: ' + error.message, '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 // Handle food deletion
async function handleDeleteFood(documentId: string) { async function handleDeleteFood(documentId: string) {
if (!documentId) return; if (!documentId) return;
@@ -1000,6 +1074,24 @@ async function handleDeleteFood(documentId: string) {
try { try {
// Delete from Appwrite // Delete from Appwrite
await AppwriteDB.deleteFoodEntry(documentId); 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); selectDate(selectedDate);
} catch (error) { } catch (error) {
@@ -1172,13 +1264,14 @@ const renderCalendar = async () => {
for (let i = startingDayOfWeek - 1; i >= 0; i--) { for (let i = startingDayOfWeek - 1; i >= 0; i--) {
const day = daysInPrevMonth - 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); grid.appendChild(dayElement);
} }
// Add days of current month // Add days of current month
for (let day = 1; day <= daysInMonth; day++) { 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); grid.appendChild(dayElement);
} }
@@ -1187,12 +1280,12 @@ const renderCalendar = async () => {
const remainingCells = 42 - totalCells; const remainingCells = 42 - totalCells;
for (let day = 1; day <= remainingCells; day++) { 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); 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; const dayElement = document.createElement('div') as HTMLElement;
dayElement.className = 'calendar-day'; dayElement.className = 'calendar-day';
@@ -1217,14 +1310,10 @@ function createDayElement(day: number, isOtherMonth: boolean, year: number, mont
} }
// Check if this day has data // Check if this day has data
const dayData = []; // FIXME get from AppWrite if (calories > 0) {
if (dayData.length > 0) {
dayElement.classList.add('has-data');
const totalCalories = 0; // dayData.reduce((sum, entry) => sum + entry.calories, 0);
dayElement.innerHTML = ` dayElement.innerHTML = `
<div>${day}</div> ${day}
<div class="day-calories">${totalCalories} cal</div> <small class="muted">${calories}</small>
`; `;
} else { } else {
dayElement.textContent = day.toString(); dayElement.textContent = day.toString();
+6
View File
@@ -27,3 +27,9 @@ export type UserSettings = {
carboGoal: number; carboGoal: number;
fiberGoal: number; fiberGoal: number;
}; };
export type DailyTotalCalories = {
documentId: string;
day: number;
totalCalories: number;
};
+4
View File
@@ -685,6 +685,10 @@ label {
display: none; display: none;
} }
.muted {
color: #ccc;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.container { .container {
margin: 0px; margin: 0px;
+2 -2
View File
@@ -1,9 +1,9 @@
# TODO # TODO
- [x] Allow search without special characters - [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] Add food type: Alkaline or Acid
- [x] Display the percentage of Alkaline daily - [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 - [x] Add logo
- [ ] Group food added by time and consider a meal - [ ] Group food added by time and consider a meal