Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
f63e51ca1f
|
|||
| 8a4435e28d | |||
|
761fd3dfc0
|
|||
|
b51a54c6db
|
|||
| adcc6b5f74 | |||
| aa97b04baa | |||
|
2a1a492523
|
|||
| 70d2d22055 | |||
| 0682649a9f | |||
|
1eba605146
|
|||
| 0746088203 | |||
| fe2f324bfa | |||
| 88f55e4f7a | |||
| 2a18a78a76 | |||
| 1a0bd0007f | |||
| 8f327c191b | |||
| 6837e4f139 |
@@ -0,0 +1,42 @@
|
||||
name: Build Only
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
# Allow only one concurrent deployment
|
||||
concurrency:
|
||||
group: "prs"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build-deploy:
|
||||
runs-on: easynode-debian
|
||||
env:
|
||||
VITE_APPWRITE_PROJECT_ID: ${{ secrets.VITE_APPWRITE_PROJECT_ID }}
|
||||
VITE_APPWRITE_ENDPOINT: ${{ secrets.VITE_APPWRITE_ENDPOINT }}
|
||||
VITE_APPWRITE_DBID: ${{ secrets.VITE_APPWRITE_DBID }}
|
||||
VITE_APPWRITE_FOODENTRIESID: ${{ secrets.VITE_APPWRITE_FOODENTRIESID }}
|
||||
VITE_APPWRITE_USERSETTINGSID: ${{ secrets.VITE_APPWRITE_USERSETTINGSID }}
|
||||
VITE_APPWRITE_MONTLYCALORIESID: ${{ secrets.VITE_APPWRITE_MONTLYCALORIESID }}
|
||||
VITE_APPWRITE_SHAREDDAYSID: ${{ secrets.VITE_APPWRITE_SHAREDDAYSID }}
|
||||
VITE_CLAUDE_API_KEY: ${{ secrets.VITE_CLAUDE_API_KEY }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cache npm dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: Deploy to GitHub Pages
|
||||
name: Build & Deploy to Pages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
@@ -11,9 +12,12 @@ concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
APP_FOLDER: "calories-tracker"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
build-deploy:
|
||||
runs-on: easynode-debian
|
||||
env:
|
||||
VITE_APPWRITE_PROJECT_ID: ${{ secrets.VITE_APPWRITE_PROJECT_ID }}
|
||||
VITE_APPWRITE_ENDPOINT: ${{ secrets.VITE_APPWRITE_ENDPOINT }}
|
||||
@@ -28,11 +32,13 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Cache npm dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -40,25 +46,24 @@ jobs:
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./dist
|
||||
- name: Install rsync + ssh client
|
||||
run: apt-get update -qq && apt-get install -y -qq rsync openssh-client
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
- name: Configure SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts
|
||||
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
- name: Ensure destination folder exists
|
||||
run: |
|
||||
ssh -i ~/.ssh/deploy_key deploy@${{ secrets.DEPLOY_HOST }} \
|
||||
"mkdir -p /srv/sites/${{ env.APP_FOLDER }} && chmod 755 /srv/sites/${{ env.APP_FOLDER }}"
|
||||
|
||||
- name: Upload site
|
||||
run: |
|
||||
rsync -avz --delete \
|
||||
-e "ssh -i ~/.ssh/deploy_key" \
|
||||
./dist/ deploy@${{ secrets.DEPLOY_HOST }}:/srv/sites/${{ env.APP_FOLDER }}/
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
@@ -8,3 +8,6 @@ senha.txt
|
||||
dist/sw.js
|
||||
dist/workbox-*.js
|
||||
dev-dist/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Development Commands
|
||||
|
||||
- `npm run dev` - Start development server (Vite)
|
||||
- `npm run build` - Build for production
|
||||
- `npm run preview` - Preview production build
|
||||
|
||||
## Environment Setup
|
||||
|
||||
Requires environment variables in `.env`:
|
||||
- `VITE_APPWRITE_ENDPOINT` - Appwrite server endpoint
|
||||
- `VITE_APPWRITE_DBID` - Database ID
|
||||
- `VITE_APPWRITE_FOODENTRIESID` - Food entries collection ID
|
||||
- `VITE_APPWRITE_USERSETTINGSID` - User settings collection ID
|
||||
- `VITE_APPWRITE_MONTLYCALORIESID` - Monthly calories collection ID
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
This is a TypeScript/Vite-based calories tracking application with Appwrite backend integration. Key architectural patterns:
|
||||
|
||||
### Core Structure
|
||||
- **Entry Point**: `src/index.ts` - Main application logic and DOM manipulation
|
||||
- **State Management**: `src/state.ts` - Centralized application state
|
||||
- **Backend Integration**: `src/appwrite.ts` - Appwrite client and database operations
|
||||
- **Type Definitions**: `src/types.ts` - TypeScript interfaces for data models
|
||||
|
||||
### Data Flow
|
||||
- User authentication managed through `AppwriteAuth` class
|
||||
- Food entries stored via `AppwriteDB` class with real-time Appwrite sync
|
||||
- Local food database in `src/foodDatabase.ts` for searching
|
||||
- Calendar view tracks daily calorie totals separately from individual entries
|
||||
|
||||
### Key Components
|
||||
- **Authentication**: Login/register forms with session management
|
||||
- **Food Search**: Real-time search with keyboard navigation and debouncing
|
||||
- **Food Entry**: Add/edit/delete food items with nutritional calculations
|
||||
- **Calendar**: Monthly calendar view with daily calorie totals
|
||||
- **Settings**: User-configurable nutritional goals
|
||||
|
||||
### State Management Patterns
|
||||
- Global app state in `appState` object
|
||||
- Date-based data loading (selectedDate/currentViewDate)
|
||||
- Monthly calorie aggregation separate from individual food entries
|
||||
- Real-time UI updates after database operations
|
||||
|
||||
### DOM Manipulation
|
||||
- Utility functions in `src/DomUtils.ts` for type-safe DOM access
|
||||
- Event listeners centralized in `setupEventListeners()`
|
||||
- Dynamic card creation for food entries with collapsible details
|
||||
|
||||
### Backend Integration
|
||||
- All data operations use Appwrite SDK
|
||||
- User-scoped queries with proper authentication
|
||||
- Concurrent operations for bulk deletions
|
||||
- Timezone-aware date handling for cross-timezone consistency
|
||||
|
||||
## Code Patterns
|
||||
|
||||
### Data Calculations
|
||||
- All nutrition values calculated from 100g base values in food database
|
||||
- Proportional scaling based on user-entered grams
|
||||
- Real-time preview updates during food entry
|
||||
|
||||
### Error Handling
|
||||
- Try-catch blocks with user-friendly SweetAlert notifications
|
||||
- Loading states during async operations
|
||||
- Graceful fallbacks for missing data
|
||||
|
||||
### Event Management
|
||||
- Event listener cleanup and re-attachment for dynamic content
|
||||
- Keyboard navigation support (arrows, enter, escape)
|
||||
- Click-outside handlers for dropdowns and modals
|
||||
@@ -1,8 +1,12 @@
|
||||
  
|
||||
|
||||
# 🍽️ Calories Tracker
|
||||
|
||||
A modern, responsive web application for tracking daily food intake, calories, and nutritional information. Built with TypeScript, Vite, and Appwrite for a seamless food logging experience.
|
||||
|
||||
  
|
||||
## Live
|
||||
|
||||
https://calories-tracker.lightroasted.vps-kinghost.net
|
||||
|
||||
## ✨ Features
|
||||
|
||||
@@ -72,7 +76,7 @@ VITE_APPWRITE_USERSETTINGSID=your_user_settings_collection_id
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone https://github.com/yourusername/calories-tracker.git
|
||||
git clone https://lightroasted.vps-kinghost.net/rmcampos/calories-tracker.git
|
||||
cd calories-tracker
|
||||
```
|
||||
|
||||
|
||||
+15
@@ -298,6 +298,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group checkbox-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="enableAlternatives" class="ai-checkbox">
|
||||
<span>Show other options</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Alternatives Container -->
|
||||
<div class="display-none" id="alternatives-container">
|
||||
<div class="alternatives-list-container">
|
||||
<h3 class="alternatives-title">Similar Food Options</h3>
|
||||
<div id="alternatives-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="enableAINutrition" class="ai-checkbox">
|
||||
|
||||
Generated
+2194
-1862
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -12,14 +12,14 @@
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^7.3.2",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"workbox-window": "^7.4.0"
|
||||
"typescript": "^7.0.0",
|
||||
"vite": "^8.1.4",
|
||||
"vite-plugin-pwa": "^1.3.0",
|
||||
"workbox-window": "^7.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.91.1",
|
||||
"appwrite": "^24.2.0",
|
||||
"@anthropic-ai/sdk": "^0.111.0",
|
||||
"appwrite": "^26.1.0",
|
||||
"sweetalert": "^2.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
]
|
||||
}
|
||||
+15
-14
@@ -3,11 +3,11 @@ import { FoodItem } from "./types";
|
||||
export const foodDatabase: FoodItem[] = [
|
||||
// Gorduras - Fats
|
||||
{ name: 'Abacate', nameEn: 'Avocado', info: { calories: 96, protein: 1.2, fat: 8.4, carbs: 6, fiber: 6.3, category: 'fats', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Amêndoas', nameEn: 'Almonds', info: { calories: 581, protein: 18.6, fat: 47.3, carbs: 29.5, fiber: 11.6, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks', 'lunch', 'dinner'] } },
|
||||
{ name: 'Amendoin natural', nameEn: 'Natural Peanut', info: { calories: 611, protein: 26, fat: 49, carbs: 16, fiber: 8.5, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks', 'lunch', 'dinner'] } },
|
||||
{ name: 'Amêndoas', nameEn: 'Almonds', info: { calories: 581, protein: 18.6, fat: 47.3, carbs: 29.5, fiber: 11.6, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Amendoin natural', nameEn: 'Natural Peanut', info: { calories: 611, protein: 26, fat: 49, carbs: 16, fiber: 8.5, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Azeite de Oliva EV', nameEn: 'Extra Virgin Olive Oil', info: { calories: 844, protein: 0, fat: 100, carbs: 0, fiber: 0, category: 'fats', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||
{ name: 'Castanha de Caju', nameEn: 'Cashew Nut', info: { calories: 570, protein: 18.5, fat: 46.3, carbs: 29.1, fiber: 3.7, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks', 'lunch', 'dinner'] } },
|
||||
{ name: 'Castanha do Pará', nameEn: 'Brazil Nut', info: { calories: 643, protein: 14.5, fat: 63.5, carbs: 15.1, fiber: 7.9, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks', 'lunch', 'dinner'] } },
|
||||
{ name: 'Castanha de Caju', nameEn: 'Cashew Nut', info: { calories: 570, protein: 18.5, fat: 46.3, carbs: 29.1, fiber: 3.7, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Castanha do Pará', nameEn: 'Brazil Nut', info: { calories: 643, protein: 14.5, fat: 63.5, carbs: 15.1, fiber: 7.9, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Semente de Chia', nameEn: 'Chia Seed', info: { calories: 490, protein: 15.6, fat: 30.7, carbs: 43.8, fiber: 37.7, category: 'fats', alkaline: true, bestFor: ['breakfast', 'brunch'] } },
|
||||
{ name: 'Semente de Linhaça Dourada', nameEn: 'Golden Flaxseed', info: { calories: 515, protein: 20, fat: 35, carbs: 30, fiber: 26, category: 'fats', alkaline: true, bestFor: ['breakfast', 'brunch'] } },
|
||||
{ name: 'Semente de Gergelim', nameEn: 'Sesame Seed', info: { calories: 584, protein: 21.2, fat: 50.4, carbs: 21.6, fiber: 11.9, category: 'fats', alkaline: true, bestFor: ['breakfast', 'brunch'] } },
|
||||
@@ -17,16 +17,16 @@ export const foodDatabase: FoodItem[] = [
|
||||
{ name: 'Pasta de Castanha (A Tal da Castanha)', nameEn: 'Cashew Butter (A Tal da Castanha)', info: { calories: 564, protein: 17, fat: 40, carbs: 34, fiber: 4.7, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
|
||||
// Proteínas - Proteins
|
||||
{ name: 'Atum, conserva em óleo', nameEn: 'Tuna, canned in oil', info: { calories: 166, protein: 26.2, fat: 6, carbs: 0, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['breakfast', 'brunch', 'lunch', 'dinner'] } },
|
||||
{ name: 'Atum, conserva em óleo', nameEn: 'Tuna, canned in oil', info: { calories: 166, protein: 26.2, fat: 6, carbs: 0, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks', 'lunch', 'dinner'] } },
|
||||
{ name: 'Filé de Tilápia, cozido', nameEn: 'Tilapia Fillet, cooked', info: { calories: 128, protein: 26.1, fat: 2.6, carbs: 0, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||
{ name: 'Ovo, de galinha, inteiro, cozido', nameEn: 'Egg, whole, boiled', info: { calories: 146, protein: 13.3, fat: 9.5, carbs: 0.6, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['breakfast', 'brunch', 'lunch', 'dinner'] } },
|
||||
{ name: 'Ovo, de galinha, gema, cozida', nameEn: 'Egg, yolk, boiled', info: { calories: 353, protein: 15.9, fat: 30.8, carbs: 1.6, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['breakfast', 'brunch', 'lunch', 'dinner'] } },
|
||||
{ name: 'Ovo, de galinha, clara, cozida', nameEn: 'Egg, white, boiled', info: { calories: 59, protein: 13.4, fat: 0.1, carbs: 0, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['breakfast', 'brunch', 'lunch', 'dinner'] } },
|
||||
{ name: 'Ovo, de galinha, inteiro, cozido', nameEn: 'Egg, whole, boiled', info: { calories: 146, protein: 13.3, fat: 9.5, carbs: 0.6, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks', 'lunch', 'dinner'] } },
|
||||
{ name: 'Ovo, de galinha, gema, cozida', nameEn: 'Egg, yolk, boiled', info: { calories: 353, protein: 15.9, fat: 30.8, carbs: 1.6, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks', 'lunch', 'dinner'] } },
|
||||
{ name: 'Ovo, de galinha, clara, cozida', nameEn: 'Egg, white, boiled', info: { calories: 59, protein: 13.4, fat: 0.1, carbs: 0, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks', 'lunch', 'dinner'] } },
|
||||
{ name: 'PTS, crua', nameEn: 'Textured Vegetable Protein, raw', info: { calories: 288, protein: 52, fat: 0, carbs: 20, fiber: 14, category: 'proteins', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||
{ name: 'Carne, bovina, patinho, sem gordura, grelhado', nameEn: 'Beef, round, lean, grilled', info: { calories: 219, protein: 35.9, fat: 7.3, carbs: 0, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||
{ name: 'Soja em Pó Growth', nameEn: 'Growth Soy Powder', info: { calories: 400, protein: 86.7, fat: 5, carbs: 3.3, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['breakfast', 'brunch'] } },
|
||||
{ name: 'Sobrecoxa s/pele, cozida', nameEn: 'Chicken Thigh, skinless, cooked', info: { calories: 245, protein: 24.9, fat: 15.4, carbs: 0, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||
{ name: 'Soja, queijo (tofu)', nameEn: 'Soy, cheese (tofu)', info: { calories: 64, protein: 6.5, fat: 2, carbs: 4, fiber: 0.8, category: 'proteins', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Soja, queijo (tofu)', nameEn: 'Soy, cheese (tofu)', info: { calories: 64, protein: 6.5, fat: 2, carbs: 4, fiber: 0.8, category: 'proteins', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks', 'lunch', 'dinner'] } },
|
||||
{ name: 'Feijão preto, cozido', nameEn: 'Black Beans, cooked', info: { calories: 77, protein: 4.5, fat: 0.5, carbs: 14, fiber: 8.4, category: 'proteins', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||
{ name: 'Lentilha, cozida', nameEn: 'Lentils, cooked', info: { calories: 116, protein: 9.4, fat: 0.2, carbs: 19.4, fiber: 5.5, category: 'proteins', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||
{ name: 'Frango, peito, sem pele, cozido', nameEn: 'Chicken Breast, skinless, cooked', info: { calories: 163, protein: 31.5, fat: 3.2, carbs: 0, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||
@@ -44,9 +44,6 @@ export const foodDatabase: FoodItem[] = [
|
||||
|
||||
// Carboidratos, amiláceos - Carbs
|
||||
{ name: 'Grão de Bico, cozido', nameEn: 'Chickpeas, cooked', info: { calories: 180, protein: 9.5, fat: 3, carbs: 30, fiber: 8.6, category: 'carbs', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||
{ name: 'Banana Nanica', nameEn: 'Banana Nanica', info: { calories: 92, protein: 1.4, fat: 0.1, carbs: 23.8, fiber: 1.9, category: 'carbs', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Manga Palmer', nameEn: 'Mango Palmer', info: { calories: 72, protein: 0.4, fat: 0.2, carbs: 19.4, fiber: 1.6, category: 'carbs', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Mamão', nameEn: 'Papaya', info: { calories: 40, protein: 0.5, fat: 0.1, carbs: 10.4, fiber: 1, category: 'carbs', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Batata doce, assada', nameEn: 'Sweet Potato, baked', info: { calories: 90, protein: 2.0, fat: 0.1, carbs: 20.7, fiber: 3.3, category: 'carbs', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||
{ name: 'Batata doce, cozida', nameEn: 'Sweet Potato, boiled', info: { calories: 77, protein: 0.6, fat: 0.1, carbs: 18.4, fiber: 2.2, category: 'carbs', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||
{ name: 'Batata inglesa, assada', nameEn: 'Potato, baked', info: { calories: 94, protein: 2.1, fat: 0.1, carbs: 21.8, fiber: 2.1, category: 'carbs', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||
@@ -57,8 +54,8 @@ export const foodDatabase: FoodItem[] = [
|
||||
{ name: 'Arroz, tipo 1 (branco), cozido', nameEn: 'Rice, type 1 (white), cooked', info: { calories: 128, protein: 2.5, fat: 0.2, carbs: 28.1, fiber: 1.6, category: 'carbs', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||
{ name: 'Arroz, integral, cozido', nameEn: 'Brown Rice, cooked', info: { calories: 124, protein: 2.6, fat: 1.0, carbs: 25.8, fiber: 2.7, category: 'carbs', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||
{ name: 'Aveia em flocos finos s/glúten (Vitalin)', nameEn: 'Gluten-free fine oat flakes (Vitalin)', info: { calories: 338, protein: 15, fat: 6, carbs: 56, fiber: 10, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Ervilha seca, cozida', nameEn: 'Dried peas, cooked', info: { calories: 88, protein: 7.5, fat: 0.5, carbs: 14.2, fiber: 9.7, category: 'carbs', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||
{ name: 'Ervilha congelada, cozida', nameEn: 'Frozen peas, cooked', info: { calories: 57, protein: 5, fat: 1, carbs: 7, fiber: 8, category: 'carbs', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||
{ name: 'Ervilha seca, cozida', nameEn: 'Dried peas, cooked', info: { calories: 88, protein: 7.5, fat: 0.5, carbs: 14.2, fiber: 9.7, category: 'proteins', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||
{ name: 'Ervilha congelada, cozida', nameEn: 'Frozen peas, cooked', info: { calories: 57, protein: 5, fat: 1, carbs: 7, fiber: 8, category: 'proteins', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||
{ name: 'Pão, trigo, forma, integral', nameEn: 'Bread, wheat, sliced, whole', info: { calories: 253, protein: 9.4, fat: 3.7, carbs: 49.9, fiber: 6.9, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch'] } },
|
||||
{ name: 'Pipoca, com óleo de soja, sem sal', nameEn: 'Popcorn, with soybean oil, unsalted', info: { calories: 448, protein: 9.9, fat: 15.9, carbs: 70.3, fiber: 14.3, category: 'carbs', alkaline: false, bestFor: ['brunch', 'snacks'] } },
|
||||
{ name: 'Pipoca, sem óleo, sem sal', nameEn: 'Popcorn, without oil, unsalted', info: { calories: 178, protein: 3.3, fat: 7.2, carbs: 25, fiber: 4.3, category: 'carbs', alkaline: false, bestFor: ['brunch', 'snacks'] } },
|
||||
@@ -82,6 +79,7 @@ export const foodDatabase: FoodItem[] = [
|
||||
{ name: 'Barra de castanhas, amendoim e frutas Nuts', nameEn: 'Nuts, peanuts and fruits bar', info: { calories: 470, protein: 16, fat: 30, carbs: 34, fiber: 12, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Barra de caramelo e chocolate Bold', nameEn: 'Caramelo and chocolate bar Bold', info: { calories: 348, protein: 30, fat: 16, carbs: 28, fiber: 10, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Brownie proteico double chocolate Belive', nameEn: 'Protein Brownie double chocolate Belive', info: { calories: 305, protein: 15, fat: 18, carbs: 33, fiber: 13, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Proto bar Paçoca Amendo Whey, Nutrata', nameEn: 'Protein bar peanuts Whey, Nutrata', info: { calories: 467, protein: 31.4, fat: 22.8, carbs: 31.4, fiber: 5, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
|
||||
// Folhas e verduras - Leaves
|
||||
{ name: 'Agrião', nameEn: 'Watercress', info: { calories: 17, protein: 2.7, fat: 0.2, carbs: 2.3, fiber: 2.1, category: 'leaves', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||
@@ -92,7 +90,10 @@ export const foodDatabase: FoodItem[] = [
|
||||
// Frutas - Fruits
|
||||
{ name: 'Ameixa, crua', nameEn: 'Raw Plum', info: { calories: 53, protein: 0.8, fat: 0, carbs: 13.9, fiber: 2.4, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Abacaxi', nameEn: 'Pineapple', info: { calories: 48, protein: 0.9, fat: 0.1, carbs: 12.3, fiber: 1, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Banana Nanica', nameEn: 'Banana Nanica', info: { calories: 92, protein: 1.4, fat: 0.1, carbs: 23.8, fiber: 1.9, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Laranja', nameEn: 'Orange', info: { calories: 37, protein: 1, fat: 0.1, carbs: 8.9, fiber: 0.8, category: 'fruits', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Manga Palmer', nameEn: 'Mango Palmer', info: { calories: 72, protein: 0.4, fat: 0.2, carbs: 19.4, fiber: 1.6, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Mamão', nameEn: 'Papaya', info: { calories: 40, protein: 0.5, fat: 0.1, carbs: 10.4, fiber: 1, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Maçã', nameEn: 'Apple', info: { calories: 56, protein: 0.3, fat: 0, carbs: 15.2, fiber: 1.3, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Melancia', nameEn: 'Watermelon', info: { calories: 33, protein: 0.9, fat: 0, carbs: 8.1, fiber: 0.1, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
{ name: 'Melão', nameEn: 'Melon', info: { calories: 29, protein: 0.7, fat: 0, carbs: 7.5, fiber: 0.3, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||
|
||||
+189
@@ -36,6 +36,13 @@ let currentViewDate = new Date();
|
||||
let selectedFood: FoodItem | null = null; // review here
|
||||
let currentUser: any | null = null;
|
||||
|
||||
type AlternativeResult = {
|
||||
food: FoodItem;
|
||||
quantityGrams: number;
|
||||
};
|
||||
|
||||
let currentAlternatives: AlternativeResult[] = [];
|
||||
|
||||
// Initialize when page loads
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
await initializeAuth();
|
||||
@@ -420,6 +427,7 @@ function clearEditing() {
|
||||
getInputById('gramAmount').value = '100';
|
||||
showFoodPreview(false);
|
||||
hideAINutritionCard();
|
||||
hideAlternativesContainer();
|
||||
getDivById('add-foot-title').innerHTML = 'Add Food Entry';
|
||||
getButtonById('add-food-btn').innerHTML = 'Add Food';
|
||||
getButtonById('cancel-food-btn').style.display = 'none';
|
||||
@@ -476,6 +484,173 @@ const getFoodItemByName = (foodName: string): FoodItem => {
|
||||
return foodDataSearch[0];
|
||||
}
|
||||
|
||||
function findAlternatives(
|
||||
current: FoodItem,
|
||||
currentQuantityGrams: number,
|
||||
allFoods: FoodItem[]
|
||||
): AlternativeResult[] {
|
||||
if (currentQuantityGrams <= 0) return [];
|
||||
|
||||
const ratio = currentQuantityGrams / 100;
|
||||
const target = {
|
||||
calories: current.info.calories * ratio,
|
||||
protein: current.info.protein * ratio,
|
||||
carbs: current.info.carbs * ratio,
|
||||
fat: current.info.fat * ratio,
|
||||
};
|
||||
|
||||
const sameBestFor = (a: string[], b: string[]) =>
|
||||
[...a].sort().join(',') === [...b].sort().join(',');
|
||||
|
||||
// Consider different macros based in the food category
|
||||
let filtered = allFoods
|
||||
.filter(f =>
|
||||
f.name !== current.name &&
|
||||
f.info.category === current.info.category &&
|
||||
sameBestFor(f.info.bestFor, current.info.bestFor)
|
||||
);
|
||||
|
||||
// Consider calories and fat
|
||||
if (current.info.category === 'fats' || current.info.category === 'dairy') {
|
||||
filtered = filtered
|
||||
.flatMap(candidate => {
|
||||
const p = candidate.info;
|
||||
|
||||
const maxGrams = Math.min(
|
||||
p.calories > 0 ? (target.calories / p.calories) * 100 : Infinity,
|
||||
p.fat > 0 ? (target.fat / p.fat) * 100 : Infinity,
|
||||
);
|
||||
|
||||
return maxGrams > 0 ? [{ food: candidate, quantityGrams: Math.floor(maxGrams) }] : [];
|
||||
});
|
||||
}
|
||||
// Consider calories and proteins
|
||||
else if (current.info.category === 'proteins') {
|
||||
filtered = filtered
|
||||
.flatMap(candidate => {
|
||||
const p = candidate.info;
|
||||
|
||||
const maxGrams = Math.min(
|
||||
p.calories > 0 ? (target.calories / p.calories) * 100 : Infinity,
|
||||
p.protein > 0 ? (target.protein / p.protein) * 100 : Infinity,
|
||||
);
|
||||
|
||||
return maxGrams > 0 ? [{ food: candidate, quantityGrams: Math.floor(maxGrams) }] : [];
|
||||
});
|
||||
}
|
||||
// Consider calories and carbs
|
||||
else if (current.info.category === 'carbs' || current.info.category === 'fruits' || current.info.category === 'low carb') {
|
||||
filtered = filtered
|
||||
.flatMap(candidate => {
|
||||
const p = candidate.info;
|
||||
|
||||
const maxGrams = Math.min(
|
||||
p.calories > 0 ? (target.calories / p.calories) * 100 : Infinity,
|
||||
p.carbs > 0 ? (target.carbs / p.carbs) * 100 : Infinity,
|
||||
);
|
||||
|
||||
return maxGrams > 0 ? [{ food: candidate, quantityGrams: Math.floor(maxGrams) }] : [];
|
||||
});
|
||||
}
|
||||
// Consider only calories
|
||||
else if (current.info.category === 'leaves') {
|
||||
filtered = filtered
|
||||
.flatMap(candidate => {
|
||||
const p = candidate.info;
|
||||
|
||||
const maxGrams = Math.min(
|
||||
p.calories > 0 ? (target.calories / p.calories) * 100 : Infinity,
|
||||
);
|
||||
|
||||
return maxGrams > 0 ? [{ food: candidate, quantityGrams: Math.floor(maxGrams) }] : [];
|
||||
});
|
||||
}
|
||||
|
||||
return filtered
|
||||
.filter(f => f.quantityGrams > 0)
|
||||
.sort((a, b) => a.food.name.localeCompare(b.food.name));
|
||||
}
|
||||
|
||||
function hideAlternativesContainer() {
|
||||
const container = document.getElementById('alternatives-container');
|
||||
if (container) {
|
||||
container.classList.add('display-none');
|
||||
container.classList.remove('display-block');
|
||||
}
|
||||
const list = document.getElementById('alternatives-list');
|
||||
if (list) {
|
||||
list.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
function updateAlternativesDisplay() {
|
||||
const checkbox = document.getElementById('enableAlternatives') as HTMLInputElement;
|
||||
if (!checkbox || !checkbox.checked) return;
|
||||
|
||||
const list = document.getElementById('alternatives-list');
|
||||
if (!list) return;
|
||||
|
||||
if (!selectedFood) {
|
||||
list.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const gramsInput = getInputById('gramAmount');
|
||||
const grams = parseFloat(gramsInput.value);
|
||||
|
||||
if (isNaN(grams) || grams <= 0) {
|
||||
list.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const alternatives = findAlternatives(selectedFood, grams, foodDatabase);
|
||||
currentAlternatives = alternatives;
|
||||
|
||||
const escapeHtml = (text: string): string =>
|
||||
text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''');
|
||||
|
||||
if (alternatives.length === 0) {
|
||||
list.innerHTML = '<div class="no-alternatives">No equivalent alternatives found</div>';
|
||||
} else {
|
||||
list.innerHTML = alternatives.map((alt, index) => `
|
||||
<div class="alternative-item" data-index="${index}" tabindex="0" title="Click to select this food">
|
||||
<span class="alternative-name">${escapeHtml(alt.food.name)}</span>
|
||||
<span class="alternative-quantity">${alt.quantityGrams}g</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
list.querySelectorAll<HTMLElement>('.alternative-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const index = parseInt(item.dataset.index ?? '-1', 10);
|
||||
if (index >= 0 && currentAlternatives[index]) {
|
||||
selectAlternative(currentAlternatives[index].food, currentAlternatives[index].quantityGrams);
|
||||
}
|
||||
});
|
||||
item.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
item.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleAlternativesCheckboxChange(e: Event) {
|
||||
const checkbox = e.target as HTMLInputElement;
|
||||
const container = document.getElementById('alternatives-container');
|
||||
if (!container) return;
|
||||
|
||||
if (checkbox.checked) {
|
||||
container.classList.remove('display-none');
|
||||
container.classList.add('display-block');
|
||||
updateAlternativesDisplay();
|
||||
} else {
|
||||
hideAlternativesContainer();
|
||||
}
|
||||
}
|
||||
|
||||
const previewCalories = (foodSelected?: FoodItem) => {
|
||||
const foodToPreview = foodSelected ?? selectedFood;
|
||||
const gramAmount = getInputById('gramAmount');
|
||||
@@ -500,6 +675,7 @@ const previewCalories = (foodSelected?: FoodItem) => {
|
||||
getDivById('fiberValuePreview').textContent = (Math.round(proportion.info.fiber * 10) / 10).toString ();
|
||||
|
||||
showFoodPreview(true);
|
||||
updateAlternativesDisplay();
|
||||
}
|
||||
|
||||
// ---- Goal management ----
|
||||
@@ -724,6 +900,10 @@ const setupEventListeners = () => {
|
||||
previewCalories();
|
||||
});
|
||||
|
||||
getInputById('gramAmount').addEventListener('input', () => {
|
||||
updateAlternativesDisplay();
|
||||
});
|
||||
|
||||
getButtonById('add-food-btn').addEventListener('click', () => {
|
||||
if (getButtonById('add-food-btn').innerHTML === 'Add Food') {
|
||||
addFood();
|
||||
@@ -797,6 +977,9 @@ const setupEventListeners = () => {
|
||||
// AI nutrition checkbox
|
||||
document.getElementById('enableAINutrition')?.addEventListener('change', handleAICheckboxChange);
|
||||
|
||||
// Alternatives checkbox
|
||||
document.getElementById('enableAlternatives')?.addEventListener('change', handleAlternativesCheckboxChange);
|
||||
|
||||
// Share button event listeners
|
||||
getButtonById('shareBtn').addEventListener('click', handleShareClick);
|
||||
getButtonById('shareBtnMobile').addEventListener('click', handleShareClick);
|
||||
@@ -1026,6 +1209,12 @@ function selectFood(food: FoodItem) {
|
||||
}
|
||||
}
|
||||
|
||||
function selectAlternative(food: FoodItem, grams: number) {
|
||||
selectFood(food);
|
||||
getInputById('gramAmount').value = grams.toString();
|
||||
previewCalories(food);
|
||||
}
|
||||
|
||||
async function setFoodToEdit(foodId: string) {
|
||||
showLoading();
|
||||
|
||||
|
||||
@@ -1198,3 +1198,66 @@ label {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Alternatives Card */
|
||||
#alternatives-container {
|
||||
margin-top: 20px;
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
.alternatives-list-container {
|
||||
background: rgba(40, 40, 40, 0.6);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(107, 141, 214, 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 20px 25px;
|
||||
}
|
||||
|
||||
.alternatives-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
|
||||
.alternative-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 4px;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.alternative-item:hover {
|
||||
background-color: rgba(107, 141, 214, 0.15);
|
||||
}
|
||||
|
||||
.alternative-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.alternative-name {
|
||||
color: #e0e0e0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.alternative-quantity {
|
||||
color: #6B8DD6;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.no-alternatives {
|
||||
color: #b0b0b0;
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# TODO
|
||||
|
||||
- [x] Allow search without special characters
|
||||
- [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
|
||||
- [x] Remove the table and create a list-like component
|
||||
- [x] Add logo
|
||||
- [ ] Group food added by time and consider a meal
|
||||
+5
-6
@@ -2,7 +2,6 @@ import { defineConfig } from 'vite'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
export default defineConfig({
|
||||
base: '/calories-tracker/',
|
||||
include: ['src', 'vite-end.d.ts'],
|
||||
|
||||
plugins: [
|
||||
@@ -18,24 +17,24 @@ export default defineConfig({
|
||||
background_color: '#121212',
|
||||
display: 'standalone', // CRITICAL: Enables fullscreen
|
||||
orientation: 'portrait',
|
||||
scope: '/calories-tracker/',
|
||||
start_url: '/calories-tracker/',
|
||||
scope: '/',
|
||||
start_url: '/',
|
||||
|
||||
icons: [
|
||||
{
|
||||
src: '/calories-tracker/assets/apple-icon-180x180.png',
|
||||
src: '/assets/apple-icon-180x180.png',
|
||||
sizes: '180x180',
|
||||
type: 'image/png',
|
||||
purpose: 'any'
|
||||
},
|
||||
{
|
||||
src: '/calories-tracker/assets/android-icon-192x192.png',
|
||||
src: '/assets/android-icon-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
purpose: 'any maskable'
|
||||
},
|
||||
{
|
||||
src: '/calories-tracker/assets/icon-512x512.png',
|
||||
src: '/assets/icon-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any maskable'
|
||||
|
||||
Reference in New Issue
Block a user