/* ═══════════════════════════════════════════ NutriVision — script.js ═══════════════════════════════════════════ */ /* ── Hamburger ── */ const hamburger = document.getElementById('hamburger'); const mobileMenu = document.getElementById('mobileMenu'); if (hamburger) { hamburger.addEventListener('click', () => { hamburger.classList.toggle('open'); mobileMenu.classList.toggle('open'); }); } /* ── Navbar scroll shadow ── */ window.addEventListener('scroll', () => { const nb = document.getElementById('navbar'); if (nb) nb.style.boxShadow = window.scrollY > 10 ? '0 2px 24px rgba(0,0,0,.4)' : 'none'; }); /* ── Reveal on scroll ── */ const revealEls = document.querySelectorAll('.reveal'); if (revealEls.length) { const io = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in-view'); io.unobserve(e.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' }); revealEls.forEach(el => io.observe(el)); } /* ── Counter animation ── */ function animateCounter(el, target, duration = 1200) { const start = performance.now(); const update = (now) => { const progress = Math.min((now - start) / duration, 1); const ease = 1 - Math.pow(1 - progress, 3); el.textContent = Math.floor(ease * target); if (progress < 1) requestAnimationFrame(update); else el.textContent = target; }; requestAnimationFrame(update); } document.querySelectorAll('.counter').forEach(el => { const io = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting) { animateCounter(el, +el.dataset.to); io.unobserve(el); } }); }, { threshold: 0.5 }); io.observe(el); }); /* ════════════════════════════════ ANALYZER PAGE ════════════════════════════════ */ const uploadZone = document.getElementById('uploadZone'); const imgInput = document.getElementById('imgInput'); const uploadPlaceholder = document.getElementById('uploadPlaceholder'); const uploadPreview = document.getElementById('uploadPreview'); const previewImg = document.getElementById('previewImg'); const removeImgBtn = document.getElementById('removeImg'); const analyzerForm = document.getElementById('analyzerForm'); const loadingOverlay = document.getElementById('loadingOverlay'); const resultsPanel = document.getElementById('resultsPanel'); if (uploadZone) { uploadZone.addEventListener('click', () => { if (uploadPreview.style.display === 'none' || !uploadPreview.style.display) imgInput.click(); }); uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('dragover'); }); uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('dragover')); uploadZone.addEventListener('drop', e => { e.preventDefault(); uploadZone.classList.remove('dragover'); if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]); }); imgInput.addEventListener('change', e => { if (e.target.files[0]) handleFile(e.target.files[0]); }); removeImgBtn.addEventListener('click', e => { e.stopPropagation(); imgInput.value = ''; previewImg.src = ''; uploadPreview.style.display = 'none'; uploadPlaceholder.style.display = 'block'; }); } function handleFile(file) { const valid = ['image/png','image/jpeg','image/jpg','image/webp']; if (!valid.includes(file.type)) { showToast('⚠️ Please upload PNG, JPG, JPEG, or WebP', 'error'); return; } if (file.size > 16 * 1024 * 1024) { showToast('⚠️ File too large. Max 16MB.', 'error'); return; } const dt = new DataTransfer(); dt.items.add(file); imgInput.files = dt.files; const reader = new FileReader(); reader.onload = e => { previewImg.src = e.target.result; uploadPlaceholder.style.display = 'none'; uploadPreview.style.display = 'block'; }; reader.readAsDataURL(file); } /* ── Form submit ── */ if (analyzerForm) { analyzerForm.addEventListener('submit', async e => { e.preventDefault(); if (!imgInput.files || !imgInput.files[0]) { showToast('⚠️ Please upload a food image first', 'error'); return; } showLoading(); const fd = new FormData(analyzerForm); try { stepProgress(1); const resp = await fetch('/analyze', { method: 'POST', body: fd }); stepProgress(2); const data = await resp.json(); stepProgress(3); if (!resp.ok) throw new Error(data.error || 'Analysis failed'); stepProgress(4); await sleep(400); hideLoading(); renderResults(data); resultsPanel.style.display = 'flex'; setTimeout(() => resultsPanel.scrollIntoView({ behavior: 'smooth', block: 'start' }), 200); } catch (err) { hideLoading(); showToast('❌ ' + err.message, 'error'); } }); } /* ── Loading helpers ── */ function showLoading() { loadingOverlay.classList.add('active'); document.querySelectorAll('.lstep').forEach(s => s.classList.remove('active')); } function hideLoading() { loadingOverlay.classList.remove('active'); } function stepProgress(n) { const el = document.getElementById('lstep' + n); if (el) el.classList.add('active'); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } /* ── Render results ── */ function renderResults(d) { // Banner document.getElementById('r_food').textContent = d.food || 'Unknown Food'; document.getElementById('r_confidence').textContent = '🎯 ' + (d.confidence || '—'); document.getElementById('r_source').textContent = '🤖 ' + (d.detection_source || '—'); // BMI if (d.bmi) { document.getElementById('r_bmi').textContent = d.bmi; document.getElementById('r_bmiCat').textContent = d.bmi_category || '—'; const bmiCat = (d.bmi_category || '').toLowerCase(); const bmiVal = document.getElementById('r_bmi'); if (bmiCat.includes('obese')) bmiVal.style.color = '#f47171'; else if (bmiCat.includes('overweight')) bmiVal.style.color = '#f5c842'; else if (bmiCat.includes('under')) bmiVal.style.color = '#60a5fa'; else bmiVal.style.color = 'var(--primary)'; } // Nutrition grid const nutr = d.nutrition || {}; const nutriMap = [ { key: 'calories', label: 'Calories', emoji: '🔥' }, { key: 'protein', label: 'Protein', emoji: '💪' }, { key: 'carbohydrates', label: 'Carbs', emoji: '🌾' }, { key: 'fat', label: 'Fat', emoji: '🥑' }, { key: 'fiber', label: 'Fiber', emoji: '🌿' }, { key: 'sugar', label: 'Sugar', emoji: '🍬' }, { key: 'sodium', label: 'Sodium', emoji: '🧂' }, { key: 'serving_size', label: 'Serving', emoji: '🥣' }, ]; const ngEl = document.getElementById('r_nutrition'); ngEl.innerHTML = ''; nutriMap.forEach(item => { const val = nutr[item.key]; if (!val) return; const div = document.createElement('div'); div.className = 'nutri-item'; div.innerHTML = `
${item.emoji} ${val}
${item.label}
`; ngEl.appendChild(div); }); // Benefits const bList = document.getElementById('r_benefits'); bList.innerHTML = ''; (d.health_benefits || []).forEach(b => { const li = document.createElement('li'); li.textContent = b; bList.appendChild(li); }); // Portion document.getElementById('r_portion').textContent = d.portion_advice || '1 standard serving'; // Health context const ctx = document.getElementById('contextCard'); if (d.health_context) { ctx.style.display = 'block'; document.getElementById('r_context').textContent = d.health_context; } else { ctx.style.display = 'none'; } // Alternatives const altCard = document.getElementById('altCard'); const altList = document.getElementById('r_alternatives'); altList.innerHTML = ''; if (d.alternatives && d.alternatives.length) { altCard.style.display = 'block'; d.alternatives.forEach(alt => { const div = document.createElement('div'); div.className = 'alt-item'; let urlsHtml = ''; if (alt.urls && alt.urls.length) { const links = alt.urls.map(u => `${u.emoji || ''} ${u.platform}` ).join(''); urlsHtml = `
🛒 Buy from:${links}
`; } div.innerHTML = `
✓ ${alt.name}
${alt.reason}
${urlsHtml} `; altList.appendChild(div); }); } else { altCard.style.display = 'none'; } } /* ── Reset analyzer ── */ function resetAnalyzer() { if (imgInput) { imgInput.value = ''; } if (previewImg) { previewImg.src = ''; } if (uploadPreview) { uploadPreview.style.display = 'none'; } if (uploadPlaceholder) { uploadPlaceholder.style.display = 'block'; } if (resultsPanel) { resultsPanel.style.display = 'none'; } window.scrollTo({ top: 0, behavior: 'smooth' }); } /* ── Toast notifications ── */ function showToast(msg, type = 'info') { const t = document.createElement('div'); t.style.cssText = ` position:fixed;bottom:1.5rem;right:1.5rem;z-index:99999; padding:.85rem 1.4rem;border-radius:12px;font-size:.9rem;font-weight:600; background:${type==='error'?'#f47171':'var(--primary)'}; color:${type==='error'?'#fff':'#080d14'}; box-shadow:0 8px 24px rgba(0,0,0,.4); animation:slideUp .3s ease both; max-width:320px;line-height:1.4; `; t.textContent = msg; document.body.appendChild(t); setTimeout(() => { t.style.opacity='0'; t.style.transition='.3s'; setTimeout(()=>t.remove(),300); }, 3500); } console.log('🥗 NutriVision loaded ✅');