Upload 8 files
Browse files- main.py +275 -0
- static/css/styles.css +117 -0
- static/data/food_items.json +42 -0
- static/js/app.js +247 -0
- templates/dashboard.html +0 -0
- templates/index.html +99 -0
- templates/login.html +0 -0
- templates/register.html +0 -0
main.py
ADDED
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, request, jsonify, render_template
|
2 |
+
import requests
|
3 |
+
import os
|
4 |
+
import math
|
5 |
+
import logging
|
6 |
+
|
7 |
+
app = Flask(__name__)
|
8 |
+
app.static_folder = 'static'
|
9 |
+
|
10 |
+
# Configure logging
|
11 |
+
logging.basicConfig(level=logging.DEBUG)
|
12 |
+
|
13 |
+
# Configuration
|
14 |
+
USDA_API_ENDPOINT = "https://api.nal.usda.gov/fdc/v1"
|
15 |
+
USDA_API_KEY = os.environ.get('USDA_API_KEY')
|
16 |
+
|
17 |
+
# Error messages
|
18 |
+
INVALID_INPUT_ERROR = "Invalid input"
|
19 |
+
MISSING_REQUIRED_FIELDS_ERROR = "Missing required fields"
|
20 |
+
FAILED_TO_FETCH_NUTRIENT_DATA_ERROR = "Failed to fetch nutrient data"
|
21 |
+
|
22 |
+
@app.route('/')
|
23 |
+
def index():
|
24 |
+
return render_template('index.html')
|
25 |
+
|
26 |
+
@app.route('/api/calculate-metrics', methods=['POST'])
|
27 |
+
def calculate_metrics():
|
28 |
+
data = request.json
|
29 |
+
app.logger.debug(f"Received data for calculate_metrics: {data}")
|
30 |
+
|
31 |
+
required_fields = ['age', 'gender', 'heightFeet', 'heightInches', 'weight', 'targetWeight', 'waist', 'neck', 'hip', 'steps', 'standingHours']
|
32 |
+
missing_fields = [field for field in required_fields if field not in data or data[field] is None]
|
33 |
+
|
34 |
+
if missing_fields:
|
35 |
+
app.logger.error(f"Missing required fields: {', '.join(missing_fields)}")
|
36 |
+
return jsonify({"error": MISSING_REQUIRED_FIELDS_ERROR, "missing_fields": missing_fields}), 400
|
37 |
+
|
38 |
+
try:
|
39 |
+
age = int(data['age'])
|
40 |
+
gender = data['gender']
|
41 |
+
height_feet = int(data['heightFeet'])
|
42 |
+
height_inches = int(data['heightInches'])
|
43 |
+
weight = float(data['weight']) # in kg
|
44 |
+
target_weight = float(data['targetWeight']) # in kg
|
45 |
+
waist = float(data['waist']) # in cm
|
46 |
+
neck = float(data['neck']) # in cm
|
47 |
+
hip = float(data['hip']) # in cm
|
48 |
+
steps = int(data['steps'])
|
49 |
+
standing_hours = float(data['standingHours'])
|
50 |
+
|
51 |
+
# Convert height to cm
|
52 |
+
height = (height_feet * 30.48) + (height_inches * 2.54) # Convert to cm
|
53 |
+
|
54 |
+
except (ValueError, KeyError) as e:
|
55 |
+
app.logger.error(f"Invalid input: {str(e)}")
|
56 |
+
return jsonify({"error": INVALID_INPUT_ERROR, "details": str(e)}), 400
|
57 |
+
|
58 |
+
if age <= 0 or height <= 0 or weight <= 0 or target_weight <= 0 or waist <= 0 or neck <= 0 or hip <= 0 or steps < 0 or standing_hours < 0:
|
59 |
+
app.logger.error("Invalid input values")
|
60 |
+
return jsonify({"error": INVALID_INPUT_ERROR, "details": "Input values must be positive numbers"}), 400
|
61 |
+
|
62 |
+
if gender not in ['male', 'female', 'other']:
|
63 |
+
app.logger.error("Invalid gender")
|
64 |
+
return jsonify({"error": INVALID_INPUT_ERROR, "details": "Gender must be 'male', 'female', or 'other'"}), 400
|
65 |
+
|
66 |
+
# Calculate BMI
|
67 |
+
bmi = weight / ((height / 100) ** 2)
|
68 |
+
|
69 |
+
# Calculate body fat percentage (using U.S. Navy method)
|
70 |
+
if gender == 'male':
|
71 |
+
body_fat = 86.010 * math.log10(waist - neck) - 70.041 * math.log10(height) + 36.76
|
72 |
+
elif gender == 'female':
|
73 |
+
body_fat = 163.205 * math.log10(waist + hip - neck) - 97.684 * math.log10(height) - 78.387
|
74 |
+
else:
|
75 |
+
# For 'other' gender, use an average of male and female calculations
|
76 |
+
body_fat_male = 86.010 * math.log10(waist - neck) - 70.041 * math.log10(height) + 36.76
|
77 |
+
body_fat_female = 163.205 * math.log10(waist + hip - neck) - 97.684 * math.log10(height) - 78.387
|
78 |
+
body_fat = (body_fat_male + body_fat_female) / 2
|
79 |
+
|
80 |
+
# Calculate lean body mass
|
81 |
+
lean_body_mass = weight * (1 - (body_fat / 100))
|
82 |
+
|
83 |
+
# Calculate recommended calorie intake (using Mifflin-St Jeor Equation)
|
84 |
+
if gender == 'male':
|
85 |
+
bmr = 10 * weight + 6.25 * height - 5 * age + 5
|
86 |
+
elif gender == 'female':
|
87 |
+
bmr = 10 * weight + 6.25 * height - 5 * age - 161
|
88 |
+
else:
|
89 |
+
# For 'other' gender, use an average of male and female calculations
|
90 |
+
bmr_male = 10 * weight + 6.25 * height - 5 * age + 5
|
91 |
+
bmr_female = 10 * weight + 6.25 * height - 5 * age - 161
|
92 |
+
bmr = (bmr_male + bmr_female) / 2
|
93 |
+
|
94 |
+
# Adjust for activity level
|
95 |
+
activity_factor = 1.2 + (steps / 10000) * 0.1 + (standing_hours / 24) * 0.1
|
96 |
+
recommended_calories = bmr * activity_factor
|
97 |
+
|
98 |
+
# Calculate time to reach target weight
|
99 |
+
weight_difference = abs(weight - target_weight)
|
100 |
+
daily_calorie_deficit = 500 # Assuming a 500 calorie deficit per day
|
101 |
+
days_to_target = (weight_difference * 7700) / daily_calorie_deficit # 7700 calories ≈ 1 kg of body fat
|
102 |
+
|
103 |
+
response = {
|
104 |
+
'bmi': round(bmi, 2),
|
105 |
+
'bodyFatPercentage': round(body_fat, 2),
|
106 |
+
'leanBodyMass': round(lean_body_mass, 2),
|
107 |
+
'recommendedCalories': round(recommended_calories),
|
108 |
+
'timeToTargetWeight': f"{round(days_to_target)} days"
|
109 |
+
}
|
110 |
+
|
111 |
+
app.logger.debug(f"Calculated metrics: {response}")
|
112 |
+
return jsonify(response)
|
113 |
+
|
114 |
+
@app.route('/api/personalized-recommendations', methods=['POST'])
|
115 |
+
def get_personalized_recommendations():
|
116 |
+
data = request.json
|
117 |
+
app.logger.debug(f"Received data for personalized_recommendations: {data}")
|
118 |
+
|
119 |
+
required_fields = ['age', 'gender', 'height', 'weight', 'targetWeight', 'bmi', 'bodyFatPercentage', 'recommendedCalories', 'steps', 'standingHours']
|
120 |
+
missing_fields = [field for field in required_fields if field not in data or data[field] is None]
|
121 |
+
if missing_fields:
|
122 |
+
app.logger.error(f"Missing required fields: {', '.join(missing_fields)}")
|
123 |
+
return jsonify({"error": MISSING_REQUIRED_FIELDS_ERROR, "missing_fields": missing_fields}), 400
|
124 |
+
|
125 |
+
try:
|
126 |
+
age = int(data['age'])
|
127 |
+
gender = data['gender']
|
128 |
+
height = float(data['height'])
|
129 |
+
weight = float(data['weight'])
|
130 |
+
target_weight = float(data['targetWeight'])
|
131 |
+
bmi = float(data['bmi'])
|
132 |
+
body_fat_percentage = float(data['bodyFatPercentage'])
|
133 |
+
recommended_calories = int(data['recommendedCalories'])
|
134 |
+
steps = int(data['steps'])
|
135 |
+
standing_hours = float(data['standingHours'])
|
136 |
+
except (ValueError, KeyError) as e:
|
137 |
+
app.logger.error(f"Invalid input: {str(e)}")
|
138 |
+
return jsonify({"error": INVALID_INPUT_ERROR, "details": str(e)}), 400
|
139 |
+
|
140 |
+
# Diet recommendations
|
141 |
+
diet_recommendations = []
|
142 |
+
if bmi < 18.5:
|
143 |
+
diet_recommendations.append("Increase calorie intake with nutrient-dense foods to reach a healthy weight")
|
144 |
+
diet_recommendations.append("Focus on foods high in healthy fats, such as avocados, nuts, and olive oil")
|
145 |
+
diet_recommendations.append("Incorporate protein-rich foods like lean meats, fish, eggs, and legumes")
|
146 |
+
elif 18.5 <= bmi < 25:
|
147 |
+
diet_recommendations.append("Maintain a balanced diet with a focus on whole foods")
|
148 |
+
diet_recommendations.append("Ensure adequate intake of fruits, vegetables, whole grains, and lean proteins")
|
149 |
+
diet_recommendations.append("Monitor portion sizes to maintain your healthy weight")
|
150 |
+
elif 25 <= bmi < 30:
|
151 |
+
diet_recommendations.append("Slightly reduce calorie intake and focus on nutrient-dense, low-calorie foods")
|
152 |
+
diet_recommendations.append("Increase fiber intake through vegetables, fruits, and whole grains")
|
153 |
+
diet_recommendations.append("Choose lean proteins and limit saturated fats")
|
154 |
+
else:
|
155 |
+
diet_recommendations.append("Reduce calorie intake and focus on whole, unprocessed foods")
|
156 |
+
diet_recommendations.append("Prioritize vegetables, lean proteins, and complex carbohydrates")
|
157 |
+
diet_recommendations.append("Avoid sugary drinks and high-calorie snacks")
|
158 |
+
|
159 |
+
if (gender == 'male' and body_fat_percentage > 25) or (gender == 'female' and body_fat_percentage > 32) or (gender == 'other' and body_fat_percentage > 28):
|
160 |
+
diet_recommendations.append("Increase protein intake to support lean muscle mass")
|
161 |
+
diet_recommendations.append("Consider adding a protein shake or Greek yogurt as a snack")
|
162 |
+
diet_recommendations.append("Include more fish, chicken, turkey, or plant-based proteins in your meals")
|
163 |
+
|
164 |
+
diet_recommendations.append(f"Aim for {recommended_calories} calories per day")
|
165 |
+
diet_recommendations.append("Include a variety of colorful fruits and vegetables in your diet")
|
166 |
+
diet_recommendations.append("Stay hydrated by drinking at least 8 glasses of water daily")
|
167 |
+
diet_recommendations.append("Limit processed foods and choose whole grains over refined grains")
|
168 |
+
|
169 |
+
if weight > target_weight:
|
170 |
+
diet_recommendations.append("Create a calorie deficit of 500 calories per day to lose weight")
|
171 |
+
diet_recommendations.append("Use smaller plates to help control portion sizes")
|
172 |
+
diet_recommendations.append("Start meals with a salad or vegetable soup to increase satiety")
|
173 |
+
elif weight < target_weight:
|
174 |
+
diet_recommendations.append("Increase your calorie intake by 500 calories per day to gain weight")
|
175 |
+
diet_recommendations.append("Add healthy, calorie-dense foods like nuts, seeds, and dried fruits to your meals")
|
176 |
+
diet_recommendations.append("Consider drinking smoothies made with fruits, oats, and protein powder")
|
177 |
+
else:
|
178 |
+
diet_recommendations.append("Maintain your current calorie intake to maintain your weight")
|
179 |
+
diet_recommendations.append("Practice mindful eating and listen to your body's hunger and fullness cues")
|
180 |
+
|
181 |
+
# Exercise recommendations
|
182 |
+
exercise_recommendations = []
|
183 |
+
if steps < 5000:
|
184 |
+
exercise_recommendations.append("Gradually increase your daily step count to at least 7,500 steps")
|
185 |
+
exercise_recommendations.append("Take short walks during breaks or after meals")
|
186 |
+
exercise_recommendations.append("Use stairs instead of elevators when possible")
|
187 |
+
elif 5000 <= steps < 10000:
|
188 |
+
exercise_recommendations.append("Aim to reach 10,000 steps per day for better health")
|
189 |
+
exercise_recommendations.append("Try brisk walking or light jogging to increase step count")
|
190 |
+
exercise_recommendations.append("Consider using a treadmill desk or walking meetings")
|
191 |
+
else:
|
192 |
+
exercise_recommendations.append("Great job on your step count! Consider adding more intense exercises")
|
193 |
+
exercise_recommendations.append("Incorporate interval training or hill walks to challenge yourself")
|
194 |
+
exercise_recommendations.append("Set new step goals to maintain motivation")
|
195 |
+
|
196 |
+
if standing_hours < 2:
|
197 |
+
exercise_recommendations.append("Try to increase your standing time to at least 2-4 hours per day")
|
198 |
+
exercise_recommendations.append("Use a standing desk or elevate your workstation for part of the day")
|
199 |
+
exercise_recommendations.append("Take phone calls while standing or walking")
|
200 |
+
elif 2 <= standing_hours < 4:
|
201 |
+
exercise_recommendations.append("Good job on standing! Aim to increase your standing time to 4-6 hours per day")
|
202 |
+
exercise_recommendations.append("Alternate between sitting and standing every 30-60 minutes")
|
203 |
+
exercise_recommendations.append("Try gentle exercises or stretches while standing")
|
204 |
+
else:
|
205 |
+
exercise_recommendations.append("Excellent standing habits! Maintain your current standing routine")
|
206 |
+
exercise_recommendations.append("Incorporate balance exercises or yoga poses while standing")
|
207 |
+
exercise_recommendations.append("Consider a treadmill desk for light walking while working")
|
208 |
+
|
209 |
+
exercise_recommendations.append("Include strength training exercises at least 2-3 times per week")
|
210 |
+
exercise_recommendations.append("Aim for at least 150 minutes of moderate-intensity aerobic activity per week")
|
211 |
+
exercise_recommendations.append("Don't forget to stretch before and after exercises to improve flexibility")
|
212 |
+
|
213 |
+
if weight > target_weight:
|
214 |
+
exercise_recommendations.append("Incorporate high-intensity interval training (HIIT) to boost fat burning")
|
215 |
+
exercise_recommendations.append("Try circuit training to combine strength and cardio exercises")
|
216 |
+
exercise_recommendations.append("Consider joining group fitness classes for motivation and guidance")
|
217 |
+
elif weight < target_weight:
|
218 |
+
exercise_recommendations.append("Focus on compound exercises and progressive overload to build muscle mass")
|
219 |
+
exercise_recommendations.append("Incorporate resistance band exercises for muscle growth")
|
220 |
+
exercise_recommendations.append("Ensure adequate rest between workouts for muscle recovery and growth")
|
221 |
+
else:
|
222 |
+
exercise_recommendations.append("Mix cardio and strength training to maintain your current weight and improve overall fitness")
|
223 |
+
exercise_recommendations.append("Try new activities or sports to keep your routine interesting")
|
224 |
+
exercise_recommendations.append("Set performance-based goals to stay motivated")
|
225 |
+
|
226 |
+
if age > 50:
|
227 |
+
exercise_recommendations.append("Include balance and flexibility exercises to maintain mobility")
|
228 |
+
exercise_recommendations.append("Consider low-impact activities like swimming or cycling to protect joints")
|
229 |
+
|
230 |
+
response = {
|
231 |
+
'dietRecommendations': diet_recommendations,
|
232 |
+
'exerciseRecommendations': exercise_recommendations
|
233 |
+
}
|
234 |
+
|
235 |
+
app.logger.debug(f"Generated recommendations: {response}")
|
236 |
+
return jsonify(response)
|
237 |
+
|
238 |
+
@app.route('/api/search-food', methods=['GET'])
|
239 |
+
def search_food():
|
240 |
+
query = request.args.get('query', '')
|
241 |
+
if not query:
|
242 |
+
return jsonify({"error": "Missing query parameter"}), 400
|
243 |
+
|
244 |
+
try:
|
245 |
+
response = requests.get(
|
246 |
+
f"{USDA_API_ENDPOINT}/foods/search",
|
247 |
+
params={
|
248 |
+
"api_key": USDA_API_KEY,
|
249 |
+
"query": query,
|
250 |
+
"dataType": ["Survey (FNDDS)"],
|
251 |
+
"pageSize": 10
|
252 |
+
}
|
253 |
+
)
|
254 |
+
response.raise_for_status()
|
255 |
+
data = response.json()
|
256 |
+
|
257 |
+
results = []
|
258 |
+
for food in data.get('foods', []):
|
259 |
+
nutrients = {nutrient['nutrientName']: nutrient['value'] for nutrient in food.get('foodNutrients', [])}
|
260 |
+
results.append({
|
261 |
+
'description': food['description'],
|
262 |
+
'calories': nutrients.get('Energy', 0),
|
263 |
+
'protein': nutrients.get('Protein', 0),
|
264 |
+
'carbs': nutrients.get('Carbohydrate, by difference', 0),
|
265 |
+
'fat': nutrients.get('Total lipid (fat)', 0)
|
266 |
+
})
|
267 |
+
|
268 |
+
return jsonify(results)
|
269 |
+
|
270 |
+
except requests.RequestException as e:
|
271 |
+
app.logger.error(f"Error fetching food data: {str(e)}")
|
272 |
+
return jsonify({"error": "Failed to fetch food data"}), 500
|
273 |
+
|
274 |
+
if __name__ == '__main__':
|
275 |
+
app.run(host='0.0.0.0', port=5000, debug=True)
|
static/css/styles.css
ADDED
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
body {
|
2 |
+
font-family: Arial, sans-serif;
|
3 |
+
line-height: 1.6;
|
4 |
+
margin: 0;
|
5 |
+
padding: 20px;
|
6 |
+
max-width: 800px;
|
7 |
+
margin: 0 auto;
|
8 |
+
}
|
9 |
+
|
10 |
+
h1, h2, h3 {
|
11 |
+
color: #333;
|
12 |
+
}
|
13 |
+
|
14 |
+
form {
|
15 |
+
margin-bottom: 20px;
|
16 |
+
}
|
17 |
+
|
18 |
+
label {
|
19 |
+
display: block;
|
20 |
+
margin-bottom: 5px;
|
21 |
+
}
|
22 |
+
|
23 |
+
input, select {
|
24 |
+
width: 100%;
|
25 |
+
padding: 5px;
|
26 |
+
margin-bottom: 10px;
|
27 |
+
}
|
28 |
+
|
29 |
+
button {
|
30 |
+
background-color: #4CAF50;
|
31 |
+
color: white;
|
32 |
+
padding: 10px 15px;
|
33 |
+
border: none;
|
34 |
+
cursor: pointer;
|
35 |
+
}
|
36 |
+
|
37 |
+
button:hover {
|
38 |
+
background-color: #45a049;
|
39 |
+
}
|
40 |
+
|
41 |
+
ul {
|
42 |
+
list-style-type: none;
|
43 |
+
padding: 0;
|
44 |
+
}
|
45 |
+
|
46 |
+
li {
|
47 |
+
cursor: pointer;
|
48 |
+
padding: 5px;
|
49 |
+
margin-bottom: 5px;
|
50 |
+
background-color: #f0f0f0;
|
51 |
+
}
|
52 |
+
|
53 |
+
li:hover {
|
54 |
+
background-color: #e0e0e0;
|
55 |
+
}
|
56 |
+
|
57 |
+
canvas {
|
58 |
+
border: 1px solid #ddd;
|
59 |
+
margin-top: 20px;
|
60 |
+
}
|
61 |
+
|
62 |
+
.flex-container {
|
63 |
+
display: flex;
|
64 |
+
justify-content: space-between;
|
65 |
+
}
|
66 |
+
|
67 |
+
.flex-container > div {
|
68 |
+
flex: 1;
|
69 |
+
margin-right: 20px;
|
70 |
+
}
|
71 |
+
|
72 |
+
#visualization {
|
73 |
+
text-align: center;
|
74 |
+
}
|
75 |
+
|
76 |
+
#bmi-display {
|
77 |
+
margin-top: 20px;
|
78 |
+
}
|
79 |
+
|
80 |
+
.notification {
|
81 |
+
position: fixed;
|
82 |
+
top: 20px;
|
83 |
+
right: 20px;
|
84 |
+
padding: 10px 20px;
|
85 |
+
border-radius: 5px;
|
86 |
+
color: white;
|
87 |
+
font-weight: bold;
|
88 |
+
opacity: 0.9;
|
89 |
+
transition: opacity 0.3s ease-in-out;
|
90 |
+
}
|
91 |
+
|
92 |
+
.notification.success {
|
93 |
+
background-color: #4CAF50;
|
94 |
+
}
|
95 |
+
|
96 |
+
.notification.info {
|
97 |
+
background-color: #2196F3;
|
98 |
+
}
|
99 |
+
|
100 |
+
.notification.error {
|
101 |
+
background-color: #f44336;
|
102 |
+
}
|
103 |
+
|
104 |
+
@media (max-width: 600px) {
|
105 |
+
body {
|
106 |
+
padding: 10px;
|
107 |
+
}
|
108 |
+
|
109 |
+
.flex-container {
|
110 |
+
flex-direction: column;
|
111 |
+
}
|
112 |
+
|
113 |
+
.flex-container > div {
|
114 |
+
margin-right: 0;
|
115 |
+
margin-bottom: 20px;
|
116 |
+
}
|
117 |
+
}
|
static/data/food_items.json
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"Apple": {
|
3 |
+
"calories": 95,
|
4 |
+
"effect": "decrease"
|
5 |
+
},
|
6 |
+
"Banana": {
|
7 |
+
"calories": 105,
|
8 |
+
"effect": "decrease"
|
9 |
+
},
|
10 |
+
"Chicken Breast": {
|
11 |
+
"calories": 165,
|
12 |
+
"effect": "decrease"
|
13 |
+
},
|
14 |
+
"Pizza Slice": {
|
15 |
+
"calories": 285,
|
16 |
+
"effect": "increase"
|
17 |
+
},
|
18 |
+
"Salad": {
|
19 |
+
"calories": 100,
|
20 |
+
"effect": "decrease"
|
21 |
+
},
|
22 |
+
"Cheeseburger": {
|
23 |
+
"calories": 300,
|
24 |
+
"effect": "increase"
|
25 |
+
},
|
26 |
+
"Yogurt": {
|
27 |
+
"calories": 150,
|
28 |
+
"effect": "decrease"
|
29 |
+
},
|
30 |
+
"Ice Cream": {
|
31 |
+
"calories": 270,
|
32 |
+
"effect": "increase"
|
33 |
+
},
|
34 |
+
"Broccoli": {
|
35 |
+
"calories": 55,
|
36 |
+
"effect": "decrease"
|
37 |
+
},
|
38 |
+
"French Fries": {
|
39 |
+
"calories": 365,
|
40 |
+
"effect": "increase"
|
41 |
+
}
|
42 |
+
}
|
static/js/app.js
ADDED
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
2 |
+
const userForm = document.getElementById('user-form');
|
3 |
+
const selectedFoodList = document.getElementById('selected-food-list');
|
4 |
+
const canvas = document.getElementById('belly-fat-canvas');
|
5 |
+
const ctx = canvas.getContext('2d');
|
6 |
+
const searchInput = document.getElementById('food-search');
|
7 |
+
const searchResults = document.getElementById('search-results');
|
8 |
+
|
9 |
+
let selectedItems = [];
|
10 |
+
let userMetrics = {};
|
11 |
+
let recommendedCalories = 2000; // Default value, will be updated after user submits metrics
|
12 |
+
|
13 |
+
function updateUserMetrics() {
|
14 |
+
userMetrics.age = parseInt(document.getElementById('age').value);
|
15 |
+
userMetrics.gender = document.getElementById('gender').value;
|
16 |
+
userMetrics.heightFeet = parseInt(document.getElementById('height-feet').value);
|
17 |
+
userMetrics.heightInches = parseInt(document.getElementById('height-inches').value);
|
18 |
+
userMetrics.weight = parseFloat(document.getElementById('weight').value);
|
19 |
+
userMetrics.targetWeight = parseFloat(document.getElementById('target-weight').value);
|
20 |
+
userMetrics.waist = parseFloat(document.getElementById('waist').value);
|
21 |
+
userMetrics.neck = parseFloat(document.getElementById('neck').value);
|
22 |
+
userMetrics.hip = parseFloat(document.getElementById('hip').value);
|
23 |
+
userMetrics.steps = parseInt(document.getElementById('steps').value);
|
24 |
+
userMetrics.standingHours = parseFloat(document.getElementById('standing-hours').value);
|
25 |
+
}
|
26 |
+
|
27 |
+
userForm.addEventListener('submit', (e) => {
|
28 |
+
e.preventDefault();
|
29 |
+
updateUserMetrics();
|
30 |
+
calculateMetrics();
|
31 |
+
});
|
32 |
+
|
33 |
+
function calculateMetrics() {
|
34 |
+
console.log('calculateMetrics function called');
|
35 |
+
const heightInCm = (userMetrics.heightFeet * 30.48) + (userMetrics.heightInches * 2.54);
|
36 |
+
const weightInKg = userMetrics.weight * 0.45359237;
|
37 |
+
const targetWeightInKg = userMetrics.targetWeight * 0.45359237;
|
38 |
+
const waistInCm = userMetrics.waist * 2.54;
|
39 |
+
const neckInCm = userMetrics.neck * 2.54;
|
40 |
+
const hipInCm = userMetrics.hip * 2.54;
|
41 |
+
|
42 |
+
const metricsData = {
|
43 |
+
age: userMetrics.age,
|
44 |
+
gender: userMetrics.gender,
|
45 |
+
heightFeet: userMetrics.heightFeet,
|
46 |
+
heightInches: userMetrics.heightInches,
|
47 |
+
weight: weightInKg,
|
48 |
+
targetWeight: targetWeightInKg,
|
49 |
+
waist: waistInCm,
|
50 |
+
neck: neckInCm,
|
51 |
+
hip: hipInCm,
|
52 |
+
steps: userMetrics.steps,
|
53 |
+
standingHours: userMetrics.standingHours
|
54 |
+
};
|
55 |
+
|
56 |
+
console.log('Sending metrics data:', metricsData);
|
57 |
+
fetch('/api/calculate-metrics', {
|
58 |
+
method: 'POST',
|
59 |
+
headers: {
|
60 |
+
'Content-Type': 'application/json',
|
61 |
+
},
|
62 |
+
body: JSON.stringify(metricsData),
|
63 |
+
})
|
64 |
+
.then(response => {
|
65 |
+
console.log('Response status:', response.status);
|
66 |
+
return response.json();
|
67 |
+
})
|
68 |
+
.then(data => {
|
69 |
+
console.log('Received metrics data:', data);
|
70 |
+
if (data.error) {
|
71 |
+
console.error('Error calculating metrics:', data.error);
|
72 |
+
showNotification(`Error: ${data.error}`, 'error');
|
73 |
+
return;
|
74 |
+
}
|
75 |
+
document.getElementById('bmi-value').textContent = data.bmi;
|
76 |
+
document.getElementById('recommended-calories').textContent = data.recommendedCalories;
|
77 |
+
document.getElementById('body-fat-percentage').textContent = data.bodyFatPercentage + '%';
|
78 |
+
document.getElementById('lean-body-mass').textContent = (data.leanBodyMass * 2.20462).toFixed(2) + ' lbs';
|
79 |
+
document.getElementById('target-weight-time').textContent = data.timeToTargetWeight;
|
80 |
+
recommendedCalories = data.recommendedCalories;
|
81 |
+
updateBellyFatVisualization({ calories: 0, protein: 0, carbs: 0, fat: 0 });
|
82 |
+
getPersonalizedRecommendations(data);
|
83 |
+
})
|
84 |
+
.catch(error => {
|
85 |
+
console.error('Error:', error);
|
86 |
+
showNotification(`Error: ${error.message}`, 'error');
|
87 |
+
});
|
88 |
+
}
|
89 |
+
|
90 |
+
function getPersonalizedRecommendations(metricsData) {
|
91 |
+
const recommendationData = {
|
92 |
+
...userMetrics,
|
93 |
+
...metricsData,
|
94 |
+
height: (userMetrics.heightFeet * 30.48) + (userMetrics.heightInches * 2.54)
|
95 |
+
};
|
96 |
+
|
97 |
+
console.log('Sending recommendation data:', recommendationData);
|
98 |
+
fetch('/api/personalized-recommendations', {
|
99 |
+
method: 'POST',
|
100 |
+
headers: {
|
101 |
+
'Content-Type': 'application/json',
|
102 |
+
},
|
103 |
+
body: JSON.stringify(recommendationData),
|
104 |
+
})
|
105 |
+
.then(response => response.json())
|
106 |
+
.then(data => {
|
107 |
+
console.log('Received recommendations:', data);
|
108 |
+
if (data.error) {
|
109 |
+
console.error('Error getting recommendations:', data.error);
|
110 |
+
showNotification(`Error: ${data.error}`, 'error');
|
111 |
+
return;
|
112 |
+
}
|
113 |
+
displayRecommendations(data);
|
114 |
+
})
|
115 |
+
.catch(error => {
|
116 |
+
console.error('Error:', error);
|
117 |
+
showNotification(`Error: ${error.message}`, 'error');
|
118 |
+
});
|
119 |
+
}
|
120 |
+
|
121 |
+
function displayRecommendations(recommendations) {
|
122 |
+
const dietRecommendationsList = document.getElementById('diet-recommendations-list');
|
123 |
+
const exerciseRecommendationsList = document.getElementById('exercise-recommendations-list');
|
124 |
+
|
125 |
+
dietRecommendationsList.innerHTML = '';
|
126 |
+
exerciseRecommendationsList.innerHTML = '';
|
127 |
+
|
128 |
+
recommendations.dietRecommendations.forEach(recommendation => {
|
129 |
+
const li = document.createElement('li');
|
130 |
+
li.textContent = recommendation;
|
131 |
+
dietRecommendationsList.appendChild(li);
|
132 |
+
});
|
133 |
+
|
134 |
+
recommendations.exerciseRecommendations.forEach(recommendation => {
|
135 |
+
const li = document.createElement('li');
|
136 |
+
li.textContent = recommendation;
|
137 |
+
exerciseRecommendationsList.appendChild(li);
|
138 |
+
});
|
139 |
+
}
|
140 |
+
|
141 |
+
function searchFood() {
|
142 |
+
const query = searchInput.value.trim();
|
143 |
+
if (query.length < 2) return;
|
144 |
+
|
145 |
+
fetch(`/api/search-food?query=${encodeURIComponent(query)}`)
|
146 |
+
.then(response => response.json())
|
147 |
+
.then(data => {
|
148 |
+
displaySearchResults(data);
|
149 |
+
})
|
150 |
+
.catch(error => console.error('Error:', error));
|
151 |
+
}
|
152 |
+
|
153 |
+
function displaySearchResults(results) {
|
154 |
+
searchResults.innerHTML = '';
|
155 |
+
results.forEach(item => {
|
156 |
+
const li = document.createElement('li');
|
157 |
+
li.textContent = item.description;
|
158 |
+
li.addEventListener('click', () => addFoodItem(item));
|
159 |
+
searchResults.appendChild(li);
|
160 |
+
});
|
161 |
+
}
|
162 |
+
|
163 |
+
function addFoodItem(item) {
|
164 |
+
selectedItems.push(item);
|
165 |
+
updateSelectedFoodList();
|
166 |
+
updateBellyFatVisualization();
|
167 |
+
}
|
168 |
+
|
169 |
+
function updateSelectedFoodList() {
|
170 |
+
selectedFoodList.innerHTML = '';
|
171 |
+
selectedItems.forEach(item => {
|
172 |
+
const li = document.createElement('li');
|
173 |
+
li.textContent = `${item.description} (${item.calories} kcal)`;
|
174 |
+
selectedFoodList.appendChild(li);
|
175 |
+
});
|
176 |
+
}
|
177 |
+
|
178 |
+
function updateBellyFatVisualization() {
|
179 |
+
const totalNutrients = calculateTotalNutrients();
|
180 |
+
|
181 |
+
// Clear the canvas
|
182 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
183 |
+
|
184 |
+
// Draw yellow plate
|
185 |
+
ctx.beginPath();
|
186 |
+
ctx.arc(canvas.width / 2, canvas.height / 2, canvas.width / 3, 0, 2 * Math.PI);
|
187 |
+
ctx.fillStyle = 'yellow';
|
188 |
+
ctx.fill();
|
189 |
+
|
190 |
+
// Draw green circles for food items
|
191 |
+
const foodItems = Math.min(selectedItems.length, 20);
|
192 |
+
for (let i = 0; i < foodItems; i++) {
|
193 |
+
const angle = (i / foodItems) * 2 * Math.PI;
|
194 |
+
const radius = canvas.width / 4;
|
195 |
+
const x = canvas.width / 2 + radius * Math.cos(angle);
|
196 |
+
const y = canvas.height / 2 + radius * Math.sin(angle);
|
197 |
+
|
198 |
+
ctx.beginPath();
|
199 |
+
ctx.arc(x, y, 10, 0, 2 * Math.PI);
|
200 |
+
ctx.fillStyle = 'green';
|
201 |
+
ctx.fill();
|
202 |
+
}
|
203 |
+
|
204 |
+
// Draw stack graph
|
205 |
+
drawStackGraph(totalNutrients);
|
206 |
+
|
207 |
+
// Update total calories display
|
208 |
+
document.getElementById('total-calories').textContent = totalNutrients.calories;
|
209 |
+
}
|
210 |
+
|
211 |
+
function calculateTotalNutrients() {
|
212 |
+
return selectedItems.reduce((total, item) => {
|
213 |
+
total.calories += item.calories || 0;
|
214 |
+
total.protein += item.protein || 0;
|
215 |
+
total.carbs += item.carbs || 0;
|
216 |
+
total.fat += item.fat || 0;
|
217 |
+
return total;
|
218 |
+
}, { calories: 0, protein: 0, carbs: 0, fat: 0 });
|
219 |
+
}
|
220 |
+
|
221 |
+
function drawStackGraph(nutrients) {
|
222 |
+
const barWidth = 40;
|
223 |
+
const barHeight = 100;
|
224 |
+
const startX = canvas.width - barWidth - 10;
|
225 |
+
const startY = canvas.height - 10;
|
226 |
+
|
227 |
+
const totalMacros = nutrients.protein + nutrients.carbs + nutrients.fat;
|
228 |
+
const proteinHeight = (nutrients.protein / totalMacros) * barHeight;
|
229 |
+
const carbsHeight = (nutrients.carbs / totalMacros) * barHeight;
|
230 |
+
const fatHeight = (nutrients.fat / totalMacros) * barHeight;
|
231 |
+
|
232 |
+
ctx.fillStyle = 'red';
|
233 |
+
ctx.fillRect(startX, startY - fatHeight, barWidth, fatHeight);
|
234 |
+
|
235 |
+
ctx.fillStyle = 'blue';
|
236 |
+
ctx.fillRect(startX, startY - fatHeight - carbsHeight, barWidth, carbsHeight);
|
237 |
+
|
238 |
+
ctx.fillStyle = 'green';
|
239 |
+
ctx.fillRect(startX, startY - fatHeight - carbsHeight - proteinHeight, barWidth, proteinHeight);
|
240 |
+
}
|
241 |
+
|
242 |
+
// Add these event listeners
|
243 |
+
searchInput.addEventListener('input', searchFood);
|
244 |
+
|
245 |
+
// Call updateBellyFatVisualization initially to draw the empty plate
|
246 |
+
updateBellyFatVisualization();
|
247 |
+
});
|
templates/dashboard.html
ADDED
File without changes
|
templates/index.html
ADDED
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Belly Fat Visualizer</title>
|
7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
8 |
+
</head>
|
9 |
+
<body>
|
10 |
+
<h1>Belly Fat Visualizer</h1>
|
11 |
+
|
12 |
+
<div id="user-metrics">
|
13 |
+
<h2>Enter Your Metrics</h2>
|
14 |
+
<form id="user-form">
|
15 |
+
<label for="age">Age:</label>
|
16 |
+
<input type="number" id="age" required>
|
17 |
+
|
18 |
+
<label for="gender">Gender:</label>
|
19 |
+
<select id="gender" required>
|
20 |
+
<option value="male">Male</option>
|
21 |
+
<option value="female">Female</option>
|
22 |
+
<option value="other">Other</option>
|
23 |
+
</select>
|
24 |
+
|
25 |
+
<label for="height-feet">Height (feet):</label>
|
26 |
+
<input type="number" id="height-feet" required>
|
27 |
+
|
28 |
+
<label for="height-inches">Height (inches):</label>
|
29 |
+
<input type="number" id="height-inches" required>
|
30 |
+
|
31 |
+
<label for="weight">Weight (lbs):</label>
|
32 |
+
<input type="number" id="weight" required>
|
33 |
+
|
34 |
+
<label for="target-weight">Target Weight (lbs):</label>
|
35 |
+
<input type="number" id="target-weight" required>
|
36 |
+
|
37 |
+
<label for="waist">Waist Circumference (inches):</label>
|
38 |
+
<input type="number" id="waist" required>
|
39 |
+
|
40 |
+
<label for="neck">Neck Circumference (inches):</label>
|
41 |
+
<input type="number" id="neck" required>
|
42 |
+
|
43 |
+
<label for="hip">Hip Circumference (inches):</label>
|
44 |
+
<input type="number" id="hip" required>
|
45 |
+
|
46 |
+
<label for="steps">Daily Steps:</label>
|
47 |
+
<input type="number" id="steps" required>
|
48 |
+
|
49 |
+
<label for="standing-hours">Standing Hours:</label>
|
50 |
+
<input type="number" id="standing-hours" required>
|
51 |
+
|
52 |
+
<button type="submit">Calculate</button>
|
53 |
+
</form>
|
54 |
+
</div>
|
55 |
+
|
56 |
+
<div id="food-selection">
|
57 |
+
<h2>Select Food Items</h2>
|
58 |
+
<div class="search-container">
|
59 |
+
<input type="text" id="food-search" placeholder="Search for food...">
|
60 |
+
<ul id="search-results"></ul>
|
61 |
+
</div>
|
62 |
+
<div class="flex-container">
|
63 |
+
<div>
|
64 |
+
<h3>Selected Food Items:</h3>
|
65 |
+
<ul id="selected-food-list"></ul>
|
66 |
+
</div>
|
67 |
+
</div>
|
68 |
+
<div>
|
69 |
+
<h3>Total Calories: <span id="total-calories">0</span></h3>
|
70 |
+
</div>
|
71 |
+
</div>
|
72 |
+
|
73 |
+
<div id="visualization">
|
74 |
+
<h2>Belly Fat Visualization</h2>
|
75 |
+
<canvas id="belly-fat-canvas" width="300" height="300"></canvas>
|
76 |
+
<div id="health-metrics">
|
77 |
+
<h3>Your BMI: <span id="bmi-value"></span></h3>
|
78 |
+
<h3>Recommended Daily Calories: <span id="recommended-calories"></span></h3>
|
79 |
+
<h3>Body Fat Percentage: <span id="body-fat-percentage"></span></h3>
|
80 |
+
<h3>Lean Body Mass: <span id="lean-body-mass"></span></h3>
|
81 |
+
<h3>Time to Reach Target Weight: <span id="target-weight-time"></span></h3>
|
82 |
+
</div>
|
83 |
+
</div>
|
84 |
+
|
85 |
+
<div id="recommendations">
|
86 |
+
<h2>Personalized Recommendations</h2>
|
87 |
+
<div id="diet-recommendations">
|
88 |
+
<h3>Diet Recommendations:</h3>
|
89 |
+
<ul id="diet-recommendations-list"></ul>
|
90 |
+
</div>
|
91 |
+
<div id="exercise-recommendations">
|
92 |
+
<h3>Exercise Recommendations:</h3>
|
93 |
+
<ul id="exercise-recommendations-list"></ul>
|
94 |
+
</div>
|
95 |
+
</div>
|
96 |
+
|
97 |
+
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
98 |
+
</body>
|
99 |
+
</html>
|
templates/login.html
ADDED
File without changes
|
templates/register.html
ADDED
File without changes
|