first commit

This commit is contained in:
Jp
2026-01-30 15:03:43 +08:00
commit 656a510c73
32 changed files with 3721 additions and 0 deletions

397
QUICK_START.md Normal file
View File

@@ -0,0 +1,397 @@
# 🚀 Quick Start Guide - Calorie Tracker App
## What You Have
A complete **Flask web application** for tracking calories, macros, water intake, and weight with focus on Filipino foods!
### ✅ Features Implemented
1. **User Authentication** - Register/Login system
2. **Dashboard** - Beautiful overview with cards, charts, and progress bars
3. **Macro Tracking** - Calories, Protein, Carbs, Fat
4. **Filipino Food Database** - 25+ pre-loaded foods (Adobo, Sinigang, Tapsilog, etc.)
5. **Food Search** - Search Filipino and international foods
6. **API Integration** - API Ninjas for 1M+ foods
7. **Water Tracking** - Quick-add buttons (250ml, 500ml)
8. **Weight Tracking** - Daily logs with trends
9. **Smart Suggestions** - AI recommendations based on remaining macros
10. **Charts & Trends** - 7-day calorie and weight trends
11. **Meal Logging** - Add meals with multiple foods
12. **Responsive Design** - Works on mobile and desktop
13. **Filipino Flag Colors** - Red/Blue theme
---
## 🏃 How to Run It
### Option 1: Quick Start (5 minutes)
```bash
# 1. Open terminal in the calorie_tracker_app folder
cd calorie_tracker_app
# 2. Create virtual environment
python -m venv venv
# 3. Activate it
# On Mac/Linux:
source venv/bin/activate
# On Windows:
venv\Scripts\activate
# 4. Install packages
pip install -r requirements.txt
# 5. Run the app
# Windows (Double click run_app.bat OR run):
venv\Scripts\python app.py
# Mac/Linux:
./venv/bin/python app.py
```
## ❓ Troubleshooting
### "pip: command not found"
If you see this error, it means the virtual environment is not activated.
1. Make sure you are in the `calorie_tracker_app` folder.
2. Run `venv\Scripts\activate` (Windows) or `source venv/bin/activate` (Mac/Linux).
3. Try `python -m pip install -r requirements.txt` instead.
### "ModuleNotFoundError"
If Python can't find modules (Flask, etc.), make sure you activated the virtual environment before running the app.
The app will:
- Create the database automatically
- Start on http://localhost:5001
### Option 2: With API Key (Recommended)
```bash
# Do steps 1-4 from above, then:
# 5. Get free API key
# Go to: https://api-ninjas.com/api/nutrition
# Sign up (free)
# Copy your API key
# 6. Create .env file
cp .env.example .env
# 7. Edit .env and add your key
# API_NINJAS_KEY=your_key_here
# 8. Seed Filipino foods
# Windows (Double click seed_db.bat OR run):
venv\Scripts\python seed_data.py
# Mac/Linux:
./venv/bin/python seed_data.py
# 9. Run the app
python app.py
```
---
## 📱 Using the App
### First Time
1. Go to http://localhost:5000
2. Click "Register"
3. Create account (username + password)
4. Login
5. Go to "Goals" page
6. Enter:
- Age: 25
- Gender: Male/Female
- Height: 170 cm
- Weight: 70 kg
- Activity: Moderate
- Goal: Recomp (for weight loss + muscle gain)
- Target weight: 65 kg
7. Click Save
**The app automatically calculates:**
- Your BMR (Basal Metabolic Rate)
- Your TDEE (Total Daily Energy Expenditure)
- Calorie target (TDEE - 500 for weight loss)
- Macro targets (Protein: 154g, Carbs: 175g, Fat: 63g)
### Daily Use
#### Morning Routine
1. **Log Weight** - Enter on dashboard
2. **Log Water** - Click +250ml or +500ml
#### Adding Meals
1. Click "Add Meal"
2. Select breakfast/lunch/dinner/snack
3. Search for food (e.g., "adobo", "kanin", "sinigang")
4. Click food to add
5. Adjust servings (0.5, 1, 1.5, 2, etc.)
6. See real-time nutrition summary
7. Click "Save Meal"
#### Dashboard Shows
- Calories consumed vs target
- Macros (protein/carbs/fat) with progress bars
- Water intake (glass icons)
- Today's weight
- All meals logged
- Charts showing trends
---
## 🍛 Filipino Foods Available
### Search these (English or Tagalog):
**Breakfast**
- Tapsilog (beef tapa + rice + egg)
- Longsilog (longganisa + rice + egg)
- Tocilog (tocino + rice + egg)
**Main Dishes**
- Chicken Adobo / Adobong Manok
- Pork Sinigang / Sinigang na Baboy
- Chicken Tinola / Tinolang Manok
- Bicol Express
- Sisig
- Menudo
- Kare-Kare
- Lechon Kawali
**Soups**
- Bulalo
- Nilaga
**Vegetables**
- Pinakbet
- Laing
- Ginisang Monggo
**Snacks**
- Pandesal
- Turon
- Bibingka
- Puto
- Lumpia
**Rice**
- White Rice / Kanin
- Fried Rice / Sinangag
---
## 💡 Tips for Success
### Body Recomposition Strategy
**Your goals: Weight loss + Muscle gain**
1. **Protein Priority**
- Eat 2.2g protein per kg body weight
- Example: 70kg = 154g protein daily
- Spread across 4-5 meals
2. **Calorie Deficit**
- 300-500 calories below TDEE
- App calculates this automatically
- Lose 0.5-1 kg per week
3. **Track Daily**
- Weight: Same time every morning
- Food: Log everything (even snacks)
- Water: Aim for 2+ liters
4. **Weekly Check-in**
- Look at 7-day average weight
- Adjust calories if needed
- Progress page shows trends
### Food Search Tips
1. **Try Filipino first** (no API needed):
- "adobo", "sinigang", "sisig"
- "kanin", "pandesal", "lumpia"
2. **Search Tagalog names**:
- Works for Filipino foods
- "Adobong Manok", "Sinigang na Baboy"
3. **International foods** (needs API):
- "chicken breast", "brown rice"
- "protein shake", "banana"
4. **Be specific**:
- Instead of "lunch", search "chicken rice"
- Instead of "food", search actual dish
### Maximize Free API Tier
API Ninjas free tier: 50 requests/day
**The app helps you save API calls:**
- Filipino foods = 0 API calls (local database)
- Cached foods = 0 API calls (saved from previous searches)
- Only new international foods use API
**Tips:**
- Use Filipino foods when possible
- Foods are cached after first search
- Mark favorites to find them quickly
---
## 📊 Understanding Your Dashboard
### Calorie Card
- **Green progress bar** = On track
- **Red progress bar** = Over target
- Shows remaining calories
### Protein Card
- Goal: Hit target every day
- Important for muscle building
- Prevents muscle loss during deficit
### Carbs & Fat Cards
- Carbs = Energy for workouts
- Fat = Hormones & satiety
- Both important, don't eliminate
### Water Tracking
- 8 glasses = 2 liters (goal)
- Blue drops fill up
- Quick add: +250ml, +500ml buttons
### Weight Card
- Log daily for best results
- Shows change from yesterday
- Green ⬇ = weight down (good for weight loss)
- Red ⬆ = weight up
### Smart Suggestions
Based on what you've eaten:
- Need protein? → Suggests Tinola, Grilled Fish
- Need carbs? → Suggests Rice, Pandesal
- Need fat? → Suggests Sisig, Bicol Express
- Balanced? → No suggestions (you're good!)
### Charts
- **Calorie Trend**: Shows if you're consistent
- **Weight Trend**: Shows if you're losing weight
- Both use 7-day data
---
## 🔧 Troubleshooting
### "No module named flask"
```bash
pip install -r requirements.txt
```
### "Database is locked"
```bash
# Stop the app, delete database, restart
rm calorie_tracker.db
python app.py
python seed_data.py
```
### Filipino foods not showing
```bash
python seed_data.py
```
### API not working
- Check .env file has API key
- App still works without API (Filipino foods + manual entry)
### Port 5000 in use
Edit app.py, last line:
```python
app.run(debug=True, host='0.0.0.0', port=5001) # Change to 5001
```
---
## 📁 Project Files
```
calorie_tracker_app/
├── app.py # Main application (Flask routes)
├── models.py # Database tables
├── api_client.py # API integration
├── utils.py # Helper functions (BMR, TDEE calculations)
├── seed_data.py # Filipino foods data
├── config.py # Settings
├── requirements.txt # Python packages
├── .env.example # API key template
├── README.md # Full documentation
└── templates/ # HTML pages
├── dashboard.html # Main page ✅
├── add_meal.html # Add meals ✅
├── login.html # Login page ✅
├── register.html # Register page ✅
├── foods.html # Food database (basic)
├── goals.html # Goals/settings (basic)
├── progress.html # Charts (placeholder)
└── meal_planner.html # Future feature
```
---
## 🎯 What's Working Now
✅ Full user system (register/login)
✅ Dashboard with all stats
✅ Add meals with multiple foods
✅ Food search (Filipino + API)
✅ Water tracking
✅ Weight tracking
✅ Macro calculations
✅ Smart suggestions
✅ Charts and trends
✅ Responsive design
## 🚧 To Be Enhanced
These are basic but functional:
- Foods page (shows foods, can be enhanced with filters)
- Goals page (works but could be prettier)
- Progress page (placeholder, can add more charts)
- Meal planner (not implemented yet)
---
## 🎉 You're Ready!
### Next Steps:
1. **Run the app** (see instructions above)
2. **Register** an account
3. **Set your goals** (Goals page)
4. **Add your first meal** (try searching "tapsilog"!)
5. **Track for a week** to see trends
### Pro Tip:
Log your meals right after eating. It's easier to remember and takes only 30 seconds!
---
## 📞 Need Help?
1. Check README.md for detailed docs
2. Check this Quick Start guide
3. Look at the code comments
4. All functions are documented
---
**Enjoy tracking your journey to better health! 💪🇵🇭**
Made with ❤️ for Filipino food lovers!

50
README_DOCKER.md Normal file
View File

@@ -0,0 +1,50 @@
# 🐳 Self-Hosting with Docker (CasaOS)
You can easily host this Calorie Tracker on your home server using Docker or CasaOS.
## 🛠️ Quick Setup for CasaOS
1. **Download the files**:
* Download `docker-compose.yml`
* Download the `calorie_tracker_app` folder
2. **Import to CasaOS**:
* Open CasaOS Dashboard
* Click **+** (Install a customized app)
* Click **Import** (top right)
* Select the `docker-compose.yml` file
* (Optional) Setup the `API_NINJAS_KEY` in the Settings
3. **Install**:
* Click **Install**
* The app will start at `http://your-server-ip:5001`
## 🐳 Manual Docker Compose
1. Navigate to the project directory:
```bash
cd calorie_tracker
```
2. Run with Docker Compose:
```bash
docker-compose up -d --build
```
3. Access the app:
* Open `http://localhost:5001`
## 💾 Data Persistence
* Database is stored in `./data/calorie_tracker.db` on your host machine.
* This ensures your data survives container restarts/updates.
## ⚙️ Environment Variables
You can configure these in `docker-compose.yml` or CasaOS settings:
| Variable | Description | Default |
| :--- | :--- | :--- |
| `API_NINJAS_KEY` | Key for nutrition API | (Empty) |
| `SECRET_KEY` | Flask secret key | `change-this...` |
| `DATABASE_URL` | Database path | `sqlite:////app/data/calorie_tracker.db` |

View File

@@ -0,0 +1,8 @@
venv/
__pycache__/
instance/
.env
.git
.gitignore
*.db
*.pyc

View File

@@ -0,0 +1,7 @@
# API Ninjas API Key
# Get your free API key from: https://api-ninjas.com/api
API_NINJAS_KEY=your_api_key_here
# Flask Configuration
SECRET_KEY=your_secret_key_here_change_in_production
FLASK_ENV=development

View File

@@ -0,0 +1,24 @@
FROM python:3.10-slim
WORKDIR /app
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV FLASK_APP=app.py
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create data directory
RUN mkdir -p /app/data
# Expose port
EXPOSE 5001
# Run commands to init db, seed data, and start server
CMD ["sh", "-c", "flask init-db && flask seed-db && gunicorn -w 4 -b 0.0.0.0:5001 app:app"]

View File

@@ -0,0 +1,384 @@
# Calorie Tracker - Filipino Food Edition 🍽️
A web application for tracking calories, macros, and water intake with special focus on Filipino foods. Perfect for weight loss and muscle gain goals!
## Features
**Macro Tracking**: Track calories, protein, carbs, and fat
**Filipino Food Database**: Pre-loaded with 25+ common Filipino foods
**Water Intake Tracking**: Quick-add water logging
**Weight Tracking**: Daily weight logs with trend analysis
**Meal Planning**: Plan meals ahead (coming soon!)
**Smart Suggestions**: Get food recommendations based on remaining macros
**API Integration**: Search international foods via API Ninjas
**Beautiful UI**: Red/blue color scheme inspired by Filipino flag
**Charts & Trends**: Visualize your progress with Chart.js
## Filipino Foods Included
- **Kanin (Rice)**: White rice, Sinangag
- **Ulam (Main Dishes)**: Adobo, Sinigang, Sisig, Bicol Express, Kare-kare, Menudo, Lechon Kawali
- **Sabaw (Soups)**: Tinola, Nilaga, Bulalo
- **Gulay (Vegetables)**: Pinakbet, Laing, Ginisang Monggo
- **Almusal (Breakfast)**: Tapsilog, Longsilog, Tocilog
- **Meryenda (Snacks)**: Pandesal, Turon, Bibingka, Puto, Lumpia
## 🐳 Docker Support
Want to self-host this on CasaOS or your own server?
👉 **[Read the Docker Guide](../README_DOCKER.md)**
## Tech Stack
- **Backend**: Flask (Python)
- **Database**: SQLite
- **Frontend**: HTML, Tailwind CSS, Vanilla JavaScript
- **Charts**: Chart.js
- **API**: API Ninjas Nutrition API
## Installation
### Prerequisites
- Python 3.10 or higher
- pip (Python package manager)
### Step 1: Clone or Download
```bash
cd calorie_tracker_app
```
### Step 2: Create Virtual Environment
```bash
python -m venv venv
# On Windows:
venv\Scripts\activate
# On Mac/Linux:
source venv/bin/activate
```
### Step 3: Install Dependencies
```bash
pip install -r requirements.txt
```
### Step 4: Get API Key (Optional but Recommended)
1. Go to [API Ninjas](https://api-ninjas.com/api/nutrition)
2. Sign up for a free account
3. Get your API key (50 requests/day free tier)
4. Create `.env` file:
```bash
cp .env.example .env
```
Edit `.env` and add your API key:
```
API_NINJAS_KEY=your_api_key_here
SECRET_KEY=your_secret_key_here
```
### Step 5: Initialize Database
Run the app for the first time:
```bash
# Windows (Double click run_app.bat OR run):
venv\Scripts\python app.py
# Mac/Linux:
./venv/bin/python app.py
```
This will create the database. Then in another terminal, seed Filipino foods:
```bash
# Windows (Double click seed_db.bat OR run):
venv\Scripts\python seed_data.py
# Mac/Linux:
./venv/bin/python seed_data.py
```
### Step 6: Run the Application
```bash
# Windows (Double click run_app.bat OR run):
venv\Scripts\python app.py
# Mac/Linux:
./venv/bin/python app.py
```
Open your browser and go to: `http://localhost:5001`
## Usage
### First Time Setup
1. **Register**: Create an account at `/register`
2. **Login**: Sign in with your credentials
3. **Set Goals**: Go to Goals page and enter:
- Age, gender, height, weight
- Activity level
- Goal type (weight loss, muscle gain, or recomp)
- Target weight
The app will automatically calculate your:
- BMR (Basal Metabolic Rate)
- TDEE (Total Daily Energy Expenditure)
- Calorie target
- Macro targets
### Daily Tracking
#### Add a Meal
1. Click "Add Meal" button
2. Select date, meal type, and time
3. Search for foods (Filipino or international)
4. Add foods and adjust servings
5. See real-time nutrition summary
6. Save meal
#### Log Water
- Quick buttons on dashboard: +250ml, +500ml
- Or use custom amount
#### Log Weight
- Enter weight on dashboard
- Track daily to see trends
### Food Search
The app searches in this order:
1. **Filipino foods** (local database) - fastest
2. **Cached foods** (previously searched)
3. **API Ninjas** (if API key configured)
4. **Manual entry** (if food not found)
Search supports both English and Tagalog:
- "Adobo" or "Chicken Adobo"
- "Kanin" or "Rice"
- "Sinigang" works
### Understanding the Dashboard
#### Top Cards
- **Calories**: Shows consumed vs target with progress bar
- **Protein**: Your protein intake vs target
- **Carbs & Fat**: Dual progress bars
- **Water & Weight**: Quick water logging + today's weight
#### Macro Distribution
- Pie chart showing protein/carbs/fat percentages
- Ideal for recomp: 30-35% protein, 40-45% carbs, 20-25% fat
#### Smart Suggestions
Based on remaining macros, suggests:
- High protein foods if protein is low
- Carb sources if carbs are low
- Balanced meals if everything is balanced
#### Today's Meals
- Shows all logged meals
- Color-coded by meal type
- Displays nutrition breakdown
#### Weekly Trends
- 7-day calorie trend vs target
- 7-day weight trend
## Database Schema
### Tables
- `users` - User accounts
- `food_items` - All foods (Filipino + API)
- `meals` - Meal entries
- `meal_foods` - Foods in each meal
- `water_logs` - Water intake
- `weight_logs` - Weight tracking
- `meal_plans` - Future meal planning
- `user_goals` - Macro targets
- `daily_summary` - Daily totals
- `api_cache` - API response caching
## API Rate Limits
**Free Tier (API Ninjas)**:
- 50 requests per day
- App caches all results
- Once cached, no API calls needed
**Tips to conserve API calls**:
- Use Filipino foods when possible (no API calls)
- Search once, use favorites
- API searches are cached for 30 days
## Customization
### Adding Custom Foods
1. Go to Foods page
2. Click "Add Custom Food"
3. Enter nutrition information
4. Save
### Adjusting Targets
Go to Goals page to adjust:
- Daily calorie target
- Macro targets (protein/carbs/fat)
- Water intake goal
- Weight goal
### Meal Templates (Coming Soon!)
Save common meal combinations:
- "My typical breakfast"
- "Post-workout meal"
- "Quick lunch"
## Tips for Body Recomposition
### Protein Priority
- 2.0-2.4g per kg body weight
- Example: 70kg person = 140-168g daily
- Spread across all meals
### Carb Timing
- Higher carbs on training days
- Lower carbs on rest days
### Calorie Cycling
- Training days: Maintenance (+100 cal)
- Rest days: Deficit (-300 to -500 cal)
### Track Weight Daily
- Weigh same time each day (morning, after bathroom)
- Look at weekly average, not daily fluctuations
- Aim for 0.5-1% body weight loss per week
## Troubleshooting
### Database Locked Error
```bash
# Stop the app, then:
rm calorie_tracker.db
python app.py
python seed_data.py
```
### API Not Working
- Check if API key is in `.env` file
- Verify API key is valid
- App works without API (Filipino foods + manual entry)
### Foods Not Showing
```bash
python seed_data.py
```
### Port Already in Use
```bash
# Change port in app.py:
app.run(debug=True, host='0.0.0.0', port=5001)
```
## Future Enhancements
- [ ] Meal planner with calendar view
- [ ] Barcode scanning (Open Food Facts API)
- [ ] Recipe builder
- [ ] Meal templates
- [ ] Export to CSV/PDF
- [ ] Photo food logging
- [ ] Workout integration
- [ ] Body measurements tracking
- [ ] Progress photos
- [ ] Mobile app version
## Project Structure
```
calorie_tracker_app/
├── app.py # Main Flask application
├── models.py # Database models
├── config.py # Configuration
├── api_client.py # API integration
├── utils.py # Helper functions
├── seed_data.py # Filipino foods data
├── requirements.txt # Python dependencies
├── .env.example # Environment variables template
├── templates/ # HTML templates
│ ├── base.html
│ ├── dashboard.html
│ ├── add_meal.html
│ ├── login.html
│ ├── register.html
│ ├── foods.html
│ ├── meal_planner.html
│ ├── progress.html
│ └── goals.html
└── static/ # CSS, JS, images
├── css/
├── js/
└── images/
```
## Contributing
This is a personal project, but suggestions are welcome!
## License
MIT License - Feel free to use and modify for your own needs.
## Credits
- **API Ninjas** for nutrition data
- **Tailwind CSS** for styling
- **Chart.js** for visualizations
- **Filipino food data** compiled from various nutrition sources
## Support
For issues or questions:
1. Check this README
2. Check the troubleshooting section
3. Review the code comments
---
**Made with ❤️ for Filipino food lovers and fitness enthusiasts!**
## Quick Start Summary
```bash
# 1. Setup
python -m venv venv
source venv/bin/activate # or venv\Scripts\activate on Windows
pip install -r requirements.txt
# 2. Configure (optional)
cp .env.example .env
# Add your API key to .env
# 3. Initialize
python app.py # Creates database
python seed_data.py # Adds Filipino foods
# 4. Run
python app.py
# 5. Open browser
# Go to http://localhost:5000
```
Enjoy tracking your nutrition! 🎉

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,209 @@
import requests
import json
from models import db, APICache, FoodItem
from datetime import datetime, timedelta
class NutritionAPI:
"""API client for nutrition data with caching"""
def __init__(self, api_key):
self.api_key = api_key
self.base_url = "https://api.api-ninjas.com/v1/nutrition"
self.headers = {'X-Api-Key': api_key}
self.cache_duration_days = 30
def search_food(self, query):
"""
Search for food nutrition data
Returns list of food items with nutrition info
"""
# Check cache first
cached = self._get_from_cache(query)
if cached:
return cached
# Make API request
try:
response = requests.get(
self.base_url,
headers=self.headers,
params={'query': query},
timeout=10
)
if response.status_code == 200:
data = response.json()
# Cache the response
self._save_to_cache(query, 'api_ninjas', data)
# Parse and return standardized format
return self._parse_api_response(data)
else:
print(f"API Error: {response.status_code}")
return []
except requests.exceptions.RequestException as e:
print(f"API Request failed: {e}")
return []
def _get_from_cache(self, query):
"""Check if query exists in cache and is not expired"""
cache_entry = APICache.query.filter_by(query=query.lower()).first()
if cache_entry:
# Check if cache is still valid (30 days)
age = datetime.utcnow() - cache_entry.cached_at
if age.days < self.cache_duration_days:
data = json.loads(cache_entry.response_json)
return self._parse_api_response(data)
return None
def _save_to_cache(self, query, source, data):
"""Save API response to cache"""
try:
cache_entry = APICache.query.filter_by(query=query.lower()).first()
if cache_entry:
# Update existing cache
cache_entry.response_json = json.dumps(data)
cache_entry.cached_at = datetime.utcnow()
else:
# Create new cache entry
cache_entry = APICache(
query=query.lower(),
api_source=source,
response_json=json.dumps(data),
cached_at=datetime.utcnow()
)
db.session.add(cache_entry)
db.session.commit()
except Exception as e:
print(f"Cache save failed: {e}")
db.session.rollback()
def _parse_api_response(self, data):
"""Parse API response into standardized format"""
foods = []
for item in data:
food = {
'name': item.get('name', '').title(),
'calories': item.get('calories', 0),
'protein_g': item.get('protein_g', 0),
'carbs_g': item.get('carbohydrates_total_g', 0),
'fat_g': item.get('fat_total_g', 0),
'fiber_g': item.get('fiber_g', 0),
'sugar_g': item.get('sugar_g', 0),
'sodium_mg': item.get('sodium_mg', 0),
'serving_size_g': item.get('serving_size_g', 100),
'source': 'api_ninjas'
}
foods.append(food)
return foods
def save_food_to_db(self, food_data):
"""Save a food item to the database"""
try:
# Check if food already exists
existing = FoodItem.query.filter_by(
name=food_data['name'],
source=food_data.get('source', 'api')
).first()
if existing:
return existing
# Create new food item
food = FoodItem(
name=food_data['name'],
calories=food_data['calories'],
protein_g=food_data.get('protein_g', 0),
carbs_g=food_data.get('carbs_g', 0),
fat_g=food_data.get('fat_g', 0),
fiber_g=food_data.get('fiber_g', 0),
sugar_g=food_data.get('sugar_g', 0),
sodium_mg=food_data.get('sodium_mg', 0),
serving_size_g=food_data.get('serving_size_g', 100),
serving_description=food_data.get('serving_description', '1 serving'),
source=food_data.get('source', 'api'),
api_data=json.dumps(food_data)
)
db.session.add(food)
db.session.commit()
return food
except Exception as e:
print(f"Error saving food to DB: {e}")
db.session.rollback()
return None
def search_all_sources(query, api_client):
"""
Search for food in all available sources
Priority: Local DB (Filipino) -> Local DB (cached) -> API
"""
results = []
# 1. Search Filipino foods first
filipino_foods = FoodItem.query.filter(
FoodItem.is_filipino == True,
db.or_(
FoodItem.name.ilike(f'%{query}%'),
FoodItem.name_tagalog.ilike(f'%{query}%')
)
).limit(5).all()
for food in filipino_foods:
results.append({
'id': food.id,
'name': food.name,
'name_tagalog': food.name_tagalog,
'calories': food.calories,
'protein_g': food.protein_g,
'carbs_g': food.carbs_g,
'fat_g': food.fat_g,
'serving_description': food.serving_description,
'source': 'filipino',
'category': food.category
})
# 2. Search other local foods
other_foods = FoodItem.query.filter(
FoodItem.is_filipino == False,
FoodItem.name.ilike(f'%{query}%')
).limit(5).all()
for food in other_foods:
results.append({
'id': food.id,
'name': food.name,
'calories': food.calories,
'protein_g': food.protein_g,
'carbs_g': food.carbs_g,
'fat_g': food.fat_g,
'serving_description': food.serving_description,
'source': food.source
})
# 3. If not enough results, search API
if len(results) < 3 and api_client.api_key:
api_results = api_client.search_food(query)
for food_data in api_results[:5]:
results.append({
'name': food_data['name'],
'calories': food_data['calories'],
'protein_g': food_data['protein_g'],
'carbs_g': food_data['carbs_g'],
'fat_g': food_data['fat_g'],
'serving_size_g': food_data['serving_size_g'],
'source': 'api',
'api_data': food_data
})
return results

494
calorie_tracker_app/app.py Normal file
View File

@@ -0,0 +1,494 @@
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import date, datetime, timedelta
import os
from config import Config
from models import db, User, FoodItem, Meal, MealFood, WaterLog, WeightLog, MealPlan, PlannedFood, UserGoal
from api_client import NutritionAPI, search_all_sources
from utils import (
calculate_daily_totals, calculate_water_total, get_weight_trend,
get_calorie_trend, update_daily_summary, calculate_bmr, calculate_tdee,
calculate_macro_targets, get_macro_percentages, suggest_foods_for_macros
)
app = Flask(__name__)
app.config.from_object(Config)
# Initialize extensions
db.init_app(app)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
# Initialize API client
api_client = NutritionAPI(app.config['API_NINJAS_KEY'])
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# ==================== ROUTES ====================
@app.route('/')
@login_required
def index():
"""Dashboard - Today's summary"""
today = date.today()
# Get daily totals
nutrition = calculate_daily_totals(current_user.id, today)
water = calculate_water_total(current_user.id, today)
# Get user goals
goals = UserGoal.query.filter_by(user_id=current_user.id).first()
if not goals:
goals = UserGoal(
user_id=current_user.id,
target_protein_g=150,
target_carbs_g=200,
target_fat_g=60,
target_water_ml=2000
)
db.session.add(goals)
db.session.commit()
# Get weight info
weight_log_today = WeightLog.query.filter_by(user_id=current_user.id, date=today).first()
weight_log_yesterday = WeightLog.query.filter_by(
user_id=current_user.id,
date=today - timedelta(days=1)
).first()
weight_change = None
if weight_log_today and weight_log_yesterday:
weight_change = weight_log_today.weight_kg - weight_log_yesterday.weight_kg
# Calculate remaining
remaining = {
'calories': current_user.target_daily_calories - nutrition['calories'],
'protein': goals.target_protein_g - nutrition['protein'],
'carbs': goals.target_carbs_g - nutrition['carbs'],
'fat': goals.target_fat_g - nutrition['fat'],
'water': goals.target_water_ml - water['total_ml']
}
# Get macro percentages
macro_percentages = get_macro_percentages(
nutrition['protein'],
nutrition['carbs'],
nutrition['fat']
)
# Get trends
weight_trend = get_weight_trend(current_user.id, 7)
calorie_trend = get_calorie_trend(current_user.id, 7)
# Suggestions
suggestions = suggest_foods_for_macros(
remaining['protein'],
remaining['carbs'],
remaining['fat']
)
return render_template('dashboard.html',
nutrition=nutrition,
water=water,
goals=goals,
remaining=remaining,
macro_percentages=macro_percentages,
weight_today=weight_log_today,
weight_change=weight_change,
weight_trend=weight_trend,
calorie_trend=calorie_trend,
suggestions=suggestions,
today=today)
@app.route('/login', methods=['GET', 'POST'])
def login():
"""User login"""
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
user = User.query.filter_by(username=username).first()
if user and check_password_hash(user.password, password):
login_user(user)
return redirect(url_for('index'))
else:
flash('Invalid username or password', 'error')
return render_template('login.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
"""User registration"""
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
name = request.form.get('name')
if User.query.filter_by(username=username).first():
flash('Username already exists', 'error')
return redirect(url_for('register'))
user = User(
username=username,
password=generate_password_hash(password),
name=name
)
db.session.add(user)
db.session.commit()
flash('Registration successful! Please login.', 'success')
return redirect(url_for('login'))
return render_template('register.html')
@app.route('/logout')
@login_required
def logout():
"""User logout"""
logout_user()
return redirect(url_for('login'))
@app.route('/add-meal', methods=['GET', 'POST'])
@login_required
def add_meal():
"""Add a meal"""
if request.method == 'POST':
meal_date = request.form.get('date', date.today())
meal_type = request.form.get('meal_type')
meal_time = request.form.get('time')
# Convert date string to date object
if isinstance(meal_date, str):
meal_date = datetime.strptime(meal_date, '%Y-%m-%d').date()
# Convert time string to time object
meal_time_obj = None
if meal_time:
meal_time_obj = datetime.strptime(meal_time, '%H:%M').time()
# Create meal
meal = Meal(
user_id=current_user.id,
date=meal_date,
meal_type=meal_type,
time=meal_time_obj
)
db.session.add(meal)
db.session.flush() # Get meal ID
# Add foods to meal
food_ids = request.form.getlist('food_id[]')
quantities = request.form.getlist('quantity[]')
for food_id, quantity in zip(food_ids, quantities):
if food_id and quantity:
meal_food = MealFood(
meal_id=meal.id,
food_id=int(food_id),
quantity=float(quantity)
)
meal_food.calculate_nutrition()
db.session.add(meal_food)
db.session.commit()
update_daily_summary(current_user.id, meal_date)
flash('Meal added successfully!', 'success')
return redirect(url_for('index'))
# GET request - show form
return render_template('add_meal.html', today=date.today())
@app.route('/api/search-food')
@login_required
def api_search_food():
"""API endpoint for food search (AJAX)"""
query = request.args.get('q', '')
if len(query) < 2:
return jsonify([])
results = search_all_sources(query, api_client)
return jsonify(results)
@app.route('/api/add-food', methods=['POST'])
@login_required
def api_add_food():
"""API endpoint to add a food from API to database"""
data = request.json
# Save food to database
food = FoodItem(
name=data['name'],
calories=data['calories'],
protein_g=data.get('protein_g', 0),
carbs_g=data.get('carbs_g', 0),
fat_g=data.get('fat_g', 0),
serving_size_g=data.get('serving_size_g', 100),
serving_description=data.get('serving_description', '1 serving'),
source='api'
)
db.session.add(food)
db.session.commit()
return jsonify({
'success': True,
'food_id': food.id,
'name': food.name
})
@app.route('/add-water', methods=['POST'])
@login_required
def add_water():
"""Add water intake"""
amount_ml = int(request.form.get('amount_ml', 250))
log_date = request.form.get('date', date.today())
if isinstance(log_date, str):
log_date = datetime.strptime(log_date, '%Y-%m-%d').date()
water_log = WaterLog(
user_id=current_user.id,
date=log_date,
amount_ml=amount_ml,
time=datetime.now().time()
)
db.session.add(water_log)
db.session.commit()
update_daily_summary(current_user.id, log_date)
flash(f'Added {amount_ml}ml of water!', 'success')
return redirect(url_for('index'))
@app.route('/add-weight', methods=['POST'])
@login_required
def add_weight():
"""Add weight log"""
weight_kg = float(request.form.get('weight_kg'))
log_date = request.form.get('date', date.today())
if isinstance(log_date, str):
log_date = datetime.strptime(log_date, '%Y-%m-%d').date()
# Check if weight log already exists for today
existing = WeightLog.query.filter_by(user_id=current_user.id, date=log_date).first()
if existing:
existing.weight_kg = weight_kg
existing.time = datetime.now().time()
else:
weight_log = WeightLog(
user_id=current_user.id,
date=log_date,
weight_kg=weight_kg,
time=datetime.now().time()
)
db.session.add(weight_log)
db.session.commit()
update_daily_summary(current_user.id, log_date)
flash(f'Weight logged: {weight_kg}kg', 'success')
return redirect(url_for('index'))
@app.route('/meal-planner')
@login_required
def meal_planner():
"""Meal planner page"""
# Get date range (current week)
today = date.today()
start_date = today - timedelta(days=today.weekday()) # Monday
dates = [start_date + timedelta(days=i) for i in range(7)]
# Get meal plans for the week
meal_plans = {}
for d in dates:
plans = MealPlan.query.filter_by(user_id=current_user.id, date=d).all()
meal_plans[d.isoformat()] = [
{
'id': p.id,
'meal_type': p.meal_type,
'is_completed': p.is_completed,
'foods': [
{
'name': pf.food.name,
'quantity': pf.quantity
}
for pf in p.foods
],
'totals': p.calculate_totals()
}
for p in plans
]
return render_template('meal_planner.html', dates=dates, meal_plans=meal_plans, today=today)
@app.route('/foods')
@login_required
def foods():
"""Food database page"""
category = request.args.get('category', 'all')
search_query = request.args.get('q', '')
query = FoodItem.query
if category != 'all':
query = query.filter_by(category=category)
if search_query:
query = query.filter(
db.or_(
FoodItem.name.ilike(f'%{search_query}%'),
FoodItem.name_tagalog.ilike(f'%{search_query}%')
)
)
# Filipino foods first
filipino_foods = query.filter_by(is_filipino=True).all()
other_foods = query.filter_by(is_filipino=False).limit(20).all()
categories = ['all', 'kanin', 'ulam', 'sabaw', 'gulay', 'meryenda', 'almusal']
return render_template('foods.html',
filipino_foods=filipino_foods,
other_foods=other_foods,
categories=categories,
current_category=category,
search_query=search_query)
@app.route('/progress')
@login_required
def progress():
"""Progress tracking page"""
days = int(request.args.get('days', 30))
weight_trend = get_weight_trend(current_user.id, days)
calorie_trend = get_calorie_trend(current_user.id, days)
# Calculate averages
if calorie_trend:
avg_calories = sum(d['calories'] for d in calorie_trend) / len(calorie_trend)
avg_protein = sum(d['protein'] for d in calorie_trend) / len(calorie_trend)
avg_carbs = sum(d['carbs'] for d in calorie_trend) / len(calorie_trend)
avg_fat = sum(d['fat'] for d in calorie_trend) / len(calorie_trend)
else:
avg_calories = avg_protein = avg_carbs = avg_fat = 0
# Weight change
weight_change = None
if len(weight_trend) >= 2:
weight_change = weight_trend[-1]['weight_kg'] - weight_trend[0]['weight_kg']
return render_template('progress.html',
weight_trend=weight_trend,
calorie_trend=calorie_trend,
avg_calories=avg_calories,
avg_protein=avg_protein,
avg_carbs=avg_carbs,
avg_fat=avg_fat,
weight_change=weight_change,
days=days)
@app.route('/goals', methods=['GET', 'POST'])
@login_required
def goals():
"""Goals and settings page"""
if request.method == 'POST':
# Update user info
current_user.age = int(request.form.get('age', 25))
current_user.gender = request.form.get('gender', 'male')
current_user.height_cm = float(request.form.get('height_cm', 170))
current_user.weight_kg = float(request.form.get('weight_kg', 70))
current_user.activity_level = request.form.get('activity_level', 'moderate')
# Calculate targets
bmr = calculate_bmr(
current_user.weight_kg,
current_user.height_cm,
current_user.age,
current_user.gender
)
tdee = calculate_tdee(bmr, current_user.activity_level)
# Get goal type
goal_type = request.form.get('goal_type', 'recomp')
# Adjust calories based on goal
if goal_type == 'weight_loss':
target_calories = tdee - 500
elif goal_type == 'muscle_gain':
target_calories = tdee + 300
else: # recomp
target_calories = tdee
current_user.target_daily_calories = int(target_calories)
# Update or create goals
user_goals = UserGoal.query.filter_by(user_id=current_user.id).first()
if not user_goals:
user_goals = UserGoal(user_id=current_user.id)
db.session.add(user_goals)
user_goals.goal_type = goal_type
user_goals.target_weight_kg = float(request.form.get('target_weight_kg', 70))
# Calculate macros
macros = calculate_macro_targets(current_user.weight_kg, goal_type)
user_goals.target_protein_g = macros['protein_g']
user_goals.target_carbs_g = macros['carbs_g']
user_goals.target_fat_g = macros['fat_g']
user_goals.target_water_ml = int(request.form.get('target_water_ml', 2000))
db.session.commit()
flash('Goals updated successfully!', 'success')
return redirect(url_for('goals'))
# GET request
user_goals = UserGoal.query.filter_by(user_id=current_user.id).first()
# Calculate current BMR and TDEE
bmr = tdee = None
if current_user.weight_kg and current_user.height_cm and current_user.age:
bmr = calculate_bmr(
current_user.weight_kg,
current_user.height_cm,
current_user.age,
current_user.gender or 'male'
)
tdee = calculate_tdee(bmr, current_user.activity_level or 'moderate')
return render_template('goals.html',
user=current_user,
goals=user_goals,
bmr=bmr,
tdee=tdee)
# ==================== DATABASE INITIALIZATION ====================
@app.cli.command()
def init_db():
"""Initialize the database"""
db.create_all()
print("Database initialized!")
@app.cli.command()
def seed_db():
"""Seed the database with Filipino foods"""
from seed_data import seed_filipino_foods
seed_filipino_foods()
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True, host='127.0.0.1', port=5001)

View File

@@ -0,0 +1,16 @@
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
# Use environment variable for DB URI if available, otherwise default to local file
# For Docker, we'll map a volume to /app/data and use sqlite:////app/data/calorie_tracker.db
SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', 'sqlite:///calorie_tracker.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
API_NINJAS_KEY = os.getenv('API_NINJAS_KEY', '')
# User defaults
DEFAULT_WATER_GOAL_ML = 2000
DEFAULT_CALORIE_TARGET = 2000

View File

@@ -0,0 +1,186 @@
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from datetime import datetime, date
db = SQLAlchemy()
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(200), nullable=False)
name = db.Column(db.String(100))
age = db.Column(db.Integer)
gender = db.Column(db.String(10))
height_cm = db.Column(db.Float)
weight_kg = db.Column(db.Float)
activity_level = db.Column(db.String(20), default='moderate')
target_daily_calories = db.Column(db.Integer, default=2000)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Relationships
meals = db.relationship('Meal', backref='user', lazy=True, cascade='all, delete-orphan')
weight_logs = db.relationship('WeightLog', backref='user', lazy=True, cascade='all, delete-orphan')
water_logs = db.relationship('WaterLog', backref='user', lazy=True, cascade='all, delete-orphan')
meal_plans = db.relationship('MealPlan', backref='user', lazy=True, cascade='all, delete-orphan')
goals = db.relationship('UserGoal', backref='user', uselist=False, cascade='all, delete-orphan')
class FoodItem(db.Model):
__tablename__ = 'food_items'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(200), nullable=False)
name_tagalog = db.Column(db.String(200))
category = db.Column(db.String(50))
calories = db.Column(db.Float, nullable=False)
protein_g = db.Column(db.Float, default=0)
carbs_g = db.Column(db.Float, default=0)
fat_g = db.Column(db.Float, default=0)
fiber_g = db.Column(db.Float, default=0)
sugar_g = db.Column(db.Float, default=0)
sodium_mg = db.Column(db.Float, default=0)
serving_size_g = db.Column(db.Float, default=100)
serving_description = db.Column(db.String(100))
source = db.Column(db.String(20), default='manual') # 'manual', 'api', 'filipino'
is_filipino = db.Column(db.Boolean, default=False)
is_favorite = db.Column(db.Boolean, default=False)
api_data = db.Column(db.Text)
last_updated = db.Column(db.DateTime, default=datetime.utcnow)
# Relationships
meal_foods = db.relationship('MealFood', backref='food', lazy=True)
planned_foods = db.relationship('PlannedFood', backref='food', lazy=True)
class Meal(db.Model):
__tablename__ = 'meals'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
date = db.Column(db.Date, nullable=False, default=date.today)
meal_type = db.Column(db.String(20), nullable=False) # breakfast, lunch, dinner, snack
time = db.Column(db.Time)
notes = db.Column(db.Text)
# Relationships
foods = db.relationship('MealFood', backref='meal', lazy=True, cascade='all, delete-orphan')
def calculate_totals(self):
"""Calculate total nutrition for this meal"""
totals = {
'calories': 0,
'protein': 0,
'carbs': 0,
'fat': 0
}
for mf in self.foods:
totals['calories'] += mf.calories_consumed
totals['protein'] += mf.protein_consumed
totals['carbs'] += mf.carbs_consumed
totals['fat'] += mf.fat_consumed
return totals
class MealFood(db.Model):
__tablename__ = 'meal_foods'
id = db.Column(db.Integer, primary_key=True)
meal_id = db.Column(db.Integer, db.ForeignKey('meals.id'), nullable=False)
food_id = db.Column(db.Integer, db.ForeignKey('food_items.id'), nullable=False)
quantity = db.Column(db.Float, nullable=False, default=1.0) # servings
quantity_grams = db.Column(db.Float)
calories_consumed = db.Column(db.Float)
protein_consumed = db.Column(db.Float)
carbs_consumed = db.Column(db.Float)
fat_consumed = db.Column(db.Float)
def calculate_nutrition(self):
"""Calculate nutrition based on quantity"""
self.calories_consumed = self.food.calories * self.quantity
self.protein_consumed = self.food.protein_g * self.quantity
self.carbs_consumed = self.food.carbs_g * self.quantity
self.fat_consumed = self.food.fat_g * self.quantity
if self.food.serving_size_g:
self.quantity_grams = self.food.serving_size_g * self.quantity
class WaterLog(db.Model):
__tablename__ = 'water_logs'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
date = db.Column(db.Date, nullable=False, default=date.today)
amount_ml = db.Column(db.Integer, nullable=False)
time = db.Column(db.Time, default=datetime.now().time)
class WeightLog(db.Model):
__tablename__ = 'weight_logs'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
date = db.Column(db.Date, nullable=False, unique=True, default=date.today)
weight_kg = db.Column(db.Float, nullable=False)
body_fat_percentage = db.Column(db.Float)
notes = db.Column(db.Text)
time = db.Column(db.Time, default=datetime.now().time)
class MealPlan(db.Model):
__tablename__ = 'meal_plans'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
date = db.Column(db.Date, nullable=False)
meal_type = db.Column(db.String(20), nullable=False)
is_completed = db.Column(db.Boolean, default=False)
notes = db.Column(db.Text)
# Relationships
foods = db.relationship('PlannedFood', backref='meal_plan', lazy=True, cascade='all, delete-orphan')
def calculate_totals(self):
"""Calculate total nutrition for this planned meal"""
totals = {
'calories': 0,
'protein': 0,
'carbs': 0,
'fat': 0
}
for pf in self.foods:
totals['calories'] += pf.food.calories * pf.quantity
totals['protein'] += pf.food.protein_g * pf.quantity
totals['carbs'] += pf.food.carbs_g * pf.quantity
totals['fat'] += pf.food.fat_g * pf.quantity
return totals
class PlannedFood(db.Model):
__tablename__ = 'planned_foods'
id = db.Column(db.Integer, primary_key=True)
meal_plan_id = db.Column(db.Integer, db.ForeignKey('meal_plans.id'), nullable=False)
food_id = db.Column(db.Integer, db.ForeignKey('food_items.id'), nullable=False)
quantity = db.Column(db.Float, nullable=False, default=1.0)
class UserGoal(db.Model):
__tablename__ = 'user_goals'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, unique=True)
goal_type = db.Column(db.String(20), default='recomp') # weight_loss, muscle_gain, recomp
target_weight_kg = db.Column(db.Float)
weekly_goal_kg = db.Column(db.Float, default=0.5)
target_protein_g = db.Column(db.Integer, default=150)
target_carbs_g = db.Column(db.Integer, default=200)
target_fat_g = db.Column(db.Integer, default=60)
target_water_ml = db.Column(db.Integer, default=2000)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class DailySummary(db.Model):
__tablename__ = 'daily_summary'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
date = db.Column(db.Date, nullable=False, unique=True)
total_calories = db.Column(db.Float, default=0)
total_protein_g = db.Column(db.Float, default=0)
total_carbs_g = db.Column(db.Float, default=0)
total_fat_g = db.Column(db.Float, default=0)
total_water_ml = db.Column(db.Integer, default=0)
calories_remaining = db.Column(db.Float)
weight_kg = db.Column(db.Float)
notes = db.Column(db.Text)
class APICache(db.Model):
__tablename__ = 'api_cache'
id = db.Column(db.Integer, primary_key=True)
query = db.Column(db.String(200), nullable=False, unique=True)
api_source = db.Column(db.String(50))
response_json = db.Column(db.Text)
cached_at = db.Column(db.DateTime, default=datetime.utcnow)

