Spaces:
Sleeping
Sleeping
| /* βββββββββββββββββββββββββββββββββββββββββββ | |
| 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 = `<div class="nutri-val">${item.emoji} ${val}</div><div class="nutri-label">${item.label}</div>`; | |
| 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 => | |
| `<a href="${u.url}" target="_blank" rel="noopener" class="buy-link">${u.emoji || ''} ${u.platform}</a>` | |
| ).join(''); | |
| urlsHtml = `<div class="alt-buy"><span class="buy-label">π Buy from:</span>${links}</div>`; | |
| } | |
| div.innerHTML = ` | |
| <div class="alt-name">β ${alt.name}</div> | |
| <div class="alt-reason">${alt.reason}</div> | |
| ${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 β '); |