diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 37618ae..a9b5f0b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,6 +3,8 @@ name: Deploy to GitHub Pages on: push: branches: [ main ] + pull_request: + branches: [ main ] # Allow only one concurrent deployment concurrency: diff --git a/.gitignore b/.gitignore index b947077..614b797 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ node_modules/ dist/ +.env +senha.txt +.vscode/ diff --git a/auth.css b/auth.css new file mode 100644 index 0000000..4513f3a --- /dev/null +++ b/auth.css @@ -0,0 +1,227 @@ +/* Authentication Header Styles */ +.auth-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 15px 0; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + position: sticky; + top: 0; + z-index: 1000; +} + +.auth-container { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.logo h2 { + margin: 0; + font-size: 1.5rem; + font-weight: 600; +} + +.auth-section { + display: flex; + align-items: center; +} + +.auth-form { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border-radius: 10px; + padding: 25px; + min-width: 300px; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.auth-form h3 { + margin: 0 0 20px 0; + text-align: center; + font-size: 1.3rem; + font-weight: 500; +} + +.auth-form .form-group { + margin-bottom: 15px; +} + +.auth-form input { + width: 100%; + padding: 12px; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 6px; + background: rgba(255, 255, 255, 0.1); + color: white; + font-size: 1rem; + backdrop-filter: blur(10px); + box-sizing: border-box; +} + +.auth-form input::placeholder { + color: rgba(255, 255, 255, 0.7); +} + +.auth-form input:focus { + outline: none; + border-color: rgba(255, 255, 255, 0.6); + background: rgba(255, 255, 255, 0.15); +} + +.auth-btn { + width: 100%; + padding: 12px; + background: rgba(255, 255, 255, 0.2); + color: white; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 6px; + cursor: pointer; + font-size: 1rem; + font-weight: 600; + transition: all 0.3s ease; + backdrop-filter: blur(10px); +} + +.auth-btn:hover { + background: rgba(255, 255, 255, 0.3); + transform: translateY(-1px); +} + +.auth-switch { + text-align: center; + margin: 15px 0 0 0; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.8); +} + +.auth-switch a { + color: white; + text-decoration: none; + font-weight: 600; +} + +.auth-switch a:hover { + text-decoration: underline; +} + +.user-info { + display: flex; + align-items: center; + gap: 15px; +} + +.user-details { + display: flex; + align-items: center; + gap: 15px; +} + +.user-details span { + font-weight: 500; + font-size: 1rem; +} + +.logout-btn { + padding: 8px 16px; + background: rgba(255, 255, 255, 0.2); + color: white; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.3s ease; + backdrop-filter: blur(10px); +} + +.logout-btn:hover { + background: rgba(255, 255, 255, 0.3); +} + +/* Loading Overlay */ +.loading-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 9999; + backdrop-filter: blur(5px); +} + +.loading-spinner { + width: 50px; + height: 50px; + border: 4px solid rgba(255, 255, 255, 0.3); + border-top: 4px solid white; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 20px; +} + +.loading-overlay p { + color: white; + font-size: 1.1rem; + font-weight: 500; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Utility Classes */ +.hidden { + display: none !important; +} + +.display-none { + display: none; +} + +.display-block { + display: block; +} + +/* Update main content spacing */ +#app-content { + padding-top: 20px; +} + +/* Mobile Responsive for Auth */ +@media (max-width: 768px) { + .auth-container { + flex-direction: column; + gap: 15px; + padding: 0 15px; + } + + .logo { + order: 1; + } + + .auth-section, + .user-info { + order: 2; + width: 100%; + justify-content: center; + } + + .auth-form { + min-width: 280px; + padding: 20px; + } + + .user-details { + flex-direction: column; + gap: 10px; + text-align: center; + } +} diff --git a/foodDatabase.js b/foodDatabase.js deleted file mode 100644 index c15f517..0000000 --- a/foodDatabase.js +++ /dev/null @@ -1,74 +0,0 @@ -export const foodDatabase = { - // Fruits - 'Frutas - Abacate': { calories: 96, protein: 1.2, fat: 8.4, carbs: 6, fiber: 6.3 }, - 'Frutas - Abacaxi': { calories: 48, protein: 0.9, fat: 0.1, carbs: 12.3, fiber: 1 }, - 'Frutas - Banana Nanica': { calories: 92, protein: 1.4, fat: 0.1, carbs: 23.8, fiber: 1.9 }, - 'Frutas - Laranja': { calories: 37, protein: 1, fat: 0.1, carbs: 8.9, fiber: 0.8 }, - 'Frutas - Maçã': { calories: 56, protein: 0.3, fat: 0, carbs: 15.2, fiber: 1.3 }, - 'Frutas - Manga Palmer': { calories: 72, protein: 0.4, fat: 0.2, carbs: 19.4, fiber: 1.6 }, - 'Frutas - Mamão': { calories: 40, protein: 0.5, fat: 0.1, carbs: 10.4, fiber: 1 }, - 'Frutas - Melancia': { calories: 33, protein: 0.9, fat: 0, carbs: 8.1, fiber: 0.1 }, - 'Frutas - Melão': { calories: 29, protein: 0.7, fat: 0, carbs: 7.5, fiber: 0.3 }, - 'Frutas - Uva': { calories: 53, protein: 0.7, fat: 0.2, carbs: 13.6, fiber: 0.9 }, - - // Vegetables - 'Vegetais - Agrião': { calories: 17, protein: 2.7, fat: 0.2, carbs: 2.3, fiber: 2.1 }, - 'Vegetais - Brócolis (cozido)': { calories: 25, protein: 2.1, fat: 0.5, carbs: 4.4, fiber: 3.4 }, - 'Vegetais - Cenoura (cozida)': { calories: 30, protein: 0.8, fat: 0.2, carbs: 6.7, fiber: 2.6 }, - 'Vegetais - Couve flor (cozida)': { calories: 19, protein: 1.2, fat: 0.3, carbs: 3.9, fiber: 2.1 }, - 'Vegetais - Ervilha (cozida)': { calories: 88, protein: 7.5, fat: 0.5, carbs: 14.2, fiber: 9.7 }, - 'Vegetais - Espinafre': { calories: 16, protein: 2, fat: 0.2, carbs: 2.6, fiber: 2.1 }, - 'Vegetais - Feijão preto (cozido)': { calories: 77, protein: 4.5, fat: 0.5, carbs: 14, fiber: 8.4 }, - 'Vegetais - Lentilha (cozida)': { calories: 93, protein: 6.3, fat: 0.5, carbs: 16.3, fiber: 7.9 }, - 'Vegetais - Mandioca (cozida)': { calories: 125, protein: 0.6, fat: 0.3, carbs: 30.1, fiber: 1.6 }, - 'Vegetais - Pepino': { calories: 10, protein: 0.9, fat: 0, carbs: 2, fiber: 1.1 }, - 'Vegetais - Rúcula': { calories: 13, protein: 1.8, fat: 0.1, carbs: 2.2, fiber: 1.7 }, - 'Vegetais - Spirulina': { calories: 290, protein: 57.5, fat: 23.9, carbs: 7.7, fiber: 0 }, - 'Vegetais - Tomate': { calories: 15, protein: 1.1, fat: 0.2, carbs: 3.1, fiber: 1.2 }, - - // Carbohydrates - 'Carboidratos - Batata doce (cozida)': { calories: 77, protein: 0.6, fat: 0.1, carbs: 18.4, fiber: 2.2 }, - 'Carboidratos - Batata doce (crua)': { calories: 118, protein: 1.3, fat: 0.1, carbs: 28.2, fiber: 2.6 }, - 'Carboidratos - Batata inglesa (cozida)': { calories: 52, protein: 1.2, fat: 0, carbs: 11.9, fiber: 1.3 }, - 'Carboidratos - Batata inglesa (crua)': { calories: 64, protein: 1.8, fat: 0, carbs: 14.7, fiber: 1.2 }, - 'Carboidratos - Pão Francês': { calories: 300, protein: 8, fat: 3.1, carbs: 58.6, fiber: 2.3 }, - 'Carboidrados - Polenta (cozida)': { calories: 192, protein: 4.8, fat: 1.2, carbs: 40, fiber: 3.8 }, - - // Proteins - 'Proteínas - Atum Sólido (natural)': { calories: 135, protein: 28.3, fat: 2.7, carbs: 0, fiber: 0 }, - 'Proteínas - Filé de Tilápia (cozido)': { calories: 128, protein: 26.1, fat: 2.6, carbs: 0, fiber: 0 }, - 'Proteínas - Ovo inteiro (cozido)': { calories: 146, protein: 13.3, fat: 9.5, carbs: 0.6, fiber: 0 }, - 'Proteínas - Ovo clara (cozido)': { calories: 59, protein: 13.4, fat: 0.1, carbs: 0, fiber: 0 }, - 'Proteínas - PTS (crua)': { calories: 288, protein: 52, fat: 0, carbs: 20, fiber: 14 }, - 'Proteínas - Patinho moído (grelhado)': { calories: 219, protein: 35.9, fat: 7.3, carbs: 0, fiber: 0 }, - 'Proteínas - Soja em Pó Growth': { calories: 400, protein: 86.7, fat: 5, carbs: 3.3, fiber: 0 }, - 'Proteínas - Sobrecoxa s/pele (cozida)': { calories: 245, protein: 24.9, fat: 15.4, carbs: 0, fiber: 0 }, - 'Proteínas - Tofu': { calories: 65, protein: 6.5, fat: 4, carbs: 2, fiber: 0.75 }, - - // Grains - 'Grãos - Arroz integral (cozido)': { calories: 130, protein: 2.7, fat: 0.3, carbs: 28, fiber: 0.4 }, - 'Grãos - Aveia': { calories: 394, protein: 13.9, fat: 8.5, carbs: 66.6, fiber: 9.1 }, - 'Grãos - Grão de Bico (cozido)': { calories: 180, protein: 9.5, fat: 3, carbs: 30, fiber: 8.6 }, - 'Grãos - Pão integral de forma': { calories: 253, protein: 9.4, fat: 3.7, carbs: 49.9, fiber: 6.9 }, - 'Grãos - Pipoca c/óleo': { calories: 448, protein: 9.9, fat: 15.9, carbs: 70.3, fiber: 14.3 }, - 'Grãos - Pipoca s/óleo': { calories: 178, protein: 3.3, fat: 7.2, carbs: 25, fiber: 4.3 }, - 'Grãos - Quinoa (cozida)': { calories: 158, protein: 5.55, fat: 2.5, carbs: 29, fiber: 2.5 }, - - // Nuts & Seeds - 'Castanhas - Amêndoas': { calories: 581, protein: 18.6, fat: 47.3, carbs: 29.5, fiber: 11.6 }, - 'Castanhas - Amendoin natural': { calories: 611, protein: 26, fat: 49, carbs: 16, fiber: 8.5 }, - 'Castanhas - Castanha de Caju': { calories: 570, protein: 18.5, fat: 46.3, carbs: 29.1, fiber: 3.7 }, - 'Castanhas - Castanha do Pará': { calories: 643, protein: 14.5, fat: 63.5, carbs: 15.1, fiber: 7.9 }, - 'Sementes - Semente de Chia': { calories: 490, protein: 15.6, fat: 30.7, carbs: 43.8, fiber: 37.7 }, - 'Sementes - Semente de Linhaça': { calories: 495, protein: 14.1, fat: 32.3, carbs: 43.3, fiber: 33.5 }, - 'Sementes - Semente de Gergelim': { calories: 584, protein: 21.2, fat: 50.4, carbs: 21.6, fiber: 11.9 }, - - // Snacks - 'Lanches - Barra de Proteína Vegana Growth': { calories: 400, protein: 25, fat: 22, carbs: 25, fiber: 8 }, - 'Lanches - Goiabada': { calories: 320, protein: 0, fat: 0, carbs: 80, fiber: 4.5 }, - 'Lanches - Mel Silvestre': { calories: 230, protein: 0, fat: 0, carbs: 65, fiber: 0 }, - 'Lanches - Melado': { calories: 297, protein: 0, fat: 0, carbs: 76.6, fiber: 0 }, - 'Lanches - Pasta de Amendoim Growth': { calories: 544, protein: 27, fat: 33, carbs: 20, fiber: 8 }, - 'Lanches - Goma de Tapioca': { calories: 226, protein: 0, fat: 0, carbs: 57, fiber: 0 }, - 'Lanches - 70% Nibs Garoto': { calories: 544, protein: 8.4, fat: 40, carbs: 34, fiber: 0 } -}; diff --git a/index.html b/index.html index 2c19241..fbdf1da 100644 --- a/index.html +++ b/index.html @@ -5,94 +5,184 @@ Daily Calorie Tracker + -
-

