first commit
This commit is contained in:
397
QUICK_START.md
Normal file
397
QUICK_START.md
Normal 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
50
README_DOCKER.md
Normal 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` |
|
||||||
8
calorie_tracker_app/.dockerignore
Normal file
8
calorie_tracker_app/.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
venv/
|
||||||
|
__pycache__/
|
||||||
|
instance/
|
||||||
|
.env
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
*.db
|
||||||
|
*.pyc
|
||||||
7
calorie_tracker_app/.env.example
Normal file
7
calorie_tracker_app/.env.example
Normal 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
|
||||||
24
calorie_tracker_app/Dockerfile
Normal file
24
calorie_tracker_app/Dockerfile
Normal 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"]
|
||||||
384
calorie_tracker_app/README.md
Normal file
384
calorie_tracker_app/README.md
Normal 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! 🎉
|
||||||
BIN
calorie_tracker_app/__pycache__/api_client.cpython-314.pyc
Normal file
BIN
calorie_tracker_app/__pycache__/api_client.cpython-314.pyc
Normal file
Binary file not shown.
BIN
calorie_tracker_app/__pycache__/app.cpython-314.pyc
Normal file
BIN
calorie_tracker_app/__pycache__/app.cpython-314.pyc
Normal file
Binary file not shown.
BIN
calorie_tracker_app/__pycache__/config.cpython-314.pyc
Normal file
BIN
calorie_tracker_app/__pycache__/config.cpython-314.pyc
Normal file
Binary file not shown.
BIN
calorie_tracker_app/__pycache__/models.cpython-314.pyc
Normal file
BIN
calorie_tracker_app/__pycache__/models.cpython-314.pyc
Normal file
Binary file not shown.
BIN
calorie_tracker_app/__pycache__/utils.cpython-314.pyc
Normal file
BIN
calorie_tracker_app/__pycache__/utils.cpython-314.pyc
Normal file
Binary file not shown.
209
calorie_tracker_app/api_client.py
Normal file
209
calorie_tracker_app/api_client.py
Normal 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
494
calorie_tracker_app/app.py
Normal 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)
|
||||||
16
calorie_tracker_app/config.py
Normal file
16
calorie_tracker_app/config.py
Normal 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
|
||||||
186
calorie_tracker_app/models.py
Normal file
186
calorie_tracker_app/models.py
Normal 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)
|
||||||
7
calorie_tracker_app/requirements.txt
Normal file
7
calorie_tracker_app/requirements.txt
Normal 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
|
||||||
9
calorie_tracker_app/run_app.bat
Normal file
9
calorie_tracker_app/run_app.bat
Normal 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
|
||||||
368
calorie_tracker_app/seed_data.py
Normal file
368
calorie_tracker_app/seed_data.py
Normal 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()
|
||||||
6
calorie_tracker_app/seed_db.bat
Normal file
6
calorie_tracker_app/seed_db.bat
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@echo off
|
||||||
|
echo Seeding Filipino Foods database...
|
||||||
|
venv\Scripts\python seed_data.py
|
||||||
|
echo.
|
||||||
|
echo Done!
|
||||||
|
pause
|
||||||
221
calorie_tracker_app/templates/add_meal.html
Normal file
221
calorie_tracker_app/templates/add_meal.html
Normal 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 %}
|
||||||
92
calorie_tracker_app/templates/base.html
Normal file
92
calorie_tracker_app/templates/base.html
Normal 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>© 2026 Calorie Tracker - Filipino Food Edition</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
300
calorie_tracker_app/templates/dashboard.html
Normal file
300
calorie_tracker_app/templates/dashboard.html
Normal 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 %}
|
||||||
14
calorie_tracker_app/templates/foods.html
Normal file
14
calorie_tracker_app/templates/foods.html
Normal 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 %}
|
||||||
21
calorie_tracker_app/templates/goals.html
Normal file
21
calorie_tracker_app/templates/goals.html
Normal 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 %}
|
||||||
32
calorie_tracker_app/templates/login.html
Normal file
32
calorie_tracker_app/templates/login.html
Normal 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 %}
|
||||||
6
calorie_tracker_app/templates/meal_planner.html
Normal file
6
calorie_tracker_app/templates/meal_planner.html
Normal 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 %}
|
||||||
8
calorie_tracker_app/templates/progress.html
Normal file
8
calorie_tracker_app/templates/progress.html
Normal 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 %}
|
||||||
36
calorie_tracker_app/templates/register.html
Normal file
36
calorie_tracker_app/templates/register.html
Normal 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 %}
|
||||||
258
calorie_tracker_app/utils.py
Normal file
258
calorie_tracker_app/utils.py
Normal 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
546
customized_build_plan.md
Normal 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
22
docker-compose.yml
Normal 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
BIN
instance/calorie_tracker.db
Normal file
Binary file not shown.
Reference in New Issue
Block a user