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:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -11,9 +12,12 @@ concurrency:
|
|||||||
group: "pages"
|
group: "pages"
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
APP_FOLDER: "calories-tracker"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: easynode-debian
|
||||||
env:
|
env:
|
||||||
VITE_APPWRITE_PROJECT_ID: ${{ secrets.VITE_APPWRITE_PROJECT_ID }}
|
VITE_APPWRITE_PROJECT_ID: ${{ secrets.VITE_APPWRITE_PROJECT_ID }}
|
||||||
VITE_APPWRITE_ENDPOINT: ${{ secrets.VITE_APPWRITE_ENDPOINT }}
|
VITE_APPWRITE_ENDPOINT: ${{ secrets.VITE_APPWRITE_ENDPOINT }}
|
||||||
@@ -28,11 +32,13 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Cache npm dependencies
|
||||||
uses: actions/setup-node@v4
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
path: ~/.npm
|
||||||
cache: 'npm'
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-npm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -40,25 +46,24 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Install rsync + ssh client
|
||||||
uses: actions/upload-pages-artifact@v3
|
run: apt-get update -qq && apt-get install -y -qq rsync openssh-client
|
||||||
with:
|
|
||||||
path: ./dist
|
|
||||||
|
|
||||||
deploy:
|
- name: Configure SSH
|
||||||
environment:
|
run: |
|
||||||
name: github-pages
|
mkdir -p ~/.ssh
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
|
||||||
runs-on: ubuntu-latest
|
chmod 600 ~/.ssh/deploy_key
|
||||||
needs: build
|
ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
steps:
|
- name: Ensure destination folder exists
|
||||||
- name: Deploy to GitHub Pages
|
run: |
|
||||||
id: deployment
|
ssh -i ~/.ssh/deploy_key deploy@${{ secrets.DEPLOY_HOST }} \
|
||||||
uses: actions/deploy-pages@v4
|
"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/sw.js
|
||||||
dist/workbox-*.js
|
dist/workbox-*.js
|
||||||
dev-dist/
|
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
|
# 🍽️ 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.
|
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
|
## ✨ Features
|
||||||
|
|
||||||
@@ -72,7 +76,7 @@ VITE_APPWRITE_USERSETTINGSID=your_user_settings_collection_id
|
|||||||
|
|
||||||
1. **Clone the repository**
|
1. **Clone the repository**
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/yourusername/calories-tracker.git
|
git clone https://lightroasted.vps-kinghost.net/rmcampos/calories-tracker.git
|
||||||
cd calories-tracker
|
cd calories-tracker
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+15
@@ -298,6 +298,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<div class="form-group checkbox-group">
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input type="checkbox" id="enableAINutrition" class="ai-checkbox">
|
<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",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^7.0.0",
|
||||||
"vite": "^7.3.2",
|
"vite": "^8.1.4",
|
||||||
"vite-plugin-pwa": "^1.2.0",
|
"vite-plugin-pwa": "^1.3.0",
|
||||||
"workbox-window": "^7.4.0"
|
"workbox-window": "^7.4.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.91.1",
|
"@anthropic-ai/sdk": "^0.111.0",
|
||||||
"appwrite": "^24.2.0",
|
"appwrite": "^26.1.0",
|
||||||
"sweetalert": "^2.1.2"
|
"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[] = [
|
export const foodDatabase: FoodItem[] = [
|
||||||
// Gorduras - Fats
|
// 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: '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: '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', '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'] } },
|
||||||
{ 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: '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 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', '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'] } },
|
||||||
{ 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 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 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'] } },
|
{ 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'] } },
|
{ 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
|
// 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: '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, 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', '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', '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: '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: '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: '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: '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: '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: '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'] } },
|
{ 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
|
// 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: '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, 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 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'] } },
|
{ 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, 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: '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: '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 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: '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: '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: '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, 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'] } },
|
{ 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 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: '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: '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
|
// 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'] } },
|
{ 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
|
// 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: '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: '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: '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: '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: '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'] } },
|
{ 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 selectedFood: FoodItem | null = null; // review here
|
||||||
let currentUser: any | null = null;
|
let currentUser: any | null = null;
|
||||||
|
|
||||||
|
type AlternativeResult = {
|
||||||
|
food: FoodItem;
|
||||||
|
quantityGrams: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentAlternatives: AlternativeResult[] = [];
|
||||||
|
|
||||||
// Initialize when page loads
|
// Initialize when page loads
|
||||||
document.addEventListener('DOMContentLoaded', async function() {
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
await initializeAuth();
|
await initializeAuth();
|
||||||
@@ -420,6 +427,7 @@ function clearEditing() {
|
|||||||
getInputById('gramAmount').value = '100';
|
getInputById('gramAmount').value = '100';
|
||||||
showFoodPreview(false);
|
showFoodPreview(false);
|
||||||
hideAINutritionCard();
|
hideAINutritionCard();
|
||||||
|
hideAlternativesContainer();
|
||||||
getDivById('add-foot-title').innerHTML = 'Add Food Entry';
|
getDivById('add-foot-title').innerHTML = 'Add Food Entry';
|
||||||
getButtonById('add-food-btn').innerHTML = 'Add Food';
|
getButtonById('add-food-btn').innerHTML = 'Add Food';
|
||||||
getButtonById('cancel-food-btn').style.display = 'none';
|
getButtonById('cancel-food-btn').style.display = 'none';
|
||||||
@@ -476,6 +484,173 @@ const getFoodItemByName = (foodName: string): FoodItem => {
|
|||||||
return foodDataSearch[0];
|
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 previewCalories = (foodSelected?: FoodItem) => {
|
||||||
const foodToPreview = foodSelected ?? selectedFood;
|
const foodToPreview = foodSelected ?? selectedFood;
|
||||||
const gramAmount = getInputById('gramAmount');
|
const gramAmount = getInputById('gramAmount');
|
||||||
@@ -500,6 +675,7 @@ const previewCalories = (foodSelected?: FoodItem) => {
|
|||||||
getDivById('fiberValuePreview').textContent = (Math.round(proportion.info.fiber * 10) / 10).toString ();
|
getDivById('fiberValuePreview').textContent = (Math.round(proportion.info.fiber * 10) / 10).toString ();
|
||||||
|
|
||||||
showFoodPreview(true);
|
showFoodPreview(true);
|
||||||
|
updateAlternativesDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Goal management ----
|
// ---- Goal management ----
|
||||||
@@ -724,6 +900,10 @@ const setupEventListeners = () => {
|
|||||||
previewCalories();
|
previewCalories();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
getInputById('gramAmount').addEventListener('input', () => {
|
||||||
|
updateAlternativesDisplay();
|
||||||
|
});
|
||||||
|
|
||||||
getButtonById('add-food-btn').addEventListener('click', () => {
|
getButtonById('add-food-btn').addEventListener('click', () => {
|
||||||
if (getButtonById('add-food-btn').innerHTML === 'Add Food') {
|
if (getButtonById('add-food-btn').innerHTML === 'Add Food') {
|
||||||
addFood();
|
addFood();
|
||||||
@@ -797,6 +977,9 @@ const setupEventListeners = () => {
|
|||||||
// AI nutrition checkbox
|
// AI nutrition checkbox
|
||||||
document.getElementById('enableAINutrition')?.addEventListener('change', handleAICheckboxChange);
|
document.getElementById('enableAINutrition')?.addEventListener('change', handleAICheckboxChange);
|
||||||
|
|
||||||
|
// Alternatives checkbox
|
||||||
|
document.getElementById('enableAlternatives')?.addEventListener('change', handleAlternativesCheckboxChange);
|
||||||
|
|
||||||
// Share button event listeners
|
// Share button event listeners
|
||||||
getButtonById('shareBtn').addEventListener('click', handleShareClick);
|
getButtonById('shareBtn').addEventListener('click', handleShareClick);
|
||||||
getButtonById('shareBtnMobile').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) {
|
async function setFoodToEdit(foodId: string) {
|
||||||
showLoading();
|
showLoading();
|
||||||
|
|
||||||
|
|||||||
@@ -1198,3 +1198,66 @@ label {
|
|||||||
font-size: 0.85rem;
|
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'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: '/calories-tracker/',
|
|
||||||
include: ['src', 'vite-end.d.ts'],
|
include: ['src', 'vite-end.d.ts'],
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -18,24 +17,24 @@ export default defineConfig({
|
|||||||
background_color: '#121212',
|
background_color: '#121212',
|
||||||
display: 'standalone', // CRITICAL: Enables fullscreen
|
display: 'standalone', // CRITICAL: Enables fullscreen
|
||||||
orientation: 'portrait',
|
orientation: 'portrait',
|
||||||
scope: '/calories-tracker/',
|
scope: '/',
|
||||||
start_url: '/calories-tracker/',
|
start_url: '/',
|
||||||
|
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: '/calories-tracker/assets/apple-icon-180x180.png',
|
src: '/assets/apple-icon-180x180.png',
|
||||||
sizes: '180x180',
|
sizes: '180x180',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
purpose: 'any'
|
purpose: 'any'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: '/calories-tracker/assets/android-icon-192x192.png',
|
src: '/assets/android-icon-192x192.png',
|
||||||
sizes: '192x192',
|
sizes: '192x192',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
purpose: 'any maskable'
|
purpose: 'any maskable'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: '/calories-tracker/assets/icon-512x512.png',
|
src: '/assets/icon-512x512.png',
|
||||||
sizes: '512x512',
|
sizes: '512x512',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
purpose: 'any maskable'
|
purpose: 'any maskable'
|
||||||
|
|||||||
Reference in New Issue
Block a user