3-Day Nutrient Intake Score
Evexia ScienceLog foods for each day using USDA / Open Food Facts, then click Analyze to see your nutrient scores and charts.
${section}
`;
for (const [label, key, unit] of fields) {
html += `${nutrients[k] % 1 === 0 ? nutrients[k] : nutrients[k].toFixed(1)} ${unit} ${label}
`)
.join('');
document.getElementById('spNutrients').innerHTML = pills || 'No nutrient data found.';
document.getElementById('suppServings').value = 1;
document.getElementById('suppPreviewBlock').style.display = 'block';
}
function clearSuppPreview() {
loadedSupp = null;
document.getElementById('suppPreviewBlock').style.display = 'none';
}
function addSuppAsFood() {
if (!loadedSupp) return;
const servCount = parseFloat(document.getElementById('suppServings').value) || 1;
const oneServing = calcRecipeServingNutrients(loadedSupp);
const servingG = oneServing._servingG;
const name = (loadedSupp.productName || 'Supplement') + (servCount !== 1 ? ` ×${servCount}` : '');
const servingLabel = loadedSupp.servingSize || `${servingG.toFixed(1)} g`;
// Scale nutrients by number of servings, store as per100 with amtG=100
const per100 = {};
for (const k of RECIPE_KEYS) {
const v = (oneServing[k] || 0) * servCount;
per100[k] = isFinite(v) ? v : 0;
}
days[currentLogDay].push({
_id: ++_idCtr,
name,
source: 'Supplement',
amtG: 100,
displayAmt: `${servCount} serving${servCount !== 1 ? 's' : ''} (${servingLabel})`,
per100
});
clearSuppPreview();
updateLogDots();
renderFoodList();
}
// ── Manual entry path ──
function addManualSupp() {
const name = (document.getElementById('suppManualName').value || '').trim();
if (!name) { alert('Please enter a supplement name.'); return; }
const per100 = {};
// Collect all manual fields
for (const { fields } of SUPP_MANUAL_FIELDS) {
for (const [, key] of fields) {
const el = document.getElementById('sm_' + key);
per100[key] = el ? (parseFloat(el.value) || 0) : 0;
}
}
// Fill any missing KEYS with 0
for (const k of KEYS) if (!(k in per100)) per100[k] = 0;
const hasData = Object.values(per100).some(v => v > 0);
if (!hasData) { alert('Please enter at least one nutrient value.'); return; }
days[currentLogDay].push({
_id: ++_idCtr,
name,
source: 'Supplement',
amtG: 100,
displayAmt: '1 serving (manual entry)',
per100
});
// Reset fields
document.getElementById('suppManualName').value = '';
for (const { fields } of SUPP_MANUAL_FIELDS) {
for (const [, key] of fields) {
const el = document.getElementById('sm_' + key);
if (el) el.value = '';
}
}
updateLogDots();
renderFoodList();
}
// ══════════════════════════════════════════
// RECIPE JSON IMPORT
// ══════════════════════════════════════════
let loadedRecipe = null; // the parsed recipe object
const RECIPE_KEYS = ['cal','fat','satFat','transFat','chol','sodium','carbs','fiber',
'totSug','addSug','protein','vitD','calcium','iron','potassium','vitA','vitC','vitE',
'vitK','thiamin','riboflavin','niacin','vitB6','folate','vitB12','biotin','pantothenic',
'phosphorus','magnesium','zinc','selenium','copper','manganese','chromium','molybdenum',
'chloride','iodine','ala','epa','dha','la'];
function loadRecipeJSON(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
try {
const recipe = JSON.parse(e.target.result);
if (!recipe.version || !Array.isArray(recipe.ingredients)) {
alert('Invalid recipe file. Please use a JSON saved by the Evexia Nutrition Facts Generator.');
event.target.value = ''; return;
}
loadedRecipe = recipe;
showRecipePreview(recipe);
} catch(err) {
alert('Could not parse recipe file: ' + err.message);
}
event.target.value = ''; // allow re-upload of same file
};
reader.readAsText(file);
}
function calcRecipeServingNutrients(recipe) {
// Replicate the label generator's computeTotals logic exactly:
// Each ingredient stores nutrients per 100g; scale by ingredient.amount (grams)
const ings = recipe.ingredients || [];
// Sum whole-recipe nutrients
const wholeRecipe = {};
for (const k of RECIPE_KEYS) {
wholeRecipe[k] = ings.reduce((a, ing) => {
const per100 = Number(ing[k]) || 0;
const grams = Number(ing.amount) || 0;
return a + per100 * (grams / 100);
}, 0);
}
const totalG = ings.reduce((a, ing) => a + (Number(ing.amount) || 0), 0);
// Determine serving grams (mirror label generator's getServingGrams logic)
let servingG;
const mode = recipe.servingMode || 'grams';
if (mode === 'full') {
servingG = totalG || 1;
} else if (mode === 'servings') {
const count = Math.max(1, parseFloat(recipe.servingsPerContainer) || 1);
servingG = totalG > 0 ? totalG / count : 1;
} else {
// grams mode
const entered = parseFloat(recipe.servingSizeG);
servingG = entered > 0 ? entered : (totalG || 1);
}
const scale = totalG > 0 ? servingG / totalG : 0;
const serving = {};
for (const k of RECIPE_KEYS) serving[k] = wholeRecipe[k] * scale;
serving._servingG = servingG;
return serving;
}
function showRecipePreview(recipe) {
const nutrients = calcRecipeServingNutrients(recipe);
const servingsCount = parseFloat(recipe.servingsPerContainer) || 1;
const name = recipe.productName || 'Unnamed Recipe';
const servingLabel = recipe.servingSize || `${nutrients._servingG.toFixed(1)} g`;
document.getElementById('rpName').textContent = name;
document.getElementById('rpMeta').textContent =
`${servingsCount} serving${servingsCount !== 1 ? 's' : ''} per recipe · 1 serving = ${servingLabel}`;
const macros = [
['Cal', nutrients.cal.toFixed(0), 'kcal'],
['Carbs', nutrients.carbs.toFixed(1), 'g'],
['Protein', nutrients.protein.toFixed(1),'g'],
['Fat', nutrients.fat.toFixed(1), 'g'],
['Fiber', nutrients.fiber.toFixed(1), 'g'],
['Sodium', nutrients.sodium.toFixed(0), 'mg'],
];
document.getElementById('rpMacros').innerHTML = macros
.map(([l,v,u]) => `${v} ${u} ${l}
`)
.join('');
document.getElementById('recipePreviewBlock').style.display = 'block';
}
function clearRecipePreview() {
loadedRecipe = null;
document.getElementById('recipePreviewBlock').style.display = 'none';
const srv = document.getElementById('recipeServings');
if (srv) srv.value = 1;
}
function stepRecipeServings(delta) {
const inp = document.getElementById('recipeServings');
if (!inp) return;
const cur = parseFloat(inp.value) || 1;
const next = Math.max(0.5, Math.round((cur + delta * 0.5) / 0.5) * 0.5);
inp.value = next % 1 === 0 ? next : next.toFixed(1);
}
function addRecipeAsFood() {
if (!loadedRecipe) return;
const servCount = parseFloat(document.getElementById('recipeServings')?.value) || 1;
const nutrients = calcRecipeServingNutrients(loadedRecipe);
const servingG = nutrients._servingG;
const name = loadedRecipe.productName || 'Recipe';
const servingLabel = loadedRecipe.servingSize || `${servingG.toFixed(1)} g`;
// Scale each nutrient by number of servings, store as per100 with amtG=100
// so computeDayTotals uses scale factor of exactly 1.0
const per100 = {};
for (const k of RECIPE_KEYS) {
const v = nutrients[k];
const base = (typeof v === 'number' && isFinite(v)) ? v : 0;
per100[k] = base * servCount;
}
const displayAmt = servCount === 1
? `1 serving (${servingLabel})`
: `${servCount} servings (${servingLabel} each)`;
days[currentLogDay].push({
_id: ++_idCtr,
name: servCount !== 1 ? `${name} ×${servCount}` : name,
source: 'Recipe JSON',
amtG: 100,
displayAmt,
per100
});
clearRecipePreview();
updateLogDots();
renderFoodList();
}
// ══════════════════════════════════════════
// INIT
// ══════════════════════════════════════════
refreshProfile(); // also calls updateMacroTargets() internally
renderFoodList();