17 Commits

Author SHA1 Message Date
rmcampos f63e51ca1f feat: add workflow to run against PRs
Build & Deploy to Pages / build-deploy (push) Has been cancelled
2026-07-10 22:25:20 +02:00
rmcampos 8a4435e28d Merge pull request 'Update dependency typescript to v7' (#3) from renovate/typescript-7.x into main
Build & Deploy to Pages / build-deploy (push) Successful in 56s
Reviewed-on: #3
Reviewed-by: Ricardo Campos <ricardompcampos@gmail.com>
2026-07-10 20:21:49 +00:00
rmcampos 761fd3dfc0 Merge branch 'main' into renovate/typescript-7.x
Build & Deploy to Pages / build-deploy (pull_request) Successful in 4m13s
2026-07-10 22:16:51 +02:00
rmcampos b51a54c6db chore: bump deps
Build & Deploy to Pages / build-deploy (push) Successful in 3m59s
2026-07-10 22:04:13 +02:00
renovate-bot adcc6b5f74 Update dependency typescript to v7
renovate/artifacts Artifact file update failure
Build & Deploy to Pages / build-deploy (pull_request) Failing after 20s
2026-07-10 16:59:18 +00:00
renovate-bot aa97b04baa Configure Renovate (#2)
Build & Deploy to Pages / build-deploy (push) Successful in 55s
Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboarding PR to help you understand and configure settings before regular Pull Requests begin.

🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged.

📚 See our [Reading List](https://docs.renovatebot.com/reading-list/) for relevant documentation you may be interested in reading.

🔡 Do you want to change how Renovate upgrades your dependencies? Add your custom config to `renovate.json` in this branch. Renovate will update the Pull Request description the next time it runs.

---
### Detected Package Files

 * `package.json` (npm)

### Configuration Summary

Based on the default config's presets, Renovate will:

  - Start dependency updates only once this onboarding PR is merged
  - Enable Renovate Dependency Dashboard creation.
  - Use semantic commit type `fix` for dependencies and `chore` for all others if semantic commits are in use.
  - Ignore `node_modules`, `bower_components`, `vendor` and various test/tests (except for nuget) directories.
  - Group known monorepo packages together.
  - Use curated list of recommended non-monorepo package groupings.
  - Show only the Age and Confidence Merge Confidence badges for pull requests.
  - Apply crowd-sourced package replacement rules.
  - Apply crowd-sourced workarounds for known problems with packages.
  - Ensure that every dependency pinned by digest and sourced from Forgejo contains a link to the commit-to-commit diff
  - Ensure that every dependency pinned by digest and sourced from Gitea contains a link to the commit-to-commit diff
  - Ensure that every dependency pinned by digest and sourced from GitHub.com and Github enterprise contains a link to the commit-to-commit diff
  - Ensure that every dependency pinned by digest and sourced from GitLab.com contains a link to the commit-to-commit diff
  - Correctly link to the source code for golang.org/x packages
  - Link to pkg.go.dev/... for golang.org/x packages' title
  - Provide a link to octochangelog's improved breakdown for Renovate's changelogs

---

### What to Expect

It looks like your repository dependencies are already up-to-date and no Pull Requests will be necessary right away.

---

 Got questions? Check out Renovate's [Docs](https://docs.renovatebot.com/), particularly the Getting Started section.
If you need any further assistance then you can also [request help here](https://github.com/renovatebot/renovate/discussions).

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).

<!--renovate-config-hash:e80b4e42a3043bc12fa0640db4bac392d2bf770acf841360d7c8ceeeac2ec1a9-->

Reviewed-on: #2
Co-authored-by: Renovate Bot <renovate-bot@lightroasted.vps-kinghost.net>
Co-committed-by: Renovate Bot <renovate-bot@lightroasted.vps-kinghost.net>
2026-07-05 21:06:39 +00:00
rmcampos 2a1a492523 hotfix: vite config with base path
Build & Deploy to Pages / build-deploy (push) Successful in 57s
2026-07-05 00:00:45 +02:00
rmcampos 70d2d22055 Update .github/workflows/deploy.yml
Build & Deploy to Pages / build-deploy (push) Successful in 4m10s
2026-07-04 21:55:24 +00:00
rmcampos 0682649a9f Merge pull request 'feat: remove old files and update workflows for gitea' (#1) from feat/updates-for-gitea into main
Reviewed-on: #1
2026-07-04 21:49:48 +00:00
rmcampos 1eba605146 feat: remove old files and update workflows for gitea 2026-07-04 23:45:30 +02:00
rmcampos 0746088203 Add protein bar entry to food database 2026-05-19 13:14:57 -03:00
Copilot fe2f324bfa Make similar food suggestions clickable to populate food entry form (#12) 2026-04-28 23:00:36 -03:00
rmcampos 88f55e4f7a Update foodDatabase.ts 2026-04-28 22:48:00 -03:00
rmcampos 2a18a78a76 Update foodDatabase.ts 2026-04-28 22:35:21 -03:00
rmcampos 1a0bd0007f chore: update gitignore to ignore idea files 2026-04-28 13:14:10 -03:00
rmcampos 8f327c191b feat: update packages dependencies 2026-04-28 13:12:33 -03:00
Copilot 6837e4f139 feat: Add "Show other options" to surface nutritionally equivalent food alternatives (#10)
* Initial plan

* feat: Add 'Show other options' feature to find similar foods by category and bestFor

Agent-Logs-Url: https://github.com/RMCampos/calories-tracker/sessions/00437e19-3c33-48b4-8d15-f29ac843a20f

Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com>

* fix: Address code review issues - string type, dedup logic, HTML escaping

Agent-Logs-Url: https://github.com/RMCampos/calories-tracker/sessions/00437e19-3c33-48b4-8d15-f29ac843a20f

Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com>

* fix: Add full HTML escaping including quotes in alternatives display

Agent-Logs-Url: https://github.com/RMCampos/calories-tracker/sessions/00437e19-3c33-48b4-8d15-f29ac843a20f

Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com>

* feat: improve food options by food type

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com>
Co-authored-by: Ricardo Campos <ricardompcampos@gmail.com>
2026-04-28 11:07:34 -03:00
14 changed files with 2579 additions and 2004 deletions
+42
View File
@@ -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
+34 -29
View File
@@ -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 }}
@@ -27,38 +31,39 @@ jobs:
steps:
- 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
- name: Build
run: npm run build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./dist
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
- name: Install rsync + ssh client
run: apt-get update -qq && apt-get install -y -qq rsync openssh-client
- 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
- 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
+3
View File
@@ -8,3 +8,6 @@ senha.txt
dist/sw.js
dist/workbox-*.js
dev-dist/
# IDE
.idea/
-75
View File
@@ -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
+6 -2
View File
@@ -1,8 +1,12 @@
![Calories Tracker](https://img.shields.io/badge/status-active-brightgreen) ![Version](https://img.shields.io/badge/version-1.0.0-blue) ![License](https://img.shields.io/badge/license-ISC-orange)
# 🍽️ 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.
![Calories Tracker](https://img.shields.io/badge/status-active-brightgreen) ![Version](https://img.shields.io/badge/version-1.0.0-blue) ![License](https://img.shields.io/badge/license-ISC-orange)
## 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
View File
@@ -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">
+2195 -1863
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -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"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}
+15 -14
View File
@@ -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
View File
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
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();
+63
View File
@@ -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;
}
-9
View File
@@ -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
View File
@@ -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'