/* ═══════════════════════════════════════════ 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 = `