Initial commit: full fitness-rewards app
Backend: FastAPI + PostgreSQL + SQLAlchemy async - Auth (JWT), onboarding (PAR-Q+, TTM, BREQ-2), activities, food log - Points engine with daily gate + weekly targets - Strava + Polar OAuth integration - Open Food Facts barcode lookup - Resource recommendation engine - 10 seeded Darebee/StrongLifts/YouTube programs Frontend: React + Vite, mobile-first dark theme - Login/Register, 8-step onboarding flow - Dashboard with daily gate status - Activity logger, food logger (manual + barcode scan + search) - Reward shop with redemption - Weekly summary view - Settings (point rules, targets, integrations) Deployment: Docker Compose for Coolify (Traefik)
This commit is contained in:
commit
4db2b0846b
61 changed files with 4280 additions and 0 deletions
111
backend/app/routers/food.py
Normal file
111
backend/app/routers/food.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
from typing import Annotated
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.food import FoodLog
|
||||
from app.schemas.food import FoodCreate, FoodSearchResult
|
||||
from app.utils.auth import get_current_user
|
||||
from app.services.food_lookup import lookup_barcode, search_food
|
||||
|
||||
router = APIRouter(prefix="/api/food", tags=["food"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_food(
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
food_date: date | None = Query(None, alias="date"),
|
||||
):
|
||||
query = select(FoodLog).where(FoodLog.user_id == user.id)
|
||||
if food_date:
|
||||
query = query.where(FoodLog.food_date == food_date)
|
||||
query = query.order_by(FoodLog.logged_at.desc()).limit(100)
|
||||
result = await db.execute(query)
|
||||
items = result.scalars().all()
|
||||
|
||||
# Group by meal type
|
||||
meals = {"breakfast": [], "lunch": [], "dinner": [], "snack": []}
|
||||
total_cals = 0
|
||||
for item in items:
|
||||
entry = {
|
||||
"id": str(item.id),
|
||||
"food_name": item.food_name,
|
||||
"brand": item.brand,
|
||||
"serving_size": item.serving_size,
|
||||
"servings": float(item.servings) if item.servings else 1,
|
||||
"calories": float(item.calories) if item.calories else None,
|
||||
"protein_g": float(item.protein_g) if item.protein_g else None,
|
||||
"carbs_g": float(item.carbs_g) if item.carbs_g else None,
|
||||
"fat_g": float(item.fat_g) if item.fat_g else None,
|
||||
}
|
||||
meals.get(item.meal_type, meals["snack"]).append(entry)
|
||||
if item.calories:
|
||||
total_cals += float(item.calories) * float(item.servings or 1)
|
||||
|
||||
return {"date": (food_date or date.today()).isoformat(), "meals": meals, "total_calories": round(total_cals, 1)}
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def log_food(
|
||||
req: FoodCreate,
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
):
|
||||
entry = FoodLog(
|
||||
user_id=user.id,
|
||||
meal_type=req.meal_type,
|
||||
food_name=req.food_name,
|
||||
brand=req.brand,
|
||||
barcode=req.barcode,
|
||||
serving_size=req.serving_size,
|
||||
servings=Decimal(str(req.servings)),
|
||||
calories=Decimal(str(req.calories)) if req.calories else None,
|
||||
protein_g=Decimal(str(req.protein_g)) if req.protein_g else None,
|
||||
carbs_g=Decimal(str(req.carbs_g)) if req.carbs_g else None,
|
||||
fat_g=Decimal(str(req.fat_g)) if req.fat_g else None,
|
||||
fiber_g=Decimal(str(req.fiber_g)) if req.fiber_g else None,
|
||||
sodium_mg=Decimal(str(req.sodium_mg)) if req.sodium_mg else None,
|
||||
sugar_g=Decimal(str(req.sugar_g)) if req.sugar_g else None,
|
||||
food_date=req.food_date,
|
||||
off_product_id=req.off_product_id,
|
||||
off_data=req.off_data,
|
||||
)
|
||||
db.add(entry)
|
||||
await db.flush()
|
||||
return {"id": str(entry.id), "food_name": entry.food_name, "calories": float(entry.calories) if entry.calories else None}
|
||||
|
||||
|
||||
@router.delete("/{food_id}")
|
||||
async def delete_food(
|
||||
food_id: str,
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
):
|
||||
import uuid
|
||||
result = await db.execute(
|
||||
select(FoodLog).where(FoodLog.id == uuid.UUID(food_id), FoodLog.user_id == user.id)
|
||||
)
|
||||
entry = result.scalar_one_or_none()
|
||||
if not entry:
|
||||
raise HTTPException(status_code=404, detail="Food entry not found")
|
||||
await db.delete(entry)
|
||||
return {"status": "deleted"}
|
||||
|
||||
|
||||
@router.get("/scan/{barcode}")
|
||||
async def scan_barcode(barcode: str):
|
||||
result = await lookup_barcode(barcode)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="Product not found in Open Food Facts")
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/search")
|
||||
async def food_search(q: str = Query(..., min_length=2)):
|
||||
results = await search_food(q)
|
||||
return {"results": results}
|
||||
Loading…
Add table
Add a link
Reference in a new issue