🍎 Daily Calorie Tracker

- -
- -
-
0
-
Calories Today
-
- -
-
-
0
-
Protein (g)
+ +
+
+ -
-
0
-
Fat (g)
-
-
-
0
-
Carbs (g)
-
-
-
0
-
Fiber (g)
-
-
- -
-

Add Food

-
-
- - + + +
+ +
+

Login

+
+
+ +
+
+ +
+ +

+ Don't have an account? + Register here +

+
-
- - + + + -
- -
-
-
Food Log
-
- - - - - - - - - - - - - - - - - - -
IDTimeFoodGramsCaloriesProtein (g)Fat (g)Carbs (g)Fiber (g)Action
+ +
+
-
-
- -
Month Year
- -
-
- -
-
+ + - + +
+
+

🍎 Daily Calorie Tracker

+ +
+ +
+
0
+
Calories Today
+
+ +
+
+
0
+
Protein (g)
+
+
+
0
+
Fat (g)
+
+
+
0
+
Carbs (g)
+
+
+
0
+
Fiber (g)
+
+
+ +
+

Add Food

+
+
+ + +
+
+ + +
+ +
+ +
+ +
+
Food Log
+
+ + + + + + + + + + + + + + + + + + +
IDTimeFoodGramsCaloriesProtein (g)Fat (g)Carbs (g)Fiber (g)Action
+
+
+ +
+
+ +
Month Year
+ +
+
+ +
+
+
+
+ + diff --git a/index.js b/index.js deleted file mode 100644 index addd93c..0000000 --- a/index.js +++ /dev/null @@ -1,349 +0,0 @@ -import { foodDatabase } from './foodDatabase.js'; - -// App state -let selectedDate = new Date().toISOString().split('T')[0]; -let currentViewDate = new Date(); - -// Initialize the app -function init() { - populateFoodSelect(); - updateCurrentDate(); - loadDayData(selectedDate); - renderCalendar(); - addEvents(); -} - -function getFoodProportion(grams, foodData) { - const multiplier = grams / 100; // Database values are per 100g - return { - calories: Math.round(foodData.calories * multiplier), - protein: Math.round(foodData.protein * multiplier * 10) / 10, - fat: Math.round(foodData.fat * multiplier * 10) / 10, - carbs: Math.round(foodData.carbs * multiplier * 10) / 10, - fiber: Math.round(foodData.fiber * multiplier * 10) / 10 - }; -} - -function previewCalories() { - const foodSelect = document.getElementById('foodSelect'); - const gramAmount = document.getElementById('gramAmount'); - - if (!foodSelect.value) { - document.getElementById('food-preview').innerHTML = ''; - document.getElementById('food-preview').classList = 'add-food-form-item display-none'; - return; - } - - let grams = 100; - if (gramAmount && gramAmount.value && parseFloat(gramAmount.value) > 0) { - grams = parseFloat(gramAmount.value); - } - - const foodName = foodSelect.value; - const foodData = foodDatabase[foodName]; - const proportion = getFoodProportion(grams, foodData); - - const textArray = []; - textArray.push(`Calories in ${grams}g: ${proportion.calories}; `); - textArray.push(`Protein: ${proportion.protein}g; `); - textArray.push(`Fat: ${proportion.fat}g; `); - textArray.push(`Carbohydrates: ${proportion.carbs}g; `); - textArray.push(`Fiber: ${proportion.fiber}g;`); - - document.getElementById('food-preview').innerHTML = textArray.join(''); - document.getElementById('food-preview').classList = 'add-food-form-item display-block'; -} - -function addEvents() { - document.getElementById('foodSelect').addEventListener('change', () => { - previewCalories(); - }); - - document.getElementById('gramAmount').addEventListener('change', () => { - previewCalories(); - }); - - document.getElementById('add-food-btn').addEventListener('click', () => { - addFood(); - }); - - document.getElementById('next-month-btn').addEventListener('click', () => { - nextMonth(); - }); - - document.getElementById('previous-month-btn').addEventListener('click', () => { - previousMonth(); - }); -} - -function addDeleteEvents() { - const elements = document.getElementsByClassName('delete-food-entry'); - Array.from(elements).forEach(el => { - el.addEventListener('click', (e) => { - const row = e.target.closest('tr'); - const entryId = row.querySelector('.hidden-column').textContent; - deleteEntry(selectedDate, entryId); - }); - }); -} - -// Populate food select dropdown -function populateFoodSelect() { - const select = document.getElementById('foodSelect'); - select.innerHTML = ''; - - const sortedFoods = Object.keys(foodDatabase).sort(); - sortedFoods.forEach(food => { - const option = document.createElement('option'); - option.value = food; - option.textContent = food.charAt(0).toUpperCase() + food.slice(1); - select.appendChild(option); - }); -} - -// Update current date display -function updateCurrentDate() { - const date = new Date(selectedDate); - const options = { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric' - }; - document.getElementById('currentDate').textContent = date.toLocaleDateString('en-US', options); -} - -// Add food to the log -function addFood() { - const foodSelect = document.getElementById('foodSelect'); - const gramAmount = document.getElementById('gramAmount'); - - if (!foodSelect.value || !gramAmount.value) { - alert('Please select a food item and enter amount'); - return; - } - - const foodName = foodSelect.value; - const grams = parseFloat(gramAmount.value); - const foodData = foodDatabase[foodName]; - - // Calculate nutrition for the amount - const proportion = getFoodProportion(grams, foodData); - const entry = { - id: Date.now(), - name: foodName, - grams: grams, - calories: proportion.calories, - protein: proportion.protein, - fat: proportion.fat, - carbs: proportion.carbs, - fiber: proportion.fiber, - time: new Date().toLocaleTimeString('en-US', { - hour12: false, - hour: '2-digit', - minute: '2-digit' - }) - }; - - // Save to localStorage - const dayData = getDayData(selectedDate); - dayData.push(entry); - saveDayData(selectedDate, dayData); - - // Update display - loadDayData(selectedDate); - renderCalendar(); - - // Reset form - foodSelect.value = ''; - gramAmount.value = '100'; - document.getElementById('food-preview').innerHTML = ''; - document.getElementById('food-preview').classList = 'add-food-form-item display-none'; -} - -// Get day data from localStorage -function getDayData(date) { - const key = `calories_${date}`; - return JSON.parse(localStorage.getItem(key)) || []; -} - -// Save day data to localStorage -function saveDayData(date, data) { - const key = `calories_${date}`; - localStorage.setItem(key, JSON.stringify(data)); -} - -// Load and display day data -function loadDayData(date) { - const dayData = getDayData(date); - - // Update counters - let totalCalories = 0; - let totalProtein = 0; - let totalFat = 0; - let totalCarbs = 0; - let totalFiber = 0; - - dayData.forEach(entry => { - totalCalories += entry.calories; - totalProtein += entry.protein; - totalFat += entry.fat; - totalCarbs += entry.carbs; - totalFiber += entry.fiber; - }); - - document.getElementById('caloriesCounter').textContent = totalCalories; - document.getElementById('proteinValue').textContent = Math.round(totalProtein * 10) / 10; - document.getElementById('fatValue').textContent = Math.round(totalFat * 10) / 10; - document.getElementById('carboValue').textContent = Math.round(totalCarbs * 10) / 10; - document.getElementById('fiberValue').textContent = Math.round(totalFiber * 10) / 10; - - // Update table - const tbody = document.getElementById('foodTableBody'); - tbody.innerHTML = ''; - - dayData.forEach(entry => { - const row = document.createElement('tr'); - row.innerHTML = ` - ${entry.id} - ${entry.time} - ${entry.name} - ${entry.grams} - ${entry.calories} - ${entry.protein} - ${entry.fat} - ${entry.carbs} - ${entry.fiber} - - `; - tbody.appendChild(row); - }); - addDeleteEvents(); -} - -// Delete an entry -function deleteEntry(date, entryId) { - if (confirm('Are you sure you want to delete this entry?')) { - let dayData = getDayData(date); - dayData = dayData.filter(entry => entry.id !== parseInt(entryId)); - saveDayData(date, dayData); - loadDayData(selectedDate); - renderCalendar(); - } -} - -// Calendar functions -function previousMonth() { - currentViewDate.setMonth(currentViewDate.getMonth() - 1); - renderCalendar(); -} - -function nextMonth() { - currentViewDate.setMonth(currentViewDate.getMonth() + 1); - renderCalendar(); -} - -function selectDate(date) { - selectedDate = date; - updateCurrentDate(); - loadDayData(selectedDate); - renderCalendar(); -} - -function renderCalendar() { - const year = currentViewDate.getFullYear(); - const month = currentViewDate.getMonth(); - - // Update calendar title - const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', - 'July', 'August', 'September', 'October', 'November', 'December']; - document.getElementById('calendarTitle').textContent = `${monthNames[month]} ${year}`; - - // Clear calendar - const grid = document.getElementById('calendarGrid'); - grid.innerHTML = ''; - - // Add day headers - const dayHeaders = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - dayHeaders.forEach(day => { - const header = document.createElement('div'); - header.className = 'calendar-day-header'; - header.textContent = day; - grid.appendChild(header); - }); - - // Get first day of month and number of days - const firstDay = new Date(year, month, 1); - const lastDay = new Date(year, month + 1, 0); - const daysInMonth = lastDay.getDate(); - const startingDayOfWeek = firstDay.getDay(); - - // Add empty cells for previous month - const prevMonth = new Date(year, month, 0); - const daysInPrevMonth = prevMonth.getDate(); - - for (let i = startingDayOfWeek - 1; i >= 0; i--) { - const day = daysInPrevMonth - i; - const dayElement = createDayElement(day, true, year, month - 1); - grid.appendChild(dayElement); - } - - // Add days of current month - for (let day = 1; day <= daysInMonth; day++) { - const dayElement = createDayElement(day, false, year, month); - grid.appendChild(dayElement); - } - - // Add days from next month to fill the grid - const totalCells = grid.children.length - 7; - const remainingCells = 42 - totalCells; - - for (let day = 1; day <= remainingCells; day++) { - const dayElement = createDayElement(day, true, year, month + 1); - grid.appendChild(dayElement); - } -} - -function createDayElement(day, isOtherMonth, year, month) { - const dayElement = document.createElement('div'); - 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 { - dayElement.onclick = () => selectDate(dateString); - } - - // 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 - if (!isOtherMonth && dateString === selectedDate) { - dayElement.classList.add('selected'); - } - - // Check if this day has data - const dayData = getDayData(dateString); - if (dayData.length > 0) { - dayElement.classList.add('has-data'); - const totalCalories = dayData.reduce((sum, entry) => sum + entry.calories, 0); - - dayElement.innerHTML = ` -
${day}
-
${totalCalories} cal
- `; - } else { - dayElement.textContent = day; - } - - return dayElement; -} - -// Initialize when page loads -document.addEventListener('DOMContentLoaded', init); diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..5379eda Binary files /dev/null and b/logo.png differ diff --git a/package-lock.json b/package-lock.json index 3def26f..baa52d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,12 @@ "name": "calories-tracker", "version": "1.0.0", "license": "ISC", + "dependencies": { + "appwrite": "^18.1.1", + "sweetalert": "^2.1.2" + }, "devDependencies": { + "typescript": "^5.8.3", "vite": "^6.3.5" } }, @@ -724,6 +729,18 @@ "dev": true, "license": "MIT" }, + "node_modules/appwrite": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/appwrite/-/appwrite-18.1.1.tgz", + "integrity": "sha512-krwHjuwJcF+9Ig2+nqOEKMA/5kPIFhwwZsaLc7Gb8y2oP6EnG4ZMRPeHTFscdevOtVQj2Ax92cYYWAEvzlrc7A==", + "license": "BSD-3-Clause" + }, + "node_modules/es6-object-assign": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", + "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==", + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", @@ -863,6 +880,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/promise-polyfill": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz", + "integrity": "sha512-g0LWaH0gFsxovsU7R5LrrhHhWAWiHRnh1GPrhXnPgYsDkIqjRYUYSZEsej/wtleDrz5xVSIDbeKfidztp2XHFQ==", + "license": "MIT" + }, "node_modules/rollup": { "version": "4.43.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz", @@ -913,6 +936,16 @@ "node": ">=0.10.0" } }, + "node_modules/sweetalert": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/sweetalert/-/sweetalert-2.1.2.tgz", + "integrity": "sha512-iWx7X4anRBNDa/a+AdTmvAzQtkN1+s4j/JJRWlHpYE8Qimkohs8/XnFcWeYHH2lMA8LRCa5tj2d244If3S/hzA==", + "license": "MIT", + "dependencies": { + "es6-object-assign": "^1.1.0", + "promise-polyfill": "^6.0.2" + } + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -930,6 +963,20 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/vite": { "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", diff --git a/package.json b/package.json index 2190a29..27200ca 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,11 @@ "license": "ISC", "description": "", "devDependencies": { + "typescript": "^5.8.3", "vite": "^6.3.5" + }, + "dependencies": { + "appwrite": "^18.1.1", + "sweetalert": "^2.1.2" } } diff --git a/src/HtmlUtil.ts b/src/HtmlUtil.ts new file mode 100644 index 0000000..ab95799 --- /dev/null +++ b/src/HtmlUtil.ts @@ -0,0 +1,35 @@ +export const getInputById = (elementId: string): HTMLInputElement => { + const el = document.getElementById(elementId) as HTMLInputElement; + if (!el) { + throw new Error(`Input Element id ${elementId} not found!`); + } + return el; +}; + +export const getDivById = (elementId: string): HTMLElement => { + const el = document.getElementById(elementId) as HTMLElement; + if (!el) { + throw new Error(`HTML Element id ${elementId} not found!`); + } + return el; +} + +export const getButtonById = (elementId: string): HTMLButtonElement => { + const el = document.getElementById(elementId) as HTMLButtonElement; + if (!el) { + throw new Error(`HTML Button Element id ${elementId} not found!`); + } + return el; +} + +export const getButtonListByClassName = (name: string): Array => { + const el = document.getElementsByClassName(name); + if (!el) { + throw new Error(`HTML Button List Elements not found for class ${name}!`); + } + return Array.from(el) as Array; +} + +export const showFoodPreview = (show: boolean) => { + getDivById('food-preview').classList = `add-food-form-item ${show ? 'display-block' : 'display-none'}`; +} diff --git a/src/appwrite.ts b/src/appwrite.ts new file mode 100644 index 0000000..d636882 --- /dev/null +++ b/src/appwrite.ts @@ -0,0 +1,152 @@ +import { Client, Account, Databases, Query } from 'appwrite'; +import { FoodStorage } from './types'; + +// Initialize Appwrite client +const client = new Client(); + +client + .setEndpoint('https://cloud.appwrite.io/v1') // Your Appwrite endpoint + .setProject("684ac3bf000ee8950f5a"); // Replace with your project ID + +// Initialize services +export const account = new Account(client); +export const databases = new Databases(client); + +// Configuration constants +export const DATABASE_ID = '684b38db000fac8c781c'; // Replace with your database ID +export const FOOD_ENTRIES_COLLECTION_ID = '684b38e300336fc605dc'; // Replace with your collection ID + +// Auth helper functions +export class AppwriteAuth { + // Register new user + static async register(email: string, password: string, name: string) { + try { + const response = await account.create('unique()', email, password, name); + console.debug('User registered:', response); + return response; + } catch (error) { + console.error('Registration error:', error); + throw error; + } + } + + // Login user + static async login(email: string, password: string) { + try { + const session = await account.createEmailPasswordSession(email, password); + console.debug('User logged in:', session); + return session; + } catch (error) { + console.error('Login error:', error); + throw error; + } + } + + // Logout user + static async logout() { + try { + await account.deleteSession('current'); + console.debug('User logged out'); + } catch (error) { + console.error('Logout error:', error); + throw error; + } + } + + // Get current user + static async getCurrentUser() { + try { + const user = await account.get(); + return user; + } catch (error) { + console.error('Get user error:', error); + return null; + } + } + + // Check if user is logged in + static async isLoggedIn() { + try { + await account.get(); + return true; + } catch { + return false; + } + } +} + +// Database helper functions +export class AppwriteDB { + // Save food entry + static async saveFoodEntry(entryData: FoodStorage) { + try { + const response = await databases.createDocument( + DATABASE_ID, + FOOD_ENTRIES_COLLECTION_ID, + 'unique()', + { + ...entryData, + userId: (await account.get()).$id, + createdAt: new Date().toISOString() + } + ); + console.debug('Food entry saved:', response); + return response; + } catch (error) { + console.error('Save food entry error:', error); + throw error; + } + } + + // Get user's food entries for a specific date + static async getFoodEntries(date: Date) { + try { + const user = await account.get(); + + const response = await databases.listDocuments( + DATABASE_ID, + FOOD_ENTRIES_COLLECTION_ID, + [ + Query.equal('userId', user.$id), + Query.equal('date', date.toISOString().split('T')[0]), + ] + ); + console.debug('Food entries retrieved:', response); + return response.documents; + } catch (error) { + console.error('Get food entries error:', error); + throw error; + } + } + + // Delete food entry + static async deleteFoodEntry(documentId: string) { + try { + await databases.deleteDocument( + DATABASE_ID, + FOOD_ENTRIES_COLLECTION_ID, + documentId + ); + console.debug('Food entry deleted:', documentId); + } catch (error) { + console.error('Delete food entry error:', error); + throw error; + } + } + + // Get all food entries for a user (for calendar view) + static async getAllUserEntries() { + try { + const user = await account.get(); + const response = await databases.listDocuments( + DATABASE_ID, + FOOD_ENTRIES_COLLECTION_ID, + [Query.equal('userId', user.$id)] + ); + return response.documents; + } catch (error) { + console.error('Get all entries error:', error); + throw error; + } + } +} diff --git a/src/foodDatabase.ts b/src/foodDatabase.ts new file mode 100644 index 0000000..0037812 --- /dev/null +++ b/src/foodDatabase.ts @@ -0,0 +1,71 @@ +import { FoodItem } from "./types"; + +export const foodDatabase: FoodItem[] = [ + { name: 'Frutas - Abacate', info: { calories: 96, protein: 1.2, fat: 8.4, carbs: 6, fiber: 6.3 } }, + { name: 'Frutas - Abacaxi', info: { calories: 48, protein: 0.9, fat: 0.1, carbs: 12.3, fiber: 1 } }, + { name: 'Frutas - Banana Nanica', info: { calories: 92, protein: 1.4, fat: 0.1, carbs: 23.8, fiber: 1.9 } }, + { name: 'Frutas - Laranja', info: { calories: 37, protein: 1, fat: 0.1, carbs: 8.9, fiber: 0.8 } }, + { name: 'Frutas - Maçã', info: { calories: 56, protein: 0.3, fat: 0, carbs: 15.2, fiber: 1.3 } }, + { name: 'Frutas - Manga Palmer', info: { calories: 72, protein: 0.4, fat: 0.2, carbs: 19.4, fiber: 1.6 } }, + { name: 'Frutas - Mamão', info: { calories: 40, protein: 0.5, fat: 0.1, carbs: 10.4, fiber: 1 } }, + { name: 'Frutas - Melancia', info: { calories: 33, protein: 0.9, fat: 0, carbs: 8.1, fiber: 0.1 } }, + { name: 'Frutas - Melão', info: { calories: 29, protein: 0.7, fat: 0, carbs: 7.5, fiber: 0.3 } }, + { name: 'Frutas - Uva', info: { calories: 53, protein: 0.7, fat: 0.2, carbs: 13.6, fiber: 0.9 } }, + + { name: 'Vegetais - Agrião', info: { calories: 17, protein: 2.7, fat: 0.2, carbs: 2.3, fiber: 2.1 } }, + { name: 'Vegetais - Brócolis (cozido)', info: { calories: 25, protein: 2.1, fat: 0.5, carbs: 4.4, fiber: 3.4 } }, + { name: 'Vegetais - Cenoura (cozida)', info: { calories: 30, protein: 0.8, fat: 0.2, carbs: 6.7, fiber: 2.6 } }, + { name: 'Vegetais - Couve flor (cozida)', info: { calories: 19, protein: 1.2, fat: 0.3, carbs: 3.9, fiber: 2.1 } }, + { name: 'Vegetais - Ervilha (cozida)', info: { calories: 88, protein: 7.5, fat: 0.5, carbs: 14.2, fiber: 9.7 } }, + { name: 'Vegetais - Espinafre', info: { calories: 16, protein: 2, fat: 0.2, carbs: 2.6, fiber: 2.1 } }, + { name: 'Vegetais - Feijão preto (cozido)', info: { calories: 77, protein: 4.5, fat: 0.5, carbs: 14, fiber: 8.4 } }, + { name: 'Vegetais - Lentilha (cozida)', info: { calories: 93, protein: 6.3, fat: 0.5, carbs: 16.3, fiber: 7.9 } }, + { name: 'Vegetais - Mandioca (cozida)', info: { calories: 125, protein: 0.6, fat: 0.3, carbs: 30.1, fiber: 1.6 } }, + { name: 'Vegetais - Pepino', info: { calories: 10, protein: 0.9, fat: 0, carbs: 2, fiber: 1.1 } }, + { name: 'Vegetais - Rúcula', info: { calories: 13, protein: 1.8, fat: 0.1, carbs: 2.2, fiber: 1.7 } }, + { name: 'Vegetais - Spirulina', info: { calories: 290, protein: 57.5, fat: 23.9, carbs: 7.7, fiber: 0 } }, + { name: 'Vegetais - Tomate', info: { calories: 15, protein: 1.1, fat: 0.2, carbs: 3.1, fiber: 1.2 } }, + + { name: 'Carboidratos - Batata doce (assada)', info: { calories: 90, protein: 2.0, fat: 0.1, carbs: 20.7, fiber: 3.3 } }, + { name: 'Carboidratos - Batata doce (cozida)', info: { calories: 77, protein: 0.6, fat: 0.1, carbs: 18.4, fiber: 2.2 } }, + { name: 'Carboidratos - Batata doce (crua)', info: { calories: 118, protein: 1.3, fat: 0.1, carbs: 28.2, fiber: 2.6 } }, + { name: 'Carboidratos - Batata inglesa (assada)', info: { calories: 94, protein: 2.1, fat: 0.1, carbs: 21.8, fiber: 2.1 } }, + { name: 'Carboidratos - Batata inglesa (cozida)', info: { calories: 52, protein: 1.2, fat: 0, carbs: 11.9, fiber: 1.3 } }, + { name: 'Carboidratos - Batata inglesa (crua)', info: { calories: 64, protein: 1.8, fat: 0, carbs: 14.7, fiber: 1.2 } }, + { name: 'Carboidratos - Pão Francês', info: { calories: 300, protein: 8, fat: 3.1, carbs: 58.6, fiber: 2.3 } }, + { name: 'Carboidrados - Polenta (cozida)', info: { calories: 192, protein: 4.8, fat: 1.2, carbs: 40, fiber: 3.8 } }, + + { name: 'Proteínas - Atum Sólido (natural)', info: { calories: 135, protein: 28.3, fat: 2.7, carbs: 0, fiber: 0 } }, + { name: 'Proteínas - Filé de Tilápia (cozido)', info: { calories: 128, protein: 26.1, fat: 2.6, carbs: 0, fiber: 0 } }, + { name: 'Proteínas - Ovo inteiro (cozido)', info: { calories: 146, protein: 13.3, fat: 9.5, carbs: 0.6, fiber: 0 } }, + { name: 'Proteínas - Ovo clara (cozido)', info: { calories: 59, protein: 13.4, fat: 0.1, carbs: 0, fiber: 0 } }, + { name: 'Proteínas - PTS (crua)', info: { calories: 288, protein: 52, fat: 0, carbs: 20, fiber: 14 } }, + { name: 'Proteínas - Patinho moído (grelhado)', info: { calories: 219, protein: 35.9, fat: 7.3, carbs: 0, fiber: 0 } }, + { name: 'Proteínas - Soja em Pó Growth', info: { calories: 400, protein: 86.7, fat: 5, carbs: 3.3, fiber: 0 } }, + { name: 'Proteínas - Sobrecoxa s/pele (cozida)', info: { calories: 245, protein: 24.9, fat: 15.4, carbs: 0, fiber: 0 } }, + { name: 'Proteínas - Tofu', info: { calories: 65, protein: 6.5, fat: 4, carbs: 2, fiber: 0.75 } }, + + { name: 'Grãos - Arroz integral (cozido)', info: { calories: 130, protein: 2.7, fat: 0.3, carbs: 28, fiber: 0.4 } }, + { name: 'Grãos - Aveia', info: { calories: 394, protein: 13.9, fat: 8.5, carbs: 66.6, fiber: 9.1 } }, + { name: 'Grãos - Grão de Bico (cozido)', info: { calories: 180, protein: 9.5, fat: 3, carbs: 30, fiber: 8.6 } }, + { name: 'Grãos - Pão integral de forma', info: { calories: 253, protein: 9.4, fat: 3.7, carbs: 49.9, fiber: 6.9 } }, + { name: 'Grãos - Pipoca c/óleo', info: { calories: 448, protein: 9.9, fat: 15.9, carbs: 70.3, fiber: 14.3 } }, + { name: 'Grãos - Pipoca s/óleo', info: { calories: 178, protein: 3.3, fat: 7.2, carbs: 25, fiber: 4.3 } }, + { name: 'Grãos - Quinoa (cozida)', info: { calories: 158, protein: 5.55, fat: 2.5, carbs: 29, fiber: 2.5 } }, + + { name: 'Castanhas - Amêndoas', info: { calories: 581, protein: 18.6, fat: 47.3, carbs: 29.5, fiber: 11.6 } }, + { name: 'Castanhas - Amendoin natural', info: { calories: 611, protein: 26, fat: 49, carbs: 16, fiber: 8.5 } }, + { name: 'Castanhas - Castanha de Caju', info: { calories: 570, protein: 18.5, fat: 46.3, carbs: 29.1, fiber: 3.7 } }, + { name: 'Castanhas - Castanha do Pará', info: { calories: 643, protein: 14.5, fat: 63.5, carbs: 15.1, fiber: 7.9 } }, + { name: 'Sementes - Semente de Chia', info: { calories: 490, protein: 15.6, fat: 30.7, carbs: 43.8, fiber: 37.7 } }, + { name: 'Sementes - Semente de Linhaça', info: { calories: 495, protein: 14.1, fat: 32.3, carbs: 43.3, fiber: 33.5 } }, + { name: 'Sementes - Semente de Gergelim', info: { calories: 584, protein: 21.2, fat: 50.4, carbs: 21.6, fiber: 11.9 } }, + + { name: 'Lanches - Barra de Proteína Vegana Growth', info: { calories: 400, protein: 25, fat: 22, carbs: 25, fiber: 8 } }, + { name: 'Lanches - Goiabada', info: { calories: 320, protein: 0, fat: 0, carbs: 80, fiber: 4.5 } }, + { name: 'Lanches - Mel Silvestre', info: { calories: 230, protein: 0, fat: 0, carbs: 65, fiber: 0 } }, + { name: 'Lanches - Melado', info: { calories: 297, protein: 0, fat: 0, carbs: 76.6, fiber: 0 } }, + { name: 'Lanches - Pasta de Amendoim Growth', info: { calories: 544, protein: 27, fat: 33, carbs: 20, fiber: 8 } }, + { name: 'Lanches - Goma de Tapioca', info: { calories: 226, protein: 0, fat: 0, carbs: 57, fiber: 0 } }, + { name: 'Lanches - 70% Nibs Garoto', info: { calories: 544, protein: 8.4, fat: 40, carbs: 34, fiber: 0 } } +]; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..4c851a9 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,596 @@ +import { foodDatabase } from './foodDatabase.js'; +import { getButtonById, getButtonListByClassName, getDivById, getInputById, showFoodPreview } from './HtmlUtil.ts'; +import { FoodItem, FoodStorage } from './types.js'; +import { AppwriteAuth, AppwriteDB } from './appwrite.js'; +import swal from 'sweetalert'; + +// App state +let selectedDate = new Date(); +let currentViewDate = new Date(); + +// Authentication state +let currentUser: any | null = null; + +// DOM Elements +const authSection = document.getElementById('auth-section'); +const userInfo = document.getElementById('user-info'); +const appContent = document.getElementById('app-content'); +const loadingOverlay = getDivById('loading-overlay'); +const loginForm = document.getElementById('login-form'); +const registerForm = document.getElementById('register-form'); + +// Initialize when page loads +document.addEventListener('DOMContentLoaded', async function() { + await initializeAuth(); + populateFoodSelect(); + updateCurrentDate(); + setupEventListeners(); +}); + +// Initialize application +async function initializeAuth() { + showLoading(); + + try { + // Check if user is already logged in + const isLoggedIn = await AppwriteAuth.isLoggedIn(); + + if (isLoggedIn) { + currentUser = await AppwriteAuth.getCurrentUser(); + showMainApp(); + selectDate(selectedDate); + } else { + showAuthForms(); + } + } catch (error) { + console.error('Initialize app error:', error); + showAuthForms(); + } finally { + hideLoading(); + } +} + +// Toggle between login and register forms +function toggleAuthForms() { + loginForm?.classList.toggle('hidden'); + registerForm?.classList.toggle('hidden'); + + // Clear form data + const logForm = document.getElementById('loginForm') as HTMLFormElement; + logForm.reset(); + + const resetForm = document.getElementById('registerForm') as HTMLFormElement; + resetForm.reset(); +} + +// Handle user registration +async function handleRegister(e: SubmitEvent) { + e.preventDefault(); + showLoading(); + + const name = getInputById('registerName').value; + const email = getInputById('registerEmail').value; + const password = getInputById('registerPassword').value; + + try { + await AppwriteAuth.register(email, password, name); + + // Auto login after registration + await AppwriteAuth.login(email, password); + currentUser = await AppwriteAuth.getCurrentUser(); + + showMainApp(); + selectDate(selectedDate); + + swal('Registration successful! Welcome to Food Tracker.'); + } catch (error) { + console.error('Registration error:', error); + swal('Oh no!', 'Registration failed: ' + error.message, 'error'); + } finally { + hideLoading(); + } +} + +// Handle user login +async function handleLogin(e: SubmitEvent) { + e.preventDefault(); + showLoading(); + + const email = getInputById('loginEmail').value; + const password = getInputById('loginPassword').value; + + try { + await AppwriteAuth.login(email, password); + currentUser = await AppwriteAuth.getCurrentUser(); + + showMainApp(); + selectDate(selectedDate); + + } catch (error) { + console.error('Login error:', error); + swal('Oh no!', 'Login failed: ' + error.message, 'error'); + } finally { + hideLoading(); + } +} + +async function handleLogout() { + showLoading(); + + try { + await AppwriteAuth.logout(); + currentUser = null; + showAuthForms(); + clearAppData(); + } catch (error) { + console.error('Logout error:', error); + swal('Oh no!', 'Logout failed: ' + error.message, 'error'); + } finally { + hideLoading(); + } +} + +// Show authentication forms +function showAuthForms() { + authSection?.classList.remove('hidden'); + userInfo?.classList.add('hidden'); + appContent?.classList.add('hidden'); +} + +function showMainApp() { + authSection?.classList.add('hidden'); + userInfo?.classList.remove('hidden'); + appContent?.classList.remove('hidden'); + + // Update user info display + if (currentUser) { + getDivById('userName').textContent = `Welcome, ${currentUser.name}!`; + } + + // Update date display + updateCurrentDate(); +} + +// Show/hide loading overlay +function showLoading() { + loadingOverlay.classList.remove('hidden'); +} + +function hideLoading() { + loadingOverlay.classList.add('hidden'); +} + +// Clear application data +function clearAppData() { + getDivById('foodTableBody').innerHTML = ''; + getDivById('caloriesCounter').textContent = '0'; + getInputById('foodSelect').value = ''; + getInputById('gramAmount').value = '100'; +} + +const getFoodData = (grams: number, foodData: FoodItem): FoodItem => { + 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, + fiber: Math.round(foodData.info.fiber * multiplier * 10) / 10 + } + }; +} + +const getFoodItemByName = (foodName: string): FoodItem => { + const foodDataSearch: FoodItem[] = foodDatabase.filter(f => f.name === foodName); + if (foodDataSearch.length === 0) { + throw new Error(`Food not found for name ${name}`); + } + return foodDataSearch[0]; +} + +const previewCalories = () => { + const foodSelect = getInputById('foodSelect'); + const gramAmount = getInputById('gramAmount'); + + if (!foodSelect.value) { + showFoodPreview(false); + return; + } + + let grams = 100; + if (gramAmount && gramAmount.value && parseFloat(gramAmount.value) > 0) { + grams = parseFloat(gramAmount.value); + } + + const foodData: FoodItem = getFoodItemByName(foodSelect.value); + const proportion = getFoodData(grams, foodData); + + 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 (); + + showFoodPreview(true); +} + +const setupEventListeners = () => { + getInputById('foodSelect').addEventListener('change', () => { + previewCalories(); + }); + + getInputById('gramAmount').addEventListener('change', () => { + previewCalories(); + }); + + getButtonById('add-food-btn').addEventListener('click', () => { + addFood(); + }); + + getButtonById('next-month-btn').addEventListener('click', () => { + nextMonth(); + }); + + getButtonById('previous-month-btn').addEventListener('click', () => { + previousMonth(); + }); + + // Form switching + document.getElementById('showRegister')?.addEventListener('click', (e) => { + e.preventDefault(); + toggleAuthForms(); + }); + + document.getElementById('showLogin')?.addEventListener('click', (e) => { + e.preventDefault(); + toggleAuthForms(); + }); + + // Auth forms + document.getElementById('loginForm')?.addEventListener('submit', handleLogin); + document.getElementById('registerForm')?.addEventListener('submit', handleRegister); + document.getElementById('logoutBtn')?.addEventListener('click', handleLogout); +} + +const addDeleteEvents = () => { + const elements = getButtonListByClassName('delete-food-entry'); + Array.from(elements).forEach((el: HTMLButtonElement) => { + el.addEventListener('click', (e) => { + const target = e.target as HTMLElement; + const row = target.closest('tr'); + const entryId = row?.querySelector('.hidden-column')?.textContent; + if (entryId && row) { + handleDeleteFood(entryId, row); + } + }); + }); +} + +// Populate food select dropdown +const populateFoodSelect = () => { + const select = getInputById('foodSelect'); + select.innerHTML = ''; + + const sortedFoods = foodDatabase.sort((f1, f2) => f1.name.localeCompare(f2.name)); + sortedFoods.forEach((food: FoodItem) => { + const option = document.createElement('option') as HTMLOptionElement; + option.value = food.name; + option.textContent = food.name; + select.appendChild(option); + }); +} + +// Update current date display +const updateCurrentDate = () => { + const date = new Date(selectedDate); + getDivById('currentDate').textContent = date.toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }); +} + +// Add food to the log +const addFood = async () => { + if (!currentUser) { + swal('Hey!', 'Please log in to add food entries.', 'info'); + return; + } + + const foodSelect = getInputById('foodSelect'); + const gramAmount = getInputById('gramAmount'); + + if (!foodSelect.value || !gramAmount.value) { + swal('Hey!', 'Please select a food item and enter amount', 'error'); + return; + } + + showLoading(); + + try { + const grams: number = parseFloat(gramAmount.value); + const foodData: FoodItem = getFoodItemByName(foodSelect.value); + + // Calculate nutrition for the amount + const proportion: FoodItem = getFoodData(grams, foodData); + const entry: FoodStorage = { + name: foodSelect.value, + grams: grams, + calories: proportion.info.calories, + protein: proportion.info.protein, + fat: proportion.info.fat, + carbs: proportion.info.carbs, + fiber: proportion.info.fiber, + date: selectedDate.toISOString().split('T')[0], + time: new Date().toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit' + }) + }; + + // Save to Appwrite + const savedEntry = await AppwriteDB.saveFoodEntry(entry); + + // Add to HTML table with Appwrite document ID + addFoodToTable(entry, savedEntry.$id); + + // Update totals + updateTotalCalories(); + + // Reset form + foodSelect.value = ''; + gramAmount.value = '100'; + showFoodPreview(false); + } catch (error) { + console.error('Add food error:', error); + swal('Oh no!', 'Failed to add food entry: ' + error.message, 'error'); + } finally { + hideLoading(); + } +} + +// Load food entries from Appwrite +async function loadFoodEntries(date: Date) { + if (!currentUser) return; + + showLoading(); + + try { + const entries = await AppwriteDB.getFoodEntries(date); + + // Clear current table + getDivById('foodTableBody').innerHTML = ''; + + // Add each entry to table + entries.forEach(entry => { + const foodData: FoodStorage = { + id: entry.$id, + name: entry.name, + grams: entry.grams, + calories: entry.calories, + protein: entry.protein, + fat: entry.fat, + carbs: entry.carbs, + fiber: entry.fiber, + time: entry.time, + date: entry.date, + }; + + addFoodToTable(foodData, entry.$id); + }); + + // Setup Delete events + addDeleteEvents(); + + // Update total calories + updateTotalCalories(); + + } catch (error) { + console.error('Load food entries error:', error); + swal('Oh no!', 'Failed to load food entries: ' + error.message, 'error'); + } finally { + hideLoading(); + } +} + +// Handle food deletion +async function handleDeleteFood(documentId: string, row: HTMLTableRowElement) { + if (!documentId) return; + + 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!"], + }); + + if (!willDelete) return; + + showLoading(); + + try { + // Delete from Appwrite + await AppwriteDB.deleteFoodEntry(documentId); + + // Remove from table + row.remove(); + + // Update totals + updateTotalCalories(); + + } catch (error) { + console.error('Delete food error:', error); + swal('Oh no!', 'Failed to delete food entry: ' + error.message, 'error'); + } finally { + hideLoading(); + } +} + +// Add food to table (existing function - update to use document ID) +function addFoodToTable(foodData: FoodStorage, documentId: string) { + const tableBody = document.getElementById('foodTableBody'); + const row = document.createElement('tr'); + + row.innerHTML = ` + ${documentId} + ${foodData.time || new Date().toLocaleTimeString()} + ${foodData.name} + ${foodData.grams} + ${foodData.calories} + ${foodData.protein} + ${foodData.fat} + ${foodData.carbs} + ${foodData.fiber} + + `; + + tableBody?.appendChild(row); + + // Setup Delete events + addDeleteEvents(); +} + +// Update total calories +function updateTotalCalories() { + const caloriesCells = document.querySelectorAll('#foodTableBody td:nth-child(5)'); // 5th column is calories + let total = 0; + + caloriesCells.forEach((cell: Element) => { + total += parseInt(cell.innerHTML) || 0; + }); + + getDivById('caloriesCounter').textContent = total.toString(); +} + +// Calendar functions +const previousMonth = () => { + currentViewDate.setMonth(currentViewDate.getMonth() - 1); + renderCalendar(); +} + +const nextMonth = () => { + currentViewDate.setMonth(currentViewDate.getMonth() + 1); + renderCalendar(); +} + +const selectDate = async (date: Date) => { + selectedDate = date; + updateCurrentDate(); + await loadFoodEntries(date); + renderCalendar(); +} + +const renderCalendar = () => { + const year = currentViewDate.getFullYear(); + const month = currentViewDate.getMonth(); + + // Update calendar title + const monthNames = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ]; + getDivById('calendarTitle').textContent = `${monthNames[month]} ${year}`; + + // Clear calendar + const grid = getDivById('calendarGrid'); + grid.innerHTML = ''; + + // Add day headers + const dayHeaders = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + dayHeaders.forEach(day => { + const header = document.createElement('div'); + header.className = 'calendar-day-header'; + header.textContent = day; + grid.appendChild(header); + }); + + // Get first day of month and number of days + const firstDay = new Date(year, month, 1); + const lastDay = new Date(year, month + 1, 0); + const daysInMonth = lastDay.getDate(); + const startingDayOfWeek = firstDay.getDay(); + + // Add empty cells for previous month + const prevMonth = new Date(year, month, 0); + const daysInPrevMonth = prevMonth.getDate(); + + for (let i = startingDayOfWeek - 1; i >= 0; i--) { + const day = daysInPrevMonth - i; + const dayElement = createDayElement(day, true, year, month - 1); + grid.appendChild(dayElement); + } + + // Add days of current month + for (let day = 1; day <= daysInMonth; day++) { + const dayElement = createDayElement(day, false, year, month); + grid.appendChild(dayElement); + } + + // Add days from next month to fill the grid + const totalCells = grid.children.length - 7; + const remainingCells = 42 - totalCells; + + for (let day = 1; day <= remainingCells; day++) { + const dayElement = createDayElement(day, true, year, month + 1); + grid.appendChild(dayElement); + } +} + +function createDayElement(day: number, isOtherMonth: boolean, year: number, month: number) { + const dayElement = document.createElement('div') as HTMLElement; + dayElement.className = 'calendar-day'; + + const dayDate = new Date(year, month, day); + const dateString = dayDate.toISOString().split('T')[0]; + + if (isOtherMonth) { + dayElement.classList.add('other-month'); + } else { + dayElement.onclick = () => selectDate(dayDate); + } + + // 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 + if (!isOtherMonth && dateString === selectedDate.toISOString().split('T')[0]) { + dayElement.classList.add('selected'); + } + + // 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); + + dayElement.innerHTML = ` +
${day}
+
${totalCalories} cal
+ `; + } else { + dayElement.textContent = day.toString(); + } + + return dayElement; +} + + diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..56cbdd9 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,17 @@ +export type FoodItem = { + name: string; + info: { calories: number, protein: number, fat: number, carbs: number, fiber: number } +} + +export type FoodStorage = { + id?: string; + name: string; + grams: number; + calories: number; + protein: number; + fat: number; + carbs: number; + fiber: number; + time: string; + date: string; +}; diff --git a/style.css b/style.css index f10af86..c1f3d8f 100644 --- a/style.css +++ b/style.css @@ -88,11 +88,6 @@ h1 { overflow: hidden; } -.add-food-form-item { - margin-top: 15px; - margin-left: 3px; -} - .display-none { display: none; }