View File

@@ -0,0 +1,7 @@
Flask==3.0.0
Flask-SQLAlchemy==3.1.1
Flask-Login==0.6.3
python-dotenv==1.0.0
requests==2.31.0
Werkzeug==3.0.1
gunicorn==21.2.0

View File

@@ -0,0 +1,9 @@
@echo off
echo Starting Calorie Tracker App...
echo.
echo If you see "ModuleNotFoundError", make sure you ran:
echo pip install -r requirements.txt
echo.
echo Opening app on http://127.0.0.1:5001
venv\Scripts\python app.py
pause

View File

@@ -0,0 +1,368 @@
from models import FoodItem, db
def seed_filipino_foods():
"""Populate database with common Filipino foods"""
filipino_foods = [
# Rice (Kanin)
{
'name': 'White Rice',
'name_tagalog': 'Kanin',
'category': 'kanin',
'calories': 206,
'protein_g': 4.3,
'carbs_g': 45,
'fat_g': 0.4,
'serving_description': '1 cup cooked',
'serving_size_g': 158
},
{
'name': 'Fried Rice',
'name_tagalog': 'Sinangag',
'category': 'kanin',
'calories': 280,
'protein_g': 5,
'carbs_g': 42,
'fat_g': 10,
'serving_description': '1 cup',
'serving_size_g': 170
},
# Main Dishes (Ulam)
{
'name': 'Chicken Adobo',
'name_tagalog': 'Adobong Manok',
'category': 'ulam',
'calories': 350,
'protein_g': 35,
'carbs_g': 5,
'fat_g': 20,
'serving_description': '1 serving (2 pieces)',
'serving_size_g': 200
},
{
'name': 'Pork Sinigang',
'name_tagalog': 'Sinigang na Baboy',
'category': 'sabaw',
'calories': 280,
'protein_g': 25,
'carbs_g': 10,
'fat_g': 15,
'serving_description': '1 bowl',
'serving_size_g': 350
},
{
'name': 'Chicken Tinola',
'name_tagalog': 'Tinolang Manok',
'category': 'sabaw',
'calories': 200,
'protein_g': 28,
'carbs_g': 8,
'fat_g': 6,
'serving_description': '1 bowl',
'serving_size_g': 350
},
{
'name': 'Bicol Express',
'name_tagalog': 'Bicol Express',
'category': 'ulam',
'calories': 400,
'protein_g': 20,
'carbs_g': 10,
'fat_g': 32,
'serving_description': '1 serving',
'serving_size_g': 200
},
{
'name': 'Pork Sisig',
'name_tagalog': 'Sisig',
'category': 'ulam',
'calories': 450,
'protein_g': 25,
'carbs_g': 8,
'fat_g': 35,
'serving_description': '1 serving',
'serving_size_g': 180
},
{
'name': 'Menudo',
'name_tagalog': 'Menudo',
'category': 'ulam',
'calories': 320,
'protein_g': 22,
'carbs_g': 12,
'fat_g': 20,
'serving_description': '1 serving',
'serving_size_g': 200
},
{
'name': 'Kare-Kare',
'name_tagalog': 'Kare-Kare',
'category': 'ulam',
'calories': 380,
'protein_g': 24,
'carbs_g': 18,
'fat_g': 25,
'serving_description': '1 serving',
'serving_size_g': 250
},
{
'name': 'Lechon Kawali',
'name_tagalog': 'Lechon Kawali',
'category': 'ulam',
'calories': 500,
'protein_g': 30,
'carbs_g': 2,
'fat_g': 42,
'serving_description': '1 serving',
'serving_size_g': 150
},
{
'name': 'Pork Nilaga',
'name_tagalog': 'Nilagang Baboy',
'category': 'sabaw',
'calories': 280,
'protein_g': 28,
'carbs_g': 12,
'fat_g': 14,
'serving_description': '1 bowl',
'serving_size_g': 350
},
{
'name': 'Beef Bulalo',
'name_tagalog': 'Bulalo',
'category': 'sabaw',
'calories': 350,
'protein_g': 32,
'carbs_g': 8,
'fat_g': 22,
'serving_description': '1 bowl',
'serving_size_g': 400
},
# Vegetables (Gulay)
{
'name': 'Pinakbet',
'name_tagalog': 'Pinakbet',
'category': 'gulay',
'calories': 150,
'protein_g': 5,
'carbs_g': 20,
'fat_g': 6,
'serving_description': '1 cup',
'serving_size_g': 200
},
{
'name': 'Laing',
'name_tagalog': 'Laing',
'category': 'gulay',
'calories': 180,
'protein_g': 6,
'carbs_g': 15,
'fat_g': 12,
'serving_description': '1 cup',
'serving_size_g': 180
},
{
'name': 'Ginisang Monggo',
'name_tagalog': 'Ginisang Monggo',
'category': 'gulay',
'calories': 200,
'protein_g': 12,
'carbs_g': 30,
'fat_g': 4,
'serving_description': '1 cup',
'serving_size_g': 220
},
# Breakfast (Almusal)
{
'name': 'Beef Tapa with Rice and Egg',
'name_tagalog': 'Tapsilog',
'category': 'almusal',
'calories': 650,
'protein_g': 45,
'carbs_g': 60,
'fat_g': 25,
'serving_description': '1 plate',
'serving_size_g': 400
},
{
'name': 'Longganisa with Rice and Egg',
'name_tagalog': 'Longsilog',
'category': 'almusal',
'calories': 700,
'protein_g': 38,
'carbs_g': 65,
'fat_g': 32,
'serving_description': '1 plate',
'serving_size_g': 420
},
{
'name': 'Tocino with Rice and Egg',
'name_tagalog': 'Tocilog',
'category': 'almusal',
'calories': 680,
'protein_g': 42,
'carbs_g': 62,
'fat_g': 28,
'serving_description': '1 plate',
'serving_size_g': 400
},
{
'name': 'Fried Egg',
'name_tagalog': 'Pritong Itlog',
'category': 'almusal',
'calories': 90,
'protein_g': 6,
'carbs_g': 1,
'fat_g': 7,
'serving_description': '1 egg',
'serving_size_g': 50
},
# Snacks (Meryenda)
{
'name': 'Pandesal',
'name_tagalog': 'Pandesal',
'category': 'meryenda',
'calories': 120,
'protein_g': 3,
'carbs_g': 22,
'fat_g': 2,
'serving_description': '1 piece',
'serving_size_g': 40
},
{
'name': 'Turon',
'name_tagalog': 'Turon',
'category': 'meryenda',
'calories': 180,
'protein_g': 2,
'carbs_g': 35,
'fat_g': 5,
'serving_description': '1 piece',
'serving_size_g': 80
},
{
'name': 'Bibingka',
'name_tagalog': 'Bibingka',
'category': 'meryenda',
'calories': 220,
'protein_g': 5,
'carbs_g': 38,
'fat_g': 6,
'serving_description': '1 piece',
'serving_size_g': 100
},
{
'name': 'Puto',
'name_tagalog': 'Puto',
'category': 'meryenda',
'calories': 90,
'protein_g': 2,
'carbs_g': 18,
'fat_g': 1,
'serving_description': '1 piece',
'serving_size_g': 40
},
{
'name': 'Lumpia',
'name_tagalog': 'Lumpia',
'category': 'meryenda',
'calories': 100,
'protein_g': 4,
'carbs_g': 10,
'fat_g': 5,
'serving_description': '1 piece',
'serving_size_g': 50
},
{
'name': 'Banana Cue',
'name_tagalog': 'Banana Cue',
'category': 'meryenda',
'calories': 150,
'protein_g': 1,
'carbs_g': 32,
'fat_g': 4,
'serving_description': '1 piece',
'serving_size_g': 100
},
# Proteins
{
'name': 'Grilled Tilapia',
'name_tagalog': 'Inihaw na Tilapia',
'category': 'ulam',
'calories': 180,
'protein_g': 32,
'carbs_g': 0,
'fat_g': 5,
'serving_description': '1 whole fish',
'serving_size_g': 150
},
{
'name': 'Grilled Chicken',
'name_tagalog': 'Inihaw na Manok',
'category': 'ulam',
'calories': 280,
'protein_g': 40,
'carbs_g': 0,
'fat_g': 13,
'serving_description': '1 breast',
'serving_size_g': 150
},
{
'name': 'Fried Bangus',
'name_tagalog': 'Pritong Bangus',
'category': 'ulam',
'calories': 220,
'protein_g': 28,
'carbs_g': 0,
'fat_g': 12,
'serving_description': '1 piece',
'serving_size_g': 120
}
]
added_count = 0
for food_data in filipino_foods:
# Check if already exists
existing = FoodItem.query.filter_by(
name=food_data['name'],
is_filipino=True
).first()
if not existing:
food = FoodItem(
name=food_data['name'],
name_tagalog=food_data.get('name_tagalog'),
category=food_data['category'],
calories=food_data['calories'],
protein_g=food_data['protein_g'],
carbs_g=food_data['carbs_g'],
fat_g=food_data['fat_g'],
fiber_g=food_data.get('fiber_g', 0),
sugar_g=food_data.get('sugar_g', 0),
sodium_mg=food_data.get('sodium_mg', 0),
serving_size_g=food_data['serving_size_g'],
serving_description=food_data['serving_description'],
source='filipino',
is_filipino=True
)
db.session.add(food)
added_count += 1
try:
db.session.commit()
print(f"Successfully added {added_count} Filipino foods to the database!")
except Exception as e:
db.session.rollback()
print(f"Error seeding Filipino foods: {e}")
if __name__ == '__main__':
from app import app, db
with app.app_context():
seed_filipino_foods()

