3-Day Nutrient Intake Score | Evexia Science

Log foods for each day using USDA / Open Food Facts, then click Analyze to see your nutrient scores and charts.

', ]; const html = parts.join(''); // ── Open as a Blob URL — completely isolated from the parent page's parser ── const blob = new Blob([html], { type: 'text/html; charset=utf-8' }); const url = URL.createObjectURL(blob); const win = window.open(url, '_blank', 'width=920,height=750,scrollbars=yes'); if (!win) { URL.revokeObjectURL(url); alert('Pop-up blocked. Please allow pop-ups for this page and try again.'); return; } // Revoke the blob URL after the window has loaded, then trigger print win.addEventListener('load', function () { URL.revokeObjectURL(url); // Small delay to let fonts render before print dialog opens setTimeout(function () { win.print(); }, 600); }); } // ══════════════════════════════════════════ // MACRO TARGETS // ══════════════════════════════════════════ let macroTargets = { carbs: 50, fat: 30, protein: 20 }; // % of calories function updateMacroTargets() { const c = parseFloat(document.getElementById('targetCarbs').value) || 0; const f = parseFloat(document.getElementById('targetFat').value) || 0; const p = parseFloat(document.getElementById('targetProtein').value) || 0; macroTargets = { carbs: c, fat: f, protein: p }; // Live gram display const goal = currentCalGoal || 2000; const gC = Math.round(c / 100 * goal / 4); const gF = Math.round(f / 100 * goal / 9); const gP = Math.round(p / 100 * goal / 4); const gcEl = document.getElementById('gCarbs'); const gfEl = document.getElementById('gFat'); const gpEl = document.getElementById('gProtein'); if (gcEl) gcEl.textContent = gC + ' g'; if (gfEl) gfEl.textContent = gF + ' g'; if (gpEl) gpEl.textContent = gP + ' g'; const sum = c + f + p; const disp = document.getElementById('macroSumDisplay'); if (disp) { disp.textContent = sum + '%'; disp.className = 'sum-val ' + (Math.abs(sum - 100) < 1 ? 'ok' : 'warn'); } // Re-render result panel live if analysis has already been run if (lastTotals.some(t => t !== null)) { renderResultPanel(); } } function toggleMacroTargets() { const body = document.getElementById('macroTargetsBody'); const arrow = document.getElementById('macroArrow'); const open = body.style.display === 'none'; body.style.display = open ? 'block' : 'none'; if (arrow) arrow.classList.toggle('open', open); } function getMacroTargetGrams(calGoal) { // Convert % targets to gram targets return { carbs: (macroTargets.carbs / 100 * calGoal) / 4, fat: (macroTargets.fat / 100 * calGoal) / 9, protein: (macroTargets.protein / 100 * calGoal) / 4, }; } // ══════════════════════════════════════════ // SUPPLEMENT LOGGER // ══════════════════════════════════════════ let loadedSupp = null; // parsed supplement JSON (same format as recipe) let suppMode = 'upload'; // Nutrient fields shown in manual entry — label, key, unit const SUPP_MANUAL_FIELDS = [ // Vitamins { section:'Vitamins', fields:[ ['Vitamin A', 'vitA', 'mcg RAE'], ['Vitamin C', 'vitC', 'mg'], ['Vitamin D', 'vitD', 'mcg'], ['Vitamin E', 'vitE', 'mg AT'], ['Vitamin K', 'vitK', 'mcg'], ['Thiamin (B1)', 'thiamin', 'mg'], ['Riboflavin (B2)','riboflavin','mg'], ['Niacin (B3)', 'niacin', 'mg'], ['Vitamin B6', 'vitB6', 'mg'], ['Folate', 'folate', 'mcg DFE'], ['Vitamin B12', 'vitB12', 'mcg'], ['Biotin', 'biotin', 'mcg'], ['Pantothenic', 'pantothenic','mg'], ]}, { section:'Minerals', fields:[ ['Calcium', 'calcium', 'mg'], ['Phosphorus', 'phosphorus', 'mg'], ['Magnesium', 'magnesium', 'mg'], ['Iron', 'iron', 'mg'], ['Zinc', 'zinc', 'mg'], ['Iodine', 'iodine', 'mcg'], ['Selenium', 'selenium', 'mcg'], ['Copper', 'copper', 'mg'], ['Manganese', 'manganese', 'mg'], ['Chromium', 'chromium', 'mcg'], ['Molybdenum', 'molybdenum', 'mcg'], ['Potassium', 'potassium', 'mg'], ]}, { section:'Macros (optional)', fields:[ ['Calories', 'cal', 'kcal'], ['Protein', 'protein', 'g'], ['Carbohydrates', 'carbs', 'g'], ['Fat', 'fat', 'g'], ['Sodium', 'sodium', 'mg'], ]}, ]; function buildSuppManualGrid() { const grid = document.getElementById('suppManualGrid'); if (!grid) return; let html = ''; for (const { section, fields } of SUPP_MANUAL_FIELDS) { html += `
${section}
`; for (const [label, key, unit] of fields) { html += `
`; } } grid.innerHTML = html; } function switchSuppTab(mode) { suppMode = mode; document.getElementById('suppTabUpload').classList.toggle('active', mode === 'upload'); document.getElementById('suppTabManual').classList.toggle('active', mode === 'manual'); document.getElementById('suppUploadMode').style.display = mode === 'upload' ? '' : 'none'; document.getElementById('suppManualMode').style.display = mode === 'manual' ? '' : 'none'; if (mode === 'manual' && !document.getElementById('suppManualGrid').innerHTML) { buildSuppManualGrid(); } } // ── Upload path ── function loadSuppJSON(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = e => { try { const data = JSON.parse(e.target.result); if (!data.version || !Array.isArray(data.ingredients)) { alert('Invalid file. Please use a JSON saved by the Evexia Supplement Facts Generator.'); event.target.value = ''; return; } loadedSupp = data; showSuppPreview(data); } catch(err) { alert('Could not parse file: ' + err.message); } event.target.value = ''; }; reader.readAsText(file); } function showSuppPreview(data) { const nutrients = calcRecipeServingNutrients(data); // reuse recipe calc — same format const name = data.productName || 'Unnamed Supplement'; const servingsCount = parseFloat(data.servingsPerContainer) || 1; const servingLabel = data.servingSize || `${nutrients._servingG.toFixed(1)} g`; document.getElementById('spName').textContent = name; document.getElementById('spMeta').textContent = `${servingsCount} serving${servingsCount !== 1 ? 's' : ''} per container · 1 serving = ${servingLabel}`; // Show non-zero nutrients as pills const PILL_KEYS = [ ['vitA','Vit A','mcg'],['vitC','Vit C','mg'],['vitD','Vit D','mcg'], ['vitE','Vit E','mg'],['vitK','Vit K','mcg'],['thiamin','B1','mg'], ['riboflavin','B2','mg'],['niacin','B3','mg'],['vitB6','B6','mg'], ['folate','Folate','mcg'],['vitB12','B12','mcg'],['biotin','Biotin','mcg'], ['pantothenic','Panto','mg'],['calcium','Ca','mg'],['magnesium','Mg','mg'], ['iron','Fe','mg'],['zinc','Zn','mg'],['iodine','I','mcg'], ['selenium','Se','mcg'],['copper','Cu','mg'],['potassium','K','mg'], ['protein','Protein','g'],['cal','Cal','kcal'],['sodium','Na','mg'], ]; const pills = PILL_KEYS .filter(([k]) => nutrients[k] > 0) .map(([k, label, unit]) => `
${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();