mirror of
https://github.com/sstent/foodplanner.git
synced 2026-01-25 11:11:36 +00:00
openfood test
This commit is contained in:
Binary file not shown.
152
main.py
152
main.py
@@ -14,7 +14,9 @@ from typing import List, Optional
|
||||
from datetime import date, datetime
|
||||
import os
|
||||
import csv
|
||||
import requests
|
||||
from fastapi import File, UploadFile
|
||||
import openfoodfacts
|
||||
|
||||
# Database setup - Use SQLite for easier setup
|
||||
DATABASE_URL = "sqlite:///./meal_planner.db"
|
||||
@@ -31,7 +33,7 @@ templates = Jinja2Templates(directory="templates")
|
||||
# Database Models
|
||||
class Food(Base):
|
||||
__tablename__ = "foods"
|
||||
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, unique=True, index=True)
|
||||
serving_size = Column(String)
|
||||
@@ -44,6 +46,7 @@ class Food(Base):
|
||||
sugar = Column(Float, default=0)
|
||||
sodium = Column(Float, default=0)
|
||||
calcium = Column(Float, default=0)
|
||||
source = Column(String, default="manual") # manual, csv, openfoodfacts
|
||||
|
||||
class Meal(Base):
|
||||
__tablename__ = "meals"
|
||||
@@ -73,6 +76,7 @@ class Plan(Base):
|
||||
person = Column(String, index=True) # Person A or Person B
|
||||
date = Column(Date, index=True) # Store actual calendar dates
|
||||
meal_id = Column(Integer, ForeignKey("meals.id"))
|
||||
meal_time = Column(String, default="Breakfast") # Breakfast, Lunch, Dinner, Snack 1, Snack 2, Beverage 1, Beverage 2
|
||||
|
||||
meal = relationship("Meal")
|
||||
|
||||
@@ -109,6 +113,7 @@ class FoodCreate(BaseModel):
|
||||
sugar: float = 0
|
||||
sodium: float = 0
|
||||
calcium: float = 0
|
||||
source: str = "manual"
|
||||
|
||||
class FoodResponse(BaseModel):
|
||||
id: int
|
||||
@@ -123,6 +128,7 @@ class FoodResponse(BaseModel):
|
||||
sugar: float
|
||||
sodium: float
|
||||
calcium: float
|
||||
source: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -257,9 +263,13 @@ async def bulk_upload_foods(file: UploadFile = File(...), db: Session = Depends(
|
||||
# Update existing food
|
||||
for key, value in food_data.items():
|
||||
setattr(existing, key, value)
|
||||
# Ensure source is set for existing foods
|
||||
if not existing.source:
|
||||
existing.source = "csv"
|
||||
stats['updated'] += 1
|
||||
else:
|
||||
# Create new food
|
||||
food_data['source'] = "csv"
|
||||
food = Food(**food_data)
|
||||
db.add(food)
|
||||
stats['created'] += 1
|
||||
@@ -281,13 +291,14 @@ async def add_food(request: Request, db: Session = Depends(get_db),
|
||||
protein: float = Form(...), carbs: float = Form(...),
|
||||
fat: float = Form(...), fiber: float = Form(0),
|
||||
sugar: float = Form(0), sodium: float = Form(0),
|
||||
calcium: float = Form(0)):
|
||||
|
||||
calcium: float = Form(0), source: str = Form("manual")):
|
||||
|
||||
try:
|
||||
food = Food(
|
||||
name=name, serving_size=serving_size, serving_unit=serving_unit,
|
||||
calories=calories, protein=protein, carbs=carbs, fat=fat,
|
||||
fiber=fiber, sugar=sugar, sodium=sodium, calcium=calcium
|
||||
fiber=fiber, sugar=sugar, sodium=sodium, calcium=calcium,
|
||||
source=source
|
||||
)
|
||||
db.add(food)
|
||||
db.commit()
|
||||
@@ -298,18 +309,19 @@ async def add_food(request: Request, db: Session = Depends(get_db),
|
||||
|
||||
@app.post("/foods/edit")
|
||||
async def edit_food(request: Request, db: Session = Depends(get_db),
|
||||
food_id: int = Form(...), name: str = Form(...),
|
||||
serving_size: str = Form(...), serving_unit: str = Form(...),
|
||||
calories: float = Form(...), protein: float = Form(...),
|
||||
carbs: float = Form(...), fat: float = Form(...),
|
||||
fiber: float = Form(0), sugar: float = Form(0),
|
||||
sodium: float = Form(0), calcium: float = Form(0)):
|
||||
|
||||
food_id: int = Form(...), name: str = Form(...),
|
||||
serving_size: str = Form(...), serving_unit: str = Form(...),
|
||||
calories: float = Form(...), protein: float = Form(...),
|
||||
carbs: float = Form(...), fat: float = Form(...),
|
||||
fiber: float = Form(0), sugar: float = Form(0),
|
||||
sodium: float = Form(0), calcium: float = Form(0),
|
||||
source: str = Form("manual")):
|
||||
|
||||
try:
|
||||
food = db.query(Food).filter(Food.id == food_id).first()
|
||||
if not food:
|
||||
return {"status": "error", "message": "Food not found"}
|
||||
|
||||
|
||||
food.name = name
|
||||
food.serving_size = serving_size
|
||||
food.serving_unit = serving_unit
|
||||
@@ -321,7 +333,8 @@ async def edit_food(request: Request, db: Session = Depends(get_db),
|
||||
food.sugar = sugar
|
||||
food.sodium = sodium
|
||||
food.calcium = calcium
|
||||
|
||||
food.source = source
|
||||
|
||||
db.commit()
|
||||
return {"status": "success", "message": "Food updated successfully"}
|
||||
except Exception as e:
|
||||
@@ -339,6 +352,110 @@ async def delete_foods(food_ids: dict = Body(...), db: Session = Depends(get_db)
|
||||
db.rollback()
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
@app.get("/foods/search_openfoodfacts")
|
||||
async def search_openfoodfacts(query: str, limit: int = 10):
|
||||
"""Search OpenFoodFacts database for foods using the official SDK"""
|
||||
try:
|
||||
# Initialize OpenFoodFacts API with User-Agent
|
||||
api = openfoodfacts.API(user_agent="FoodPlanner/1.0")
|
||||
|
||||
# Perform text search
|
||||
search_result = api.product.text_search(query, page_size=limit)
|
||||
|
||||
results = []
|
||||
|
||||
if search_result and 'products' in search_result:
|
||||
for product in search_result['products']:
|
||||
# Extract nutritional information
|
||||
nutriments = product.get('nutriments', {})
|
||||
|
||||
# Get serving size
|
||||
serving_size = product.get('serving_size', '100g')
|
||||
if not serving_size or serving_size == '':
|
||||
serving_size = '100g'
|
||||
|
||||
# Extract serving quantity and unit
|
||||
serving_quantity = 100 # default to 100g
|
||||
serving_unit = 'g'
|
||||
|
||||
try:
|
||||
# Try to parse serving size (e.g., "30g", "1 cup")
|
||||
import re
|
||||
match = re.match(r'(\d+(?:\.\d+)?)\s*([a-zA-Z]+)', serving_size)
|
||||
if match:
|
||||
serving_quantity = float(match.group(1))
|
||||
serving_unit = match.group(2)
|
||||
else:
|
||||
# If no match, assume 100g
|
||||
serving_quantity = 100
|
||||
serving_unit = 'g'
|
||||
except:
|
||||
serving_quantity = 100
|
||||
serving_unit = 'g'
|
||||
|
||||
# Calculate per serving values (SDK returns per 100g values)
|
||||
def get_nutrient_value(key, default=0):
|
||||
value = nutriments.get(key, default)
|
||||
if value:
|
||||
try:
|
||||
# Convert to float if it's a string or other type
|
||||
numeric_value = float(value)
|
||||
if serving_quantity != 100:
|
||||
# Convert to per serving
|
||||
numeric_value = (numeric_value * serving_quantity) / 100
|
||||
return round(numeric_value, 2)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
return default
|
||||
|
||||
food_data = {
|
||||
'name': product.get('product_name', product.get('product_name_en', 'Unknown Product')),
|
||||
'serving_size': str(serving_quantity),
|
||||
'serving_unit': serving_unit,
|
||||
'calories': get_nutrient_value('energy-kcal_100g', 0),
|
||||
'protein': get_nutrient_value('proteins_100g', 0),
|
||||
'carbs': get_nutrient_value('carbohydrates_100g', 0),
|
||||
'fat': get_nutrient_value('fat_100g', 0),
|
||||
'fiber': get_nutrient_value('fiber_100g', 0),
|
||||
'sugar': get_nutrient_value('sugars_100g', 0),
|
||||
'sodium': get_nutrient_value('sodium_100g', 0),
|
||||
'calcium': get_nutrient_value('calcium_100g', 0),
|
||||
'source': 'openfoodfacts',
|
||||
'openfoodfacts_id': product.get('code', ''),
|
||||
'brand': product.get('brands', ''),
|
||||
'image_url': product.get('image_url', '')
|
||||
}
|
||||
|
||||
results.append(food_data)
|
||||
|
||||
return {"status": "success", "results": results}
|
||||
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": f"OpenFoodFacts search failed: {str(e)}"}
|
||||
|
||||
@app.post("/foods/add_openfoodfacts")
|
||||
async def add_openfoodfacts_food(request: Request, db: Session = Depends(get_db),
|
||||
name: str = Form(...), serving_size: str = Form(...),
|
||||
serving_unit: str = Form(...), calories: float = Form(...),
|
||||
protein: float = Form(...), carbs: float = Form(...),
|
||||
fat: float = Form(...), fiber: float = Form(0),
|
||||
sugar: float = Form(0), sodium: float = Form(0),
|
||||
calcium: float = Form(0), openfoodfacts_id: str = Form("")):
|
||||
|
||||
try:
|
||||
food = Food(
|
||||
name=name, serving_size=serving_size, serving_unit=serving_unit,
|
||||
calories=calories, protein=protein, carbs=carbs, fat=fat,
|
||||
fiber=fiber, sugar=sugar, sodium=sodium, calcium=calcium,
|
||||
source="openfoodfacts"
|
||||
)
|
||||
db.add(food)
|
||||
db.commit()
|
||||
return {"status": "success", "message": "Food added from OpenFoodFacts successfully"}
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
# Meals tab
|
||||
@app.get("/meals", response_class=HTMLResponse)
|
||||
async def meals_page(request: Request, db: Session = Depends(get_db)):
|
||||
@@ -590,12 +707,12 @@ async def plan_page(request: Request, person: str = "Person A", week_start_date:
|
||||
@app.post("/plan/add")
|
||||
async def add_to_plan(request: Request, person: str = Form(...),
|
||||
plan_date: str = Form(...), meal_id: int = Form(...),
|
||||
db: Session = Depends(get_db)):
|
||||
meal_time: str = Form("Breakfast"), db: Session = Depends(get_db)):
|
||||
|
||||
try:
|
||||
from datetime import datetime
|
||||
plan_date_obj = datetime.fromisoformat(plan_date).date()
|
||||
plan = Plan(person=person, date=plan_date_obj, meal_id=meal_id)
|
||||
plan = Plan(person=person, date=plan_date_obj, meal_id=meal_id, meal_time=meal_time)
|
||||
db.add(plan)
|
||||
db.commit()
|
||||
return {"status": "success"}
|
||||
@@ -616,7 +733,8 @@ async def get_day_plan(person: str, date: str, db: Session = Depends(get_db)):
|
||||
"id": plan.id,
|
||||
"meal_id": plan.meal_id,
|
||||
"meal_name": plan.meal.name,
|
||||
"meal_type": plan.meal.meal_type
|
||||
"meal_type": plan.meal.meal_type,
|
||||
"meal_time": plan.meal_time
|
||||
})
|
||||
return result
|
||||
except Exception as e:
|
||||
@@ -639,7 +757,7 @@ async def update_day_plan(request: Request, person: str = Form(...),
|
||||
|
||||
# Add new plans
|
||||
for meal_id in meal_id_list:
|
||||
plan = Plan(person=person, date=plan_date, meal_id=meal_id)
|
||||
plan = Plan(person=person, date=plan_date, meal_id=meal_id, meal_time="Breakfast")
|
||||
db.add(plan)
|
||||
|
||||
db.commit()
|
||||
|
||||
63
migrate_db_schema.py
Normal file
63
migrate_db_schema.py
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Database migration script to add new fields for food sources and meal times.
|
||||
Run this script to update the database schema.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from sqlalchemy import create_engine, text
|
||||
from main import Base, engine
|
||||
|
||||
def migrate_database():
|
||||
"""Add new columns to existing tables"""
|
||||
|
||||
# Connect to database
|
||||
conn = sqlite3.connect('./meal_planner.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check if source column exists in foods table
|
||||
cursor.execute("PRAGMA table_info(foods)")
|
||||
columns = cursor.fetchall()
|
||||
column_names = [col[1] for col in columns]
|
||||
|
||||
if 'source' not in column_names:
|
||||
print("Adding 'source' column to foods table...")
|
||||
cursor.execute("ALTER TABLE foods ADD COLUMN source TEXT DEFAULT 'manual'")
|
||||
print("✓ Added source column to foods table")
|
||||
|
||||
# Check if meal_time column exists in plans table
|
||||
cursor.execute("PRAGMA table_info(plans)")
|
||||
columns = cursor.fetchall()
|
||||
column_names = [col[1] for col in columns]
|
||||
|
||||
if 'meal_time' not in column_names:
|
||||
print("Adding 'meal_time' column to plans table...")
|
||||
cursor.execute("ALTER TABLE plans ADD COLUMN meal_time TEXT DEFAULT 'Breakfast'")
|
||||
print("✓ Added meal_time column to plans table")
|
||||
|
||||
# Update existing records to have proper source values
|
||||
print("Updating existing food records with source information...")
|
||||
|
||||
# Set source to 'csv' for foods that might have been imported
|
||||
# Note: This is a heuristic - you may need to adjust based on your data
|
||||
cursor.execute("""
|
||||
UPDATE foods
|
||||
SET source = 'csv'
|
||||
WHERE name LIKE '%(%'
|
||||
AND source = 'manual'
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
print("✓ Migration completed successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Migration failed: {e}")
|
||||
conn.rollback()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting database migration...")
|
||||
migrate_database()
|
||||
print("Migration script completed.")
|
||||
@@ -3,4 +3,5 @@ uvicorn[standard]==0.24.0
|
||||
sqlalchemy>=2.0.24
|
||||
#psycopg2-binary==2.9.9
|
||||
python-multipart==0.0.6
|
||||
jinja2==3.1.2
|
||||
jinja2==3.1.2
|
||||
openfoodfacts>=3.0.0
|
||||
@@ -145,6 +145,9 @@
|
||||
<div class="meal-header">
|
||||
<span>
|
||||
<i class="bi bi-egg-fried"></i> {{ meal_detail.plan.meal.name }} - {{ meal_detail.plan.meal.meal_type.title() }}
|
||||
{% if meal_detail.plan.meal_time %}
|
||||
<small class="text-muted">({{ meal_detail.plan.meal_time }})</small>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="badge bg-light text-dark">{{ "%.0f"|format(meal_detail.nutrition.calories) }} cal</span>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-4">
|
||||
<h3>Food Import</h3>
|
||||
<form action="/foods/upload" method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
@@ -11,8 +11,22 @@
|
||||
<button type="submit" class="btn btn-secondary mb-4">Upload Foods CSV</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
|
||||
<div class="col-md-4">
|
||||
<h3>OpenFoodFacts Search</h3>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Search for Food</label>
|
||||
<input type="text" class="form-control" id="offSearch" placeholder="e.g., apple, banana, pizza">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary mb-4" onclick="searchOpenFoodFacts()">Search</button>
|
||||
|
||||
<div id="offResults" class="mt-3" style="display: none;">
|
||||
<h6>Search Results:</h6>
|
||||
<div id="offResultsList" class="list-group"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<h3>Meal Import</h3>
|
||||
<form action="/meals/upload" method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
@@ -40,10 +54,10 @@ document.querySelectorAll('form').forEach(form => {
|
||||
e.preventDefault();
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const resultsDiv = document.getElementById('upload-results');
|
||||
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status"></span> Uploading...';
|
||||
|
||||
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
const response = await fetch(form.action, {
|
||||
@@ -52,7 +66,7 @@ document.querySelectorAll('form').forEach(form => {
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
|
||||
|
||||
const results = await response.json();
|
||||
resultsDiv.style.display = 'block';
|
||||
document.getElementById('created-count').textContent = results.created || 0;
|
||||
@@ -79,5 +93,107 @@ document.querySelectorAll('form').forEach(form => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// OpenFoodFacts search functionality
|
||||
async function searchOpenFoodFacts() {
|
||||
const query = document.getElementById('offSearch').value.trim();
|
||||
if (!query) {
|
||||
alert('Please enter a search term');
|
||||
return;
|
||||
}
|
||||
|
||||
const resultsDiv = document.getElementById('offResults');
|
||||
const resultsList = document.getElementById('offResultsList');
|
||||
|
||||
// Show loading
|
||||
resultsDiv.style.display = 'block';
|
||||
resultsList.innerHTML = '<div class="text-center"><div class="spinner-border" role="status"></div> Searching...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/foods/search_openfoodfacts?query=${encodeURIComponent(query)}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
displayOpenFoodFactsResults(data.results);
|
||||
} else {
|
||||
resultsList.innerHTML = `<div class="alert alert-danger">Error: ${data.message}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultsList.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function displayOpenFoodFactsResults(results) {
|
||||
const resultsList = document.getElementById('offResultsList');
|
||||
|
||||
if (results.length === 0) {
|
||||
resultsList.innerHTML = '<div class="alert alert-info">No results found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
results.forEach((food, index) => {
|
||||
html += `
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-1">${food.name}</h6>
|
||||
<p class="mb-1 text-muted small">
|
||||
${food.serving_size}${food.serving_unit} |
|
||||
${food.calories} cal |
|
||||
P: ${food.protein}g, C: ${food.carbs}g, F: ${food.fat}g
|
||||
</p>
|
||||
${food.brand ? `<small class="text-muted">Brand: ${food.brand}</small>` : ''}
|
||||
</div>
|
||||
<button class="btn btn-sm btn-success" onclick="addOpenFoodFactsFood(${index})">
|
||||
<i class="bi bi-plus-circle"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
resultsList.innerHTML = html;
|
||||
|
||||
// Store results for later use
|
||||
window.offSearchResults = results;
|
||||
}
|
||||
|
||||
async function addOpenFoodFactsFood(index) {
|
||||
const food = window.offSearchResults[index];
|
||||
if (!food) return;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
Object.keys(food).forEach(key => {
|
||||
if (key !== 'image_url' && key !== 'openfoodfacts_id' && key !== 'brand') {
|
||||
formData.append(key, food[key]);
|
||||
}
|
||||
});
|
||||
|
||||
const response = await fetch('/foods/add_openfoodfacts', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'success') {
|
||||
alert('Food added successfully!');
|
||||
// Optionally reload the page or update UI
|
||||
} else {
|
||||
alert('Error adding food: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error adding food: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow Enter key to trigger search
|
||||
document.getElementById('offSearch').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
searchOpenFoodFacts();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -43,7 +43,9 @@
|
||||
</td>
|
||||
<td>
|
||||
{% for plan in plans[day.date.isoformat()] %}
|
||||
<span class="badge bg-secondary me-1">{{ plan.meal.name }}</span>
|
||||
<span class="badge bg-secondary me-1" title="{{ plan.meal_time }}">
|
||||
<small>{{ plan.meal_time }}:</small> {{ plan.meal.name }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% if not plans[day.date.isoformat()] %}
|
||||
<em class="text-muted">No meals</em>
|
||||
@@ -89,6 +91,18 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Meal Time</label>
|
||||
<select class="form-control" name="meal_time" required>
|
||||
<option value="Breakfast">Breakfast</option>
|
||||
<option value="Lunch">Lunch</option>
|
||||
<option value="Dinner">Dinner</option>
|
||||
<option value="Snack 1">Snack 1</option>
|
||||
<option value="Snack 2">Snack 2</option>
|
||||
<option value="Beverage 1">Beverage 1</option>
|
||||
<option value="Beverage 2">Beverage 2</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -127,6 +141,18 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Meal Time</label>
|
||||
<select class="form-control" id="mealTimeSelectForDay" name="meal_time" required>
|
||||
<option value="Breakfast">Breakfast</option>
|
||||
<option value="Lunch">Lunch</option>
|
||||
<option value="Dinner">Dinner</option>
|
||||
<option value="Snack 1">Snack 1</option>
|
||||
<option value="Snack 2">Snack 2</option>
|
||||
<option value="Beverage 1">Beverage 1</option>
|
||||
<option value="Beverage 2">Beverage 2</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="addMealToCurrentDay()">
|
||||
<i class="bi bi-plus"></i> Add Selected Meal
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user