v0.2 - 19 features: comparator, allergies, gamification, shopping list, achievements, stats, profile, share, bottom nav
This commit is contained in:
@@ -3,6 +3,7 @@ import { useState, useEffect, useRef } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { api } from '@/lib/api';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
import BottomNav from '@/components/BottomNav';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function ScanPage() {
|
||||
@@ -10,7 +11,12 @@ export default function ScanPage() {
|
||||
const [manualCode, setManualCode] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
const [photoLoading, setPhotoLoading] = useState(false);
|
||||
const [result, setResult] = useState<any>(null);
|
||||
const [addedToList, setAddedToList] = useState(false);
|
||||
const [newBadgeBanner, setNewBadgeBanner] = useState<string[]>([]);
|
||||
const photoInputRef = useRef<HTMLInputElement>(null);
|
||||
const { user, hydrate } = useAuthStore();
|
||||
const router = useRouter();
|
||||
const scannerRef = useRef<any>(null);
|
||||
@@ -23,8 +29,7 @@ export default function ScanPage() {
|
||||
}, []);
|
||||
|
||||
const startScanner = async () => {
|
||||
setScanning(true);
|
||||
setError('');
|
||||
setScanning(true); setError('');
|
||||
try {
|
||||
const { Html5Qrcode } = await import('html5-qrcode');
|
||||
const scanner = new Html5Qrcode('scanner-view');
|
||||
@@ -32,36 +37,51 @@ export default function ScanPage() {
|
||||
await scanner.start(
|
||||
{ facingMode: 'environment' },
|
||||
{ fps: 10, qrbox: { width: 250, height: 150 } },
|
||||
(decodedText) => {
|
||||
scanner.stop().catch(() => {});
|
||||
setScanning(false);
|
||||
handleScan(decodedText);
|
||||
},
|
||||
(decodedText) => { scanner.stop().catch(() => {}); setScanning(false); handleScan(decodedText); },
|
||||
() => {}
|
||||
);
|
||||
} catch (err) {
|
||||
setScanning(false);
|
||||
setError('Não foi possível acessar a câmera. Use o código manual.');
|
||||
}
|
||||
} catch { setScanning(false); setError('Não foi possível acessar a câmera.'); }
|
||||
};
|
||||
|
||||
const stopScanner = () => {
|
||||
scannerRef.current?.stop().catch(() => {});
|
||||
setScanning(false);
|
||||
};
|
||||
const stopScanner = () => { scannerRef.current?.stop().catch(() => {}); setScanning(false); };
|
||||
|
||||
const handleScan = async (barcode: string) => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setResult(null);
|
||||
setLoading(true); setError(''); setNotFound(false); setResult(null); setAddedToList(false);
|
||||
try {
|
||||
const data = await api.scan(barcode);
|
||||
setResult(data);
|
||||
if (data.new_badges?.length) { setNewBadgeBanner(data.new_badges); setTimeout(() => setNewBadgeBanner([]), 5000); }
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
if (err.message.includes('não encontrado')) setNotFound(true);
|
||||
else setError(err.message);
|
||||
} finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const handlePhoto = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]; if (!file) return;
|
||||
setPhotoLoading(true); setError(''); setNotFound(false);
|
||||
try {
|
||||
const data = await api.scanPhoto(file);
|
||||
setResult(data);
|
||||
if (data.new_badges?.length) { setNewBadgeBanner(data.new_badges); setTimeout(() => setNewBadgeBanner([]), 5000); }
|
||||
} catch (err: any) { setError(err.message); }
|
||||
finally { setPhotoLoading(false); }
|
||||
};
|
||||
|
||||
const handleShare = () => {
|
||||
if (!result) return;
|
||||
const shareUrl = `${window.location.origin}/api/scan/${result.id}/share`;
|
||||
const shareData = { title: `ALETHEIA: ${result.product_name}`, text: `Score: ${result.score}/100 - ${result.summary}`, url: shareUrl };
|
||||
if (navigator.share) { navigator.share(shareData).catch(() => {}); }
|
||||
else { navigator.clipboard.writeText(shareUrl); alert('Link copiado!'); }
|
||||
};
|
||||
|
||||
const handleAddToList = async () => {
|
||||
if (!result) return;
|
||||
try {
|
||||
await api.addToShoppingList(result.product_name || 'Produto', result.barcode);
|
||||
setAddedToList(true);
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const getScoreLabel = (s: number) => {
|
||||
@@ -72,23 +92,19 @@ export default function ScanPage() {
|
||||
return { label: 'Péssimo', emoji: '🚫', desc: 'Muito prejudicial à saúde' };
|
||||
};
|
||||
|
||||
const getScoreColor = (score: number) => {
|
||||
if (score >= 71) return '#10B981';
|
||||
if (score >= 51) return '#EAB308';
|
||||
if (score >= 31) return '#F97316';
|
||||
return '#EF4444';
|
||||
};
|
||||
const getScoreColor = (score: number) => score >= 71 ? '#10B981' : score >= 51 ? '#EAB308' : score >= 31 ? '#F97316' : '#EF4444';
|
||||
const getClassColor = (c: string) => c === 'good' ? 'text-green-400' : c === 'warning' ? 'text-yellow-400' : 'text-red-400';
|
||||
const getClassIcon = (c: string) => c === 'good' ? '🟢' : c === 'warning' ? '🟡' : '🔴';
|
||||
|
||||
const getClassColor = (c: string) => {
|
||||
if (c === 'good') return 'text-green-400';
|
||||
if (c === 'warning') return 'text-yellow-400';
|
||||
return 'text-red-400';
|
||||
};
|
||||
|
||||
const getClassIcon = (c: string) => {
|
||||
if (c === 'good') return '🟢';
|
||||
if (c === 'warning') return '🟡';
|
||||
return '🔴';
|
||||
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';
|
||||
if (nutrient === 'proteinas') return num > 10 ? 'low' : num > 3 ? 'mid' : 'high';
|
||||
if (nutrient === 'calorias') return num > 300 ? 'high' : num > 150 ? 'mid' : 'low';
|
||||
return 'mid';
|
||||
};
|
||||
|
||||
const getNutritionBar = (label: string, value: string, level: string) => {
|
||||
@@ -112,26 +128,34 @@ export default function ScanPage() {
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
const allergenAlerts = result.allergen_alerts || [];
|
||||
const substitutions = result.substitutions || [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-dark px-4 py-6 max-w-lg mx-auto">
|
||||
<div className="min-h-screen bg-dark px-4 py-6 pb-24 max-w-lg mx-auto">
|
||||
{/* New badge banner */}
|
||||
{newBadgeBanner.length > 0 && (
|
||||
<div className="fixed top-0 left-0 right-0 z-50 bg-gradient-to-r from-primary/20 to-accent/20 backdrop-blur-xl border-b border-primary/20 p-4 text-center animate-fade-up">
|
||||
<p className="text-sm">🏆 Nova conquista: <span className="font-bold text-primary">{newBadgeBanner.join(', ')}</span></p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Allergen Alert Banner */}
|
||||
{allergenAlerts.length > 0 && (
|
||||
<div className="bg-red-500/10 border border-red-500/30 rounded-2xl p-4 mb-4 animate-pulse">
|
||||
<h3 className="font-bold text-red-400 text-sm mb-2">⚠️ ALERTA DE ALÉRGENOS!</h3>
|
||||
{allergenAlerts.map((a: any, i: number) => (
|
||||
<p key={i} className="text-red-300 text-xs">🔴 <b>{a.ingredient}</b> — contém <b>{a.allergy}</b></p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button onClick={() => setResult(null)} className="text-gray-400 mb-4 hover:text-white">← Novo Scan</button>
|
||||
|
||||
{/* Score */}
|
||||
@@ -153,30 +177,18 @@ export default function ScanPage() {
|
||||
<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>
|
||||
|
||||
{/* Why this score */}
|
||||
{/* Summary */}
|
||||
<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 */}
|
||||
{/* Nutrition */}
|
||||
{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>
|
||||
)}
|
||||
{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))}
|
||||
@@ -193,17 +205,13 @@ export default function ScanPage() {
|
||||
{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>
|
||||
))}
|
||||
{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>
|
||||
))}
|
||||
{result.negatives.map((n: string, i: number) => <p key={i} className="text-gray-300 text-xs mb-1">• {n}</p>)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -214,11 +222,12 @@ export default function ScanPage() {
|
||||
<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 key={i} className={`bg-dark-light rounded-xl p-3 ${ing.is_allergen ? 'border-2 border-red-500/50 animate-pulse' : ''}`}>
|
||||
<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>{ing.is_allergen ? '🚨' : getClassIcon(ing.classification)}</span>
|
||||
<span className={`font-medium text-sm ${ing.is_allergen ? 'text-red-400 font-bold' : getClassColor(ing.classification)}`}>
|
||||
{ing.name}{ing.popular_name && ing.popular_name !== ing.name ? ` (${ing.popular_name})` : ''}
|
||||
{ing.is_allergen && ' ⚠️ ALÉRGENO'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-400 text-xs ml-6">{ing.explanation}</p>
|
||||
@@ -229,6 +238,27 @@ export default function ScanPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Substitutions */}
|
||||
{substitutions?.length > 0 && result.score < 50 && (
|
||||
<div className="bg-gradient-to-br from-green-500/10 to-primary/10 border border-green-500/20 rounded-2xl p-4 mb-4">
|
||||
<h3 className="font-semibold text-sm mb-3">🔄 Alternativas Mais Saudáveis</h3>
|
||||
<div className="space-y-3">
|
||||
{substitutions.map((sub: any, i: number) => (
|
||||
<div key={i} className="bg-dark/40 rounded-xl p-3">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="font-medium text-sm text-green-400">{sub.name}</span>
|
||||
{sub.estimated_score && (
|
||||
<span className="text-xs bg-green-500/10 text-green-400 px-2 py-0.5 rounded-full">~{sub.estimated_score}</span>
|
||||
)}
|
||||
</div>
|
||||
{sub.brand && <p className="text-gray-500 text-xs">{sub.brand}</p>}
|
||||
<p className="text-gray-400 text-xs mt-1">{sub.reason}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recipe */}
|
||||
{recipe && (
|
||||
<div className="bg-gradient-to-br from-primary/10 to-accent/10 border border-primary/20 rounded-2xl p-4 mb-4">
|
||||
@@ -239,65 +269,47 @@ export default function ScanPage() {
|
||||
{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>
|
||||
{recipe.ingredients_list && (
|
||||
<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>
|
||||
)}
|
||||
{recipe.steps && (
|
||||
<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 });
|
||||
}
|
||||
}} className="flex-1 bg-primary text-dark font-bold py-3 rounded-xl">
|
||||
📤 Compartilhar
|
||||
</button>
|
||||
<button onClick={() => setResult(null)} className="flex-1 bg-dark-light text-white font-bold py-3 rounded-xl">
|
||||
📷 Novo Scan
|
||||
<div className="flex gap-2 mb-4">
|
||||
<button onClick={handleShare} className="flex-1 bg-primary text-dark font-bold py-3 rounded-xl">📤 Compartilhar</button>
|
||||
<button onClick={handleAddToList} disabled={addedToList}
|
||||
className={`flex-1 py-3 rounded-xl font-bold ${addedToList ? 'bg-green-500/20 text-green-400' : 'bg-accent/20 text-accent hover:bg-accent/30'}`}>
|
||||
{addedToList ? '✓ Na lista' : '🛒 Adicionar'}
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={() => setResult(null)} className="w-full bg-dark-light text-white font-bold py-3 rounded-xl">📷 Novo Scan</button>
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-dark px-4 py-6 max-w-lg mx-auto">
|
||||
<div className="min-h-screen bg-dark px-4 py-6 pb-24 max-w-lg mx-auto">
|
||||
<nav className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">👁️</span>
|
||||
<span className="font-bold tracking-wider text-primary">ALETHEIA</span>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Link href="/history" className="text-gray-400 text-sm hover:text-white">Histórico</Link>
|
||||
<Link href="/premium" className="text-primary text-sm font-semibold">Premium</Link>
|
||||
<Link href="/compare" className="text-gray-400 text-sm hover:text-white">Comparar</Link>
|
||||
<Link href="/achievements" className="text-gray-400 text-sm hover:text-white">🏆</Link>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -307,68 +319,83 @@ export default function ScanPage() {
|
||||
</div>
|
||||
|
||||
{error && <div className="bg-red-500/10 text-red-400 text-sm p-3 rounded-xl mb-4">{error}</div>}
|
||||
{loading && (
|
||||
<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>
|
||||
|
||||
{notFound && !photoLoading && (
|
||||
<div className="bg-orange-500/10 border border-orange-500/30 rounded-2xl p-6 mb-6 text-center">
|
||||
<span className="text-4xl mb-3 block">🔍</span>
|
||||
<h3 className="font-bold text-orange-400 mb-2">Produto não encontrado</h3>
|
||||
<p className="text-gray-400 text-sm mb-4">Tire uma foto do <b>rótulo</b> e nossa IA analisa.</p>
|
||||
<input type="file" accept="image/*" capture="environment" ref={photoInputRef} onChange={handlePhoto} className="hidden" />
|
||||
<button onClick={() => photoInputRef.current?.click()}
|
||||
className="w-full bg-gradient-to-r from-orange-500 to-amber-500 text-white py-4 rounded-xl font-bold text-lg">
|
||||
📷 Fotografar Rótulo
|
||||
</button>
|
||||
<button onClick={() => setNotFound(false)} className="text-gray-500 text-xs mt-3 underline">Tentar outro código</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && (
|
||||
{(photoLoading || loading) && (
|
||||
<div className="text-center py-20">
|
||||
<div className="animate-spin text-4xl mb-4">{photoLoading ? '📷' : '👁️'}</div>
|
||||
<p className="text-gray-400">{photoLoading ? 'Analisando foto...' : 'Analisando produto...'}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !notFound && !photoLoading && (
|
||||
<>
|
||||
{/* Camera Scanner */}
|
||||
<div className="mb-6">
|
||||
{scanning ? (
|
||||
<div>
|
||||
<div id="scanner-view" ref={scannerDivRef} className="rounded-2xl overflow-hidden mb-4" />
|
||||
<button onClick={stopScanner} className="w-full bg-red-500/20 text-red-400 py-3 rounded-xl font-semibold">
|
||||
Parar Scanner
|
||||
</button>
|
||||
<button onClick={stopScanner} className="w-full bg-red-500/20 text-red-400 py-3 rounded-xl font-semibold">Parar</button>
|
||||
</div>
|
||||
) : (
|
||||
<button onClick={startScanner} className="w-full bg-primary text-dark py-6 rounded-2xl font-bold text-xl hover:bg-primary-dark transition transform hover:scale-[1.02] active:scale-95">
|
||||
📷 Escanear Código de Barras
|
||||
</button>
|
||||
<div className="space-y-3">
|
||||
<button onClick={startScanner} className="w-full bg-primary text-dark py-6 rounded-2xl font-bold text-xl hover:bg-primary-dark transition">
|
||||
📷 Escanear Código de Barras
|
||||
</button>
|
||||
<div>
|
||||
<input type="file" accept="image/*" capture="environment" ref={photoInputRef} onChange={handlePhoto} className="hidden" />
|
||||
<button onClick={() => photoInputRef.current?.click()}
|
||||
className="w-full bg-accent/20 text-accent py-4 rounded-2xl font-bold text-lg hover:bg-accent/30 transition">
|
||||
📸 Fotografar Rótulo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Manual Input */}
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute inset-0 flex items-center"><div className="w-full border-t border-gray-700" /></div>
|
||||
<div className="relative flex justify-center"><span className="bg-dark px-4 text-gray-500 text-sm">ou digite o código</span></div>
|
||||
<div className="relative flex justify-center"><span className="bg-dark px-4 text-gray-500 text-sm">ou digite</span></div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 mb-8">
|
||||
<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"
|
||||
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
|
||||
</button>
|
||||
className="bg-primary text-dark px-6 py-3 rounded-xl font-bold disabled:opacity-50">Buscar</button>
|
||||
</div>
|
||||
|
||||
{/* Quick Demo */}
|
||||
<div className="mt-8">
|
||||
<p className="text-gray-500 text-sm mb-3">🧪 Teste com produtos demo:</p>
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm mb-3">🧪 Teste rápido:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[
|
||||
{ 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">
|
||||
{p.name}
|
||||
</button>
|
||||
className="bg-dark-light text-gray-300 px-3 py-1.5 rounded-lg text-xs hover:bg-gray-600 transition">{p.name}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user