View File

@@ -0,0 +1,6 @@
@echo off
echo Seeding Filipino Foods database...
venv\Scripts\python seed_data.py
echo.
echo Done!
pause

View File

@@ -0,0 +1,221 @@
{% extends "base.html" %}
{% block title %}Add Meal - Calorie Tracker{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto">
<h1 class="text-3xl font-bold text-gray-800 mb-6">Add Meal</h1>
<form method="POST" id="mealForm" class="glass rounded-xl p-8 shadow-lg">
<!-- Meal Details -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Date</label>
<input type="date" name="date" value="{{ today }}" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary" required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Meal Type</label>
<select name="meal_type" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary" required>
<option value="breakfast">🌅 Breakfast</option>
<option value="lunch">🌞 Lunch</option>
<option value="dinner">🌙 Dinner</option>
<option value="snack">🍪 Snack</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Time (optional)</label>
<input type="time" name="time" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary">
</div>
</div>
<!-- Food Search -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">Search Foods</label>
<input type="text" id="foodSearch" placeholder="Search Filipino or international foods..." class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary">
<div id="searchResults" class="mt-2 max-h-64 overflow-y-auto hidden"></div>
</div>
<!-- Selected Foods -->
<div class="mb-6">
<h3 class="text-lg font-bold mb-3">Selected Foods</h3>
<div id="selectedFoods" class="space-y-3">
<p class="text-gray-500 text-center py-8" id="emptyMessage">No foods added yet. Search and add foods above.</p>
</div>
</div>
<!-- Summary -->
<div class="bg-blue-50 border-l-4 border-blue-500 p-4 rounded mb-6">
<h4 class="font-bold text-blue-900 mb-2">Meal Summary</h4>
<div class="grid grid-cols-4 gap-4 text-center">
<div>
<p class="text-sm text-blue-700">Calories</p>
<p class="text-xl font-bold text-blue-900" id="totalCalories">0</p>
</div>
<div>
<p class="text-sm text-blue-700">Protein</p>
<p class="text-xl font-bold text-blue-900" id="totalProtein">0g</p>
</div>
<div>
<p class="text-sm text-blue-700">Carbs</p>
<p class="text-xl font-bold text-blue-900" id="totalCarbs">0g</p>
</div>
<div>
<p class="text-sm text-blue-700">Fat</p>
<p class="text-xl font-bold text-blue-900" id="totalFat">0g</p>
</div>
</div>
</div>
<!-- Actions -->
<div class="flex justify-between">
<a href="{{ url_for('index') }}" class="px-6 py-3 border border-gray-300 rounded-lg hover:bg-gray-100 transition">
Cancel
</a>
<button type="submit" class="bg-primary text-white px-8 py-3 rounded-lg hover:bg-red-700 transition">
Save Meal
</button>
</div>
</form>
</div>
<script>
let selectedFoods = [];
let searchTimeout;
// Food Search
document.getElementById('foodSearch').addEventListener('input', function(e) {
clearTimeout(searchTimeout);
const query = e.target.value;
if (query.length < 2) {
document.getElementById('searchResults').classList.add('hidden');
return;
}
searchTimeout = setTimeout(() => {
fetch(`/api/search-food?q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => displaySearchResults(data));
}, 300);
});
function displaySearchResults(results) {
const container = document.getElementById('searchResults');
if (results.length === 0) {
container.innerHTML = '<p class="p-4 text-gray-500">No foods found</p>';
container.classList.remove('hidden');
return;
}
container.innerHTML = results.map(food => `
<div class="p-3 border rounded hover:bg-gray-50 cursor-pointer" onclick='addFood(${JSON.stringify(food)})'>
<div class="flex justify-between items-start">
<div>
<p class="font-semibold">${food.name}</p>
${food.name_tagalog ? `<p class="text-sm text-gray-600">${food.name_tagalog}</p>` : ''}
<p class="text-xs text-gray-500">${food.serving_description || '1 serving'}</p>
</div>
<div class="text-right">
<p class="font-bold text-primary">${Math.round(food.calories)} cal</p>
<p class="text-xs text-gray-600">P:${Math.round(food.protein_g)}g C:${Math.round(food.carbs_g)}g F:${Math.round(food.fat_g)}g</p>
</div>
</div>
</div>
`).join('');
container.classList.remove('hidden');
}
function addFood(food) {
// Save to database if from API
if (food.source === 'api' && !food.id) {
fetch('/api/add-food', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(food)
})
.then(response => response.json())
.then(data => {
food.id = data.food_id;
addFoodToList(food);
});
} else {
addFoodToList(food);
}
// Clear search
document.getElementById('foodSearch').value = '';
document.getElementById('searchResults').classList.add('hidden');
}
function addFoodToList(food) {
selectedFoods.push({...food, quantity: 1});
renderSelectedFoods();
}
function removeFood(index) {
selectedFoods.splice(index, 1);
renderSelectedFoods();
}
function updateQuantity(index, quantity) {
selectedFoods[index].quantity = parseFloat(quantity);
updateSummary();
}
function renderSelectedFoods() {
const container = document.getElementById('selectedFoods');
const emptyMessage = document.getElementById('emptyMessage');
if (selectedFoods.length === 0) {
emptyMessage.classList.remove('hidden');
return;
}
emptyMessage.classList.add('hidden');
container.innerHTML = selectedFoods.map((food, index) => `
<div class="flex items-center justify-between p-4 border rounded-lg bg-white">
<div class="flex-1">
<p class="font-semibold">${food.name}</p>
<p class="text-sm text-gray-600">${Math.round(food.calories * food.quantity)} cal | P:${Math.round(food.protein_g * food.quantity)}g C:${Math.round(food.carbs_g * food.quantity)}g F:${Math.round(food.fat_g * food.quantity)}g</p>
<input type="hidden" name="food_id[]" value="${food.id}">
</div>
<div class="flex items-center space-x-3">
<label class="text-sm text-gray-600">Servings:</label>
<input type="number" step="0.5" min="0.5" name="quantity[]" value="${food.quantity}"
class="w-20 px-2 py-1 border rounded" onchange="updateQuantity(${index}, this.value)">
<button type="button" onclick="removeFood(${index})" class="text-red-600 hover:text-red-800">
</button>
</div>
</div>
`).join('');
updateSummary();
}
function updateSummary() {
const totals = selectedFoods.reduce((acc, food) => ({
calories: acc.calories + (food.calories * food.quantity),
protein: acc.protein + (food.protein_g * food.quantity),
carbs: acc.carbs + (food.carbs_g * food.quantity),
fat: acc.fat + (food.fat_g * food.quantity)
}), {calories: 0, protein: 0, carbs: 0, fat: 0});
document.getElementById('totalCalories').textContent = Math.round(totals.calories);
document.getElementById('totalProtein').textContent = Math.round(totals.protein) + 'g';
document.getElementById('totalCarbs').textContent = Math.round(totals.carbs) + 'g';
document.getElementById('totalFat').textContent = Math.round(totals.fat) + 'g';
}
// Prevent form submission if no foods
document.getElementById('mealForm').addEventListener('submit', function(e) {
if (selectedFoods.length === 0) {
e.preventDefault();
alert('Please add at least one food item');
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Calorie Tracker{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#D62828',
secondary: '#003F87',
success: '#06D6A0',
warning: '#FFB703',
}
}
}
}
</script>
<style>
.glass {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
}
</style>
</head>
<body class="bg-gray-50">
{% if current_user.is_authenticated %}
<!-- Navigation -->
<nav class="bg-primary text-white shadow-lg">
<div class="container mx-auto px-4">
<div class="flex justify-between items-center py-4">
<div class="flex items-center space-x-8">
<a href="{{ url_for('index') }}" class="text-2xl font-bold">🍽️ Calorie Tracker</a>
<div class="hidden md:flex space-x-6">
<a href="{{ url_for('index') }}" class="hover:text-gray-200 transition {% if request.endpoint == 'index' %}font-bold{% endif %}">
🏠 Dashboard
</a>
<a href="{{ url_for('meal_planner') }}" class="hover:text-gray-200 transition {% if request.endpoint == 'meal_planner' %}font-bold{% endif %}">
📅 Meal Planner
</a>
<a href="{{ url_for('foods') }}" class="hover:text-gray-200 transition {% if request.endpoint == 'foods' %}font-bold{% endif %}">
🍛 Foods
</a>
<a href="{{ url_for('progress') }}" class="hover:text-gray-200 transition {% if request.endpoint == 'progress' %}font-bold{% endif %}">
📊 Progress
</a>
<a href="{{ url_for('goals') }}" class="hover:text-gray-200 transition {% if request.endpoint == 'goals' %}font-bold{% endif %}">
🎯 Goals
</a>
</div>
</div>
<div class="flex items-center space-x-4">
<span class="hidden md:inline">👤 {{ current_user.name or current_user.username }}</span>
<a href="{{ url_for('logout') }}" class="bg-white text-primary px-4 py-2 rounded hover:bg-gray-100 transition">
Logout
</a>
</div>
</div>
</div>
</nav>
{% endif %}
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="container mx-auto px-4 mt-4">
{% for category, message in messages %}
<div class="{% if category == 'error' %}bg-red-100 border-red-400 text-red-700{% else %}bg-green-100 border-green-400 text-green-700{% endif %} border px-4 py-3 rounded relative mb-4" role="alert">
<span class="block sm:inline">{{ message }}</span>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- Main Content -->
<main class="container mx-auto px-4 py-8">
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer class="bg-gray-800 text-white text-center py-4 mt-12">
<p>&copy; 2026 Calorie Tracker - Filipino Food Edition</p>
</footer>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,300 @@
{% extends "base.html" %}
{% block title %}Dashboard - Calorie Tracker{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto">
<!-- Header -->
<div class="flex justify-between items-center mb-6">
<div>
<h1 class="text-3xl font-bold text-gray-800">Today's Summary</h1>
<p class="text-gray-600">{{ today.strftime('%A, %B %d, %Y') }}</p>
</div>
<div class="space-x-2">
<a href="{{ url_for('add_meal') }}" class="bg-primary text-white px-6 py-3 rounded-lg hover:bg-red-700 transition inline-block">
Add Meal
</a>
</div>
</div>
<!-- Top Cards Row -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<!-- Calories Card -->
<div class="glass rounded-xl p-6 shadow-lg">
<div class="flex justify-between items-start mb-4">
<div>
<p class="text-gray-600 text-sm">Calories</p>
<h2 class="text-3xl font-bold text-primary">{{ nutrition.calories|round|int }}</h2>
<p class="text-sm text-gray-500">/ {{ current_user.target_daily_calories }}</p>
</div>
<span class="text-4xl">🔥</span>
</div>
<!-- Progress Bar -->
<div class="w-full bg-gray-200 rounded-full h-3">
{% set cal_percent = (nutrition.calories / current_user.target_daily_calories * 100)|int if current_user.target_daily_calories > 0 else 0 %}
<div class="bg-primary h-3 rounded-full transition-all" style="width: {{ [cal_percent, 100]|min }}%"></div>
</div>
<p class="text-sm mt-2 {% if remaining.calories >= 0 %}text-success{% else %}text-red-600{% endif %}">
{{ remaining.calories|round|int }} remaining
</p>
</div>
<!-- Protein Card -->
<div class="glass rounded-xl p-6 shadow-lg">
<div class="flex justify-between items-start mb-4">
<div>
<p class="text-gray-600 text-sm">Protein</p>
<h2 class="text-3xl font-bold text-secondary">{{ nutrition.protein|round|int }}g</h2>
<p class="text-sm text-gray-500">/ {{ goals.target_protein_g }}g</p>
</div>
<span class="text-4xl">💪</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-3">
{% set prot_percent = (nutrition.protein / goals.target_protein_g * 100)|int if goals.target_protein_g > 0 else 0 %}
<div class="bg-secondary h-3 rounded-full" style="width: {{ [prot_percent, 100]|min }}%"></div>
</div>
<p class="text-sm mt-2 {% if remaining.protein >= 0 %}text-success{% else %}text-red-600{% endif %}">
{{ remaining.protein|round|int }}g remaining
</p>
</div>
<!-- Carbs & Fat Card -->
<div class="glass rounded-xl p-6 shadow-lg">
<div class="mb-3">
<div class="flex justify-between items-center">
<span class="text-gray-600 text-sm">Carbs</span>
<span class="font-bold text-warning">{{ nutrition.carbs|round|int }}g / {{ goals.target_carbs_g }}g</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 mt-1">
{% set carb_percent = (nutrition.carbs / goals.target_carbs_g * 100)|int if goals.target_carbs_g > 0 else 0 %}
<div class="bg-warning h-2 rounded-full" style="width: {{ [carb_percent, 100]|min }}%"></div>
</div>
</div>
<div>
<div class="flex justify-between items-center">
<span class="text-gray-600 text-sm">Fat</span>
<span class="font-bold text-orange-600">{{ nutrition.fat|round|int }}g / {{ goals.target_fat_g }}g</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 mt-1">
{% set fat_percent = (nutrition.fat / goals.target_fat_g * 100)|int if goals.target_fat_g > 0 else 0 %}
<div class="bg-orange-600 h-2 rounded-full" style="width: {{ [fat_percent, 100]|min }}%"></div>
</div>
</div>
</div>
<!-- Water & Weight Card -->
<div class="glass rounded-xl p-6 shadow-lg">
<div class="mb-3">
<div class="flex justify-between items-center">
<span class="text-gray-600 text-sm">Water</span>
<span class="font-bold text-blue-600">{{ water.total_ml }}ml / {{ goals.target_water_ml }}ml</span>
</div>
<div class="flex space-x-1 mt-2">
{% set glasses_filled = (water.total_ml / 250)|int %}
{% for i in range(8) %}
<span class="{% if i < glasses_filled %}text-blue-500{% else %}text-gray-300{% endif %}">💧</span>
{% endfor %}
</div>
<!-- Quick add water buttons -->
<form method="POST" action="{{ url_for('add_water') }}" class="flex space-x-1 mt-2">
<button type="submit" name="amount_ml" value="250" class="bg-blue-100 text-blue-700 px-2 py-1 rounded text-xs hover:bg-blue-200">+250ml</button>
<button type="submit" name="amount_ml" value="500" class="bg-blue-100 text-blue-700 px-2 py-1 rounded text-xs hover:bg-blue-200">+500ml</button>
</form>
</div>
<div class="pt-3 border-t">
<div class="flex justify-between items-center">
<span class="text-gray-600 text-sm">Weight</span>
{% if weight_today %}
<div class="text-right">
<span class="font-bold">{{ weight_today.weight_kg }}kg</span>
{% if weight_change %}
<span class="text-xs {% if weight_change < 0 %}text-success{% else %}text-red-600{% endif %}">
{{ weight_change|round(1) }}kg
</span>
{% endif %}
</div>
{% else %}
<form method="POST" action="{{ url_for('add_weight') }}" class="flex items-center space-x-2">
<input type="number" step="0.1" name="weight_kg" placeholder="kg" class="w-20 px-2 py-1 border rounded text-sm" required>
<button type="submit" class="bg-green-500 text-white px-2 py-1 rounded text-xs hover:bg-green-600">Log</button>
</form>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Macro Distribution Pie Chart -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<div class="glass rounded-xl p-6 shadow-lg">
<h3 class="text-lg font-bold mb-4">Macro Distribution</h3>
<canvas id="macroChart" height="200"></canvas>
</div>
<!-- Suggestions -->
<div class="glass rounded-xl p-6 shadow-lg lg:col-span-2">
<h3 class="text-lg font-bold mb-4">💡 Smart Suggestions</h3>
{% if suggestions %}
<div class="space-y-3">
{% for suggestion in suggestions %}
<div class="bg-blue-50 border-l-4 border-blue-500 p-3 rounded">
<p class="font-semibold text-blue-900">{{ suggestion.category }}</p>
<p class="text-sm text-blue-700">Try: {{ suggestion.examples|join(', ') }}</p>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-gray-600">You're on track! Keep up the good work! 🎉</p>
{% endif %}
</div>
</div>
<!-- Today's Meals -->
<div class="glass rounded-xl p-6 shadow-lg mb-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">Today's Meals</h3>
<a href="{{ url_for('add_meal') }}" class="text-primary hover:underline">+ Add Meal</a>
</div>
{% if nutrition.meals %}
<div class="space-y-4">
{% for meal in nutrition.meals %}
<div class="border-l-4 {% if meal.type == 'breakfast' %}border-yellow-500{% elif meal.type == 'lunch' %}border-green-500{% elif meal.type == 'dinner' %}border-blue-500{% else %}border-purple-500{% endif %} pl-4 py-2">
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center space-x-2">
<span class="text-2xl">
{% if meal.type == 'breakfast' %}🌅{% elif meal.type == 'lunch' %}🌞{% elif meal.type == 'dinner' %}🌙{% else %}🍪{% endif %}
</span>
<h4 class="font-bold text-lg capitalize">{{ meal.type }}</h4>
{% if meal.time %}
<span class="text-sm text-gray-500">{{ meal.time }}</span>
{% endif %}
</div>
<div class="mt-2 space-y-1">
{% for food in meal.foods %}
<p class="text-sm text-gray-700">• {{ food.name }} ({{ food.quantity }}x) - {{ food.calories|round|int }} cal</p>
{% endfor %}
</div>
</div>
<div class="text-right">
<p class="text-2xl font-bold text-primary">{{ meal.totals.calories|round|int }}</p>
<p class="text-xs text-gray-500">calories</p>
<p class="text-xs text-gray-600 mt-1">
P: {{ meal.totals.protein|round|int }}g |
C: {{ meal.totals.carbs|round|int }}g |
F: {{ meal.totals.fat|round|int }}g
</p>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-12 text-gray-500">
<p class="text-4xl mb-4">🍽️</p>
<p>No meals logged yet today.</p>
<a href="{{ url_for('add_meal') }}" class="text-primary hover:underline mt-2 inline-block">Add your first meal</a>
</div>
{% endif %}
</div>
<!-- Weekly Trends -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Calorie Trend -->
<div class="glass rounded-xl p-6 shadow-lg">
<h3 class="text-lg font-bold mb-4">📈 Calorie Trend (7 Days)</h3>
<canvas id="calorieChart" height="200"></canvas>
</div>
<!-- Weight Trend -->
<div class="glass rounded-xl p-6 shadow-lg">
<h3 class="text-lg font-bold mb-4">⚖️ Weight Trend (7 Days)</h3>
<canvas id="weightChart" height="200"></canvas>
</div>
</div>
</div>
<script>
// Macro Distribution Chart
const macroCtx = document.getElementById('macroChart').getContext('2d');
new Chart(macroCtx, {
type: 'doughnut',
data: {
labels: ['Protein', 'Carbs', 'Fat'],
datasets: [{
data: [{{ macro_percentages.protein }}, {{ macro_percentages.carbs }}, {{ macro_percentages.fat }}],
backgroundColor: ['#003F87', '#FFB703', '#FF6B35']
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
// Calorie Trend Chart
const calorieCtx = document.getElementById('calorieChart').getContext('2d');
new Chart(calorieCtx, {
type: 'line',
data: {
labels: [{% for day in calorie_trend %}'{{ day.date }}'{% if not loop.last %}, {% endif %}{% endfor %}],
datasets: [{
label: 'Calories',
data: [{% for day in calorie_trend %}{{ day.calories }}{% if not loop.last %}, {% endif %}{% endfor %}],
borderColor: '#D62828',
backgroundColor: 'rgba(214, 40, 40, 0.1)',
tension: 0.4
}, {
label: 'Target',
data: Array(7).fill({{ current_user.target_daily_calories }}),
borderColor: '#06D6A0',
borderDash: [5, 5],
pointRadius: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
display: true
}
}
}
});
// Weight Trend Chart
{% if weight_trend %}
const weightCtx = document.getElementById('weightChart').getContext('2d');
new Chart(weightCtx, {
type: 'line',
data: {
labels: [{% for day in weight_trend %}'{{ day.date }}'{% if not loop.last %}, {% endif %}{% endfor %}],
datasets: [{
label: 'Weight (kg)',
data: [{% for day in weight_trend %}{{ day.weight_kg }}{% if not loop.last %}, {% endif %}{% endfor %}],
borderColor: '#003F87',
backgroundColor: 'rgba(0, 63, 135, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
display: false
}
}
}
});
{% endif %}
</script>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block title %}Foods Database{% endblock %}
{% block content %}
<h1 class="text-3xl font-bold mb-6">Filipino Foods Database</h1>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
{% for food in filipino_foods %}
<div class="glass rounded-lg p-4">
<h3 class="font-bold">{{ food.name }}</h3>
<p class="text-sm text-gray-600">{{ food.name_tagalog }}</p>
<p class="text-primary font-bold">{{ food.calories|round|int }} cal</p>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block title %}Goals & Settings{% endblock %}
{% block content %}
<h1 class="text-3xl font-bold mb-6">Goals & Settings</h1>
<form method="POST" class="glass rounded-xl p-6 max-w-2xl">
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium mb-2">Age</label>
<input type="number" name="age" value="{{ user.age or 25 }}" class="w-full px-4 py-2 border rounded" required>
</div>
<div>
<label class="block text-sm font-medium mb-2">Gender</label>
<select name="gender" class="w-full px-4 py-2 border rounded" required>
<option value="male" {% if user.gender == 'male' %}selected{% endif %}>Male</option>
<option value="female" {% if user.gender == 'female' %}selected{% endif %}>Female</option>
</select>
</div>
</div>
<button type="submit" class="bg-primary text-white px-6 py-3 rounded hover:bg-red-700">Save</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block title %}Login - Calorie Tracker{% endblock %}
{% block content %}
<div class="max-w-md mx-auto">
<div class="glass rounded-xl p-8 shadow-lg">
<h1 class="text-3xl font-bold text-center text-gray-800 mb-6">🍽️ Calorie Tracker</h1>
<p class="text-center text-gray-600 mb-8">Filipino Food Edition</p>
<form method="POST">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Username</label>
<input type="text" name="username" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary" required>
</div>
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">Password</label>
<input type="password" name="password" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary" required>
</div>
<button type="submit" class="w-full bg-primary text-white py-3 rounded-lg hover:bg-red-700 transition font-semibold">
Login
</button>
</form>
<p class="text-center mt-6 text-gray-600">
Don't have an account? <a href="{{ url_for('register') }}" class="text-primary hover:underline font-semibold">Register</a>
</p>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,6 @@
{% extends "base.html" %}
{% block title %}Meal Planner{% endblock %}
{% block content %}
<h1 class="text-3xl font-bold mb-6">Meal Planner</h1>
<p class="text-gray-600">Coming soon! Plan your meals for the week ahead.</p>
{% endblock %}

View File

@@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block title %}Progress{% endblock %}
{% block content %}
<h1 class="text-3xl font-bold mb-6">Your Progress</h1>
<div class="glass rounded-xl p-6">
<canvas id="progressChart"></canvas>
</div>
{% endblock %}

View File

@@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block title %}Register - Calorie Tracker{% endblock %}
{% block content %}
<div class="max-w-md mx-auto">
<div class="glass rounded-xl p-8 shadow-lg">
<h1 class="text-3xl font-bold text-center text-gray-800 mb-6">Create Account</h1>
<form method="POST">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Name</label>
<input type="text" name="name" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary" required>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Username</label>
<input type="text" name="username" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary" required>
</div>
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">Password</label>
<input type="password" name="password" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary" required>
</div>
<button type="submit" class="w-full bg-primary text-white py-3 rounded-lg hover:bg-red-700 transition font-semibold">
Register
</button>
</form>
<p class="text-center mt-6 text-gray-600">
Already have an account? <a href="{{ url_for('login') }}" class="text-primary hover:underline font-semibold">Login</a>
</p>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,258 @@
from datetime import date, timedelta
from models import db, Meal, WaterLog, WeightLog, DailySummary, UserGoal
def calculate_bmr(weight_kg, height_cm, age, gender):
"""
Calculate Basal Metabolic Rate using Mifflin-St Jeor Equation
"""
if gender.lower() == 'male':
bmr = (10 * weight_kg) + (6.25 * height_cm) - (5 * age) + 5
else: # female
bmr = (10 * weight_kg) + (6.25 * height_cm) - (5 * age) - 161
return round(bmr)
def calculate_tdee(bmr, activity_level):
"""
Calculate Total Daily Energy Expenditure
"""
multipliers = {
'sedentary': 1.2,
'light': 1.375,
'moderate': 1.55,
'active': 1.725,
'very_active': 1.9
}
multiplier = multipliers.get(activity_level, 1.55)
return round(bmr * multiplier)
def calculate_macro_targets(weight_kg, goal_type='recomp'):
"""
Calculate macro targets based on body weight and goal
"""
if goal_type == 'muscle_gain':
protein = weight_kg * 2.4 # High protein for muscle building
carbs = weight_kg * 3.5 # Higher carbs for energy
fat = weight_kg * 1.0 # Moderate fat
elif goal_type == 'weight_loss':
protein = weight_kg * 2.2 # High protein to preserve muscle
carbs = weight_kg * 2.0 # Lower carbs for deficit
fat = weight_kg * 0.8 # Lower fat
else: # recomp (body recomposition)
protein = weight_kg * 2.2 # High protein
carbs = weight_kg * 2.5 # Moderate carbs
fat = weight_kg * 0.9 # Moderate fat
return {
'protein_g': round(protein),
'carbs_g': round(carbs),
'fat_g': round(fat)
}
def calculate_daily_totals(user_id, target_date=None):
"""
Calculate total nutrition consumed for a given date
"""
if target_date is None:
target_date = date.today()
# Get all meals for the date
meals = Meal.query.filter_by(user_id=user_id, date=target_date).all()
totals = {
'calories': 0,
'protein': 0,
'carbs': 0,
'fat': 0,
'meals': []
}
for meal in meals:
meal_totals = meal.calculate_totals()
totals['calories'] += meal_totals['calories']
totals['protein'] += meal_totals['protein']
totals['carbs'] += meal_totals['carbs']
totals['fat'] += meal_totals['fat']
totals['meals'].append({
'id': meal.id,
'type': meal.meal_type,
'time': meal.time.strftime('%H:%M') if meal.time else None,
'totals': meal_totals,
'foods': [
{
'name': mf.food.name,
'quantity': mf.quantity,
'calories': mf.calories_consumed
}
for mf in meal.foods
]
})
return totals
def calculate_water_total(user_id, target_date=None):
"""
Calculate total water intake for a given date
"""
if target_date is None:
target_date = date.today()
water_logs = WaterLog.query.filter_by(user_id=user_id, date=target_date).all()
total = sum(log.amount_ml for log in water_logs)
return {
'total_ml': total,
'logs': [
{
'id': log.id,
'amount_ml': log.amount_ml,
'time': log.time.strftime('%H:%M') if log.time else None
}
for log in water_logs
]
}
def get_weight_trend(user_id, days=7):
"""
Get weight trend for the past N days
"""
end_date = date.today()
start_date = end_date - timedelta(days=days-1)
weight_logs = WeightLog.query.filter(
WeightLog.user_id == user_id,
WeightLog.date >= start_date,
WeightLog.date <= end_date
).order_by(WeightLog.date).all()
return [
{
'date': log.date.strftime('%Y-%m-%d'),
'weight_kg': log.weight_kg
}
for log in weight_logs
]
def get_calorie_trend(user_id, days=7):
"""
Get calorie intake trend for the past N days
"""
end_date = date.today()
start_date = end_date - timedelta(days=days-1)
trend = []
current_date = start_date
while current_date <= end_date:
totals = calculate_daily_totals(user_id, current_date)
trend.append({
'date': current_date.strftime('%Y-%m-%d'),
'calories': round(totals['calories']),
'protein': round(totals['protein']),
'carbs': round(totals['carbs']),
'fat': round(totals['fat'])
})
current_date += timedelta(days=1)
return trend
def update_daily_summary(user_id, target_date=None):
"""
Update or create daily summary for a user
"""
if target_date is None:
target_date = date.today()
# Calculate totals
nutrition = calculate_daily_totals(user_id, target_date)
water = calculate_water_total(user_id, target_date)
# Get weight for the day
weight_log = WeightLog.query.filter_by(user_id=user_id, date=target_date).first()
weight = weight_log.weight_kg if weight_log else None
# Get user's calorie target
from models import User
user = User.query.get(user_id)
target_calories = user.target_daily_calories if user else 2000
# Find or create summary
summary = DailySummary.query.filter_by(user_id=user_id, date=target_date).first()
if not summary:
summary = DailySummary(user_id=user_id, date=target_date)
db.session.add(summary)
# Update values
summary.total_calories = nutrition['calories']
summary.total_protein_g = nutrition['protein']
summary.total_carbs_g = nutrition['carbs']
summary.total_fat_g = nutrition['fat']
summary.total_water_ml = water['total_ml']
summary.calories_remaining = target_calories - nutrition['calories']
summary.weight_kg = weight
try:
db.session.commit()
return summary
except Exception as e:
db.session.rollback()
print(f"Error updating daily summary: {e}")
return None
def get_macro_percentages(protein_g, carbs_g, fat_g):
"""
Calculate macro distribution as percentages
"""
protein_cal = protein_g * 4
carbs_cal = carbs_g * 4
fat_cal = fat_g * 9
total_cal = protein_cal + carbs_cal + fat_cal
if total_cal == 0:
return {'protein': 0, 'carbs': 0, 'fat': 0}
return {
'protein': round((protein_cal / total_cal) * 100),
'carbs': round((carbs_cal / total_cal) * 100),
'fat': round((fat_cal / total_cal) * 100)
}
def suggest_foods_for_macros(remaining_protein, remaining_carbs, remaining_fat):
"""
Suggest Filipino foods based on remaining macros
Returns category suggestions
"""
suggestions = []
# High protein needed
if remaining_protein > 30:
suggestions.append({
'category': 'High Protein Ulam',
'examples': ['Grilled Tilapia', 'Chicken Tinola', 'Grilled Chicken']
})
# High carbs needed
if remaining_carbs > 40:
suggestions.append({
'category': 'Carbs',
'examples': ['White Rice', 'Pandesal', 'Sweet Potato']
})
# High fat needed
if remaining_fat > 20:
suggestions.append({
'category': 'Healthy Fats',
'examples': ['Sisig', 'Lechon Kawali', 'Bicol Express']
})
# Balanced meal needed
if remaining_protein > 20 and remaining_carbs > 30:
suggestions.append({
'category': 'Balanced Meals',
'examples': ['Tapsilog', 'Chicken Adobo with Rice', 'Sinigang']
})
return suggestions

546
customized_build_plan.md Normal file
View File

@@ -0,0 +1,546 @@
# Customized Calorie Tracker - Filipino Food Edition
## Web Application for Weight Loss & Muscle Gain
---
## Your Specific Requirements
**Interface**: Web Application (Flask-based)
**Goals**: Weight Loss + Muscle Gain (Body Recomposition)
**Tracking**: Calories, Macros (Protein/Carbs/Fat), Water Intake
**Precision**: Approximate tracking (user-friendly)
**Weight Tracking**: Daily weigh-ins with trend analysis
**Diet**: No restrictions, Filipino food focus
**Planning**: Meal planning ahead feature (not just logging)
---
## Enhanced Database Schema
### Additional Tables for Your Needs
```sql
-- Water intake tracking
CREATE TABLE water_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
date DATE NOT NULL,
amount_ml INTEGER NOT NULL,
time TIME,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Weight tracking
CREATE TABLE weight_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
date DATE NOT NULL UNIQUE,
weight_kg REAL NOT NULL,
body_fat_percentage REAL,
notes TEXT,
time TIME DEFAULT CURRENT_TIME,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Meal plans (future meals)
CREATE TABLE meal_plans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
date DATE NOT NULL,
meal_type TEXT,
is_completed BOOLEAN DEFAULT 0,
notes TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Planned foods (linked to meal_plans)
CREATE TABLE planned_foods (
id INTEGER PRIMARY KEY AUTOINCREMENT,
meal_plan_id INTEGER NOT NULL,
food_id INTEGER NOT NULL,
quantity REAL NOT NULL,
FOREIGN KEY (meal_plan_id) REFERENCES meal_plans(id) ON DELETE CASCADE,
FOREIGN KEY (food_id) REFERENCES food_items(id) ON DELETE CASCADE
);
-- Filipino food database (pre-populated)
CREATE TABLE filipino_foods (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name_english TEXT,
name_tagalog TEXT,
category TEXT, -- 'ulam', 'kanin', 'meryenda', 'sabaw', etc.
is_common BOOLEAN DEFAULT 1,
calories REAL,
protein_g REAL,
carbs_g REAL,
fat_g REAL,
serving_description TEXT
);
-- User preferences and goals
CREATE TABLE user_goals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER UNIQUE,
goal_type TEXT, -- 'weight_loss', 'muscle_gain', 'recomp'
target_weight_kg REAL,
weekly_goal_kg REAL,
target_protein_g INTEGER,
target_carbs_g INTEGER,
target_fat_g INTEGER,
target_water_ml INTEGER DEFAULT 2000,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
```
---
## Body Recomposition Strategy
### Macro Targets for Weight Loss + Muscle Gain
**Protein Priority (Muscle Preservation/Growth)**
- Target: 2.0-2.4g per kg body weight
- Example: 70kg person = 140-168g protein/day
**Moderate Carbs (Energy for Workouts)**
- Target: 2-3g per kg body weight
- Example: 70kg person = 140-210g carbs/day
**Healthy Fats (Hormones & Satiety)**
- Target: 0.8-1.0g per kg body weight
- Example: 70kg person = 56-70g fat/day
**Calorie Cycling Option**
- Training days: Maintenance or slight surplus (+100-200 cal)
- Rest days: Deficit (-300-500 cal)
---
## Filipino Food Database
### Pre-populated Common Filipino Foods
**Kanin (Rice)**
- White rice (1 cup) - 206 cal, 45g carbs, 4g protein
- Fried rice - 280 cal, 40g carbs, 5g protein, 10g fat
- Sinangag - 250 cal, 42g carbs, 4g protein, 8g fat
**Ulam (Main Dishes)**
- Adobo (chicken, 1 serving) - 350 cal, 35g protein, 5g carbs, 20g fat
- Sinigang (pork, 1 bowl) - 280 cal, 25g protein, 10g carbs, 15g fat
- Tinola (chicken soup) - 200 cal, 28g protein, 8g carbs, 6g fat
- Bicol Express - 400 cal, 20g protein, 10g carbs, 30g fat
- Sisig (pork) - 450 cal, 25g protein, 8g carbs, 35g fat
- Menudo - 320 cal, 22g protein, 12g carbs, 20g fat
- Kare-kare - 380 cal, 24g protein, 18g carbs, 25g fat
- Lechon kawali - 500 cal, 30g protein, 2g carbs, 42g fat
**Gulay (Vegetables)**
- Pinakbet - 150 cal, 5g protein, 20g carbs, 6g fat
- Laing - 180 cal, 6g protein, 15g carbs, 12g fat
- Ginisang monggo - 200 cal, 12g protein, 30g carbs, 4g fat
**Meryenda (Snacks)**
- Pandesal (1 piece) - 120 cal, 3g protein, 22g carbs, 2g fat
- Turon - 180 cal, 2g protein, 35g carbs, 5g fat
- Bibingka - 220 cal, 5g protein, 38g carbs, 6g fat
- Puto - 90 cal, 2g protein, 18g carbs, 1g fat
- Lumpia (2 pieces) - 200 cal, 8g protein, 20g carbs, 10g fat
**Sabaw (Soups)**
- Bulalo - 350 cal, 32g protein, 8g carbs, 20g fat
- Nilaga - 280 cal, 28g protein, 12g carbs, 14g fat
**Breakfast**
- Tapsilog - 650 cal, 45g protein, 60g carbs, 25g fat
- Longsilog - 700 cal, 38g protein, 65g carbs, 32g fat
- Tocilog - 680 cal, 42g protein, 62g carbs, 28g fat
---
## Web Application Features
### Pages Structure
**1. Dashboard (Home)**
- Today's summary card
- Calories consumed vs target (progress bar)
- Macros breakdown (protein/carbs/fat) with color-coded bars
- Water intake tracker (glasses icon)
- Weight today vs yesterday
- Quick add meal button
- Quick add water button
- Weekly trend chart (calories & weight)
**2. Meal Planner**
- Calendar view (7-day week view)
- Click date to plan meals
- Drag-and-drop Filipino foods
- Copy previous day's meals
- Templates for common Filipino meal combos
- Calculate totals before committing
- Mark meals as completed
**3. Food Database**
- Search bar (English or Tagalog)
- Filter by category (Ulam, Kanin, Meryenda, etc.)
- Filipino food section (pre-populated)
- API search for international foods
- Add custom foods
- Favorite foods list
- Recent foods
**4. Tracking Log**
- Today's meals (editable)
- Add meal form
- Food search autocomplete
- Quick portions (1/2 serving, 1 serving, 2 servings)
- Edit/delete entries
**5. Progress**
- Weight chart (line graph)
- Body composition trends
- Weekly averages
- Monthly summaries
- Photo progress (optional upload)
- Measurements (waist, chest, arms)
**6. Goals & Settings**
- Set target weight
- Calculate TDEE
- Set macro targets
- Choose goal (weight loss, muscle gain, recomp)
- Water intake goal
- Activity level
- Profile info
---
## Tech Stack
### Backend
```
Flask==3.0.0
Flask-SQLAlchemy==3.1.1
Flask-Login==0.6.3
python-dotenv==1.0.0
requests==2.31.0
```
### Frontend
```
HTML5
CSS3 / Tailwind CSS
JavaScript (Vanilla)
Chart.js (for graphs)
FullCalendar.js (for meal planner)
```
### Database
```
SQLite3 (development)
PostgreSQL (production optional)
```
---
## UI Design Mockup
### Color Scheme (Filipino-inspired)
- Primary: #D62828 (Red - like Filipino flag)
- Secondary: #003F87 (Blue)
- Success: #06D6A0 (Green - for hitting goals)
- Warning: #FFB703 (Yellow/Gold)
- Background: #F8F9FA (Light gray)
### Dashboard Layout
```
┌─────────────────────────────────────────────────────┐
│ 🏠 Dashboard 📅 Meal Planner 🍽️ Foods 📊 Progress │
├─────────────────────────────────────────────────────┤
│ │
│ Today: January 30, 2026 ⚙️ Settings 👤 User │
│ │
│ ┌──────────────────────┐ ┌────────────────────┐ │
│ │ Calories │ │ Macros │ │
│ │ ████████░░ 1,645 │ │ Protein: 120g ✓ │ │
│ │ Target: 2,000 │ │ Carbs: 180g │ │
│ │ Remaining: 355 │ │ Fat: 55g │ │
│ └──────────────────────┘ └────────────────────┘ │
│ │
│ ┌──────────────────────┐ ┌────────────────────┐ │
│ │ Water Intake │ │ Weight │ │
│ │ 💧💧💧💧💧⚪⚪⚪ │ │ Today: 72.5 kg │ │
│ │ 1,250 / 2,000 ml │ │ Change: -0.3 kg ⬇│ │
│ └──────────────────────┘ └────────────────────┘ │
│ │
│ Today's Meals [+ Add Meal]│
│ ┌─────────────────────────────────────────────────┤
│ │ 🌅 Breakfast (7:30 AM) 520 cal│
│ │ • Tapsilog │
│ │ • Coffee with milk │
│ ├─────────────────────────────────────────────────┤
│ │ 🌞 Lunch (12:30 PM) 680 cal│
│ │ • Chicken Adobo │
│ │ • White rice (1.5 cups) │
│ │ • Pinakbet │
│ ├─────────────────────────────────────────────────┤
│ │ 🍪 Snack (3:00 PM) 180 cal│
│ │ • Turon (1 piece) │
│ ├─────────────────────────────────────────────────┤
│ │ 🌙 Dinner (Planned) [Edit] │
│ │ • Sinigang na baboy │
│ │ • White rice (1 cup) │
│ └─────────────────────────────────────────────────┘
│ │
│ Weekly Trend │
│ ┌─────────────────────────────────────────────────┤
│ │ [Chart: Calories & Weight] │
│ └─────────────────────────────────────────────────┘
│ │
└─────────────────────────────────────────────────────┘
```
---
## Meal Planning Workflow
### Planning Mode
1. Navigate to Meal Planner
2. Select future date
3. Add meals for each time slot
4. Search Filipino foods or API
5. Adjust quantities
6. See macro totals update in real-time
7. Save meal plan
### Execution Mode
1. Dashboard shows today's planned meals
2. Check off meals as you eat them
3. Edit portions if needed (ate more/less)
4. Actual consumption updates automatically
5. Quick add water after meals
### Smart Features
- **Suggest meals based on remaining macros**
- Low on protein? Suggests Tinola, Grilled fish
- Need carbs? Suggests Rice, Pandesal
- Need fats? Suggests Sisig, Bicol Express
- **Common Filipino meal combos**
- Silog meals (Tapsilog, Longsilog, etc.)
- Typical lunch: Ulam + Rice + Gulay
- Merienda ideas
- **Copy from history**
- Yesterday's meals
- Last week's Tuesday
- Favorite meal combinations
---
## API Integration Strategy
### Primary: API Ninjas
- Use for international/branded foods
- Cache results in local database
- Fallback to Open Food Facts if not found
### Secondary: Open Food Facts
- Barcode scanning capability
- Community database
- Free, no rate limits
### Local Database Priority
1. Check Filipino foods table first (fastest)
2. Check cached API results
3. Query API Ninjas
4. Query Open Food Facts
5. Allow manual entry if not found
---
## Water Intake Tracking
### Quick Add Buttons
- Small glass (250ml)
- Medium glass (350ml)
- Large glass (500ml)
- Bottle (750ml)
- Custom amount
### Visual Progress
- Glass icons fill up (8 glasses = 2L goal)
- Progress bar
- Notifications/reminders (optional)
### Tracking Table
```
Time | Amount | Type
---------|--------|-------
07:30 AM | 250ml | Glass
12:45 PM | 500ml | Bottle
03:00 PM | 350ml | Glass
```
---
## Implementation Phases
### Phase 1: Core Backend (Week 1)
- [ ] Database setup with all tables
- [ ] User authentication (Flask-Login)
- [ ] API integration with caching
- [ ] Filipino food database population
- [ ] CRUD operations for meals, foods, water, weight
### Phase 2: Dashboard & Logging (Week 2)
- [ ] Dashboard page with cards
- [ ] Today's summary calculations
- [ ] Add meal form
- [ ] Food search with autocomplete
- [ ] Water logging
- [ ] Weight logging
### Phase 3: Meal Planner (Week 3)
- [ ] Calendar interface
- [ ] Plan future meals
- [ ] Copy meals functionality
- [ ] Meal templates
- [ ] Mark meals as completed
- [ ] Smart suggestions based on macros
### Phase 4: Progress & Polish (Week 4)
- [ ] Progress page with charts
- [ ] Weight trend analysis
- [ ] Weekly/monthly reports
- [ ] Goals page
- [ ] Settings page
- [ ] Mobile responsive design
- [ ] Testing and bug fixes
---
## Deployment
### Recommended: Heroku (Free/Hobby Tier)
```bash
# Free tier includes:
# - 550 dyno hours/month
# - PostgreSQL database
# - HTTPS by default
```
### Alternative: PythonAnywhere
```bash
# Free tier includes:
# - One web app
# - 512MB storage
# - Good for personal projects
```
### Alternative: DigitalOcean App Platform
```bash
# $5/month
# - More reliable
# - Better performance
# - PostgreSQL included
```
---
## Special Features for Filipino Users
### Language Support
- Search in English or Tagalog
- "Kanin" or "Rice" both work
- "Adobong manok" or "Chicken adobo"
### Common Portions
- Tagayan (ladle)
- Tasa (cup)
- Kutsara (tablespoon)
- Standard plate serving
### Meal Categories
- Almusal (Breakfast)
- Tanghalian (Lunch)
- Merienda (Snack)
- Hapunan (Dinner)
### Cultural Considerations
- Family-style eating (estimate portions)
- Typical Filipino meal structure
- Common food pairings
- Celebration foods (adjust for occasions)
---
## Starter Prompt for AI Code Generation
```
Create a Flask web application for calorie and macro tracking with these specifications:
1. Database (SQLite with Flask-SQLAlchemy):
- Users, food_items, meals, meal_foods, water_logs, weight_logs
- meal_plans, planned_foods (for meal planning)
- filipino_foods (pre-populated)
- user_goals (macro targets)
2. Filipino Food Support:
- Pre-populate database with 50+ common Filipino foods
- Categories: Ulam, Kanin, Meryenda, Sabaw, Gulay
- English and Tagalog names searchable
3. Pages:
- Dashboard (today's summary, macros, water, weight)
- Meal Planner (calendar view for planning future meals)
- Food Database (search Filipino + API foods)
- Tracking Log (add/edit today's meals)
- Progress (weight chart, trends)
- Goals/Settings
4. Features:
- Macro tracking (protein, carbs, fat)
- Water intake logging
- Daily weight tracking
- Meal planning (plan ahead, not just log)
- Copy previous meals
- Smart macro suggestions
- API Ninjas integration with caching
5. Frontend:
- Tailwind CSS for styling
- Chart.js for graphs
- Red/Blue color scheme (Filipino flag inspired)
- Mobile responsive
6. User Flow:
- Plan meals in advance
- Mark meals as completed when eaten
- Quick add water/weight
- See progress charts
Include:
- User authentication (Flask-Login)
- Form validation
- Error handling
- Responsive design
- Sample data
- Setup instructions
Generate complete code with folder structure.
```
---
## Next Steps
1. **Review this plan** - Make sure it matches your vision
2. **Start with backend** - Database and API integration
3. **Build dashboard** - Core tracking interface
4. **Add meal planner** - Planning ahead feature
5. **Test and iterate** - Use it yourself, refine
Ready to start building? Let's create the actual application! 🚀

22
docker-compose.yml Normal file
View File

@@ -0,0 +1,22 @@
version: '3.8'
services:
calorie-tracker:
build:
context: ./calorie_tracker_app
container_name: calorie-tracker
restart: unless-stopped
ports:
- "5001:5001"
volumes:
- ./data:/app/data
environment:
- DATABASE_URL=sqlite:////app/data/calorie_tracker.db
- SECRET_KEY=change-this-secret-key-in-production
- API_NINJAS_KEY=${API_NINJAS_KEY}
networks:
- casaos-net
networks:
casaos-net:
driver: bridge

BIN
instance/calorie_tracker.db Normal file

Binary file not shown.