Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
f63e51ca1f
|
|||
| 8a4435e28d | |||
|
761fd3dfc0
|
|||
|
b51a54c6db
|
|||
| adcc6b5f74 | |||
| aa97b04baa | |||
|
2a1a492523
|
|||
| 70d2d22055 | |||
| 0682649a9f | |||
|
1eba605146
|
|||
| 0746088203 | |||
| fe2f324bfa | |||
| 88f55e4f7a | |||
| 2a18a78a76 | |||
| 1a0bd0007f | |||
| 8f327c191b | |||
| 6837e4f139 | |||
| 44847d561f | |||
| df70762505 | |||
| 4e9e4edc54 | |||
| fa7571e8aa | |||
| 7119d21b6c | |||
| e4fa1f9c1c | |||
| b07861f88f | |||
| 372a06eb6b | |||
| d173329e19 | |||
| a5329af7b3 | |||
| 6826a4256d | |||
| 8a187fbe15 | |||
| 3f89eca586 | |||
| 5c856300cd | |||
| 78f709bee7 | |||
| 2fbba3b6b2 | |||
| 78220fdbf0 | |||
| 1ca0706923 | |||
| a3b1747bda | |||
| 228c7e4661 | |||
| 915cfba809 | |||
| 6b36bf0e19 | |||
| 39ce9e4684 | |||
| 37e3d4268b | |||
| 4117c54d24 | |||
| 0499e4dc4e | |||
| 761c19d394 | |||
| 5948d976a8 | |||
| 63312b1bc0 | |||
| ae7d5aa5b5 | |||
| c0489dd5e8 | |||
| 531c5642d6 |
@@ -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 }}
|
||||||
@@ -22,18 +26,19 @@ jobs:
|
|||||||
VITE_APPWRITE_USERSETTINGSID: ${{ secrets.VITE_APPWRITE_USERSETTINGSID }}
|
VITE_APPWRITE_USERSETTINGSID: ${{ secrets.VITE_APPWRITE_USERSETTINGSID }}
|
||||||
VITE_APPWRITE_MONTLYCALORIESID: ${{ secrets.VITE_APPWRITE_MONTLYCALORIESID }}
|
VITE_APPWRITE_MONTLYCALORIESID: ${{ secrets.VITE_APPWRITE_MONTLYCALORIESID }}
|
||||||
VITE_APPWRITE_SHAREDDAYSID: ${{ secrets.VITE_APPWRITE_SHAREDDAYSID }}
|
VITE_APPWRITE_SHAREDDAYSID: ${{ secrets.VITE_APPWRITE_SHAREDDAYSID }}
|
||||||
VITE_APPWRITE_MEALPLANTEMPLATESID: ${{ secrets.VITE_APPWRITE_MEALPLANTEMPLATESID }}
|
VITE_CLAUDE_API_KEY: ${{ secrets.VITE_CLAUDE_API_KEY }}
|
||||||
VITE_APPWRITE_PLANNEDFOODITEMSID: ${{ secrets.VITE_APPWRITE_PLANNEDFOODITEMSID }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- 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
|
||||||
@@ -41,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
|
|
||||||
|
|||||||
@@ -3,3 +3,11 @@ dist/
|
|||||||
.env
|
.env
|
||||||
senha.txt
|
senha.txt
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
# PWA
|
||||||
|
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
|
# 🍽️ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"projectId": "684ac3bf000ee8950f5a",
|
|
||||||
"endpoint": "https://fra.cloud.appwrite.io/v1"
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
@@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "App",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/assets/android-icon-36x36.png",
|
|
||||||
"sizes": "36x36",
|
|
||||||
"type": "image/png",
|
|
||||||
"density": "0.75"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/assets/android-icon-48x48.png",
|
|
||||||
"sizes": "48x48",
|
|
||||||
"type": "image/png",
|
|
||||||
"density": "1.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/assets/android-icon-72x72.png",
|
|
||||||
"sizes": "72x72",
|
|
||||||
"type": "image/png",
|
|
||||||
"density": "1.5"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/assets/android-icon-96x96.png",
|
|
||||||
"sizes": "96x96",
|
|
||||||
"type": "image/png",
|
|
||||||
"density": "2.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/assets/android-icon-144x144.png",
|
|
||||||
"sizes": "144x144",
|
|
||||||
"type": "image/png",
|
|
||||||
"density": "3.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/assets/android-icon-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png",
|
|
||||||
"density": "4.0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -19,6 +19,13 @@
|
|||||||
min-height: 50px;
|
min-height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
.logo h2 {
|
.logo h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
@@ -30,20 +37,108 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-form {
|
.auth-buttons {
|
||||||
background: rgba(40, 40, 40, 0.9);
|
display: flex;
|
||||||
backdrop-filter: blur(10px);
|
align-items: center;
|
||||||
border-radius: 6px;
|
gap: 12px;
|
||||||
padding: 25px;
|
}
|
||||||
min-width: 300px;
|
|
||||||
|
.auth-buttons .header-btn.primary {
|
||||||
|
background: linear-gradient(135deg, #6B8DD6 0%, #5A7BC6 100%);
|
||||||
|
border: 1px solid rgba(107, 141, 214, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-buttons .header-btn.primary:hover {
|
||||||
|
box-shadow: 0 4px 15px rgba(107, 141, 214, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Authentication Modal */
|
||||||
|
.auth-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 10000;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-modal-content {
|
||||||
|
background: rgba(40, 40, 40, 0.95);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px;
|
||||||
|
max-width: 450px;
|
||||||
|
width: 90%;
|
||||||
border: 1px solid rgba(100, 100, 100, 0.3);
|
border: 1px solid rgba(100, 100, 100, 0.3);
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||||
|
position: relative;
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-modal-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-modal-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-width: auto;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-form h3 {
|
.auth-form h3 {
|
||||||
margin: 0 0 20px 0;
|
margin: 0 0 25px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1.3rem;
|
font-size: 1.6rem;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-form .form-group {
|
.auth-form .form-group {
|
||||||
@@ -308,6 +403,10 @@
|
|||||||
order: 1;
|
order: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
.logo h2 {
|
.logo h2 {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
@@ -318,6 +417,16 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-buttons {
|
||||||
|
order: 2;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-buttons .header-btn {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 8px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
order: 2;
|
order: 2;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -356,6 +465,163 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Landing Page Styles */
|
||||||
|
.landing-page {
|
||||||
|
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
min-height: calc(100vh - 80px);
|
||||||
|
padding: 60px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-hero {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
animation: fadeInUp 0.8s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-title {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
background: linear-gradient(135deg, #6B8DD6 0%, #9B7BC6 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-features {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
animation: fadeInUp 0.8s ease-out 0.2s backwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
background: rgba(40, 40, 40, 0.6);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(100, 100, 100, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
border-color: rgba(107, 141, 214, 0.5);
|
||||||
|
box-shadow: 0 8px 30px rgba(107, 141, 214, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
color: white;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-cta {
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(74, 93, 138, 0.4);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(100, 100, 100, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 50px 30px;
|
||||||
|
animation: fadeInUp 0.8s ease-out 0.4s backwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-cta h2 {
|
||||||
|
color: white;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-cta p {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin: 0 0 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-btn {
|
||||||
|
padding: 15px 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: none;
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-btn.primary {
|
||||||
|
background: linear-gradient(135deg, #6B8DD6 0%, #5A7BC6 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 15px rgba(107, 141, 214, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-btn.primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 25px rgba(107, 141, 214, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-btn.secondary {
|
||||||
|
background: rgba(40, 40, 40, 0.8);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(107, 141, 214, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-btn.secondary:hover {
|
||||||
|
background: rgba(60, 60, 60, 0.9);
|
||||||
|
border-color: rgba(107, 141, 214, 0.8);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* For very small screens */
|
/* For very small screens */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.auth-container {
|
.auth-container {
|
||||||
@@ -376,7 +642,67 @@
|
|||||||
right: -10px;
|
right: -10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.logo h2 {
|
.logo h2 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-buttons {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-buttons .header-btn {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-modal-content {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Landing Page Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.landing-page {
|
||||||
|
padding: 40px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-subtitle {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-features {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-cta {
|
||||||
|
padding: 35px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-cta h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-btn {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+273
-192
@@ -7,7 +7,6 @@
|
|||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<link rel="stylesheet" href="auth.css">
|
<link rel="stylesheet" href="auth.css">
|
||||||
<link rel="stylesheet" href="settings.css">
|
<link rel="stylesheet" href="settings.css">
|
||||||
<link rel="stylesheet" href="mealPlanner.css">
|
|
||||||
<link rel="apple-touch-icon" sizes="57x57" href="/assets/apple-icon-57x57.png">
|
<link rel="apple-touch-icon" sizes="57x57" href="/assets/apple-icon-57x57.png">
|
||||||
<link rel="apple-touch-icon" sizes="60x60" href="/assets/apple-icon-60x60.png">
|
<link rel="apple-touch-icon" sizes="60x60" href="/assets/apple-icon-60x60.png">
|
||||||
<link rel="apple-touch-icon" sizes="72x72" href="/assets/apple-icon-72x72.png">
|
<link rel="apple-touch-icon" sizes="72x72" href="/assets/apple-icon-72x72.png">
|
||||||
@@ -21,24 +20,61 @@
|
|||||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="96x96" href="/assets/favicon-96x96.png">
|
<link rel="icon" type="image/png" sizes="96x96" href="/assets/favicon-96x96.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png">
|
||||||
<link rel="manifest" href="/assets/manifest.json">
|
|
||||||
<meta name="msapplication-TileColor" content="#ffffff">
|
<meta name="msapplication-TileColor" content="#ffffff">
|
||||||
<meta name="msapplication-TileImage" content="/assets/ms-icon-144x144.png">
|
<meta name="msapplication-TileImage" content="/assets/ms-icon-144x144.png">
|
||||||
<meta name="theme-color" content="#121212">
|
<meta name="theme-color" content="#121212">
|
||||||
|
<!-- PWA Meta Tags -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Food Tracker">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Authentication Header -->
|
<!-- Authentication Header -->
|
||||||
<header class="auth-header">
|
<header class="auth-header">
|
||||||
<div class="auth-container">
|
<div class="auth-container">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<h2>🍎 Food Tracker</h2>
|
<h1>🍎 Food Tracker</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Authentication Forms -->
|
<!-- Authentication Buttons (shown when not logged in) -->
|
||||||
<div id="auth-section" class="auth-section">
|
<div id="auth-buttons" class="auth-buttons">
|
||||||
|
<button id="open-login-btn" class="header-btn">Login</button>
|
||||||
|
<button id="open-register-btn" class="header-btn primary">Get Started</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Info (shown when logged in) -->
|
||||||
|
<div id="user-info" class="user-info hidden">
|
||||||
|
<div class="user-details">
|
||||||
|
<span id="userName">Welcome, User!</span>
|
||||||
|
<div class="header-buttons">
|
||||||
|
<button id="shareBtn" class="header-btn">🔗 Share</button>
|
||||||
|
<button id="calendarBtn" class="header-btn">📅 Calendar</button>
|
||||||
|
<button id="settingsBtn" class="header-btn">⚙️ Settings</button>
|
||||||
|
<button id="logoutBtn" class="header-btn logout">🚪 Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Mobile menu toggle -->
|
||||||
|
<button id="mobileMenuToggle" class="mobile-menu-toggle hidden">☰</button>
|
||||||
|
<!-- Mobile dropdown menu -->
|
||||||
|
<div id="mobileMenu" class="mobile-menu hidden">
|
||||||
|
<button id="shareBtnMobile" class="mobile-menu-item">🔗 Share</button>
|
||||||
|
<button id="calendarBtnMobile" class="mobile-menu-item">📅 Calendar</button>
|
||||||
|
<button id="settingsBtnMobile" class="mobile-menu-item">⚙️ Settings</button>
|
||||||
|
<button id="logoutBtnMobile" class="mobile-menu-item logout">🚪 Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Authentication Modal -->
|
||||||
|
<div id="auth-modal" class="auth-modal hidden">
|
||||||
|
<div class="auth-modal-content">
|
||||||
|
<button id="close-auth-modal" class="close-modal-btn">×</button>
|
||||||
|
|
||||||
<!-- Login Form -->
|
<!-- Login Form -->
|
||||||
<div id="login-form" class="auth-form">
|
<div id="login-form" class="auth-form">
|
||||||
<h3>Login</h3>
|
<h3>Welcome Back</h3>
|
||||||
<form id="loginForm">
|
<form id="loginForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="email" id="loginEmail" placeholder="Email" required>
|
<input type="email" id="loginEmail" placeholder="Email" required>
|
||||||
@@ -56,7 +92,7 @@
|
|||||||
|
|
||||||
<!-- Register Form -->
|
<!-- Register Form -->
|
||||||
<div id="register-form" class="auth-form hidden">
|
<div id="register-form" class="auth-form hidden">
|
||||||
<h3>Register</h3>
|
<h3>Create Your Account</h3>
|
||||||
<form id="registerForm">
|
<form id="registerForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text" id="registerName" placeholder="Name" required>
|
<input type="text" id="registerName" placeholder="Name" required>
|
||||||
@@ -75,32 +111,64 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- User Info (shown when logged in) -->
|
<!-- Landing Page Content (shown when not logged in) -->
|
||||||
<div id="user-info" class="user-info hidden">
|
<section id="landing-page" class="landing-page hidden">
|
||||||
<div class="user-details">
|
<div class="landing-container">
|
||||||
<span id="userName">Welcome, User!</span>
|
<div class="landing-hero">
|
||||||
<div class="header-buttons">
|
<h1 class="landing-title">Track Your Nutrition, Transform Your Health</h1>
|
||||||
<button id="shareBtn" class="header-btn">🔗 Share</button>
|
<p class="landing-subtitle">Take control of your diet with our comprehensive calorie and nutrition tracker. Perfect for anyone who wants to maintain a balanced diet and achieve their health goals.</p>
|
||||||
<button id="calendarBtn" class="header-btn">📅 Calendar</button>
|
</div>
|
||||||
<button id="mealPlannerBtn" class="header-btn">📋 Meal Planner</button>
|
|
||||||
<button id="settingsBtn" class="header-btn">⚙️ Settings</button>
|
<div class="landing-features">
|
||||||
<button id="logoutBtn" class="header-btn logout">🚪 Logout</button>
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">📊</div>
|
||||||
|
<h3>Detailed Tracking</h3>
|
||||||
|
<p>Monitor calories, protein, fat, carbs, and fiber for every meal. Get a complete picture of your daily nutrition.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">🎯</div>
|
||||||
|
<h3>Set Goals</h3>
|
||||||
|
<p>Define your daily macros targets and track your progress. Stay motivated and on track with your health journey.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">📅</div>
|
||||||
|
<h3>Calendar View</h3>
|
||||||
|
<p>Visualize your nutrition history with an intuitive calendar. See patterns and maintain consistency over time.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">🥗</div>
|
||||||
|
<h3>Alkaline Balance</h3>
|
||||||
|
<p>Track your alkaline food intake to maintain a healthy pH balance and support overall wellness.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">🔍</div>
|
||||||
|
<h3>Smart Search</h3>
|
||||||
|
<p>Quickly find and log foods from our comprehensive database. Make tracking effortless and accurate.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">🔗</div>
|
||||||
|
<h3>Share Progress</h3>
|
||||||
|
<p>Share your daily food log with friends, trainers, or nutritionists. Get support and accountability.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Mobile menu toggle -->
|
|
||||||
<button id="mobileMenuToggle" class="mobile-menu-toggle hidden">☰</button>
|
<div class="landing-cta">
|
||||||
<!-- Mobile dropdown menu -->
|
<h2>Ready to Start Your Health Journey?</h2>
|
||||||
<div id="mobileMenu" class="mobile-menu hidden">
|
<p>Join today and take the first step towards a healthier, more balanced lifestyle.</p>
|
||||||
<button id="shareBtnMobile" class="mobile-menu-item">🔗 Share</button>
|
<div class="cta-buttons">
|
||||||
<button id="calendarBtnMobile" class="mobile-menu-item">📅 Calendar</button>
|
<button id="cta-register-btn" class="cta-btn primary">Get Started Free</button>
|
||||||
<button id="mealPlannerBtnMobile" class="mobile-menu-item">📋 Meal Planner</button>
|
<button id="cta-login-btn" class="cta-btn secondary">Already Have an Account?</button>
|
||||||
<button id="settingsBtnMobile" class="mobile-menu-item">⚙️ Settings</button>
|
|
||||||
<button id="logoutBtnMobile" class="mobile-menu-item logout">🚪 Logout</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</section>
|
||||||
|
|
||||||
<!-- Loading Overlay -->
|
<!-- Loading Overlay -->
|
||||||
<div id="loading-overlay" class="loading-overlay hidden">
|
<div id="loading-overlay" class="loading-overlay hidden">
|
||||||
@@ -169,14 +237,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick Add from Meal Plan -->
|
|
||||||
<div class="quick-add-section hidden" id="quickAddSection">
|
|
||||||
<h2 class="section-title">📋 Quick Add from Meal Plan</h2>
|
|
||||||
<div class="quick-add-items" id="quickAddItemsContainer">
|
|
||||||
<!-- Quick add items will be populated here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="add-food-section">
|
<div class="add-food-section">
|
||||||
<h2 class="add-food-title" id="add-foot-title">Add Food Entry</h2>
|
<h2 class="add-food-title" id="add-foot-title">Add Food Entry</h2>
|
||||||
<div class="add-food-form">
|
<div class="add-food-form">
|
||||||
@@ -207,6 +267,10 @@
|
|||||||
<label for="gramAmount">Amount (grams)</label>
|
<label for="gramAmount">Amount (grams)</label>
|
||||||
<input type="number" id="gramAmount" min="1" step="1" value="100">
|
<input type="number" id="gramAmount" min="1" step="1" value="100">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group hidden" id="foodTimeGroup">
|
||||||
|
<label for="foodEntryTime">Time</label>
|
||||||
|
<input type="time" id="foodEntryTime">
|
||||||
|
</div>
|
||||||
<button class="add-btn" id="add-food-btn">Add Food</button>
|
<button class="add-btn" id="add-food-btn">Add Food</button>
|
||||||
<button class="cancel-btn" id="cancel-food-btn">Cancel</button>
|
<button class="cancel-btn" id="cancel-food-btn">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -234,6 +298,75 @@
|
|||||||
</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">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="enableAINutrition" class="ai-checkbox">
|
||||||
|
<span>Show AI nutrition information</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AI Nutrition Info Card -->
|
||||||
|
<div class="display-none" id="ai-nutrition-card">
|
||||||
|
<div class="ai-nutrition-container">
|
||||||
|
<h3 class="ai-nutrition-title">
|
||||||
|
<span class="ai-badge">AI</span>
|
||||||
|
Detailed Nutrition Information
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div id="ai-nutrition-loading" class="ai-loading">
|
||||||
|
<div class="ai-spinner"></div>
|
||||||
|
<p>Analyzing nutritional content...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error State -->
|
||||||
|
<div id="ai-nutrition-error" class="ai-error hidden">
|
||||||
|
<p>Unable to load detailed nutrition info. Please try again.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div id="ai-nutrition-content" class="ai-nutrition-content hidden">
|
||||||
|
<!-- Vitamins Section -->
|
||||||
|
<div class="ai-nutrition-section">
|
||||||
|
<h4 class="ai-section-title">Vitamins</h4>
|
||||||
|
<ul id="ai-vitamins-list" class="ai-list"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Minerals Section -->
|
||||||
|
<div class="ai-nutrition-section">
|
||||||
|
<h4 class="ai-section-title">Minerals</h4>
|
||||||
|
<ul id="ai-minerals-list" class="ai-list"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Benefits Section -->
|
||||||
|
<div class="ai-nutrition-section">
|
||||||
|
<h4 class="ai-section-title">Health Benefits</h4>
|
||||||
|
<ul id="ai-benefits-list" class="ai-list"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes Section -->
|
||||||
|
<div class="ai-nutrition-section" id="ai-notes-section">
|
||||||
|
<h4 class="ai-section-title">Notes</h4>
|
||||||
|
<p id="ai-notes-text" class="ai-notes"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section-title">Food Log</div>
|
<div class="section-title">Food Log</div>
|
||||||
@@ -260,193 +393,141 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>⚙️ Settings</h1>
|
<h1>⚙️ Settings</h1>
|
||||||
|
|
||||||
<div class="date-display">Define your daily macros goal</div>
|
<div class="date-display">Body Metrics</div>
|
||||||
|
|
||||||
<form id="settingsForm" class="settings-form">
|
<form id="settingsForm" class="settings-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="caloriesGoal">Daily calorie goal</label>
|
<label for="bodyWeight">Body Weight (kg)</label>
|
||||||
<input type="number" id="caloriesGoal" placeholder="Calories" required>
|
<input type="text" id="bodyWeight" placeholder="Body Weight">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="proteinGoal">Daily protein goal</label>
|
<label for="height">Height (cm)</label>
|
||||||
<input type="number" id="proteinGoal" placeholder="Proteins in grams" required>
|
<input type="number" id="height" placeholder="Height">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="fatGoal">Daily fat goal</label>
|
<label for="bmi">Body Mass Index (BMI)</label>
|
||||||
<input type="number" id="fatGoal" placeholder="Fat in grams" required>
|
<input type="number" id="bmi" placeholder="BMI" readonly>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="carboGoal">Daily carbs goal</label>
|
<label for="bmiResult">Body Mass Index (BMI) Result</label>
|
||||||
<input type="number" id="carboGoal" placeholder="Carbo in grams" required>
|
<input type="text" id="bmiResult" placeholder="BMI Result" readonly>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="fiberGoal">Daily fiber goal</label>
|
<label for="timezone">Timezone</label>
|
||||||
<input type="number" id="fiberGoal" placeholder="Fiber in grams" required>
|
<select id="timezone">
|
||||||
|
<option value="">— Use browser time —</option>
|
||||||
|
<optgroup label="UTC">
|
||||||
|
<option value="UTC">UTC</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Americas">
|
||||||
|
<option value="America/New_York">America/New_York</option>
|
||||||
|
<option value="America/Chicago">America/Chicago</option>
|
||||||
|
<option value="America/Denver">America/Denver</option>
|
||||||
|
<option value="America/Los_Angeles">America/Los_Angeles</option>
|
||||||
|
<option value="America/Toronto">America/Toronto</option>
|
||||||
|
<option value="America/Vancouver">America/Vancouver</option>
|
||||||
|
<option value="America/Mexico_City">America/Mexico_City</option>
|
||||||
|
<option value="America/Bogota">America/Bogota</option>
|
||||||
|
<option value="America/Lima">America/Lima</option>
|
||||||
|
<option value="America/Caracas">America/Caracas</option>
|
||||||
|
<option value="America/La_Paz">America/La_Paz</option>
|
||||||
|
<option value="America/Santiago">America/Santiago</option>
|
||||||
|
<option value="America/Sao_Paulo">America/Sao_Paulo</option>
|
||||||
|
<option value="America/Argentina/Buenos_Aires">America/Argentina/Buenos_Aires</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Europe">
|
||||||
|
<option value="Europe/London">Europe/London</option>
|
||||||
|
<option value="Europe/Lisbon">Europe/Lisbon</option>
|
||||||
|
<option value="Europe/Amsterdam">Europe/Amsterdam</option>
|
||||||
|
<option value="Europe/Berlin">Europe/Berlin</option>
|
||||||
|
<option value="Europe/Madrid">Europe/Madrid</option>
|
||||||
|
<option value="Europe/Paris">Europe/Paris</option>
|
||||||
|
<option value="Europe/Rome">Europe/Rome</option>
|
||||||
|
<option value="Europe/Helsinki">Europe/Helsinki</option>
|
||||||
|
<option value="Europe/Moscow">Europe/Moscow</option>
|
||||||
|
<option value="Europe/Istanbul">Europe/Istanbul</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Africa">
|
||||||
|
<option value="Africa/Cairo">Africa/Cairo</option>
|
||||||
|
<option value="Africa/Lagos">Africa/Lagos</option>
|
||||||
|
<option value="Africa/Nairobi">Africa/Nairobi</option>
|
||||||
|
<option value="Africa/Johannesburg">Africa/Johannesburg</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Asia">
|
||||||
|
<option value="Asia/Karachi">Asia/Karachi</option>
|
||||||
|
<option value="Asia/Kolkata">Asia/Kolkata</option>
|
||||||
|
<option value="Asia/Dhaka">Asia/Dhaka</option>
|
||||||
|
<option value="Asia/Bangkok">Asia/Bangkok</option>
|
||||||
|
<option value="Asia/Jakarta">Asia/Jakarta</option>
|
||||||
|
<option value="Asia/Singapore">Asia/Singapore</option>
|
||||||
|
<option value="Asia/Shanghai">Asia/Shanghai</option>
|
||||||
|
<option value="Asia/Seoul">Asia/Seoul</option>
|
||||||
|
<option value="Asia/Tokyo">Asia/Tokyo</option>
|
||||||
|
<option value="Asia/Dubai">Asia/Dubai</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Oceania">
|
||||||
|
<option value="Australia/Sydney">Australia/Sydney</option>
|
||||||
|
<option value="Pacific/Auckland">Pacific/Auckland</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="settings-btn">Save</button>
|
<button type="submit" class="settings-btn">Save</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Meal Planner -->
|
<div class="date-display" style="margin-top: 40px;">Nutrition Goals</div>
|
||||||
<section id="meal-planner-content" class="hidden">
|
|
||||||
<div class="container">
|
|
||||||
<h1>📋 Meal Planner</h1>
|
|
||||||
|
|
||||||
<div class="date-display">Plan your meals and compare with your goals</div>
|
<div id="goalsContainer">
|
||||||
|
<!-- Dynamically rendered list of goals -->
|
||||||
<!-- Template Management -->
|
|
||||||
<div class="template-management">
|
|
||||||
<div class="template-selector-container">
|
|
||||||
<label for="templateSelector">Current Plan:</label>
|
|
||||||
<select id="templateSelector" class="template-selector">
|
|
||||||
<option value="">-- Select a plan --</option>
|
|
||||||
</select>
|
|
||||||
<button id="newTemplateBtn" class="template-btn">+ New Plan</button>
|
|
||||||
<button id="deleteTemplateBtn" class="template-btn delete-btn">Delete Plan</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Plan Progress Summary -->
|
<button type="button" id="addGoalBtn" class="settings-btn" style="margin-top: 15px;">+ Add Goal</button>
|
||||||
<div class="plan-progress-container">
|
|
||||||
<div class="calories-banner">
|
|
||||||
<div class="calories-number" id="planCaloriesTotal">0</div>
|
|
||||||
<div class="calories-text">Total Calories</div>
|
|
||||||
<div class="calories-text-goal" id="planCaloriesGoalText">of 0</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nutrition-summary" id="planNutritionSummary">
|
<div id="goalFormContainer" class="hidden">
|
||||||
<div class="nutrition-item">
|
<form id="goalForm" class="settings-form" style="margin-top: 15px;">
|
||||||
<div class="nutrition-label">Protein (g)</div>
|
|
||||||
<div class="nutrition-value" id="planProteinValue">0</div>
|
|
||||||
<div class="nutrition-progress-bar">
|
|
||||||
<div class="nutrition-progress-fill" id="planProteinProgress"></div>
|
|
||||||
</div>
|
|
||||||
<div class="nutrition-label" id="planProteinGoalText">of 0</div>
|
|
||||||
</div>
|
|
||||||
<div class="nutrition-item">
|
|
||||||
<div class="nutrition-label">Fat (g)</div>
|
|
||||||
<div class="nutrition-value" id="planFatValue">0</div>
|
|
||||||
<div class="nutrition-progress-bar">
|
|
||||||
<div class="nutrition-progress-fill" id="planFatProgress"></div>
|
|
||||||
</div>
|
|
||||||
<div class="nutrition-label" id="planFatGoalText">of 0</div>
|
|
||||||
</div>
|
|
||||||
<div class="nutrition-item">
|
|
||||||
<div class="nutrition-label">Carbs (g)</div>
|
|
||||||
<div class="nutrition-value" id="planCarboValue">0</div>
|
|
||||||
<div class="nutrition-progress-bar">
|
|
||||||
<div class="nutrition-progress-fill" id="planCarboProgress"></div>
|
|
||||||
</div>
|
|
||||||
<div class="nutrition-label" id="planCarboGoalText">of 0</div>
|
|
||||||
</div>
|
|
||||||
<div class="nutrition-item">
|
|
||||||
<div class="nutrition-label">Fiber (g)</div>
|
|
||||||
<div class="nutrition-value" id="planFiberValue">0</div>
|
|
||||||
<div class="nutrition-progress-bar">
|
|
||||||
<div class="nutrition-progress-fill" id="planFiberProgress"></div>
|
|
||||||
</div>
|
|
||||||
<div class="nutrition-label" id="planFiberGoalText">of 0</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Meal Sections -->
|
|
||||||
<div class="meals-container">
|
|
||||||
<!-- Breakfast -->
|
|
||||||
<div class="meal-section" data-meal-type="breakfast">
|
|
||||||
<h2 class="meal-section-title">🌅 Breakfast</h2>
|
|
||||||
<button class="add-meal-item-btn" data-meal-type="breakfast">+ Add Food</button>
|
|
||||||
<div class="meal-items-container" id="breakfastItemsContainer">
|
|
||||||
<!-- Planned items will be added here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Lunch -->
|
|
||||||
<div class="meal-section" data-meal-type="lunch">
|
|
||||||
<h2 class="meal-section-title">☀️ Lunch</h2>
|
|
||||||
<button class="add-meal-item-btn" data-meal-type="lunch">+ Add Food</button>
|
|
||||||
<div class="meal-items-container" id="lunchItemsContainer">
|
|
||||||
<!-- Planned items will be added here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Dinner -->
|
|
||||||
<div class="meal-section" data-meal-type="dinner">
|
|
||||||
<h2 class="meal-section-title">🌙 Dinner</h2>
|
|
||||||
<button class="add-meal-item-btn" data-meal-type="dinner">+ Add Food</button>
|
|
||||||
<div class="meal-items-container" id="dinnerItemsContainer">
|
|
||||||
<!-- Planned items will be added here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Snacks -->
|
|
||||||
<div class="meal-section" data-meal-type="snacks">
|
|
||||||
<h2 class="meal-section-title">🍿 Snacks</h2>
|
|
||||||
<button class="add-meal-item-btn" data-meal-type="snacks">+ Add Food</button>
|
|
||||||
<div class="meal-items-container" id="snacksItemsContainer">
|
|
||||||
<!-- Planned items will be added here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add Food to Plan Form (initially hidden) -->
|
|
||||||
<div class="add-food-to-plan-section hidden" id="addFoodToPlanSection">
|
|
||||||
<h2 class="add-food-title" id="addFoodToPlanTitle">Add Food to <span id="currentMealType"></span></h2>
|
|
||||||
<div class="add-food-form">
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="planFoodSearchInput">Food Item</label>
|
<label for="goalNameInput">Goal Name</label>
|
||||||
<div class="food-search-container">
|
<input type="text" id="goalNameInput" placeholder="e.g. Workout Day" required>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="food-search-input"
|
|
||||||
id="planFoodSearchInput"
|
|
||||||
placeholder="Search food (e.g., apple, chicken breast, rice...)"
|
|
||||||
autocomplete="off"
|
|
||||||
>
|
|
||||||
<div class="search-loading" id="planSearchLoading"></div>
|
|
||||||
|
|
||||||
<!-- Search Results Overlay -->
|
|
||||||
<div class="search-results" id="planSearchResults">
|
|
||||||
<!-- Results will be populated here -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hidden input for current meal type -->
|
|
||||||
<input type="hidden" id="currentMealTypeInput">
|
|
||||||
<input type="hidden" id="planItemIdToUpdate">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="planGramAmount">Amount (grams)</label>
|
<label for="goalCaloriesInput">Daily calorie goal</label>
|
||||||
<input type="number" id="planGramAmount" min="1" step="1" value="100">
|
<input type="number" id="goalCaloriesInput" placeholder="Calories">
|
||||||
</div>
|
</div>
|
||||||
<button class="add-btn" id="addFoodToPlanBtn">Add Food</button>
|
<div class="form-group">
|
||||||
<button class="cancel-btn" id="cancelFoodToPlanBtn">Cancel</button>
|
<label for="goalProteinInput">Daily protein goal (g)</label>
|
||||||
|
<input type="number" id="goalProteinInput" placeholder="Proteins in grams">
|
||||||
</div>
|
</div>
|
||||||
<div class="display-none" id="plan-food-preview">
|
<div class="form-group">
|
||||||
<div class="nutrition-summary">
|
<label for="goalFatInput">Daily fat goal (g)</label>
|
||||||
<div class="nutrition-item">
|
<input type="number" id="goalFatInput" placeholder="Fat in grams">
|
||||||
<div class="nutrition-value" id="planCalorieValuePreview">0</div>
|
|
||||||
<div class="nutrition-label">Calories</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="nutrition-item">
|
<div class="form-group">
|
||||||
<div class="nutrition-value" id="planProteinValuePreview">0</div>
|
<label for="goalCarboInput">Daily carbs goal (g)</label>
|
||||||
<div class="nutrition-label">Protein (g)</div>
|
<input type="number" id="goalCarboInput" placeholder="Carbs in grams">
|
||||||
</div>
|
</div>
|
||||||
<div class="nutrition-item">
|
<div class="form-group">
|
||||||
<div class="nutrition-value" id="planFatValuePreview">0</div>
|
<label for="goalFiberInput">Daily fiber goal (g)</label>
|
||||||
<div class="nutrition-label">Fat (g)</div>
|
<input type="number" id="goalFiberInput" placeholder="Fiber in grams">
|
||||||
</div>
|
</div>
|
||||||
<div class="nutrition-item">
|
<input type="hidden" id="editingGoalId" value="">
|
||||||
<div class="nutrition-value" id="planCarboValuePreview">0</div>
|
<button type="submit" class="settings-btn">Save Goal</button>
|
||||||
<div class="nutrition-label">Carbs (g)</div>
|
<button type="button" id="cancelGoalBtn" class="settings-btn" style="margin-top: 10px; background: #555; border-color: #555;">Cancel</button>
|
||||||
</div>
|
</form>
|
||||||
<div class="nutrition-item">
|
|
||||||
<div class="nutrition-value" id="planFiberValuePreview">0</div>
|
|
||||||
<div class="nutrition-label">Fiber (g)</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="date-display" style="margin-top: 40px;">AI Nutrition Cache</div>
|
||||||
|
|
||||||
|
<div class="cache-info-section">
|
||||||
|
<div class="cache-stats">
|
||||||
|
<div class="cache-stat-item">
|
||||||
|
<span class="cache-stat-label">Cached Items:</span>
|
||||||
|
<span class="cache-stat-value" id="cacheItemCount">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="cache-stat-item">
|
||||||
|
<span class="cache-stat-label">Cache Size:</span>
|
||||||
|
<span class="cache-stat-value" id="cacheSize">0 KB</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" id="clearCacheBtn" class="settings-btn clear-cache-btn">Clear Cache</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
-283
@@ -1,283 +0,0 @@
|
|||||||
/* Meal Planner Page Styles */
|
|
||||||
|
|
||||||
/* Template Management */
|
|
||||||
.template-management {
|
|
||||||
margin: 20px 0;
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--card-bg, #1e1e1e);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-selector-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-selector-container label {
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-color, #ffffff);
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-selector {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
padding: 10px;
|
|
||||||
background: var(--input-bg, #2a2a2a);
|
|
||||||
color: var(--text-color, #ffffff);
|
|
||||||
border: 1px solid var(--border-color, #3a3a3a);
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-btn {
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: var(--primary-color, #4CAF50);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-btn:hover {
|
|
||||||
background: var(--primary-hover, #45a049);
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-btn.delete-btn {
|
|
||||||
background: #f44336;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-btn.delete-btn:hover {
|
|
||||||
background: #da190b;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Plan Progress Container */
|
|
||||||
.plan-progress-container {
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Nutrition Progress Bars */
|
|
||||||
.nutrition-progress-bar {
|
|
||||||
width: 100%;
|
|
||||||
height: 8px;
|
|
||||||
background: var(--progress-bg, #2a2a2a);
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nutrition-progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: #4CAF50;
|
|
||||||
transition: width 0.3s ease, background-color 0.3s ease;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Meals Container */
|
|
||||||
.meals-container {
|
|
||||||
margin: 30px 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Meal Section */
|
|
||||||
.meal-section {
|
|
||||||
background: var(--card-bg, #1e1e1e);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 20px;
|
|
||||||
border: 1px solid var(--border-color, #3a3a3a);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meal-section-title {
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
color: var(--text-color, #ffffff);
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-meal-item-btn {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
background: var(--primary-color, #4CAF50);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-meal-item-btn:hover {
|
|
||||||
background: var(--primary-hover, #45a049);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meal-items-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-items-message {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-secondary, #888);
|
|
||||||
padding: 20px;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Planned Item Cards */
|
|
||||||
.planned-item-card {
|
|
||||||
background: var(--input-bg, #2a2a2a);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid var(--border-color, #3a3a3a);
|
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.planned-item-card:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.planned-item-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.planned-item-name {
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-color, #ffffff);
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.planned-item-grams {
|
|
||||||
color: var(--text-secondary, #888);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.planned-item-nutrition {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-secondary, #888);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-planned-item-btn {
|
|
||||||
width: 100%;
|
|
||||||
padding: 6px;
|
|
||||||
background: #f44336;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-planned-item-btn:hover {
|
|
||||||
background: #da190b;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add Food to Plan Section */
|
|
||||||
.add-food-to-plan-section {
|
|
||||||
margin: 30px 0;
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--card-bg, #1e1e1e);
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 2px solid var(--primary-color, #4CAF50);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Quick Add Section in Food Log */
|
|
||||||
.quick-add-section {
|
|
||||||
margin: 20px 0;
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--card-bg, #1e1e1e);
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid var(--border-color, #3a3a3a);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-add-items {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-add-meal-header {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
margin: 10px 0 5px 0;
|
|
||||||
color: var(--text-color, #ffffff);
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-add-btn {
|
|
||||||
padding: 12px;
|
|
||||||
background: var(--input-bg, #2a2a2a);
|
|
||||||
border: 1px solid var(--border-color, #3a3a3a);
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
transition: all 0.2s;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-add-btn:hover {
|
|
||||||
background: var(--primary-color, #4CAF50);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 8px rgba(76, 175, 80, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-add-name {
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-color, #ffffff);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-add-details {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-secondary, #888);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-add-btn:hover .quick-add-name,
|
|
||||||
.quick-add-btn:hover .quick-add-details {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile Responsiveness */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.template-selector-container {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-selector {
|
|
||||||
min-width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meals-container {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-add-items {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.planned-item-nutrition {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-add-btn {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
+6025
-433
File diff suppressed because it is too large
Load Diff
+6
-3
@@ -12,11 +12,14 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^7.0.0",
|
||||||
"vite": "^7.1.12"
|
"vite": "^8.1.4",
|
||||||
|
"vite-plugin-pwa": "^1.3.0",
|
||||||
|
"workbox-window": "^7.4.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"appwrite": "^21.3.0",
|
"@anthropic-ai/sdk": "^0.111.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"
|
||||||
|
]
|
||||||
|
}
|
||||||
+171
@@ -32,6 +32,35 @@
|
|||||||
border-color: #6B8DD6;
|
border-color: #6B8DD6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-form select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
padding-right: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid #404040;
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23a0a0a0' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 12px center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-form select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #6B8DD6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-form select option,
|
||||||
|
.settings-form select optgroup {
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-btn {
|
.settings-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
@@ -51,3 +80,145 @@
|
|||||||
border: 1px solid #5A7BC4;
|
border: 1px solid #5A7BC4;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cache-info-section {
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 25px;
|
||||||
|
min-width: 300px;
|
||||||
|
background: rgba(40, 40, 40, 0.9);
|
||||||
|
border: 1px solid rgba(100, 100, 100, 0.3);
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-stat-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(60, 60, 60, 0.5);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-stat-label {
|
||||||
|
color: #b0b0b0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-stat-value {
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-cache-btn {
|
||||||
|
background: #d66b6b;
|
||||||
|
border: 1px solid #d66b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-cache-btn:hover {
|
||||||
|
background: #c45a5a;
|
||||||
|
border: 1px solid #c45a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-goals-msg {
|
||||||
|
color: #888;
|
||||||
|
padding: 15px 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-card {
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: rgba(40, 40, 40, 0.9);
|
||||||
|
border: 1px solid rgba(100, 100, 100, 0.3);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-card-active {
|
||||||
|
border-color: #6B8DD6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-name {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-active-badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6B8DD6;
|
||||||
|
background: rgba(107, 141, 214, 0.15);
|
||||||
|
border: 1px solid #6B8DD6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-summary {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #b0b0b0;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-summary span {
|
||||||
|
background: rgba(60, 60, 60, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-actions .btn {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #555;
|
||||||
|
background: #333;
|
||||||
|
color: #e0e0e0;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-actions .btn:hover {
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-actions .goal-delete-btn {
|
||||||
|
border-color: #d66b6b;
|
||||||
|
color: #d66b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-actions .goal-delete-btn:hover {
|
||||||
|
background: rgba(214, 107, 107, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-actions .goal-select-btn {
|
||||||
|
border-color: #6B8DD6;
|
||||||
|
color: #6B8DD6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-actions .goal-select-btn:hover {
|
||||||
|
background: rgba(107, 141, 214, 0.15);
|
||||||
|
}
|
||||||
|
|||||||
+3
-3
@@ -95,8 +95,7 @@ export function delay(seconds: number) {
|
|||||||
return new Promise(resolve => setTimeout(resolve, seconds));
|
return new Promise(resolve => setTimeout(resolve, seconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getIcon(category: string) {
|
const icons = {
|
||||||
const icons = {
|
|
||||||
'fats': '🥑 🍳 🍟',
|
'fats': '🥑 🍳 🍟',
|
||||||
'proteins': '🫘 🥩 🥚',
|
'proteins': '🫘 🥩 🥚',
|
||||||
'carbs': '🍞 🥔 🍠',
|
'carbs': '🍞 🥔 🍠',
|
||||||
@@ -104,7 +103,8 @@ export function getIcon(category: string) {
|
|||||||
'fruits': '🍊 🍇 🍎',
|
'fruits': '🍊 🍇 🍎',
|
||||||
'low carb': '🥦 🍅 🍓',
|
'low carb': '🥦 🍅 🍓',
|
||||||
'dairy': '🧀 🧈 🥛'
|
'dairy': '🧀 🧈 🥛'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getIcon(category: keyof typeof icons) {
|
||||||
return icons[category];
|
return icons[category];
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-214
@@ -1,5 +1,5 @@
|
|||||||
import { Client, Account, Databases, Query, ID } from 'appwrite';
|
import { Client, Account, Databases, Query, ID } from 'appwrite';
|
||||||
import { DailyTotalCalories, FoodStorage, UserSettings, SharedDay, MealPlanTemplate, PlannedFoodItem, MealType } from './types';
|
import { FoodStorage, UserSettings } from './types';
|
||||||
|
|
||||||
// Initialize Appwrite client
|
// Initialize Appwrite client
|
||||||
const client = new Client();
|
const client = new Client();
|
||||||
@@ -18,8 +18,6 @@ export const FOOD_ENTRIES_COLLECTION_ID = import.meta.env.VITE_APPWRITE_FOODENTR
|
|||||||
export const USER_SETTINGS_COLLECTION_ID = import.meta.env.VITE_APPWRITE_USERSETTINGSID
|
export const USER_SETTINGS_COLLECTION_ID = import.meta.env.VITE_APPWRITE_USERSETTINGSID
|
||||||
export const MONTHLY_CALORIES_COLLECTION_ID = import.meta.env.VITE_APPWRITE_MONTLYCALORIESID
|
export const MONTHLY_CALORIES_COLLECTION_ID = import.meta.env.VITE_APPWRITE_MONTLYCALORIESID
|
||||||
export const SHARED_DAYS_COLLECTION_ID = import.meta.env.VITE_APPWRITE_SHAREDDAYSID
|
export const SHARED_DAYS_COLLECTION_ID = import.meta.env.VITE_APPWRITE_SHAREDDAYSID
|
||||||
export const MEAL_PLAN_TEMPLATES_COLLECTION_ID = import.meta.env.VITE_APPWRITE_MEALPLANTEMPLATESID
|
|
||||||
export const PLANNED_FOOD_ITEMS_COLLECTION_ID = import.meta.env.VITE_APPWRITE_PLANNEDFOODITEMSID
|
|
||||||
|
|
||||||
// Auth helper functions
|
// Auth helper functions
|
||||||
export class AppwriteAuth {
|
export class AppwriteAuth {
|
||||||
@@ -266,6 +264,97 @@ export class AppwriteDB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async updateUserSettings(documentId: string, settings: Partial<UserSettings>) {
|
||||||
|
try {
|
||||||
|
const response = await databases.updateDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
USER_SETTINGS_COLLECTION_ID,
|
||||||
|
documentId,
|
||||||
|
{
|
||||||
|
...settings,
|
||||||
|
userId: (await account.get()).$id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.debug('User settings updated:', response);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update user settings error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async saveUserGoal(goal: UserSettings) {
|
||||||
|
try {
|
||||||
|
const response = await databases.createDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
USER_SETTINGS_COLLECTION_ID,
|
||||||
|
'unique()',
|
||||||
|
{
|
||||||
|
...goal,
|
||||||
|
userId: (await account.get()).$id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.debug('User goal saved:', response);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Save user goal error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateUserGoal(documentId: string, goal: UserSettings) {
|
||||||
|
try {
|
||||||
|
const response = await databases.updateDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
USER_SETTINGS_COLLECTION_ID,
|
||||||
|
documentId,
|
||||||
|
{
|
||||||
|
...goal,
|
||||||
|
userId: (await account.get()).$id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.debug('User goal updated:', response);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update user goal error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteUserGoal(documentId: string) {
|
||||||
|
try {
|
||||||
|
await databases.deleteDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
USER_SETTINGS_COLLECTION_ID,
|
||||||
|
documentId
|
||||||
|
);
|
||||||
|
console.debug('User goal deleted:', documentId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete user goal error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async setActiveGoal(goalId: string, allGoals: UserSettings[]) {
|
||||||
|
try {
|
||||||
|
const updates = allGoals
|
||||||
|
.filter(goal => goal.id)
|
||||||
|
.map(goal =>
|
||||||
|
databases.updateDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
USER_SETTINGS_COLLECTION_ID,
|
||||||
|
goal.id!,
|
||||||
|
{ isActive: goal.id === goalId }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await Promise.all(updates);
|
||||||
|
console.debug('Active goal set:', goalId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Set active goal error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Delete food entry
|
// Delete food entry
|
||||||
static async deleteFoodEntry(documentId: string) {
|
static async deleteFoodEntry(documentId: string) {
|
||||||
try {
|
try {
|
||||||
@@ -332,215 +421,4 @@ export class AppwriteDB {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Meal Plan Template methods
|
|
||||||
|
|
||||||
// Create meal plan template
|
|
||||||
static async createMealPlanTemplate(name: string) {
|
|
||||||
try {
|
|
||||||
const user = await account.get();
|
|
||||||
const response = await databases.createDocument(
|
|
||||||
DATABASE_ID,
|
|
||||||
MEAL_PLAN_TEMPLATES_COLLECTION_ID,
|
|
||||||
'unique()',
|
|
||||||
{
|
|
||||||
name: name,
|
|
||||||
userId: user.$id,
|
|
||||||
isActive: false,
|
|
||||||
createdAt: new Date().toISOString()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.debug('Meal plan template created:', response);
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Create meal plan template error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all meal plan templates for user
|
|
||||||
static async getMealPlanTemplates() {
|
|
||||||
try {
|
|
||||||
const user = await account.get();
|
|
||||||
const response = await databases.listDocuments(
|
|
||||||
DATABASE_ID,
|
|
||||||
MEAL_PLAN_TEMPLATES_COLLECTION_ID,
|
|
||||||
[
|
|
||||||
Query.equal('userId', user.$id),
|
|
||||||
Query.limit(50)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
console.debug('Meal plan templates retrieved:', response);
|
|
||||||
return response.documents;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Get meal plan templates error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get active meal plan template
|
|
||||||
static async getActiveMealPlanTemplate() {
|
|
||||||
try {
|
|
||||||
const user = await account.get();
|
|
||||||
const response = await databases.listDocuments(
|
|
||||||
DATABASE_ID,
|
|
||||||
MEAL_PLAN_TEMPLATES_COLLECTION_ID,
|
|
||||||
[
|
|
||||||
Query.equal('userId', user.$id),
|
|
||||||
Query.equal('isActive', true),
|
|
||||||
Query.limit(1)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
console.debug('Active meal plan template retrieved:', response);
|
|
||||||
return response.documents.length > 0 ? response.documents[0] : null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Get active meal plan template error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set active meal plan template
|
|
||||||
static async setActiveMealPlanTemplate(templateId: string) {
|
|
||||||
try {
|
|
||||||
const user = await account.get();
|
|
||||||
|
|
||||||
// First, deactivate all templates
|
|
||||||
const allTemplates = await this.getMealPlanTemplates();
|
|
||||||
const deactivatePromises = allTemplates.map(template =>
|
|
||||||
databases.updateDocument(
|
|
||||||
DATABASE_ID,
|
|
||||||
MEAL_PLAN_TEMPLATES_COLLECTION_ID,
|
|
||||||
template.$id,
|
|
||||||
{ isActive: false }
|
|
||||||
)
|
|
||||||
);
|
|
||||||
await Promise.all(deactivatePromises);
|
|
||||||
|
|
||||||
// Then activate the selected template
|
|
||||||
const response = await databases.updateDocument(
|
|
||||||
DATABASE_ID,
|
|
||||||
MEAL_PLAN_TEMPLATES_COLLECTION_ID,
|
|
||||||
templateId,
|
|
||||||
{ isActive: true }
|
|
||||||
);
|
|
||||||
console.debug('Meal plan template set as active:', response);
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Set active meal plan template error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete meal plan template
|
|
||||||
static async deleteMealPlanTemplate(templateId: string) {
|
|
||||||
try {
|
|
||||||
// First delete all planned items for this template
|
|
||||||
const plannedItems = await this.getPlannedItemsByTemplate(templateId);
|
|
||||||
const deletePromises = plannedItems.map(item =>
|
|
||||||
this.deletePlannedFoodItem(item.$id)
|
|
||||||
);
|
|
||||||
await Promise.all(deletePromises);
|
|
||||||
|
|
||||||
// Then delete the template
|
|
||||||
await databases.deleteDocument(
|
|
||||||
DATABASE_ID,
|
|
||||||
MEAL_PLAN_TEMPLATES_COLLECTION_ID,
|
|
||||||
templateId
|
|
||||||
);
|
|
||||||
console.debug('Meal plan template deleted:', templateId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Delete meal plan template error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Planned Food Item methods
|
|
||||||
|
|
||||||
// Create planned food item
|
|
||||||
static async createPlannedFoodItem(item: Omit<PlannedFoodItem, 'id'>) {
|
|
||||||
try {
|
|
||||||
const response = await databases.createDocument(
|
|
||||||
DATABASE_ID,
|
|
||||||
PLANNED_FOOD_ITEMS_COLLECTION_ID,
|
|
||||||
'unique()',
|
|
||||||
item
|
|
||||||
);
|
|
||||||
console.debug('Planned food item created:', response);
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Create planned food item error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all planned items for a template
|
|
||||||
static async getPlannedItemsByTemplate(templateId: string) {
|
|
||||||
try {
|
|
||||||
const response = await databases.listDocuments(
|
|
||||||
DATABASE_ID,
|
|
||||||
PLANNED_FOOD_ITEMS_COLLECTION_ID,
|
|
||||||
[
|
|
||||||
Query.equal('templateId', templateId),
|
|
||||||
Query.limit(100)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
console.debug('Planned items retrieved for template:', response);
|
|
||||||
return response.documents;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Get planned items by template error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get planned items by template and meal type
|
|
||||||
static async getPlannedItemsByMealType(templateId: string, mealType: MealType) {
|
|
||||||
try {
|
|
||||||
const response = await databases.listDocuments(
|
|
||||||
DATABASE_ID,
|
|
||||||
PLANNED_FOOD_ITEMS_COLLECTION_ID,
|
|
||||||
[
|
|
||||||
Query.equal('templateId', templateId),
|
|
||||||
Query.equal('mealType', mealType),
|
|
||||||
Query.limit(50)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
console.debug('Planned items retrieved for meal type:', response);
|
|
||||||
return response.documents;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Get planned items by meal type error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update planned food item
|
|
||||||
static async updatePlannedFoodItem(itemId: string, updates: Partial<PlannedFoodItem>) {
|
|
||||||
try {
|
|
||||||
const response = await databases.updateDocument(
|
|
||||||
DATABASE_ID,
|
|
||||||
PLANNED_FOOD_ITEMS_COLLECTION_ID,
|
|
||||||
itemId,
|
|
||||||
updates
|
|
||||||
);
|
|
||||||
console.debug('Planned food item updated:', response);
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Update planned food item error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete planned food item
|
|
||||||
static async deletePlannedFoodItem(itemId: string) {
|
|
||||||
try {
|
|
||||||
await databases.deleteDocument(
|
|
||||||
DATABASE_ID,
|
|
||||||
PLANNED_FOOD_ITEMS_COLLECTION_ID,
|
|
||||||
itemId
|
|
||||||
);
|
|
||||||
console.debug('Planned food item deleted:', itemId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Delete planned food item error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-1
@@ -1,9 +1,24 @@
|
|||||||
import { getDivById } from "./DomUtils";
|
import { getDivById } from "./DomUtils";
|
||||||
|
|
||||||
export function showAuthForms() {
|
export function showAuthForms() {
|
||||||
getDivById('auth-section').classList.remove('hidden');
|
getDivById('auth-buttons').classList.remove('hidden');
|
||||||
getDivById('user-info').classList.add('hidden');
|
getDivById('user-info').classList.add('hidden');
|
||||||
getDivById('app-content').classList.add('hidden');
|
getDivById('app-content').classList.add('hidden');
|
||||||
|
getDivById('landing-page').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hideAuthForms() {
|
||||||
|
getDivById('auth-buttons').classList.add('hidden');
|
||||||
|
getDivById('auth-modal').classList.add('hidden');
|
||||||
|
getDivById('landing-page').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openAuthModal() {
|
||||||
|
getDivById('auth-modal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeAuthModal() {
|
||||||
|
getDivById('auth-modal').classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleAuthForms() {
|
export function toggleAuthForms() {
|
||||||
@@ -16,3 +31,23 @@ export function toggleAuthForms() {
|
|||||||
const resetForm = document.getElementById('registerForm') as HTMLFormElement;
|
const resetForm = document.getElementById('registerForm') as HTMLFormElement;
|
||||||
resetForm.reset();
|
resetForm.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function showRegisterForm() {
|
||||||
|
document.getElementById('login-form')?.classList.add('hidden');
|
||||||
|
document.getElementById('register-form')?.classList.remove('hidden');
|
||||||
|
|
||||||
|
const resetForm = document.getElementById('registerForm') as HTMLFormElement;
|
||||||
|
resetForm.reset();
|
||||||
|
|
||||||
|
openAuthModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showLoginForm() {
|
||||||
|
document.getElementById('login-form')?.classList.remove('hidden');
|
||||||
|
document.getElementById('register-form')?.classList.add('hidden');
|
||||||
|
|
||||||
|
const logForm = document.getElementById('loginForm') as HTMLFormElement;
|
||||||
|
logForm.reset();
|
||||||
|
|
||||||
|
openAuthModal();
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import Anthropic from '@anthropic-ai/sdk';
|
||||||
|
|
||||||
|
const API_KEY = import.meta.env.VITE_CLAUDE_API_KEY;
|
||||||
|
|
||||||
|
// Initialize Claude client
|
||||||
|
const anthropic = new Anthropic({
|
||||||
|
apiKey: API_KEY,
|
||||||
|
dangerouslyAllowBrowser: true // Allow usage in browser (for development/demo purposes)
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface NutritionInfo {
|
||||||
|
vitamins: string[];
|
||||||
|
minerals: string[];
|
||||||
|
benefits: string[];
|
||||||
|
notes: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// localStorage-based cache for nutrition info
|
||||||
|
// Key format: "nutrition_cache_foodName_grams" (e.g., "nutrition_cache_apple_100")
|
||||||
|
const CACHE_PREFIX = 'nutrition_cache_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate cache key from food name and grams
|
||||||
|
*/
|
||||||
|
function getCacheKey(foodName: string, grams: number): string {
|
||||||
|
return `${CACHE_PREFIX}${foodName.toLowerCase().trim()}_${grams}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached nutrition info if available
|
||||||
|
*/
|
||||||
|
function getCachedNutrition(foodName: string, grams: number): NutritionInfo | null {
|
||||||
|
try {
|
||||||
|
const key = getCacheKey(foodName, grams);
|
||||||
|
const cached = localStorage.getItem(key);
|
||||||
|
if (cached) {
|
||||||
|
return JSON.parse(cached) as NutritionInfo;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading from cache:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store nutrition info in cache
|
||||||
|
*/
|
||||||
|
function cacheNutrition(foodName: string, grams: number, info: NutritionInfo): void {
|
||||||
|
try {
|
||||||
|
const key = getCacheKey(foodName, grams);
|
||||||
|
localStorage.setItem(key, JSON.stringify(info));
|
||||||
|
const stats = getCacheStats();
|
||||||
|
console.log(`Cached nutrition info for: ${key} (total cached: ${stats.size})`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error writing to cache:', error);
|
||||||
|
// If localStorage is full, try to clear some space
|
||||||
|
if (error instanceof Error && error.name === 'QuotaExceededError') {
|
||||||
|
console.warn('localStorage quota exceeded, clearing old cache entries');
|
||||||
|
clearNutritionCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get detailed nutritional information for a food item using Claude AI
|
||||||
|
* Uses localStorage cache to avoid repeated API calls for the same food/amount
|
||||||
|
* @param foodName - The name of the food item
|
||||||
|
* @returns Promise with nutritional information
|
||||||
|
*/
|
||||||
|
export async function getNutritionInfo(foodName: string): Promise<NutritionInfo> {
|
||||||
|
// Check cache first
|
||||||
|
const cached = getCachedNutrition(foodName, 100);
|
||||||
|
if (cached) {
|
||||||
|
console.log(`Using cached nutrition info for: ${foodName} (100g)`);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Fetching nutrition info from AI for: ${foodName} (100g)`);
|
||||||
|
try {
|
||||||
|
const message = await anthropic.messages.create({
|
||||||
|
model: 'claude-haiku-4-5-20251001',
|
||||||
|
max_tokens: 1024,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: `Provide detailed nutritional information for 100g of ${foodName}.
|
||||||
|
|
||||||
|
IMPORTANT: Respond in Brazilian Portuguese (pt-BR).
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
1. Key vitamins (e.g., A, B complex, C, D, E, K)
|
||||||
|
2. Important minerals (e.g., iron, calcium, magnesium, zinc, potassium)
|
||||||
|
3. Main health benefits
|
||||||
|
4. Any important notes or considerations
|
||||||
|
|
||||||
|
Format your response as JSON with this structure:
|
||||||
|
{
|
||||||
|
"vitamins": ["Vitamina A: descrição", "Vitamina C: descrição", ...],
|
||||||
|
"minerals": ["Ferro: descrição", "Cálcio: descrição", ...],
|
||||||
|
"benefits": ["benefício 1", "benefício 2", ...],
|
||||||
|
"notes": "Informações adicionais importantes"
|
||||||
|
}
|
||||||
|
|
||||||
|
Be concise but informative. Only include significant amounts of vitamins and minerals. All text must be in Brazilian Portuguese.`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract the text content from Claude's response
|
||||||
|
const textContent = message.content.find(block => block.type === 'text');
|
||||||
|
if (!textContent || textContent.type !== 'text') {
|
||||||
|
throw new Error('No text content in response');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the JSON response
|
||||||
|
const responseText = textContent.text;
|
||||||
|
|
||||||
|
// Extract JSON from the response (handle cases where Claude adds markdown formatting)
|
||||||
|
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
|
||||||
|
if (!jsonMatch) {
|
||||||
|
throw new Error('Could not parse JSON from response');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nutritionInfo: NutritionInfo = JSON.parse(jsonMatch[0]);
|
||||||
|
|
||||||
|
// Cache the result for future use
|
||||||
|
cacheNutrition(foodName, 100, nutritionInfo);
|
||||||
|
|
||||||
|
return nutritionInfo;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting nutrition info from Claude:', error);
|
||||||
|
throw new Error('Failed to get nutrition information. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the nutrition info cache
|
||||||
|
* Removes all cached nutrition data from localStorage
|
||||||
|
*/
|
||||||
|
export function clearNutritionCache(): void {
|
||||||
|
try {
|
||||||
|
const keys = Object.keys(localStorage).filter(key => key.startsWith(CACHE_PREFIX));
|
||||||
|
keys.forEach(key => localStorage.removeItem(key));
|
||||||
|
console.log(`Cleared nutrition cache (${keys.length} entries removed)`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing cache:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache statistics including size and storage usage
|
||||||
|
*/
|
||||||
|
export function getCacheStats(): { size: number; keys: string[]; sizeInBytes: number; sizeInKB: number } {
|
||||||
|
try {
|
||||||
|
const keys = Object.keys(localStorage).filter(key => key.startsWith(CACHE_PREFIX));
|
||||||
|
|
||||||
|
// Calculate total size in bytes
|
||||||
|
let totalBytes = 0;
|
||||||
|
keys.forEach(key => {
|
||||||
|
const value = localStorage.getItem(key);
|
||||||
|
if (value) {
|
||||||
|
// Each character in localStorage is stored as UTF-16, which is 2 bytes per character
|
||||||
|
totalBytes += (key.length + value.length) * 2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
size: keys.length,
|
||||||
|
keys: keys.map(k => k.replace(CACHE_PREFIX, '')),
|
||||||
|
sizeInBytes: totalBytes,
|
||||||
|
sizeInKB: Math.round(totalBytes / 1024 * 100) / 100
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting cache stats:', error);
|
||||||
|
return { size: 0, keys: [], sizeInBytes: 0, sizeInKB: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
export interface CurrentDateResult {
|
||||||
|
date: string; // "2026-02-13"
|
||||||
|
dateTime: Date; // new Date(year, month-1, day) — local-midnight Date
|
||||||
|
timeString: string; // "HH:MM"
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCurrentDate(timezone: string): Promise<CurrentDateResult> {
|
||||||
|
if (timezone) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://timeapi.io/api/v1/time/current/zone?timezone=${encodeURIComponent(timezone)}`
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
// data.date is "YYYY-MM-DD", data.time is "HH:MM:SS.mmm" or "HH:MM"
|
||||||
|
const [year, month, day] = data.date.split('-').map(Number);
|
||||||
|
const dateTime = new Date(year, month - 1, day);
|
||||||
|
const timeParts = data.time.split(':');
|
||||||
|
const timeString = `${timeParts[0].padStart(2, '0')}:${timeParts[1].padStart(2, '0')}`;
|
||||||
|
return { date: data.date, dateTime, timeString };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('timeapi.io request failed, falling back to browser time:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: browser local time
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = now.getMonth() + 1;
|
||||||
|
const day = now.getDate();
|
||||||
|
const date = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||||
|
const dateTime = new Date(year, now.getMonth(), day);
|
||||||
|
const timeString = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
||||||
|
return { date, dateTime, timeString };
|
||||||
|
}
|
||||||
+122
-111
@@ -2,127 +2,138 @@ import { FoodItem } from "./types";
|
|||||||
|
|
||||||
export const foodDatabase: FoodItem[] = [
|
export const foodDatabase: FoodItem[] = [
|
||||||
// Gorduras - Fats
|
// Gorduras - Fats
|
||||||
{ name: 'Abacate', info: { calories: 96, protein: 1.2, fat: 8.4, carbs: 6, fiber: 6.3, category: 'fats', alkaline: true } },
|
{ 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', info: { calories: 581, protein: 18.6, fat: 47.3, carbs: 29.5, fiber: 11.6, category: 'fats', alkaline: false } },
|
{ 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', info: { calories: 611, protein: 26, fat: 49, carbs: 16, fiber: 8.5, category: 'fats', alkaline: true } },
|
{ 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', info: { calories: 844, protein: 0, fat: 100, carbs: 0, fiber: 0, category: 'fats', alkaline: false } },
|
{ 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', info: { calories: 570, protein: 18.5, fat: 46.3, carbs: 29.1, fiber: 3.7, category: 'fats', alkaline: false } },
|
{ 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á', info: { calories: 643, protein: 14.5, fat: 63.5, carbs: 15.1, fiber: 7.9, category: 'fats', alkaline: false } },
|
{ 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', info: { calories: 490, protein: 15.6, fat: 30.7, carbs: 43.8, fiber: 37.7, category: 'fats', alkaline: true } },
|
{ 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', info: { calories: 515, protein: 20, fat: 35, carbs: 30, fiber: 26, category: 'fats', alkaline: true } },
|
{ 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', info: { calories: 584, protein: 21.2, fat: 50.4, carbs: 21.6, fiber: 11.9, category: 'fats', alkaline: true } },
|
{ 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: 'Pasta de Amendoim Growth', info: { calories: 544, protein: 27, fat: 33, carbs: 20, fiber: 8, category: 'fats', alkaline: false } },
|
{ name: 'Pasta de Amendoim DaColônia', nameEn: 'DaColônia Peanut Butter', info: { calories: 608, protein: 30, fat: 51, carbs: 7.3, fiber: 7.3, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Semente de Abóbora', info: { calories: 446, protein: 18.55, fat: 19.4, carbs: 53.75, fiber: 6, category: 'fats', alkaline: true } },
|
{ name: 'Semente de Abóbora', nameEn: 'Pumpkin Seed', info: { calories: 446, protein: 18.55, fat: 19.4, carbs: 53.75, fiber: 6, category: 'fats', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
|
{ name: 'Semente de Girassol', nameEn: 'Sunflower Seed', info: { calories: 570, protein: 22.8, fat: 49.6, carbs: 18.7, fiber: 10.6, category: 'fats', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
|
{ 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', info: { calories: 166, protein: 26.2, fat: 6, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } },
|
{ 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', info: { calories: 128, protein: 26.1, fat: 2.6, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } },
|
{ 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', info: { calories: 146, protein: 13.3, fat: 9.5, carbs: 0.6, fiber: 0, category: 'proteins', alkaline: false } },
|
{ 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', info: { calories: 353, protein: 15.9, fat: 30.8, carbs: 1.6, fiber: 0, category: 'proteins', alkaline: false } },
|
{ 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', info: { calories: 59, protein: 13.4, fat: 0.1, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } },
|
{ 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', info: { calories: 288, protein: 52, fat: 0, carbs: 20, fiber: 14, category: 'proteins', alkaline: false } },
|
{ 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', info: { calories: 219, protein: 35.9, fat: 7.3, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } },
|
{ 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', info: { calories: 400, protein: 86.7, fat: 5, carbs: 3.3, fiber: 0, category: 'proteins', alkaline: false } },
|
{ 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', info: { calories: 245, protein: 24.9, fat: 15.4, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } },
|
{ 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)', info: { calories: 64, protein: 6.5, fat: 2, carbs: 4, fiber: 0.8, category: 'proteins', alkaline: false } },
|
{ 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', info: { calories: 77, protein: 4.5, fat: 0.5, carbs: 14, fiber: 8.4, category: 'proteins', alkaline: false } },
|
{ 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', info: { calories: 116, protein: 9.4, fat: 0.2, carbs: 19.4, fiber: 5.5, category: 'proteins', alkaline: false } },
|
{ 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', info: { calories: 163, protein: 31.5, fat: 3.2, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } },
|
{ 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, grelhado', info: { calories: 159, protein: 32, fat: 2.5, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } },
|
{ name: 'Frango, peito, sem pele, grelhado', nameEn: 'Chicken Breast, skinless, grilled', info: { calories: 159, protein: 32, fat: 2.5, carbs: 0, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Carne, bovina, acém, moído, cozido', info: { calories: 212, protein: 26.7, fat: 10.9, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } },
|
{ name: 'Carne, bovina, acém, moído, cozido', nameEn: 'Beef, chuck, ground, cooked', info: { calories: 212, protein: 26.7, fat: 10.9, carbs: 0, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Carne, bovina, contra-filé, sem gordura, grelhado', info: { calories: 194, protein: 35.9, fat: 4.5, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } },
|
{ name: 'Carne, bovina, contra-filé, sem gordura, grelhado', nameEn: 'Beef, sirloin, lean, grilled', info: { calories: 194, protein: 35.9, fat: 4.5, carbs: 0, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Carne, bovina, costela, assada', info: { calories: 373, protein: 28.8, fat: 27.7, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } },
|
{ name: 'Carne, bovina, costela, assada', nameEn: 'Beef, rib, roasted', info: { calories: 373, protein: 28.8, fat: 27.7, carbs: 0, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Carne, bovina, cupim, assado', info: { calories: 330, protein: 28.6, fat: 23, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } },
|
{ name: 'Carne, bovina, cupim, assado', nameEn: 'Beef, hump, roasted', info: { calories: 330, protein: 28.6, fat: 23, carbs: 0, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Carne, bovina, fígado, grelhado', info: { calories: 225, protein: 29.9, fat: 9, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } },
|
{ name: 'Carne, bovina, fígado, grelhado', nameEn: 'Beef, liver, grilled', info: { calories: 225, protein: 29.9, fat: 9, carbs: 0, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Carne, bovina, picanha, sem gordura, grelhada', info: { calories: 238, protein: 31.9, fat: 11.3, carbs: 0, fiber: 0, category: 'proteins', alkaline: false } },
|
{ name: 'Carne, bovina, picanha, sem gordura, grelhada', nameEn: 'Beef, rump cap, lean, grilled', info: { calories: 238, protein: 31.9, fat: 11.3, carbs: 0, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Apresuntado', info: { calories: 129, protein: 13.5, fat: 6.7, carbs: 2.9, fiber: 0, category: 'proteins', alkaline: false } },
|
{ name: 'Apresuntado', nameEn: 'Cooked Ham', info: { calories: 129, protein: 13.5, fat: 6.7, carbs: 2.9, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Presunto, com capa de gordura', info: { calories: 128, protein: 14.4, fat: 6.8, carbs: 1.4, fiber: 0, category: 'proteins', alkaline: false } },
|
{ name: 'Presunto, com capa de gordura', nameEn: 'Ham, with fat layer', info: { calories: 128, protein: 14.4, fat: 6.8, carbs: 1.4, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Presunto, sem capa de gordura', info: { calories: 94, protein: 14.3, fat: 2.7, carbs: 2.1, fiber: 0, category: 'proteins', alkaline: false } },
|
{ name: 'Presunto, sem capa de gordura', nameEn: 'Ham, without fat layer', info: { calories: 94, protein: 14.3, fat: 2.7, carbs: 2.1, fiber: 0, category: 'proteins', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
|
{ name: 'Proteína da Ervilha em pó, Growth', nameEn: 'Pea Protein Powder, Growth', info: { calories: 437, protein: 83.3, fat: 9.7, carbs: 2.3, fiber: 2.7, category: 'proteins', alkaline: false, bestFor: ['breakfast', 'brunch'] } },
|
||||||
|
|
||||||
// Carboidratos, amiláceos - Carbs
|
// Carboidratos, amiláceos - Carbs
|
||||||
{ name: 'Grão de Bico, cozido', info: { calories: 180, protein: 9.5, fat: 3, carbs: 30, fiber: 8.6, category: 'carbs', alkaline: false } },
|
{ 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', info: { calories: 92, protein: 1.4, fat: 0.1, carbs: 23.8, fiber: 1.9, category: 'carbs', alkaline: true } },
|
{ 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: 'Manga Palmer', info: { calories: 72, protein: 0.4, fat: 0.2, carbs: 19.4, fiber: 1.6, category: 'carbs', alkaline: true } },
|
{ 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: 'Mamão', info: { calories: 40, protein: 0.5, fat: 0.1, carbs: 10.4, fiber: 1, category: 'carbs', alkaline: true } },
|
{ 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 doce, assada', info: { calories: 90, protein: 2.0, fat: 0.1, carbs: 20.7, fiber: 3.3, category: 'carbs', alkaline: true } },
|
{ name: 'Batata inglesa, cozida', nameEn: 'Potato, boiled', info: { calories: 52, protein: 1.2, fat: 0, carbs: 11.9, fiber: 1.3, category: 'carbs', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Batata doce, cozida', info: { calories: 77, protein: 0.6, fat: 0.1, carbs: 18.4, fiber: 2.2, category: 'carbs', alkaline: true } },
|
{ name: 'Mandioca, cozida', nameEn: 'Cassava, boiled', info: { calories: 125, protein: 0.6, fat: 0.3, carbs: 30.1, fiber: 1.6, category: 'carbs', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Batata inglesa, assada', info: { calories: 94, protein: 2.1, fat: 0.1, carbs: 21.8, fiber: 2.1, category: 'carbs', alkaline: true } },
|
{ name: 'Pão, trigo, francês', nameEn: 'Bread, wheat, French', info: { calories: 300, protein: 8, fat: 3.1, carbs: 58.6, fiber: 2.3, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Batata inglesa, cozida', info: { calories: 52, protein: 1.2, fat: 0, carbs: 11.9, fiber: 1.3, category: 'carbs', alkaline: true } },
|
{ name: 'Polenta, cozida', nameEn: 'Polenta, cooked', info: { calories: 192, protein: 4.8, fat: 1.2, carbs: 40, fiber: 3.8, category: 'carbs', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Mandioca, cozida', info: { calories: 125, protein: 0.6, fat: 0.3, carbs: 30.1, fiber: 1.6, category: 'carbs', alkaline: false } },
|
{ 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: 'Pão, trigo, francês', info: { calories: 300, protein: 8, fat: 3.1, carbs: 58.6, fiber: 2.3, category: 'carbs', alkaline: false } },
|
{ 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: 'Polenta, cozida', info: { calories: 192, protein: 4.8, fat: 1.2, carbs: 40, fiber: 3.8, category: 'carbs', alkaline: false } },
|
{ 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: 'Arroz, tipo 1 (branco), cozido', info: { calories: 128, protein: 2.5, fat: 0.2, carbs: 28.1, fiber: 1.6, category: 'carbs', alkaline: false } },
|
{ 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: 'Arroz, integral, cozido', info: { calories: 124, protein: 2.6, fat: 1.0, carbs: 25.8, fiber: 2.7, category: 'carbs', alkaline: false } },
|
{ 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: 'Aveia em flocos finos s/glúten (Vitalin)', info: { calories: 338, protein: 15, fat: 6, carbs: 56, fiber: 10, category: 'carbs', alkaline: false } },
|
{ 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: 'Ervilha seca, cozida', info: { calories: 88, protein: 7.5, fat: 0.5, carbs: 14.2, fiber: 9.7, category: 'carbs', alkaline: true } },
|
{ 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: 'Ervilha congelada, cozida', info: { calories: 57, protein: 5, fat: 1, carbs: 7, fiber: 8, category: 'carbs', alkaline: true } },
|
{ 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: 'Pão, trigo, forma, integral', info: { calories: 253, protein: 9.4, fat: 3.7, carbs: 49.9, fiber: 6.9, category: 'carbs', alkaline: false } },
|
{ name: 'Quinoa, cozida', nameEn: 'Quinoa, cooked', info: { calories: 158, protein: 5.55, fat: 2.5, carbs: 29, fiber: 2.5, category: 'carbs', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Pipoca, com óleo de soja, sem sal', info: { calories: 448, protein: 9.9, fat: 15.9, carbs: 70.3, fiber: 14.3, category: 'carbs', alkaline: false } },
|
{ name: '70% Nibs Garoto', nameEn: '70% Garoto Nibs', info: { calories: 544, protein: 8.4, fat: 40, carbs: 34, fiber: 0, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Pipoca, sem óleo, sem sal', info: { calories: 178, protein: 3.3, fat: 7.2, carbs: 25, fiber: 4.3, category: 'carbs', alkaline: false } },
|
{ name: 'Barra de Proteína Vegana Growth', nameEn: 'Growth Vegan Protein Bar', info: { calories: 400, protein: 25, fat: 22, carbs: 25, fiber: 8, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Quinoa, cozida', info: { calories: 158, protein: 5.55, fat: 2.5, carbs: 29, fiber: 2.5, category: 'carbs', alkaline: true } },
|
{ name: 'Goiabada cascão', nameEn: 'Goiabada cascão', info: { calories: 286, protein: 0.4, fat: 0.1, carbs: 78.7, fiber: 4.4, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: '70% Nibs Garoto', info: { calories: 544, protein: 8.4, fat: 40, carbs: 34, fiber: 0, category: 'carbs', alkaline: false } },
|
{ name: 'Goma de Tapioca', nameEn: 'Tapioca Starch', info: { calories: 226, protein: 0, fat: 0, carbs: 57, fiber: 0, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Barra de Proteína Vegana Growth', info: { calories: 400, protein: 25, fat: 22, carbs: 25, fiber: 8, category: 'carbs', alkaline: false } },
|
{ name: 'Mel Silvestre', nameEn: 'Wild Honey', info: { calories: 230, protein: 0, fat: 0, carbs: 65, fiber: 0, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Goiabada cascão', info: { calories: 286, protein: 0.4, fat: 0.1, carbs: 78.7, fiber: 4.4, category: 'carbs', alkaline: false } },
|
{ name: 'Melado', nameEn: 'Molasses', info: { calories: 297, protein: 0, fat: 0, carbs: 76.6, fiber: 0, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Goma de Tapioca', info: { calories: 226, protein: 0, fat: 0, carbs: 57, fiber: 0, category: 'carbs', alkaline: false } },
|
{ name: 'Tâmara desidratada', nameEn: 'Dried Date', info: { calories: 230, protein: 0.3, fat: 33, carbs: 63.3, fiber: 7, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Mel Silvestre', info: { calories: 230, protein: 0, fat: 0, carbs: 65, fiber: 0, category: 'carbs', alkaline: false } },
|
{ name: 'Pão, de queijo, assado', nameEn: 'Baked Cheese Bread', info: { calories: 363, protein: 5.1, fat: 24.6, carbs: 34.2, fiber: 0.6, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Melado', info: { calories: 297, protein: 0, fat: 0, carbs: 76.6, fiber: 0, category: 'carbs', alkaline: false } },
|
{ name: 'Cuscuz, de milho , cozido com sal', nameEn: 'Corn Couscous, cooked with salt', info: { calories: 113, protein: 2.2, fat: 0.7, carbs: 25.3, fiber: 2.1, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch'] } },
|
||||||
{ name: 'Tâmara desidratada', info: { calories: 230, protein: 0.3, fat: 33, carbs: 63.3, fiber: 7, category: 'carbs', alkaline: false } },
|
{ name: 'Paçoca, amendoim', nameEn: 'Peanut Paçoca', info: { calories: 487, protein: 16, fat: 26.1, carbs: 52.4, fiber: 7.3, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Pão, de queijo, assado', info: { calories: 363, protein: 5.1, fat: 24.6, carbs: 34.2, fiber: 0.6, category: 'carbs', alkaline: false } },
|
{ name: 'Tortelete de mousse chocolate zero Quiero Café', nameEn: 'Zero Chocolate Mousse Tartlet Quiero Café', info: { calories: 250, protein: 2.3, fat: 8.9, carbs: 40, fiber: 5, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Cuscuz, de milho , cozido com sal', info: { calories: 113, protein: 2.2, fat: 0.7, carbs: 25.3, fiber: 2.1, category: 'carbs', alkaline: false } },
|
{ name: 'Pão de beijo com chia e pequi Empório Veggie', nameEn: 'Kiss Bread with Chia and Pequi Empório Veggie', info: { calories: 316, protein: 0.6, fat: 14, carbs: 47, fiber: 1, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Paçoca, amendoim', info: { calories: 487, protein: 16, fat: 26.1, carbs: 52.4, fiber: 7.3, category: 'carbs', alkaline: false } },
|
{ name: 'Bananinha Paraibuna zero', nameEn: 'Zero Paraibuna Banana', info: { calories: 296, protein: 4.8, fat: 0, carbs: 70, fiber: 4.3, category: 'carbs', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Tortelete de mousse chocolate zero Quiero Café', info: { calories: 250, protein: 2.3, fat: 8.9, carbs: 40, fiber: 5, category: 'carbs', alkaline: false } },
|
{ name: 'Farinha de castanha de caju (Zaya)', nameEn: 'Cashew Nut Flour (Zaya)', info: { calories: 500, protein: 20, fat: 53.3, carbs: 20, fiber: 13.3, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Pão de beijo com chia e pequi Empório Veggie', info: { calories: 316, protein: 0.6, fat: 14, carbs: 47, fiber: 1, category: 'carbs', alkaline: false } },
|
{ name: 'Batata baroa, cozida', nameEn: 'Cooked Baroa Potato', info: { calories: 80, protein: 0.9, fat: 0.2, carbs: 18.9, fiber: 1.8, category: 'carbs', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Bananinha Paraibuna zero', info: { calories: 296, protein: 4.8, fat: 0, carbs: 70, fiber: 4.3, category: 'carbs', alkaline: true } },
|
{ name: 'Paçoquita original Santa Helena', nameEn: 'Original peanut candy Santa Helena', info: { calories: 532, protein: 18, fat: 28, carbs: 50, fiber: 4, category: 'carbs', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Farinha de castanha de caju (Zaya)', info: { calories: 500, protein: 20, fat: 53.3, carbs: 20, fiber: 13.3, category: 'carbs', alkaline: false } },
|
{ 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: 'Batata baroa, cozida', info: { calories: 80, protein: 0.9, fat: 0.2, carbs: 18.9, fiber: 1.8, category: 'carbs', alkaline: true } },
|
{ 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
|
// Folhas e verduras - Leaves
|
||||||
{ name: 'Agrião', info: { calories: 17, protein: 2.7, fat: 0.2, carbs: 2.3, fiber: 2.1, category: 'leaves', alkaline: true } },
|
{ 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: 'Espinafre', info: { calories: 16, protein: 2, fat: 0.2, carbs: 2.6, fiber: 2.1, category: 'leaves', alkaline: true } },
|
{ name: 'Espinafre', nameEn: 'Spinach', info: { calories: 16, protein: 2, fat: 0.2, carbs: 2.6, fiber: 2.1, category: 'leaves', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Rúcula', info: { calories: 13, protein: 1.8, fat: 0.1, carbs: 2.2, fiber: 1.7, category: 'leaves', alkaline: true } },
|
{ name: 'Rúcula', nameEn: 'Arugula', info: { calories: 13, protein: 1.8, fat: 0.1, carbs: 2.2, fiber: 1.7, category: 'leaves', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Spirulina', info: { calories: 290, protein: 57.5, fat: 23.9, carbs: 7.7, fiber: 0, category: 'leaves', alkaline: true } },
|
{ name: 'Spirulina', nameEn: 'Spirulina', info: { calories: 290, protein: 57.5, fat: 23.9, carbs: 7.7, fiber: 0, category: 'leaves', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
|
|
||||||
// Frutas - Fruits
|
// Frutas - Fruits
|
||||||
{ name: 'Ameixa, crua', info: { calories: 53, protein: 0.8, fat: 0, carbs: 13.9, fiber: 2.4, category: 'fruits', alkaline: true } },
|
{ 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', info: { calories: 48, protein: 0.9, fat: 0.1, carbs: 12.3, fiber: 1, category: 'fruits', alkaline: true } },
|
{ 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: 'Laranja', info: { calories: 37, protein: 1, fat: 0.1, carbs: 8.9, fiber: 0.8, category: 'fruits', alkaline: false } },
|
{ 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: 'Maçã', info: { calories: 56, protein: 0.3, fat: 0, carbs: 15.2, fiber: 1.3, category: 'fruits', alkaline: true } },
|
{ 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: 'Melancia', info: { calories: 33, protein: 0.9, fat: 0, carbs: 8.1, fiber: 0.1, category: 'fruits', alkaline: true } },
|
{ 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: 'Melão', info: { calories: 29, protein: 0.7, fat: 0, carbs: 7.5, fiber: 0.3, category: 'fruits', alkaline: true } },
|
{ 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: 'Uva', info: { calories: 53, protein: 0.7, fat: 0.2, carbs: 13.6, fiber: 0.9, category: 'fruits', alkaline: true } },
|
{ 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: 'Goiaba, branca, com casca, crua', info: { calories: 52, protein: 0.9, fat: 0.5, carbs: 12.4, fiber: 6.3, category: 'fruits', alkaline: true } },
|
{ 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: 'Morango, cru', info: { calories: 30, protein: 0.9, fat: 0.3, carbs: 6.8, fiber: 1.7, category: 'fruits', alkaline: true } },
|
{ 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: 'Uva', nameEn: 'Grape', info: { calories: 53, protein: 0.7, fat: 0.2, carbs: 13.6, fiber: 0.9, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
|
{ name: 'Goiaba, branca, com casca, crua', nameEn: 'White Guava, with skin, raw', info: { calories: 52, protein: 0.9, fat: 0.5, carbs: 12.4, fiber: 6.3, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
|
{ name: 'Morango, cru', nameEn: 'Strawberry, raw', info: { calories: 30, protein: 0.9, fat: 0.3, carbs: 6.8, fiber: 1.7, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
|
|
||||||
// Dairy
|
// Dairy
|
||||||
{ name: 'Iogurte, natural, desnatado', info: { calories: 41, protein: 3.8, fat: 0.3, carbs: 5.8, fiber: 0, category: 'dairy', alkaline: true } },
|
{ name: 'Iogurte, natural, desnatado', nameEn: 'Natural Yogurt, skimmed', info: { calories: 41, protein: 3.8, fat: 0.3, carbs: 5.8, fiber: 0, category: 'dairy', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Queijo, mozarela, mussarela, muçarela', info: { calories: 330, protein: 22.6, fat: 25.2, carbs: 3, fiber: 0, category: 'dairy', alkaline: true } },
|
{ name: 'Queijo, mozarela, mussarela, muçarela', nameEn: 'Mozzarella Cheese', info: { calories: 330, protein: 22.6, fat: 25.2, carbs: 3, fiber: 0, category: 'dairy', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Queijo, parmesão', info: { calories: 453, protein: 35.6, fat: 33.5, carbs: 1.7, fiber: 0, category: 'dairy', alkaline: true } },
|
{ name: 'Queijo, parmesão', nameEn: 'Parmesan Cheese', info: { calories: 453, protein: 35.6, fat: 33.5, carbs: 1.7, fiber: 0, category: 'dairy', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Queijo, prato', info: { calories: 360, protein: 22.7, fat: 29.1, carbs: 1.9, fiber: 0, category: 'dairy', alkaline: true } },
|
{ name: 'Queijo, prato', nameEn: 'Prato Cheese', info: { calories: 360, protein: 22.7, fat: 29.1, carbs: 1.9, fiber: 0, category: 'dairy', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Queijo, requeijão, cremoso', info: { calories: 257, protein: 9.6, fat: 23.4, carbs: 2.4, fiber: 0, category: 'dairy', alkaline: true } },
|
{ name: 'Queijo, requeijão, cremoso', nameEn: 'Creamy Requeijão Cheese', info: { calories: 257, protein: 9.6, fat: 23.4, carbs: 2.4, fiber: 0, category: 'dairy', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Requeijão light (Tirol)', info: { calories: 177, protein: 11, fat: 13.3, carbs: 3.3, fiber: 0, category: 'dairy', alkaline: true } },
|
{ name: 'Requeijão light (Tirol)', nameEn: 'Light Requeijão (Tirol)', info: { calories: 177, protein: 11, fat: 13.3, carbs: 3.3, fiber: 0, category: 'dairy', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Queijo, ricota', info: { calories: 140, protein: 12.6, fat: 8.1, carbs: 3.8, fiber: 0, category: 'dairy', alkaline: true } },
|
{ name: 'Queijo, ricota', nameEn: 'Ricotta Cheese', info: { calories: 140, protein: 12.6, fat: 8.1, carbs: 3.8, fiber: 0, category: 'dairy', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Creme de leite', info: { calories: 221, protein: 1.5, fat: 22.5, carbs: 4.6, fiber: 0, category: 'dairy', alkaline: true } },
|
{ name: 'Creme de leite', nameEn: 'Cream', info: { calories: 221, protein: 1.5, fat: 22.5, carbs: 4.6, fiber: 0, category: 'dairy', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Leite, condensado', info: { calories: 313, protein: 7.7, fat: 6.7, carbs: 57, fiber: 0, category: 'dairy', alkaline: true } },
|
{ name: 'Leite, condensado', nameEn: 'Condensed Milk', info: { calories: 313, protein: 7.7, fat: 6.7, carbs: 57, fiber: 0, category: 'dairy', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Leite, de vaca, desnatado, pó', info: { calories: 362, protein: 34.7, fat: 0.9, carbs: 53, fiber: 0, category: 'dairy', alkaline: true } },
|
{ name: 'Leite, de vaca, desnatado, pó', nameEn: 'Skimmed Cow Milk, Powdered', info: { calories: 362, protein: 34.7, fat: 0.9, carbs: 53, fiber: 0, category: 'dairy', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Leite, de vaca, desnatado, UTH (Tirol)', info: { calories: 30.5, protein: 3.15, fat: 0, carbs: 4.5, fiber: 0, category: 'dairy', alkaline: true } },
|
{ name: 'Leite, de vaca, desnatado, UTH (Tirol)', nameEn: 'Skimmed Cow Milk, UHT (Tirol)', info: { calories: 30.5, protein: 3.15, fat: 0, carbs: 4.5, fiber: 0, category: 'dairy', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Leite, de vaca, integral, UTH (Tirol)', info: { calories: 57.5, protein: 3.15, fat: 3, carbs: 4.5, fiber: 0, category: 'dairy', alkaline: true } },
|
{ name: 'Leite, de vaca, integral, UTH (Tirol)', nameEn: 'Whole Cow Milk, UHT (Tirol)', info: { calories: 57.5, protein: 3.15, fat: 3, carbs: 4.5, fiber: 0, category: 'dairy', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Leite, de vaca, integral, pó', info: { calories: 496, protein: 26, fat: 26.4, carbs: 38.4, fiber: 0, category: 'dairy', alkaline: true } },
|
{ name: 'Leite, de vaca, integral, pó', nameEn: 'Whole Cow Milk, Powdered', info: { calories: 496, protein: 26, fat: 26.4, carbs: 38.4, fiber: 0, category: 'dairy', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
|
{ name: 'Leite Caju+côco A Tal da Castanha', nameEn: 'Cashew+Coconut Milk A Tal da Castanha', info: { calories: 51, protein: 2, fat: 3.4, carbs: 2.6, fiber: 0, category: 'dairy', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
|
{ name: 'Leite Caju Original A Tal da Castanha', nameEn: 'Original Cashew Milk A Tal da Castanha', info: { calories: 48, protein: 1.5, fat: 4, carbs: 1.5, fiber: 0.7, category: 'dairy', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
|
|
||||||
|
|
||||||
// Low carb
|
// Low carb
|
||||||
{ name: 'Abóbrinha, italinaa, cozida', info: { calories: 15, protein: 1.1, fat: 0.2, carbs: 3.0, fiber: 1.6, category: 'low carb', alkaline: true } },
|
{ name: 'Abóbrinha, italinaa, cozida', nameEn: 'Italian Zucchini, cooked', info: { calories: 15, protein: 1.1, fat: 0.2, carbs: 3.0, fiber: 1.6, category: 'low carb', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Abóbora Cabotian, crua', info: { calories: 39, protein: 1.7, fat: 0.5, carbs: 8.4, fiber: 2.2, category: 'low carb', alkaline: true } },
|
{ name: 'Abóbora Cabotian, crua', nameEn: 'Cabotian Pumpkin, raw', info: { calories: 39, protein: 1.7, fat: 0.5, carbs: 8.4, fiber: 2.2, category: 'low carb', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Abóbora Cabotian, cozida', info: { calories: 48, protein: 1.4, fat: 0.7, carbs: 10.8, fiber: 2.5, category: 'low carb', alkaline: true } },
|
{ name: 'Abóbora Cabotian, cozida', nameEn: 'Cabotian Pumpkin, cooked', info: { calories: 48, protein: 1.4, fat: 0.7, carbs: 10.8, fiber: 2.5, category: 'low carb', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Abóbora Cabotian, assada', info: { calories: 50, protein: 2, fat: 1, carbs: 11, fiber: 2.5, category: 'low carb', alkaline: true } },
|
{ name: 'Abóbora Cabotian, assada', nameEn: 'Cabotian Pumpkin, roasted', info: { calories: 50, protein: 2, fat: 1, carbs: 11, fiber: 2.5, category: 'low carb', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Cenoura, cozida', info: { calories: 30, protein: 0.8, fat: 0.2, carbs: 6.7, fiber: 2.6, category: 'low carb', alkaline: true } },
|
{ name: 'Cenoura, cozida', nameEn: 'Carrot, cooked', info: { calories: 30, protein: 0.8, fat: 0.2, carbs: 6.7, fiber: 2.6, category: 'low carb', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Tomate', info: { calories: 15, protein: 1.1, fat: 0.2, carbs: 3.1, fiber: 1.2, category: 'low carb', alkaline: true } },
|
{ name: 'Tomate', nameEn: 'Tomato', info: { calories: 15, protein: 1.1, fat: 0.2, carbs: 3.1, fiber: 1.2, category: 'low carb', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Beterraba, cozida', info: { calories: 32, protein: 1.3, fat: 0.1, carbs: 7.2, fiber: 1.9, category: 'low carb', alkaline: true } },
|
{ name: 'Beterraba, cozida', nameEn: 'Beetroot, cooked', info: { calories: 32, protein: 1.3, fat: 0.1, carbs: 7.2, fiber: 1.9, category: 'low carb', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Beterraba, crua', info: { calories: 49, protein: 1.9, fat: 0.1, carbs: 11.1, fiber: 3.4, category: 'low carb', alkaline: true } },
|
{ name: 'Beterraba, crua', nameEn: 'Beetroot, raw', info: { calories: 49, protein: 1.9, fat: 0.1, carbs: 11.1, fiber: 3.4, category: 'low carb', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Brócolis, cozido', info: { calories: 25, protein: 2.1, fat: 0.5, carbs: 4.4, fiber: 3.4, category: 'low carb', alkaline: true } },
|
{ name: 'Brócolis, cozido', nameEn: 'Broccoli, cooked', info: { calories: 25, protein: 2.1, fat: 0.5, carbs: 4.4, fiber: 3.4, category: 'low carb', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Brócolis, cru', info: { calories: 25, protein: 3.6, fat: 0.3, carbs: 4, fiber: 2.9, category: 'low carb', alkaline: true } },
|
{ name: 'Brócolis, cru', nameEn: 'Broccoli, raw', info: { calories: 25, protein: 3.6, fat: 0.3, carbs: 4, fiber: 2.9, category: 'low carb', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Couve flor, cozida', info: { calories: 19, protein: 1.2, fat: 0.3, carbs: 3.9, fiber: 2.1, category: 'low carb', alkaline: true } },
|
{ name: 'Couve flor, cozida', nameEn: 'Cauliflower, cooked', info: { calories: 19, protein: 1.2, fat: 0.3, carbs: 3.9, fiber: 2.1, category: 'low carb', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Pepino', info: { calories: 10, protein: 0.9, fat: 0, carbs: 2, fiber: 1.1, category: 'low carb', alkaline: true } },
|
{ name: 'Pepino', nameEn: 'Cucumber', info: { calories: 10, protein: 0.9, fat: 0, carbs: 2, fiber: 1.1, category: 'low carb', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Chuchu, cozido', info: { calories: 19, protein: 0.4, fat: 0, carbs: 4.9, fiber: 1.0, category: 'low carb', alkaline: true } },
|
{ name: 'Chuchu, cozido', nameEn: 'Chayote, cooked', info: { calories: 19, protein: 0.4, fat: 0, carbs: 4.9, fiber: 1.0, category: 'low carb', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Cogumelo Paris, in natura', info: { calories: 17, protein: 4.9, fat: 0, carbs: 1, fiber: 1.7, category: 'low carb', alkaline: true } },
|
{ name: 'Cogumelo Paris, in natura', nameEn: 'Paris Mushroom, raw', info: { calories: 17, protein: 4.9, fat: 0, carbs: 1, fiber: 1.7, category: 'low carb', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Milho, verde, enlatado, drenado', info: { calories: 98, protein: 3.2, fat: 2.4, carbs: 17.1, fiber: 4.6, category: 'low carb', alkaline: true } },
|
{ name: 'Milho, verde, enlatado, drenado', nameEn: 'Green Corn, canned, drained', info: { calories: 98, protein: 3.2, fat: 2.4, carbs: 17.1, fiber: 4.6, category: 'low carb', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Broto de Feijão', info: { calories: 23, protein: 2.8, fat: 0, carbs: 2.8, fiber: 0.7, category: 'low carb', alkaline: true } },
|
{ name: 'Broto de Feijão', nameEn: 'Bean Sprout', info: { calories: 23, protein: 2.8, fat: 0, carbs: 2.8, fiber: 0.7, category: 'low carb', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
];
|
];
|
||||||
|
|||||||
+1021
-137
File diff suppressed because it is too large
Load Diff
@@ -1,770 +0,0 @@
|
|||||||
import { foodDatabase } from './foodDatabase.js';
|
|
||||||
import { getButtonById, getDivById, getInputById, showFoodPreview } from './DomUtils.ts';
|
|
||||||
import { FoodItem, MealType, PlannedFoodItem } from './types.js';
|
|
||||||
import { AppwriteDB } from './appwrite.js';
|
|
||||||
import swal from 'sweetalert';
|
|
||||||
import { hideLoading, showLoading, getCleanName, getIcon } from './Utils.ts';
|
|
||||||
import { appState } from "./state";
|
|
||||||
|
|
||||||
let selectedPlanFood: FoodItem | null = null;
|
|
||||||
let currentMealType: MealType | null = null;
|
|
||||||
|
|
||||||
// Navigation functions
|
|
||||||
export async function toggleMealPlannerView() {
|
|
||||||
const showMealPlanner = getButtonById('mealPlannerBtn').textContent?.includes('Meal Planner');
|
|
||||||
|
|
||||||
if (showMealPlanner) {
|
|
||||||
showLoading();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Load templates
|
|
||||||
await loadMealPlanTemplates();
|
|
||||||
|
|
||||||
// Load active template if exists
|
|
||||||
const activeTemplate = await AppwriteDB.getActiveMealPlanTemplate();
|
|
||||||
if (activeTemplate) {
|
|
||||||
appState.currentTemplate = activeTemplate as any;
|
|
||||||
await loadPlannedItemsForTemplate(activeTemplate.$id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show meal planner page
|
|
||||||
getDivById('app-content').classList.add('hidden');
|
|
||||||
getDivById('settings-content').classList.add('hidden');
|
|
||||||
getDivById('meal-planner-content').classList.remove('hidden');
|
|
||||||
getButtonById('mealPlannerBtn').textContent = '← Back';
|
|
||||||
getButtonById('mealPlannerBtnMobile').textContent = '← Back';
|
|
||||||
hideLoading();
|
|
||||||
} catch (error) {
|
|
||||||
hideLoading();
|
|
||||||
console.error('Show meal planner error:', error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
swal('Oh no!', 'Failed to load meal planner: ' + error.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Go back to main app
|
|
||||||
getDivById('app-content').classList.remove('hidden');
|
|
||||||
getDivById('meal-planner-content').classList.add('hidden');
|
|
||||||
getButtonById('mealPlannerBtn').textContent = '📋 Meal Planner';
|
|
||||||
getButtonById('mealPlannerBtnMobile').textContent = '📋 Meal Planner';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleMobileMealPlannerClick() {
|
|
||||||
toggleMealPlannerView();
|
|
||||||
// Close mobile menu
|
|
||||||
const mobileMenu = document.getElementById('mobileMenu');
|
|
||||||
if (mobileMenu) {
|
|
||||||
mobileMenu.classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Template management functions
|
|
||||||
export async function loadMealPlanTemplates() {
|
|
||||||
try {
|
|
||||||
const templates = await AppwriteDB.getMealPlanTemplates();
|
|
||||||
const templateSelector = document.getElementById('templateSelector') as HTMLSelectElement;
|
|
||||||
|
|
||||||
if (!templateSelector) return;
|
|
||||||
|
|
||||||
// Clear existing options except the first one
|
|
||||||
templateSelector.innerHTML = '<option value="">-- Select a plan --</option>';
|
|
||||||
|
|
||||||
// Add templates
|
|
||||||
templates.forEach(template => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = template.$id;
|
|
||||||
option.textContent = template.name;
|
|
||||||
if (template.isActive) {
|
|
||||||
option.selected = true;
|
|
||||||
}
|
|
||||||
templateSelector.appendChild(option);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Load templates error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createNewTemplate() {
|
|
||||||
const templateName = await swal({
|
|
||||||
title: 'New Meal Plan',
|
|
||||||
text: 'Enter a name for your meal plan:',
|
|
||||||
content: {
|
|
||||||
element: 'input',
|
|
||||||
attributes: {
|
|
||||||
placeholder: 'e.g., Workout Day, Rest Day',
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
buttons: {
|
|
||||||
cancel: true,
|
|
||||||
confirm: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!templateName || templateName.trim() === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoading();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const newTemplate = await AppwriteDB.createMealPlanTemplate(templateName.trim());
|
|
||||||
await loadMealPlanTemplates();
|
|
||||||
|
|
||||||
// Set as active and load it
|
|
||||||
await AppwriteDB.setActiveMealPlanTemplate(newTemplate.$id);
|
|
||||||
appState.currentTemplate = newTemplate as any;
|
|
||||||
appState.plannedItems = [];
|
|
||||||
|
|
||||||
// Update selector
|
|
||||||
const templateSelector = document.getElementById('templateSelector') as HTMLSelectElement;
|
|
||||||
if (templateSelector) {
|
|
||||||
templateSelector.value = newTemplate.$id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh display
|
|
||||||
displayPlannedItems();
|
|
||||||
updatePlanTotals();
|
|
||||||
|
|
||||||
hideLoading();
|
|
||||||
swal('Success!', 'Meal plan created successfully!', 'success');
|
|
||||||
} catch (error) {
|
|
||||||
hideLoading();
|
|
||||||
console.error('Create template error:', error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
swal('Oh no!', 'Failed to create meal plan: ' + error.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteCurrentTemplate() {
|
|
||||||
if (!appState.currentTemplate) {
|
|
||||||
swal('Oh no!', 'No meal plan selected', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirm = await swal({
|
|
||||||
title: 'Delete Meal Plan',
|
|
||||||
text: `Are you sure you want to delete "${appState.currentTemplate.name}"? This will also delete all planned items.`,
|
|
||||||
icon: 'warning',
|
|
||||||
buttons: {
|
|
||||||
cancel: true,
|
|
||||||
confirm: {
|
|
||||||
text: 'Delete',
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dangerMode: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirm) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoading();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await AppwriteDB.deleteMealPlanTemplate(appState.currentTemplate.id!);
|
|
||||||
appState.currentTemplate = null;
|
|
||||||
appState.plannedItems = [];
|
|
||||||
|
|
||||||
await loadMealPlanTemplates();
|
|
||||||
displayPlannedItems();
|
|
||||||
updatePlanTotals();
|
|
||||||
|
|
||||||
hideLoading();
|
|
||||||
swal('Success!', 'Meal plan deleted successfully!', 'success');
|
|
||||||
} catch (error) {
|
|
||||||
hideLoading();
|
|
||||||
console.error('Delete template error:', error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
swal('Oh no!', 'Failed to delete meal plan: ' + error.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleTemplateChange(templateId: string) {
|
|
||||||
if (!templateId) {
|
|
||||||
appState.currentTemplate = null;
|
|
||||||
appState.plannedItems = [];
|
|
||||||
displayPlannedItems();
|
|
||||||
updatePlanTotals();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoading();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Set as active template
|
|
||||||
await AppwriteDB.setActiveMealPlanTemplate(templateId);
|
|
||||||
|
|
||||||
// Load template details
|
|
||||||
const templates = await AppwriteDB.getMealPlanTemplates();
|
|
||||||
const template = templates.find(t => t.$id === templateId);
|
|
||||||
|
|
||||||
if (template) {
|
|
||||||
appState.currentTemplate = template as any;
|
|
||||||
await loadPlannedItemsForTemplate(templateId);
|
|
||||||
}
|
|
||||||
|
|
||||||
hideLoading();
|
|
||||||
} catch (error) {
|
|
||||||
hideLoading();
|
|
||||||
console.error('Handle template change error:', error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
swal('Oh no!', 'Failed to load meal plan: ' + error.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadPlannedItemsForTemplate(templateId: string) {
|
|
||||||
try {
|
|
||||||
const items = await AppwriteDB.getPlannedItemsByTemplate(templateId);
|
|
||||||
appState.plannedItems = items as any[];
|
|
||||||
displayPlannedItems();
|
|
||||||
updatePlanTotals();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Load planned items error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Food search and add functions
|
|
||||||
export function setupPlanFoodSearch() {
|
|
||||||
const searchInput = document.getElementById('planFoodSearchInput') as HTMLInputElement;
|
|
||||||
const gramAmountInput = document.getElementById('planGramAmount') as HTMLInputElement;
|
|
||||||
|
|
||||||
if (!searchInput || !gramAmountInput) return;
|
|
||||||
|
|
||||||
searchInput.addEventListener('input', (e) => {
|
|
||||||
const target = e.target as HTMLInputElement;
|
|
||||||
const searchTerm = target.value.trim().toLowerCase();
|
|
||||||
|
|
||||||
if (searchTerm === '') {
|
|
||||||
hidePlanSearchResults();
|
|
||||||
selectedPlanFood = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounce search
|
|
||||||
if (appState.planSearchTimeout) {
|
|
||||||
clearTimeout(appState.planSearchTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
appState.planSearchTimeout = window.setTimeout(() => {
|
|
||||||
performPlanFoodSearch(searchTerm);
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
|
|
||||||
searchInput.addEventListener('keydown', (e) => {
|
|
||||||
navigatePlanResultsKeyboard(e as KeyboardEvent);
|
|
||||||
});
|
|
||||||
|
|
||||||
gramAmountInput.addEventListener('input', () => {
|
|
||||||
previewPlanCalories();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function performPlanFoodSearch(searchTerm: string) {
|
|
||||||
const results = foodDatabase.filter(food =>
|
|
||||||
food.name.toLowerCase().includes(searchTerm)
|
|
||||||
).slice(0, 10);
|
|
||||||
|
|
||||||
appState.planSearchResults = results;
|
|
||||||
appState.planCurrentHighlightIndex = -1;
|
|
||||||
displayPlanSearchResults(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayPlanSearchResults(results: FoodItem[]) {
|
|
||||||
const searchResults = document.getElementById('planSearchResults');
|
|
||||||
if (!searchResults) return;
|
|
||||||
|
|
||||||
if (results.length === 0) {
|
|
||||||
searchResults.innerHTML = '<div class="no-results">No foods found</div>';
|
|
||||||
searchResults.classList.remove('hidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchResults.innerHTML = '';
|
|
||||||
results.forEach((food, index) => {
|
|
||||||
const resultItem = document.createElement('div');
|
|
||||||
resultItem.className = 'search-result-item';
|
|
||||||
resultItem.setAttribute('data-index', index.toString());
|
|
||||||
resultItem.innerHTML = `
|
|
||||||
${getIcon(food.info.category)}
|
|
||||||
<span class="food-name">${getCleanName(food.name)}</span>
|
|
||||||
<span class="food-calories">${food.info.calories} cal</span>
|
|
||||||
`;
|
|
||||||
resultItem.addEventListener('click', () => selectPlanFood(food));
|
|
||||||
searchResults.appendChild(resultItem);
|
|
||||||
});
|
|
||||||
|
|
||||||
searchResults.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
function hidePlanSearchResults() {
|
|
||||||
const searchResults = document.getElementById('planSearchResults');
|
|
||||||
if (searchResults) {
|
|
||||||
searchResults.classList.add('hidden');
|
|
||||||
searchResults.innerHTML = '';
|
|
||||||
}
|
|
||||||
appState.planSearchResults = [];
|
|
||||||
appState.planCurrentHighlightIndex = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigatePlanResultsKeyboard(e: KeyboardEvent) {
|
|
||||||
const results = appState.planSearchResults;
|
|
||||||
|
|
||||||
if (results.length === 0) return;
|
|
||||||
|
|
||||||
if (e.key === 'ArrowDown') {
|
|
||||||
e.preventDefault();
|
|
||||||
appState.planCurrentHighlightIndex = Math.min(appState.planCurrentHighlightIndex + 1, results.length - 1);
|
|
||||||
updatePlanResultsHighlight();
|
|
||||||
} else if (e.key === 'ArrowUp') {
|
|
||||||
e.preventDefault();
|
|
||||||
appState.planCurrentHighlightIndex = Math.max(appState.planCurrentHighlightIndex - 1, -1);
|
|
||||||
updatePlanResultsHighlight();
|
|
||||||
} else if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
if (appState.planCurrentHighlightIndex >= 0) {
|
|
||||||
selectPlanFood(results[appState.planCurrentHighlightIndex]);
|
|
||||||
}
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
hidePlanSearchResults();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePlanResultsHighlight() {
|
|
||||||
const searchResults = document.getElementById('planSearchResults');
|
|
||||||
if (!searchResults) return;
|
|
||||||
|
|
||||||
const items = searchResults.querySelectorAll('.search-result-item');
|
|
||||||
items.forEach((item, index) => {
|
|
||||||
if (index === appState.planCurrentHighlightIndex) {
|
|
||||||
item.classList.add('highlighted');
|
|
||||||
} else {
|
|
||||||
item.classList.remove('highlighted');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectPlanFood(food: FoodItem) {
|
|
||||||
selectedPlanFood = food;
|
|
||||||
const searchInput = document.getElementById('planFoodSearchInput') as HTMLInputElement;
|
|
||||||
if (searchInput) {
|
|
||||||
searchInput.value = getCleanName(food.name);
|
|
||||||
}
|
|
||||||
hidePlanSearchResults();
|
|
||||||
previewPlanCalories();
|
|
||||||
}
|
|
||||||
|
|
||||||
function previewPlanCalories() {
|
|
||||||
const gramAmountInput = document.getElementById('planGramAmount') as HTMLInputElement;
|
|
||||||
const previewContainer = document.getElementById('plan-food-preview');
|
|
||||||
|
|
||||||
if (!selectedPlanFood || !gramAmountInput || !previewContainer) {
|
|
||||||
if (previewContainer) previewContainer.classList.add('display-none');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const grams = parseFloat(gramAmountInput.value) || 100;
|
|
||||||
const proportion = grams / 100;
|
|
||||||
|
|
||||||
const calories = Math.round(selectedPlanFood.info.calories * proportion);
|
|
||||||
const protein = Math.round(selectedPlanFood.info.protein * proportion * 10) / 10;
|
|
||||||
const fat = Math.round(selectedPlanFood.info.fat * proportion * 10) / 10;
|
|
||||||
const carbs = Math.round(selectedPlanFood.info.carbs * proportion * 10) / 10;
|
|
||||||
const fiber = Math.round(selectedPlanFood.info.fiber * proportion * 10) / 10;
|
|
||||||
|
|
||||||
const calorieEl = document.getElementById('planCalorieValuePreview');
|
|
||||||
const proteinEl = document.getElementById('planProteinValuePreview');
|
|
||||||
const fatEl = document.getElementById('planFatValuePreview');
|
|
||||||
const carbsEl = document.getElementById('planCarboValuePreview');
|
|
||||||
const fiberEl = document.getElementById('planFiberValuePreview');
|
|
||||||
|
|
||||||
if (calorieEl) calorieEl.textContent = calories.toString();
|
|
||||||
if (proteinEl) proteinEl.textContent = protein.toString();
|
|
||||||
if (fatEl) fatEl.textContent = fat.toString();
|
|
||||||
if (carbsEl) carbsEl.textContent = carbs.toString();
|
|
||||||
if (fiberEl) fiberEl.textContent = fiber.toString();
|
|
||||||
|
|
||||||
previewContainer.classList.remove('display-none');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function showAddFoodToPlan(mealType: MealType) {
|
|
||||||
if (!appState.currentTemplate) {
|
|
||||||
swal('Oh no!', 'Please select or create a meal plan first', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentMealType = mealType;
|
|
||||||
const addSection = document.getElementById('addFoodToPlanSection');
|
|
||||||
const mealTypeSpan = document.getElementById('currentMealType');
|
|
||||||
const currentMealTypeInput = document.getElementById('currentMealTypeInput') as HTMLInputElement;
|
|
||||||
|
|
||||||
if (addSection) addSection.classList.remove('hidden');
|
|
||||||
if (mealTypeSpan) mealTypeSpan.textContent = mealType.charAt(0).toUpperCase() + mealType.slice(1);
|
|
||||||
if (currentMealTypeInput) currentMealTypeInput.value = mealType;
|
|
||||||
|
|
||||||
// Scroll to form
|
|
||||||
addSection?.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hideAddFoodToPlan() {
|
|
||||||
const addSection = document.getElementById('addFoodToPlanSection');
|
|
||||||
const searchInput = document.getElementById('planFoodSearchInput') as HTMLInputElement;
|
|
||||||
const gramInput = document.getElementById('planGramAmount') as HTMLInputElement;
|
|
||||||
const previewContainer = document.getElementById('plan-food-preview');
|
|
||||||
|
|
||||||
if (addSection) addSection.classList.add('hidden');
|
|
||||||
if (searchInput) searchInput.value = '';
|
|
||||||
if (gramInput) gramInput.value = '100';
|
|
||||||
if (previewContainer) previewContainer.classList.add('display-none');
|
|
||||||
|
|
||||||
selectedPlanFood = null;
|
|
||||||
currentMealType = null;
|
|
||||||
hidePlanSearchResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function addFoodToPlan() {
|
|
||||||
if (!selectedPlanFood || !currentMealType || !appState.currentTemplate) {
|
|
||||||
swal('Oh no!', 'Please select a food item', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gramAmountInput = document.getElementById('planGramAmount') as HTMLInputElement;
|
|
||||||
const grams = parseFloat(gramAmountInput.value);
|
|
||||||
|
|
||||||
if (!grams || grams <= 0) {
|
|
||||||
swal('Oh no!', 'Please enter a valid amount in grams', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoading();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const proportion = grams / 100;
|
|
||||||
const plannedItem: Omit<PlannedFoodItem, 'id'> = {
|
|
||||||
templateId: appState.currentTemplate.id!,
|
|
||||||
mealType: currentMealType,
|
|
||||||
name: selectedPlanFood.name,
|
|
||||||
grams: grams,
|
|
||||||
calories: Math.round(selectedPlanFood.info.calories * proportion),
|
|
||||||
protein: Math.round(selectedPlanFood.info.protein * proportion * 10) / 10,
|
|
||||||
fat: Math.round(selectedPlanFood.info.fat * proportion * 10) / 10,
|
|
||||||
carbs: Math.round(selectedPlanFood.info.carbs * proportion * 10) / 10,
|
|
||||||
fiber: Math.round(selectedPlanFood.info.fiber * proportion * 10) / 10,
|
|
||||||
alkaline: selectedPlanFood.info.alkaline
|
|
||||||
};
|
|
||||||
|
|
||||||
await AppwriteDB.createPlannedFoodItem(plannedItem);
|
|
||||||
await loadPlannedItemsForTemplate(appState.currentTemplate.id!);
|
|
||||||
|
|
||||||
hideAddFoodToPlan();
|
|
||||||
hideLoading();
|
|
||||||
swal('Success!', 'Food added to meal plan!', 'success');
|
|
||||||
} catch (error) {
|
|
||||||
hideLoading();
|
|
||||||
console.error('Add food to plan error:', error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
swal('Oh no!', 'Failed to add food: ' + error.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display functions
|
|
||||||
export function displayPlannedItems() {
|
|
||||||
const mealTypes: MealType[] = ['breakfast', 'lunch', 'dinner', 'snacks'];
|
|
||||||
|
|
||||||
mealTypes.forEach(mealType => {
|
|
||||||
const container = document.getElementById(`${mealType}ItemsContainer`);
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
const items = appState.plannedItems.filter(item => item.mealType === mealType);
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
|
||||||
container.innerHTML = '<p class="no-items-message">No foods planned for this meal</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = '';
|
|
||||||
items.forEach(item => {
|
|
||||||
const card = createPlannedItemCard(item);
|
|
||||||
container.appendChild(card);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPlannedItemCard(item: PlannedFoodItem): HTMLElement {
|
|
||||||
const card = document.createElement('div');
|
|
||||||
card.className = 'planned-item-card';
|
|
||||||
card.innerHTML = `
|
|
||||||
<div class="planned-item-header">
|
|
||||||
<span class="planned-item-name">${getCleanName(item.name)}</span>
|
|
||||||
<span class="planned-item-grams">${item.grams}g</span>
|
|
||||||
</div>
|
|
||||||
<div class="planned-item-nutrition">
|
|
||||||
<span>${item.calories} cal</span>
|
|
||||||
<span>P: ${item.protein}g</span>
|
|
||||||
<span>F: ${item.fat}g</span>
|
|
||||||
<span>C: ${item.carbs}g</span>
|
|
||||||
</div>
|
|
||||||
<button class="delete-planned-item-btn" data-item-id="${item.id}">Delete</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const deleteBtn = card.querySelector('.delete-planned-item-btn');
|
|
||||||
if (deleteBtn) {
|
|
||||||
deleteBtn.addEventListener('click', () => deletePlannedItem(item.id!));
|
|
||||||
}
|
|
||||||
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deletePlannedItem(itemId: string) {
|
|
||||||
const confirm = await swal({
|
|
||||||
title: 'Delete Item',
|
|
||||||
text: 'Are you sure you want to delete this item from your meal plan?',
|
|
||||||
icon: 'warning',
|
|
||||||
buttons: {
|
|
||||||
cancel: true,
|
|
||||||
confirm: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirm) return;
|
|
||||||
|
|
||||||
showLoading();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await AppwriteDB.deletePlannedFoodItem(itemId);
|
|
||||||
if (appState.currentTemplate) {
|
|
||||||
await loadPlannedItemsForTemplate(appState.currentTemplate.id!);
|
|
||||||
}
|
|
||||||
hideLoading();
|
|
||||||
} catch (error) {
|
|
||||||
hideLoading();
|
|
||||||
console.error('Delete planned item error:', error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
swal('Oh no!', 'Failed to delete item: ' + error.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updatePlanTotals() {
|
|
||||||
const totals = appState.plannedItems.reduce((acc, item) => {
|
|
||||||
acc.calories += item.calories;
|
|
||||||
acc.protein += item.protein;
|
|
||||||
acc.fat += item.fat;
|
|
||||||
acc.carbs += item.carbs;
|
|
||||||
acc.fiber += item.fiber;
|
|
||||||
return acc;
|
|
||||||
}, { calories: 0, protein: 0, fat: 0, carbs: 0, fiber: 0 });
|
|
||||||
|
|
||||||
// Update totals display
|
|
||||||
const caloriesTotalEl = document.getElementById('planCaloriesTotal');
|
|
||||||
const proteinValueEl = document.getElementById('planProteinValue');
|
|
||||||
const fatValueEl = document.getElementById('planFatValue');
|
|
||||||
const carboValueEl = document.getElementById('planCarboValue');
|
|
||||||
const fiberValueEl = document.getElementById('planFiberValue');
|
|
||||||
|
|
||||||
if (caloriesTotalEl) caloriesTotalEl.textContent = totals.calories.toString();
|
|
||||||
if (proteinValueEl) proteinValueEl.textContent = Math.round(totals.protein * 10) / 10 + '';
|
|
||||||
if (fatValueEl) fatValueEl.textContent = Math.round(totals.fat * 10) / 10 + '';
|
|
||||||
if (carboValueEl) carboValueEl.textContent = Math.round(totals.carbs * 10) / 10 + '';
|
|
||||||
if (fiberValueEl) fiberValueEl.textContent = Math.round(totals.fiber * 10) / 10 + '';
|
|
||||||
|
|
||||||
// Update progress bars and goals
|
|
||||||
updatePlanProgressBars(totals);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updatePlanProgressBars(totals: { calories: number, protein: number, fat: number, carbs: number, fiber: number }) {
|
|
||||||
try {
|
|
||||||
const settings = await AppwriteDB.getUserSettings();
|
|
||||||
if (!settings || settings.length === 0) return;
|
|
||||||
|
|
||||||
const userSettings = settings[settings.length - 1];
|
|
||||||
|
|
||||||
// Update goal texts
|
|
||||||
const caloriesGoalEl = document.getElementById('planCaloriesGoalText');
|
|
||||||
const proteinGoalEl = document.getElementById('planProteinGoalText');
|
|
||||||
const fatGoalEl = document.getElementById('planFatGoalText');
|
|
||||||
const carboGoalEl = document.getElementById('planCarboGoalText');
|
|
||||||
const fiberGoalEl = document.getElementById('planFiberGoalText');
|
|
||||||
|
|
||||||
if (caloriesGoalEl) caloriesGoalEl.textContent = `of ${userSettings.caloriesGoal || 0}`;
|
|
||||||
if (proteinGoalEl) proteinGoalEl.textContent = `of ${userSettings.proteinGoal}`;
|
|
||||||
if (fatGoalEl) fatGoalEl.textContent = `of ${userSettings.fatGoal}`;
|
|
||||||
if (carboGoalEl) carboGoalEl.textContent = `of ${userSettings.carboGoal}`;
|
|
||||||
if (fiberGoalEl) fiberGoalEl.textContent = `of ${userSettings.fiberGoal}`;
|
|
||||||
|
|
||||||
// Update progress bars
|
|
||||||
updateProgressBar('planProteinProgress', totals.protein, userSettings.proteinGoal);
|
|
||||||
updateProgressBar('planFatProgress', totals.fat, userSettings.fatGoal);
|
|
||||||
updateProgressBar('planCarboProgress', totals.carbs, userSettings.carboGoal);
|
|
||||||
updateProgressBar('planFiberProgress', totals.fiber, userSettings.fiberGoal);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Update progress bars error:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProgressBar(elementId: string, current: number, goal: number) {
|
|
||||||
const progressBar = document.getElementById(elementId);
|
|
||||||
if (!progressBar) return;
|
|
||||||
|
|
||||||
const percentage = Math.min((current / goal) * 100, 100);
|
|
||||||
progressBar.style.width = `${percentage}%`;
|
|
||||||
|
|
||||||
// Color coding: green when within range, yellow when getting close, red when over
|
|
||||||
if (percentage < 80) {
|
|
||||||
progressBar.style.backgroundColor = '#4CAF50'; // green
|
|
||||||
} else if (percentage < 100) {
|
|
||||||
progressBar.style.backgroundColor = '#FFC107'; // yellow
|
|
||||||
} else {
|
|
||||||
progressBar.style.backgroundColor = '#f44336'; // red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quick add functions for food log
|
|
||||||
export async function displayQuickAddItems() {
|
|
||||||
const quickAddSection = document.getElementById('quickAddSection');
|
|
||||||
const quickAddContainer = document.getElementById('quickAddItemsContainer');
|
|
||||||
|
|
||||||
if (!quickAddSection || !quickAddContainer) return;
|
|
||||||
|
|
||||||
// Check if there's an active template
|
|
||||||
const activeTemplate = await AppwriteDB.getActiveMealPlanTemplate();
|
|
||||||
|
|
||||||
if (!activeTemplate) {
|
|
||||||
quickAddSection.classList.add('hidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load planned items
|
|
||||||
const plannedItems = await AppwriteDB.getPlannedItemsByTemplate(activeTemplate.$id);
|
|
||||||
|
|
||||||
if (plannedItems.length === 0) {
|
|
||||||
quickAddSection.classList.add('hidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show section and populate items
|
|
||||||
quickAddSection.classList.remove('hidden');
|
|
||||||
quickAddContainer.innerHTML = '';
|
|
||||||
|
|
||||||
// Group by meal type
|
|
||||||
const mealTypes: MealType[] = ['breakfast', 'lunch', 'dinner', 'snacks'];
|
|
||||||
const mealIcons = {
|
|
||||||
breakfast: '🌅',
|
|
||||||
lunch: '☀️',
|
|
||||||
dinner: '🌙',
|
|
||||||
snacks: '🍿'
|
|
||||||
};
|
|
||||||
|
|
||||||
mealTypes.forEach(mealType => {
|
|
||||||
const items = plannedItems.filter(item => item.mealType === mealType);
|
|
||||||
|
|
||||||
if (items.length > 0) {
|
|
||||||
// Add meal type header
|
|
||||||
const mealHeader = document.createElement('h3');
|
|
||||||
mealHeader.className = 'quick-add-meal-header';
|
|
||||||
mealHeader.textContent = `${mealIcons[mealType]} ${mealType.charAt(0).toUpperCase() + mealType.slice(1)}`;
|
|
||||||
quickAddContainer.appendChild(mealHeader);
|
|
||||||
|
|
||||||
// Add items
|
|
||||||
items.forEach(item => {
|
|
||||||
const quickAddItem = createQuickAddButton(item);
|
|
||||||
quickAddContainer.appendChild(quickAddItem);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createQuickAddButton(item: any): HTMLElement {
|
|
||||||
const button = document.createElement('button');
|
|
||||||
button.className = 'quick-add-btn';
|
|
||||||
button.innerHTML = `
|
|
||||||
<span class="quick-add-name">${getCleanName(item.name)}</span>
|
|
||||||
<span class="quick-add-details">${item.grams}g • ${item.calories} cal</span>
|
|
||||||
`;
|
|
||||||
button.addEventListener('click', () => quickAddToFoodLog(item));
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function quickAddToFoodLog(plannedItem: any) {
|
|
||||||
showLoading();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const now = new Date();
|
|
||||||
const time = now.toLocaleTimeString('en-US', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedDate = new Date(); // This should be the actual selected date from the app
|
|
||||||
const localDateTime = new Date(selectedDate.getTime() - (selectedDate.getTimezoneOffset() * 60000)).toISOString();
|
|
||||||
|
|
||||||
const foodEntry = {
|
|
||||||
name: plannedItem.name,
|
|
||||||
grams: plannedItem.grams,
|
|
||||||
calories: plannedItem.calories,
|
|
||||||
protein: plannedItem.protein,
|
|
||||||
fat: plannedItem.fat,
|
|
||||||
carbs: plannedItem.carbs,
|
|
||||||
fiber: plannedItem.fiber,
|
|
||||||
time: time,
|
|
||||||
date: localDateTime.split('T')[0],
|
|
||||||
alkaline: plannedItem.alkaline
|
|
||||||
};
|
|
||||||
|
|
||||||
await AppwriteDB.saveFoodEntry(foodEntry);
|
|
||||||
|
|
||||||
// Dispatch custom event to trigger food log reload
|
|
||||||
const event = new CustomEvent('foodAdded');
|
|
||||||
window.dispatchEvent(event);
|
|
||||||
|
|
||||||
hideLoading();
|
|
||||||
swal('Success!', `${getCleanName(plannedItem.name)} added to food log!`, 'success');
|
|
||||||
} catch (error) {
|
|
||||||
hideLoading();
|
|
||||||
console.error('Quick add to food log error:', error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
swal('Oh no!', 'Failed to add food: ' + error.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event listener setup
|
|
||||||
export function setupMealPlannerEventListeners() {
|
|
||||||
// Navigation buttons
|
|
||||||
document.getElementById('mealPlannerBtn')?.addEventListener('click', toggleMealPlannerView);
|
|
||||||
document.getElementById('mealPlannerBtnMobile')?.addEventListener('click', handleMobileMealPlannerClick);
|
|
||||||
|
|
||||||
// Template management
|
|
||||||
document.getElementById('newTemplateBtn')?.addEventListener('click', createNewTemplate);
|
|
||||||
document.getElementById('deleteTemplateBtn')?.addEventListener('click', deleteCurrentTemplate);
|
|
||||||
document.getElementById('templateSelector')?.addEventListener('change', (e) => {
|
|
||||||
const target = e.target as HTMLSelectElement;
|
|
||||||
handleTemplateChange(target.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add food buttons for each meal
|
|
||||||
const addMealButtons = document.querySelectorAll('.add-meal-item-btn');
|
|
||||||
addMealButtons.forEach(button => {
|
|
||||||
button.addEventListener('click', (e) => {
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
const mealType = target.getAttribute('data-meal-type') as MealType;
|
|
||||||
showAddFoodToPlan(mealType);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add food to plan form
|
|
||||||
document.getElementById('addFoodToPlanBtn')?.addEventListener('click', addFoodToPlan);
|
|
||||||
document.getElementById('cancelFoodToPlanBtn')?.addEventListener('click', hideAddFoodToPlan);
|
|
||||||
|
|
||||||
// Setup food search
|
|
||||||
setupPlanFoodSearch();
|
|
||||||
}
|
|
||||||
+5
-13
@@ -1,4 +1,4 @@
|
|||||||
import { DailyTotalCalories, FoodItem, SharedDay, MealPlanTemplate, PlannedFoodItem } from "./types";
|
import { DailyTotalCalories, FoodItem, SharedDay } from "./types";
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
currentHighlightIndex: number;
|
currentHighlightIndex: number;
|
||||||
@@ -7,12 +7,8 @@ interface AppState {
|
|||||||
calendarMonthlyCalories: DailyTotalCalories[];
|
calendarMonthlyCalories: DailyTotalCalories[];
|
||||||
isSharedView: boolean;
|
isSharedView: boolean;
|
||||||
sharedData: SharedDay | null;
|
sharedData: SharedDay | null;
|
||||||
// Meal Planner state
|
userTimezone: string;
|
||||||
currentTemplate: MealPlanTemplate | null;
|
todayDateString: string;
|
||||||
plannedItems: PlannedFoodItem[];
|
|
||||||
planSearchResults: FoodItem[];
|
|
||||||
planCurrentHighlightIndex: number;
|
|
||||||
planSearchTimeout: number | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appState: AppState = {
|
export const appState: AppState = {
|
||||||
@@ -22,10 +18,6 @@ export const appState: AppState = {
|
|||||||
calendarMonthlyCalories: [],
|
calendarMonthlyCalories: [],
|
||||||
isSharedView: false,
|
isSharedView: false,
|
||||||
sharedData: null,
|
sharedData: null,
|
||||||
// Meal Planner state
|
userTimezone: '',
|
||||||
currentTemplate: null,
|
todayDateString: ''
|
||||||
plannedItems: [],
|
|
||||||
planSearchResults: [],
|
|
||||||
planCurrentHighlightIndex: -1,
|
|
||||||
planSearchTimeout: null
|
|
||||||
};
|
};
|
||||||
|
|||||||
+23
-26
@@ -2,7 +2,8 @@ type FoodCategory = 'fats' | 'proteins' | 'carbs' | 'leaves' | 'fruits' | 'low c
|
|||||||
|
|
||||||
export type FoodItem = {
|
export type FoodItem = {
|
||||||
name: string;
|
name: string;
|
||||||
info: { calories: number, protein: number, fat: number, carbs: number, fiber: number, category: FoodCategory, alkaline: boolean }
|
nameEn: string;
|
||||||
|
info: { calories: number, protein: number, fat: number, carbs: number, fiber: number, category: FoodCategory, alkaline: boolean, bestFor: Array<String> }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FoodStorage = {
|
export type FoodStorage = {
|
||||||
@@ -22,10 +23,17 @@ export type FoodStorage = {
|
|||||||
export type UserSettings = {
|
export type UserSettings = {
|
||||||
id?: string;
|
id?: string;
|
||||||
caloriesGoal?: number;
|
caloriesGoal?: number;
|
||||||
proteinGoal: number;
|
proteinGoal?: number;
|
||||||
fatGoal: number;
|
fatGoal?: number;
|
||||||
carboGoal: number;
|
carboGoal?: number;
|
||||||
fiberGoal: number;
|
fiberGoal?: number;
|
||||||
|
bodyWeight?: number;
|
||||||
|
height?: number;
|
||||||
|
bmi?: number;
|
||||||
|
bmiResult?: string;
|
||||||
|
goalName?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
timezone?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DailyTotalCalories = {
|
export type DailyTotalCalories = {
|
||||||
@@ -44,26 +52,15 @@ export type SharedDay = {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MealType = 'breakfast' | 'lunch' | 'dinner' | 'snacks';
|
export type MealPeriod = 'pre-workout' | 'breakfast' | 'second-breakfast' | 'lunch' | 'snacks' | 'dinner' | 'night-snacks';
|
||||||
|
|
||||||
export type MealPlanTemplate = {
|
export type MealGroup = {
|
||||||
id?: string;
|
period: MealPeriod;
|
||||||
name: string;
|
label: string;
|
||||||
userId: string;
|
entries: FoodStorage[];
|
||||||
isActive: boolean;
|
totalCalories: number;
|
||||||
createdAt: string;
|
totalProtein: number;
|
||||||
};
|
totalFat: number;
|
||||||
|
totalCarbs: number;
|
||||||
export type PlannedFoodItem = {
|
isExpanded: boolean;
|
||||||
id?: string;
|
|
||||||
templateId: string;
|
|
||||||
mealType: MealType;
|
|
||||||
name: string;
|
|
||||||
grams: number;
|
|
||||||
calories: number;
|
|
||||||
protein: number;
|
|
||||||
fat: number;
|
|
||||||
carbs: number;
|
|
||||||
fiber: number;
|
|
||||||
alkaline: boolean | null;
|
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+3
@@ -1,3 +1,6 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="vite-plugin-pwa/client" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_APPWRITE_PROJECT_ID: string;
|
readonly VITE_APPWRITE_PROJECT_ID: string;
|
||||||
readonly VITE_APPWRITE_ENDPOINT: string;
|
readonly VITE_APPWRITE_ENDPOINT: string;
|
||||||
@@ -349,6 +349,133 @@ h1 {
|
|||||||
background: #1dd1a1;
|
background: #1dd1a1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Meal Group Styles */
|
||||||
|
.meal-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-group-header {
|
||||||
|
background: linear-gradient(135deg, #3A4D6A 0%, #4A2E5A 100%);
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-group-header:hover {
|
||||||
|
background: linear-gradient(135deg, #2E3F58 0%, #3E2448 100%);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-group-header.collapsed {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-group-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-group-label {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-group-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-summary-item {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-summary-divider {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-group-toggle {
|
||||||
|
font-size: 18px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-group-header .meal-group-toggle.rotated {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-group-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-left: 8px;
|
||||||
|
border-left: 3px solid rgba(107, 141, 214, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
max-height: 5000px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-group-items.collapsed {
|
||||||
|
max-height: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
opacity: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust food card styles within meal groups */
|
||||||
|
.meal-group-items .food-card {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.meal-group-header {
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-group-label {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-group-summary {
|
||||||
|
font-size: 13px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-summary-item {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stack summary items on very small screens */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.meal-group-summary {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-summary-divider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 60px 20px;
|
padding: 60px 20px;
|
||||||
@@ -461,6 +588,36 @@ label {
|
|||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-weight: 400;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-checkbox {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin-right: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: #6B8DD6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label span {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #b0b0b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label:hover span {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
.add-btn {
|
.add-btn {
|
||||||
padding: 17px 25px;
|
padding: 17px 25px;
|
||||||
background: linear-gradient(135deg, #4CAF50, #45a049);
|
background: linear-gradient(135deg, #4CAF50, #45a049);
|
||||||
@@ -490,6 +647,7 @@ label {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
display: none; /* Hidden by default, shown when editing */
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel-btn:hover {
|
.cancel-btn:hover {
|
||||||
@@ -901,3 +1059,205 @@ label {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
cursor: not-allowed !important;
|
cursor: not-allowed !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* AI Nutrition Info Card */
|
||||||
|
#ai-nutrition-card {
|
||||||
|
margin-top: 20px;
|
||||||
|
animation: fadeIn 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-nutrition-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: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-nutrition-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-badge {
|
||||||
|
background: linear-gradient(135deg, #6B8DD6 0%, #9B7BC6 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading State */
|
||||||
|
.ai-loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin: 0 auto 15px;
|
||||||
|
border: 3px solid rgba(107, 141, 214, 0.3);
|
||||||
|
border-top: 3px solid #6B8DD6;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-loading p {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error State */
|
||||||
|
.ai-error {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: rgba(255, 107, 107, 0.9);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
|
.ai-nutrition-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-nutrition-section {
|
||||||
|
background: rgba(30, 30, 30, 0.4);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-section-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6B8DD6;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid rgba(107, 141, 214, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-list li {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding-left: 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-list li:before {
|
||||||
|
content: "•";
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
color: #6B8DD6;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-notes {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.ai-nutrition-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-nutrition-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-list li {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-notes {
|
||||||
|
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
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
+68
-2
@@ -1,6 +1,72 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
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: [
|
||||||
|
VitePWA({
|
||||||
|
registerType: 'prompt',
|
||||||
|
includeAssets: ['assets/*.png', 'assets/*.ico', 'assets/*.xml'],
|
||||||
|
|
||||||
|
manifest: {
|
||||||
|
name: 'Food Tracker - Daily Calorie Tracker',
|
||||||
|
short_name: 'Food Tracker',
|
||||||
|
description: 'Track your nutrition, transform your health. Monitor calories, protein, fat, carbs, and fiber for every meal.',
|
||||||
|
theme_color: '#121212',
|
||||||
|
background_color: '#121212',
|
||||||
|
display: 'standalone', // CRITICAL: Enables fullscreen
|
||||||
|
orientation: 'portrait',
|
||||||
|
scope: '/',
|
||||||
|
start_url: '/',
|
||||||
|
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/assets/apple-icon-180x180.png',
|
||||||
|
sizes: '180x180',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/assets/android-icon-192x192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any maskable'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/assets/icon-512x512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any maskable'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
workbox: {
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
|
||||||
|
handler: 'CacheFirst',
|
||||||
|
options: {
|
||||||
|
cacheName: 'google-fonts-cache',
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 10,
|
||||||
|
maxAgeSeconds: 60 * 60 * 24 * 365
|
||||||
|
},
|
||||||
|
cacheableResponse: {
|
||||||
|
statuses: [0, 200]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
cleanupOutdatedCaches: true,
|
||||||
|
globPatterns: ['**/*.{js,css,html,ico,png,svg}']
|
||||||
|
},
|
||||||
|
|
||||||
|
devOptions: {
|
||||||
|
enabled: false, // Disable in dev to avoid warnings; test with preview mode
|
||||||
|
type: 'module'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user