🏗️ Código fonte completo: PostgreSQL, nutrição, receita, score labels, PWA fixes

This commit is contained in:
2026-02-10 15:19:39 -03:00
parent ccef350294
commit 532cfe46e9
13 changed files with 517 additions and 55 deletions

View File

@@ -64,6 +64,14 @@ export default function ScanPage() {
}
};
const getScoreLabel = (s: number) => {
if (s >= 90) return { label: 'Excelente', emoji: '🌟', desc: 'Alimento natural e saudável' };
if (s >= 70) return { label: 'Bom', emoji: '✅', desc: 'Saudável, com poucos aditivos' };
if (s >= 50) return { label: 'Regular', emoji: '⚠️', desc: 'Processado, consumir com moderação' };
if (s >= 30) return { label: 'Ruim', emoji: '🔶', desc: 'Ultraprocessado, vários aditivos' };
return { label: 'Péssimo', emoji: '🚫', desc: 'Muito prejudicial à saúde' };
};
const getScoreColor = (score: number) => {
if (score >= 71) return '#10B981';
if (score >= 51) return '#EAB308';
@@ -83,64 +91,134 @@ export default function ScanPage() {
return '🔴';
};
const getNutritionBar = (label: string, value: string, level: string) => {
const barColor = level === 'low' ? 'bg-green-500' : level === 'mid' ? 'bg-yellow-500' : 'bg-red-500';
const pillColor = level === 'low' ? 'text-green-400 bg-green-500/10' : level === 'mid' ? 'text-yellow-400 bg-yellow-500/10' : 'text-red-400 bg-red-500/10';
const pillText = level === 'low' ? 'Baixo' : level === 'mid' ? 'Médio' : 'Alto';
const width = level === 'low' ? '25%' : level === 'mid' ? '55%' : '85%';
return (
<div key={label} className="mb-2">
<div className="flex justify-between text-xs mb-1">
<span className="text-gray-300">{label}</span>
<div className="flex items-center gap-2">
<span className="text-gray-400">{value}</span>
<span className={'px-2 py-0.5 rounded-full text-[10px] ' + pillColor}>{pillText}</span>
</div>
</div>
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden">
<div className={'h-full rounded-full transition-all duration-1000 ' + barColor} style={{ width }} />
</div>
</div>
);
};
const guessLevel = (nutrient: string, val: string) => {
const num = parseFloat(val) || 0;
if (nutrient === 'acucar') return num > 15 ? 'high' : num > 5 ? 'mid' : 'low';
if (nutrient === 'gordura_total' || nutrient === 'gordura_saturada') return num > 10 ? 'high' : num > 3 ? 'mid' : 'low';
if (nutrient === 'sodio') return num > 400 ? 'high' : num > 120 ? 'mid' : 'low';
if (nutrient === 'fibras') return num > 5 ? 'low' : num > 2 ? 'mid' : 'high'; // inverted: more fiber = better
if (nutrient === 'proteinas') return num > 10 ? 'low' : num > 3 ? 'mid' : 'high'; // inverted
if (nutrient === 'calorias') return num > 300 ? 'high' : num > 150 ? 'mid' : 'low';
return 'mid';
};
// Result view
if (result) {
const color = getScoreColor(result.score);
const dashArray = result.score * 3.267 + ' 326.7';
const nutrition = result.nutrition || {};
const recipe = result.recipe;
return (
<div className="min-h-screen bg-dark px-4 py-6 max-w-lg mx-auto">
<button onClick={() => setResult(null)} className="text-gray-400 mb-4 hover:text-white"> Voltar</button>
<button onClick={() => setResult(null)} className="text-gray-400 mb-4 hover:text-white"> Novo Scan</button>
{/* Score */}
<div className="text-center mb-8">
<div className="text-center mb-6">
<h2 className="text-lg font-semibold mb-1">{result.product_name || 'Produto'}</h2>
{result.brand && <p className="text-gray-500 text-sm">{result.brand}</p>}
<div className="relative w-40 h-40 mx-auto mt-6">
<div className="relative w-36 h-36 mx-auto mt-4">
<svg viewBox="0 0 120 120" className="w-full h-full -rotate-90">
<circle cx="60" cy="60" r="52" fill="none" stroke="#374151" strokeWidth="10" />
<circle cx="60" cy="60" r="52" fill="none" stroke={color} strokeWidth="10"
strokeDasharray={`${result.score * 3.267} 326.7`} strokeLinecap="round" className="transition-all duration-1000" />
strokeDasharray={dashArray} strokeLinecap="round" className="transition-all duration-1000" />
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-4xl font-black" style={{ color }}>{result.score}</span>
<span className="text-gray-500 text-sm">/100</span>
<span className="text-gray-500 text-xs">/100</span>
</div>
</div>
<div className="mt-3 bg-dark-light rounded-2xl px-4 py-3 inline-block">
<span className="text-lg">{getScoreLabel(result.score).emoji}</span>
<span className="font-bold text-lg ml-1" style={{ color }}>{getScoreLabel(result.score).label}</span>
</div>
{result.nutri_score && result.nutri_score !== 'unknown' && (
<div className="flex justify-center gap-3 mt-3">
<span className="text-xs bg-dark-light px-3 py-1 rounded-full">Nutri-Score: <b className="uppercase">{result.nutri_score}</b></span>
{result.nova_group && <span className="text-xs bg-dark-light px-3 py-1 rounded-full">NOVA: <b>{result.nova_group}</b></span>}
</div>
)}
</div>
{/* Summary */}
{/* Why this score */}
<div className="bg-dark-light rounded-2xl p-4 mb-4">
<h3 className="font-semibold text-sm mb-2" style={{ color }}>
{getScoreLabel(result.score).emoji} Por que é {getScoreLabel(result.score).label}?
</h3>
<p className="text-gray-300 text-sm leading-relaxed">{result.summary}</p>
<p className="text-gray-500 text-xs mt-2 italic">{getScoreLabel(result.score).desc}</p>
</div>
{/* Nutrition Table */}
{Object.keys(nutrition).length > 0 && (
<div className="bg-dark-light rounded-2xl p-4 mb-4">
<h3 className="font-semibold mb-3 text-sm">📊 Informações Nutricionais</h3>
{result.nutrition_verdict && (
<p className="text-gray-400 text-xs mb-3 italic">{result.nutrition_verdict}</p>
)}
{nutrition.calorias && getNutritionBar('Calorias', nutrition.calorias, guessLevel('calorias', nutrition.calorias))}
{nutrition.acucar && getNutritionBar('Açúcar', nutrition.acucar, guessLevel('acucar', nutrition.acucar))}
{nutrition.gordura_total && getNutritionBar('Gordura Total', nutrition.gordura_total, guessLevel('gordura_total', nutrition.gordura_total))}
{nutrition.gordura_saturada && getNutritionBar('Gordura Saturada', nutrition.gordura_saturada, guessLevel('gordura_saturada', nutrition.gordura_saturada))}
{nutrition.sodio && getNutritionBar('Sódio', nutrition.sodio, guessLevel('sodio', nutrition.sodio))}
{nutrition.carboidratos && getNutritionBar('Carboidratos', nutrition.carboidratos, guessLevel('carboidratos', nutrition.carboidratos))}
{nutrition.fibras && getNutritionBar('Fibras', nutrition.fibras, guessLevel('fibras', nutrition.fibras))}
{nutrition.proteinas && getNutritionBar('Proteínas', nutrition.proteinas, guessLevel('proteinas', nutrition.proteinas))}
</div>
)}
{/* Positives & Negatives */}
{result.positives?.length > 0 && (
<div className="mb-4">
<h3 className="font-semibold text-green-400 mb-2"> Positivos</h3>
{result.positives.map((p: string, i: number) => (
<p key={i} className="text-gray-300 text-sm ml-4 mb-1"> {p}</p>
))}
</div>
)}
{result.negatives?.length > 0 && (
<div className="mb-4">
<h3 className="font-semibold text-red-400 mb-2"> Negativos</h3>
{result.negatives.map((n: string, i: number) => (
<p key={i} className="text-gray-300 text-sm ml-4 mb-1"> {n}</p>
))}
</div>
)}
<div className="grid grid-cols-2 gap-3 mb-4">
{result.positives?.length > 0 && (
<div className="bg-green-500/5 border border-green-500/20 rounded-xl p-3">
<h3 className="font-semibold text-green-400 text-xs mb-2"> Positivos</h3>
{result.positives.map((p: string, i: number) => (
<p key={i} className="text-gray-300 text-xs mb-1"> {p}</p>
))}
</div>
)}
{result.negatives?.length > 0 && (
<div className="bg-red-500/5 border border-red-500/20 rounded-xl p-3">
<h3 className="font-semibold text-red-400 text-xs mb-2"> Negativos</h3>
{result.negatives.map((n: string, i: number) => (
<p key={i} className="text-gray-300 text-xs mb-1"> {n}</p>
))}
</div>
)}
</div>
{/* Ingredients */}
{result.ingredients?.length > 0 && (
<div className="mb-6">
<h3 className="font-semibold mb-3">📋 Ingredientes</h3>
<div className="mb-4">
<h3 className="font-semibold mb-3 text-sm">📋 Ingredientes</h3>
<div className="space-y-2">
{result.ingredients.map((ing: any, i: number) => (
<div key={i} className="bg-dark-light rounded-xl p-3">
<div className="flex items-center gap-2 mb-1">
<span>{getClassIcon(ing.classification)}</span>
<span className={`font-medium text-sm ${getClassColor(ing.classification)}`}>
{ing.name}{ing.popular_name && ing.popular_name !== ing.name ? ` (${ing.popular_name})` : ''}
<span className={'font-medium text-sm ' + getClassColor(ing.classification)}>
{ing.name}{ing.popular_name && ing.popular_name !== ing.name ? ' (' + ing.popular_name + ')' : ''}
</span>
</div>
<p className="text-gray-400 text-xs ml-6">{ing.explanation}</p>
@@ -151,11 +229,53 @@ export default function ScanPage() {
</div>
)}
{/* Share */}
{/* Recipe */}
{recipe && (
<div className="bg-gradient-to-br from-primary/10 to-accent/10 border border-primary/20 rounded-2xl p-4 mb-4">
<h3 className="font-semibold mb-2 text-sm">🍳 {result.score > 70 ? 'Receita com este produto' : 'Alternativa Saudável'}</h3>
<h4 className="text-primary font-bold mb-1">{recipe.title}</h4>
<p className="text-gray-400 text-xs mb-3">{recipe.description}</p>
<div className="flex gap-3 mb-3">
{recipe.prep_time && <span className="text-xs bg-dark/50 px-2 py-1 rounded-lg"> {recipe.prep_time}</span>}
{recipe.calories && <span className="text-xs bg-dark/50 px-2 py-1 rounded-lg">🔥 {recipe.calories}</span>}
</div>
<div className="mb-3">
<p className="text-xs font-semibold text-gray-300 mb-1">Ingredientes:</p>
{recipe.ingredients_list?.map((ing: string, i: number) => (
<p key={i} className="text-gray-400 text-xs ml-2"> {ing}</p>
))}
</div>
<div className="mb-3">
<p className="text-xs font-semibold text-gray-300 mb-1">Preparo:</p>
{recipe.steps?.map((step: string, i: number) => (
<p key={i} className="text-gray-400 text-xs ml-2 mb-1">{i + 1}. {step}</p>
))}
</div>
{recipe.tip && (
<div className="bg-dark/30 rounded-lg p-2 mt-2">
<p className="text-primary text-xs">💡 {recipe.tip}</p>
</div>
)}
</div>
)}
{/* Score Legend */}
<div className="bg-dark-light rounded-2xl p-4 mb-4">
<h3 className="font-semibold mb-3 text-sm">📏 O que significa o Score?</h3>
<div className="space-y-2">
<div className="flex items-center gap-2"><span className="w-8 text-center">🌟</span><div className="flex-1"><div className="flex justify-between"><span className="text-green-400 text-xs font-bold">90-100 Excelente</span></div><div className="h-1 bg-green-500 rounded-full mt-0.5" style={{width:'100%'}} /></div></div>
<div className="flex items-center gap-2"><span className="w-8 text-center"></span><div className="flex-1"><div className="flex justify-between"><span className="text-green-300 text-xs font-bold">70-89 Bom</span></div><div className="h-1 bg-green-400 rounded-full mt-0.5" style={{width:'80%'}} /></div></div>
<div className="flex items-center gap-2"><span className="w-8 text-center"></span><div className="flex-1"><div className="flex justify-between"><span className="text-yellow-400 text-xs font-bold">50-69 Regular</span></div><div className="h-1 bg-yellow-500 rounded-full mt-0.5" style={{width:'60%'}} /></div></div>
<div className="flex items-center gap-2"><span className="w-8 text-center">🔶</span><div className="flex-1"><div className="flex justify-between"><span className="text-orange-400 text-xs font-bold">30-49 Ruim</span></div><div className="h-1 bg-orange-500 rounded-full mt-0.5" style={{width:'40%'}} /></div></div>
<div className="flex items-center gap-2"><span className="w-8 text-center">🚫</span><div className="flex-1"><div className="flex justify-between"><span className="text-red-400 text-xs font-bold">0-29 Péssimo</span></div><div className="h-1 bg-red-500 rounded-full mt-0.5" style={{width:'20%'}} /></div></div>
</div>
</div>
{/* Actions */}
<div className="flex gap-3">
<button onClick={() => {
if (navigator.share) {
navigator.share({ title: `Aletheia: ${result.product_name}`, text: `Score: ${result.score}/100 - ${result.summary}`, url: window.location.href });
navigator.share({ title: 'Aletheia: ' + result.product_name, text: 'Score: ' + result.score + '/100 - ' + result.summary, url: window.location.href });
}
}} className="flex-1 bg-primary text-dark font-bold py-3 rounded-xl">
📤 Compartilhar
@@ -191,6 +311,7 @@ export default function ScanPage() {
<div className="text-center py-20">
<div className="animate-spin text-4xl mb-4">👁</div>
<p className="text-gray-400">Analisando produto...</p>
<p className="text-gray-600 text-xs mt-2">Nossa IA está analisando cada ingrediente</p>
</div>
)}
@@ -220,7 +341,8 @@ export default function ScanPage() {
<div className="flex gap-2">
<input type="text" placeholder="Ex: 7891000100103" value={manualCode} onChange={e => setManualCode(e.target.value)}
className="flex-1 bg-dark-light rounded-xl px-4 py-3 text-white placeholder-gray-500 outline-none focus:ring-2 focus:ring-primary" />
className="flex-1 bg-dark-light rounded-xl px-4 py-3 text-white placeholder-gray-500 outline-none focus:ring-2 focus:ring-primary"
onKeyDown={e => e.key === 'Enter' && manualCode && handleScan(manualCode)} />
<button onClick={() => manualCode && handleScan(manualCode)} disabled={!manualCode}
className="bg-primary text-dark px-6 py-3 rounded-xl font-bold disabled:opacity-50">
Buscar
@@ -232,11 +354,11 @@ export default function ScanPage() {
<p className="text-gray-500 text-sm mb-3">🧪 Teste com produtos demo:</p>
<div className="flex flex-wrap gap-2">
{[
{ name: 'Coca-Cola', code: '7891000100103' },
{ name: 'Nescau', code: '7891000053508' },
{ name: 'Miojo', code: '7891000305232' },
{ name: 'Aveia', code: '7891000362006' },
{ name: 'Oreo', code: '7622300830236' },
{ name: 'Coca-Cola', code: '7894900011517' },
{ name: 'Nescau', code: '7891000379691' },
{ name: 'Miojo', code: '7891079000212' },
{ name: 'Aveia', code: '7894321219820' },
{ name: 'Oreo', code: '7622300830151' },
].map(p => (
<button key={p.code} onClick={() => handleScan(p.code)}
className="bg-dark-light text-gray-300 px-3 py-1.5 rounded-lg text-xs hover:bg-gray-600 transition">