first commit

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

494
calorie_tracker_app/app.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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