feat: complete NutriScan redesign

This commit is contained in:
evandrodevbr committed 2026-07-16 16:31:50 -03:00
1 parent c1fe4af997
commit f71cd167b3
163 files changed
+30802 -14169

No files matched your search

+777 -158
View File
@@ -7,19 +7,173 @@ import {
ArrowLeft,
AlertCircle,
AlertTriangle,
Database,
Globe,
ShieldCheck,
Beaker,
ClipboardList,
Heart,
Share2,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import Image from "next/image";
import { searchByBarcode, Product, formatNutritionData } from "@/lib/openFoodFactsApi";
import { NutriScoreBadge, NovaBadge } from "@/app/components/NutriScoreBadge";
import { NutritionGrid } from "@/app/components/NutritionBar";
import { ProductSkeleton } from "@/app/components/ProductSkeleton";
/* ── Nutri-Score letter circle ─────────────────────────────────────────── */
const NUTRI_BG: Record<string, string> = {
a: "#038141",
b: "#85bb2f",
c: "#fecb02",
d: "#ee8100",
e: "#e63e11",
};
const NUTRI_INK: Record<string, string> = {
a: "#fff",
b: "#fff",
c: "#3a2a00",
d: "#fff",
e: "#fff",
};
const GRADE_DESC: Record<string, string> = {
a: "Excelente qualidade nutricional",
b: "Boa qualidade nutricional",
c: "Qualidade regular",
d: "Qualidade ruim",
e: "Qualidade muito ruim",
};
const NOVA_DESC: Record<number, string> = {
1: "Alimento in natura ou minimamente processado.",
2: "Ingrediente culinário processado.",
3: "Alimento processado com adição de sal, açúcar ou óleo.",
4: "Ultraprocessado — formulação industrial com aditivos.",
};
const NOVA_BG: Record<number, string> = {
1: "#038141",
2: "#fecb02",
3: "#ee8100",
4: "#e63e11",
};
const NOVA_INK: Record<number, string> = {
1: "#fff",
2: "#3a2a00",
3: "#fff",
4: "#fff",
};
function NutriLetter({ grade, size = 92 }: { grade: string; size?: number }) {
const g = grade?.toLowerCase();
if (!NUTRI_BG[g]) return null;
return (
<div
style={{
width: size,
height: size,
borderRadius: "50%",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
fontFamily: "'Instrument Serif', Georgia, serif",
fontSize: Math.round(size * 0.63),
color: NUTRI_INK[g],
background: NUTRI_BG[g],
fontWeight: 400,
letterSpacing: "-0.03em",
lineHeight: 1,
flexShrink: 0,
boxShadow: "inset 0 -3px 0 rgba(0,0,0,0.15), 0 8px 24px -8px rgba(0,0,0,0.2)",
}}
>
{g.toUpperCase()}
</div>
);
}
/* ── Nutri-Scale row (A–E with active highlight) ────────────────────────── */
function NutriScale({ active, size = "sm" }: { active: string; size?: "sm" | "md" }) {
const px = size === "sm" ? 18 : 26;
const fs = size === "sm" ? 9 : 12;
return (
<div
style={{
display: "inline-flex",
alignItems: "center",
background: "var(--ink-100, #f3efe7)",
padding: 3,
borderRadius: 999,
gap: 2,
}}
>
{["a", "b", "c", "d", "e"].map((l) => (
<div
key={l}
style={{
width: px,
height: px,
borderRadius: 999,
display: "flex",
alignItems: "center",
justifyContent: "center",
color: l === "c" ? "#3a2a00" : "#fff",
fontSize: fs,
fontWeight: 700,
background: NUTRI_BG[l],
opacity: active?.toLowerCase() === l ? 1 : 0.3,
filter: active?.toLowerCase() === l ? "none" : "saturate(0.3)",
transform: active?.toLowerCase() === l ? "scale(1.18)" : "none",
boxShadow:
active?.toLowerCase() === l
? "0 2px 8px rgba(0,0,0,0.15)"
: "none",
transition: "all 0.25s",
}}
>
{l.toUpperCase()}
</div>
))}
</div>
);
}
/* ── NOVA pill ───────────────────────────────────────────────────────────── */
function NovaPill({ group }: { group: number }) {
const labels: Record<number, string> = {
1: "Não processado",
2: "Ingrediente culinário",
3: "Processado",
4: "Ultraprocessado",
};
return (
<div
style={{
display: "inline-flex",
alignItems: "center",
gap: 8,
padding: "4px 10px 4px 4px",
borderRadius: 999,
background: "var(--ink-100, #f3efe7)",
border: "1px solid var(--ink-200, #e4dfd7)",
fontSize: 12,
color: "var(--ink-700, #3d3934)",
flexShrink: 0,
}}
>
<span
style={{
width: 22,
height: 22,
borderRadius: 999,
background: NOVA_BG[group] || "#a39c93",
color: NOVA_INK[group] || "#fff",
display: "grid",
placeItems: "center",
fontFamily: "ui-monospace, monospace",
fontWeight: 700,
fontSize: 11,
}}
>
{group}
</span>
NOVA · {labels[group] || "Desconhecido"}
</div>
);
}
export default function ProdutoPage() {
const params = useParams();
const router = useRouter();
@@ -35,9 +189,9 @@ export default function ProdutoPage() {
setError(null);
const result = await searchByBarcode(barcode);
if (result) setProduct(result);
else setError("Produto não catalogado na base ODbL.");
else setError("Produto não encontrado na base de dados.");
} catch {
setError("Falha crítica na sincronização de dados.");
setError("Falha ao carregar os dados do produto.");
} finally {
setLoading(false);
}
@@ -49,21 +203,94 @@ export default function ProdutoPage() {
if (loading) return <ProductSkeleton />;
// Handler de Erro - Estética Industrial
/* ── Error state ─────────────────────────────────────────────────────── */
if (error || !product) {
return (
<div className="min-h-screen bg-white flex items-center justify-center p-6 dark:bg-[#030712]">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="max-w-md w-full text-center space-y-6 border border-slate-200 p-10 rounded-[2.5rem] bg-slate-50 dark:bg-red-950/10 dark:border-red-900/20"
<div
style={{
minHeight: "100vh",
background: "var(--ink-0, #fdfbf5)",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 24,
}}
>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
style={{
maxWidth: 420,
width: "100%",
background: "var(--ink-0, #fdfbf5)",
border: "1px solid var(--ink-200, #e4dfd7)",
borderRadius: 20,
padding: 48,
textAlign: "center",
}}
>
<AlertCircle className="w-16 h-16 text-red-500 mx-auto" />
<h1 className="text-2xl font-black text-slate-900 dark:text-white uppercase tracking-tighter">Erro de Protocolo</h1>
<p className="text-slate-600 text-sm dark:text-slate-400 font-medium">{error}</p>
<div className="flex gap-3">
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={() => router.push("/")}>Nova Busca</Button>
<Button variant="outline" className="w-full border-slate-300 dark:border-slate-800" onClick={() => router.back()}>Retornar</Button>
<AlertCircle
style={{
width: 40,
height: 40,
color: "#e63e11",
margin: "0 auto 20px",
}}
strokeWidth={1.5}
/>
<h1
style={{
fontFamily: "'Instrument Serif', Georgia, serif",
fontSize: 28,
color: "var(--ink-900, #141312)",
marginBottom: 10,
}}
>
Produto não encontrado
</h1>
<p
style={{
color: "var(--ink-600, #595149)",
fontSize: 14,
marginBottom: 28,
lineHeight: 1.55,
}}
>
{error}
</p>
<div style={{ display: "flex", gap: 10 }}>
<button
onClick={() => router.push("/")}
style={{
flex: 1,
padding: "12px 0",
background: "var(--accent, oklch(52% 0.11 155))",
color: "#fff",
border: "none",
borderRadius: 10,
fontSize: 14,
fontWeight: 500,
cursor: "pointer",
}}
>
Nova Busca
</button>
<button
onClick={() => router.back()}
style={{
flex: 1,
padding: "12px 0",
background: "transparent",
color: "var(--ink-900, #141312)",
border: "1px solid var(--ink-200, #e4dfd7)",
borderRadius: 10,
fontSize: 14,
fontWeight: 500,
cursor: "pointer",
}}
>
Voltar
</button>
</div>
</motion.div>
</div>
@@ -71,161 +298,553 @@ export default function ProdutoPage() {
}
const imageUrl = product.image_front_url || product.image_url || null;
const grade = product.nutrition_grades?.toLowerCase();
const nova = product.nova_group;
const gradeDesc = grade ? GRADE_DESC[grade] : null;
const novaDesc = nova ? NOVA_DESC[nova] : null;
/* ── Nutrition bars — % of daily reference ─────────────────────────── */
const dailyRef: Record<string, number> = {
energy_100g: 2000,
fat_100g: 70,
saturated_fat_100g: 20,
sugars_100g: 90,
salt_100g: 6,
fiber_100g: 30,
proteins_100g: 50,
};
const nutriments = product.nutriments || {};
const nutritionRows = [
{ key: "energy_100g", label: "Energia", unit: "kcal" },
{ key: "fat_100g", label: "Gorduras totais", unit: "g" },
{ key: "saturated_fat_100g",label: "Gorduras saturadas", unit: "g" },
{ key: "sugars_100g", label: "Açúcares", unit: "g" },
{ key: "salt_100g", label: "Sódio", unit: "g" },
{ key: "fiber_100g", label: "Fibras", unit: "g" },
{ key: "proteins_100g", label: "Proteínas", unit: "g" },
];
const allergenTags = product.allergens_tags || [];
return (
<div className="min-h-screen bg-slate-50 text-slate-900 dark:bg-[#030712] dark:text-slate-200 transition-colors duration-500">
{/* 1. NAVEGAÇÃO E TÍTULO (Sticky Header) */}
<header className="sticky top-0 z-50 bg-white/80 backdrop-blur-xl border-b border-slate-200 dark:bg-[#030712]/80 dark:border-white/5">
<div className="max-w-7xl mx-auto px-6 h-20 flex items-center gap-6">
<Button
variant="ghost"
size="icon"
onClick={() => router.back()}
className="rounded-full hover:bg-slate-100 dark:hover:bg-white/5"
>
<ArrowLeft className="w-5 h-5" />
</Button>
<div className="flex-1 min-w-0">
<h1 className="text-xl font-black tracking-tighter text-slate-900 dark:text-white truncate uppercase">
{product.product_name || "Unlabeled Product"}
</h1>
<div className="flex items-center gap-2 text-[10px] font-mono font-bold text-slate-500 uppercase tracking-widest">
<Database size={10} className="text-blue-500" /> {product.code}
</div>
</div>
<div
style={{
minHeight: "100vh",
background: "var(--ink-0, #fdfbf5)",
color: "var(--ink-900, #141312)",
}}
>
{/* ── Top bar ──────────────────────────────────────────────────────── */}
<div
style={{
padding: "18px clamp(20px, 5vw, 40px)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
borderBottom: "1px solid var(--ink-200, #e4dfd7)",
position: "sticky",
top: 0,
zIndex: 50,
background: "rgba(253,251,245,0.92)",
backdropFilter: "blur(12px)",
}}
>
<button
onClick={() => router.back()}
style={{
display: "inline-flex",
alignItems: "center",
gap: 10,
fontSize: 13,
color: "var(--ink-700, #3d3934)",
background: "none",
border: "none",
cursor: "pointer",
}}
>
<ArrowLeft style={{ width: 16, height: 16 }} strokeWidth={1.75} />
Voltar
</button>
<div
style={{
fontFamily: "ui-monospace, monospace",
fontSize: 11,
color: "var(--ink-500, #7a7269)",
letterSpacing: "0.12em",
}}
>
EAN · {product.code}
</div>
</header>
<div style={{ display: "flex", gap: 8 }}>
<button
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: 36,
height: 36,
border: "1px solid var(--ink-200, #e4dfd7)",
borderRadius: 8,
background: "transparent",
color: "var(--ink-700, #3d3934)",
cursor: "pointer",
}}
title="Salvar"
>
<Heart style={{ width: 14, height: 14 }} strokeWidth={1.75} />
</button>
<button
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: 36,
height: 36,
border: "1px solid var(--ink-200, #e4dfd7)",
borderRadius: 8,
background: "transparent",
color: "var(--ink-700, #3d3934)",
cursor: "pointer",
}}
title="Compartilhar"
>
<Share2 style={{ width: 14, height: 14 }} strokeWidth={1.75} />
</button>
</div>
</div>
<main className="max-w-7xl mx-auto px-6 py-12">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12">
{/* COLUNA ESQUERDA: Ativos Visuais (5 Colunas) */}
<div className="lg:col-span-5 space-y-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="relative aspect-square bg-white rounded-[3rem] border border-slate-200 overflow-hidden shadow-2xl dark:bg-slate-900 dark:border-white/5 group"
{/* ── Hero: image + headline + Nutri-Score ────────────────────────── */}
<div
style={{
padding: "40px clamp(20px, 5vw, 40px) 32px",
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))",
gap: 48,
maxWidth: 1100,
margin: "0 auto",
}}
>
{/* Product image */}
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45 }}
style={{
background: "var(--ink-50, #f9f6f0)",
borderRadius: 18,
aspectRatio: "1/1",
display: "grid",
placeItems: "center",
border: "1px solid var(--ink-200, #e4dfd7)",
overflow: "hidden",
position: "relative",
}}
>
{imageUrl ? (
<Image
src={imageUrl}
alt={product.product_name || "Produto"}
fill
className="object-contain p-10"
priority
sizes="(max-width: 768px) 100vw, 500px"
/>
) : (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
color: "var(--ink-400, #a39c93)",
}}
>
{imageUrl ? (
<Image
src={imageUrl}
alt={product.product_name || "Product"}
fill
className="object-contain p-12 transition-transform duration-700 group-hover:scale-110"
priority
sizes="(max-width: 768px) 100vw, 500px"
/>
) : (
<div className="h-full flex flex-col items-center justify-center text-slate-300 dark:text-slate-800">
<Beaker size={80} className="mb-4 opacity-50" />
<span className="text-xs font-mono font-black uppercase tracking-[0.4em]">No Visual Metadata</span>
<Beaker style={{ width: 64, height: 64 }} strokeWidth={1} />
<span
style={{
fontSize: 12,
fontFamily: "ui-monospace, monospace",
textTransform: "uppercase",
letterSpacing: "0.1em",
marginTop: 12,
}}
>
Sem imagem
</span>
</div>
)}
</motion.div>
{/* Right column — headline + score cards */}
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.05 }}
>
{/* Brand eyebrow */}
<div
style={{
fontSize: 12,
color: "var(--ink-500, #7a7269)",
fontFamily: "ui-monospace, monospace",
letterSpacing: "0.12em",
textTransform: "uppercase",
marginBottom: 12,
}}
>
{product.brands || "Marca desconhecida"}
</div>
{/* Product name */}
<h1
style={{
fontFamily: "'Instrument Serif', Georgia, serif",
fontSize: "clamp(28px, 4vw, 44px)",
letterSpacing: "-0.01em",
lineHeight: 1.08,
color: "var(--ink-900, #141312)",
marginBottom: 28,
}}
>
{product.product_name || "Produto sem nome"}
</h1>
{/* ── Nutri-Score hero metric ─────────────────────────────────── */}
{grade && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.15 }}
style={{
display: "flex",
alignItems: "center",
gap: 20,
padding: 24,
background: "var(--ink-50, #f9f6f0)",
border: "1px solid var(--ink-200, #e4dfd7)",
borderRadius: 16,
marginBottom: 16,
}}
>
<NutriLetter grade={grade} size={92} />
<div style={{ flex: 1 }}>
<div
style={{
fontFamily: "ui-monospace, monospace",
fontSize: 10,
letterSpacing: "0.14em",
textTransform: "uppercase",
color: "var(--ink-500, #7a7269)",
fontWeight: 500,
marginBottom: 6,
}}
>
Nutri‑Score
</div>
<div
style={{
fontFamily: "'Instrument Serif', Georgia, serif",
fontSize: 26,
letterSpacing: "-0.01em",
color: "var(--ink-900, #141312)",
lineHeight: 1.1,
marginBottom: 12,
}}
>
{gradeDesc || `Nota ${grade.toUpperCase()}`}
</div>
<NutriScale active={grade} size="sm" />
</div>
</motion.div>
)}
{/* ── NOVA row ─────────────────────────────────────────────────── */}
{nova && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
style={{
padding: 20,
border: "1px solid var(--ink-200, #e4dfd7)",
borderRadius: 14,
marginBottom: 16,
display: "flex",
gap: 18,
alignItems: "center",
}}
>
<NovaPill group={nova} />
{novaDesc && (
<div
style={{
fontSize: 14,
color: "var(--ink-600, #595149)",
lineHeight: 1.55,
flex: 1,
}}
>
{novaDesc}
</div>
)}
</motion.div>
)}
{/* Ficha Técnica (Bento-style) */}
<div className="p-8 rounded-[2.5rem] bg-white border border-slate-200 shadow-sm dark:bg-white/[0.02] dark:border-white/5 space-y-6">
<h3 className="text-[10px] font-black uppercase tracking-[0.4em] text-blue-600 dark:text-blue-400">SKU Datasheet</h3>
<div className="space-y-4 font-medium">
{[
{ label: "Fabricante", val: product.brands, icon: ShieldCheck },
{ label: "Região", val: product.countries, icon: Globe },
{ label: "Registro", val: "Auditado", icon: ClipboardList }
].map((item, i) => item.val && (
<div key={i} className="flex justify-between items-center text-sm">
<span className="text-slate-400 flex items-center gap-2 font-bold uppercase text-[11px] tracking-tight">
<item.icon size={14} className="text-slate-300" /> {item.label}
</span>
<span className="text-slate-900 dark:text-white text-right max-w-[60%] truncate">{item.val}</span>
{/* ── Allergens ────────────────────────────────────────────────── */}
<AnimatePresence>
{allergenTags.length > 0 && (
<motion.div
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.25 }}
style={{
padding: 18,
borderRadius: 14,
background: "#fef3c7",
border: "1px solid #fcd34d",
display: "flex",
alignItems: "flex-start",
gap: 14,
}}
>
<AlertTriangle
style={{ width: 20, height: 20, color: "#b45309", flexShrink: 0, marginTop: 1 }}
strokeWidth={1.75}
/>
<div>
<div
style={{
fontSize: 13,
fontWeight: 600,
color: "#b45309",
marginBottom: 6,
}}
>
Contém alérgenos
</div>
))}
</div>
</div>
</div>
{/* COLUNA DIREITA: Inteligência de Dados (7 Colunas) */}
<div className="lg:col-span-7 space-y-8">
{/* Veredito Rápido: Scores */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<DataCard
title="Nutri-Score"
desc="Indicador de Qualidade Nutricional"
badge={<NutriScoreBadge grade={product.nutrition_grades || ""} size="lg" />}
/>
<DataCard
title="Protocolo NOVA"
desc="Nível de Processamento Industrial"
badge={<NovaBadge group={product.nova_group || 0} size="lg" />}
/>
</div>
{/* Análise Macronutricional */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="p-1 rounded-[3rem] bg-gradient-to-br from-slate-200 to-slate-100 dark:from-white/10 dark:to-transparent"
>
<div className="bg-white rounded-[2.9rem] p-10 dark:bg-[#030712]">
<h3 className="text-lg font-black text-slate-900 dark:text-white mb-8 flex items-center gap-3 uppercase tracking-tighter">
<div className="w-2 h-6 bg-blue-600 rounded-full" />
Composição Nutricional <span className="text-slate-400 font-mono text-xs font-normal">/ 100g</span>
</h3>
<NutritionGrid nutritionData={formatNutritionData(product) || {}} />
</div>
</motion.div>
{/* Bloqueadores e Alérgenos */}
<AnimatePresence>
{product.allergens_tags && product.allergens_tags.length > 0 && (
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
className="p-8 rounded-[2.5rem] bg-orange-50 border border-orange-200 dark:bg-orange-500/5 dark:border-orange-500/20"
>
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-orange-600 rounded-xl text-white shadow-lg shadow-orange-600/20">
<AlertTriangle size={20} />
</div>
<h3 className="text-lg font-black text-orange-950 dark:text-orange-500 uppercase tracking-tighter">Detecção de Alérgenos</h3>
</div>
<div className="flex flex-wrap gap-2">
{product.allergens_tags.map(tag => (
<span key={tag} className="px-4 py-2 rounded-xl bg-white border border-orange-200 text-orange-700 text-[10px] font-black uppercase tracking-widest dark:bg-orange-500/10 dark:text-orange-400 dark:border-orange-500/20 shadow-sm">
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
{allergenTags.map((tag) => (
<span
key={tag}
style={{
fontSize: 12,
padding: "3px 10px",
background: "rgba(255,255,255,0.7)",
border: "1px solid #fde68a",
borderRadius: 999,
color: "#78350f",
}}
>
{tag.replace(/en:|pt:/g, "")}
</span>
))}
</div>
</motion.div>
)}
</AnimatePresence>
{/* Lista de Ingredientes (OCR Interpretation) */}
<div className="p-10 rounded-[2.5rem] bg-white border border-slate-200 shadow-sm dark:bg-white/[0.02] dark:border-white/5">
<h3 className="text-lg font-black text-slate-900 dark:text-white mb-6 uppercase tracking-tighter">Relatório de Ingredientes</h3>
<p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400 font-medium">
{product.ingredients_text || "O fabricante não forneceu a lista de ingredientes para este SKU."}
</p>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
</div>
{/* ── Nutrition composition section ───────────────────────────────── */}
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
style={{
borderTop: "1px solid var(--ink-200, #e4dfd7)",
padding: "32px clamp(20px, 5vw, 40px) 60px",
maxWidth: 1100,
margin: "0 auto",
}}
>
<div
style={{
display: "flex",
alignItems: "baseline",
justifyContent: "space-between",
marginBottom: 24,
gap: 12,
flexWrap: "wrap",
}}
>
<h2
style={{
fontFamily: "'Instrument Serif', Georgia, serif",
fontSize: "clamp(22px, 3vw, 30px)",
letterSpacing: "-0.01em",
color: "var(--ink-900, #141312)",
}}
>
Composição nutricional
</h2>
<div
style={{
fontFamily: "ui-monospace, monospace",
fontSize: 10,
letterSpacing: "0.14em",
textTransform: "uppercase",
color: "var(--ink-500, #7a7269)",
fontWeight: 500,
}}
>
por 100g · %VD de referência
</div>
</div>
</main>
</div>
);
}
// Sub-componente para cards de dados
function DataCard({ title, desc, badge }: { title: string, desc: string, badge: React.ReactNode }) {
return (
<div className="p-8 rounded-[2.5rem] bg-white border border-slate-200 shadow-sm dark:bg-white/[0.02] dark:border-white/5 hover:border-blue-500/30 transition-all duration-300">
<h3 className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-400 mb-1">{title}</h3>
<p className="text-sm text-slate-600 dark:text-slate-300 mb-8 font-bold leading-tight">{desc}</p>
<div className="flex justify-start transform hover:scale-105 transition-transform origin-left">
{badge}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))",
gap: "0 48px",
}}
>
{nutritionRows.map((row) => {
const raw = nutriments[row.key as keyof typeof nutriments];
const v = raw != null ? Number(raw) : null;
if (v == null) return null;
const ref = dailyRef[row.key] || 100;
const pct = Math.min(100, Math.round((v / ref) * 100));
const isHigh =
pct > 40 &&
["fat_100g", "saturated_fat_100g", "sugars_100g", "salt_100g"].includes(row.key);
const isGood = ["fiber_100g", "proteins_100g"].includes(row.key);
const barColor = isHigh
? "#e63e11"
: isGood
? "var(--accent, oklch(52% 0.11 155))"
: "var(--ink-500, #7a7269)";
return (
<div
key={row.key}
style={{
padding: "14px 0",
borderBottom: "1px solid var(--ink-200, #e4dfd7)",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "baseline",
marginBottom: 8,
}}
>
<span style={{ fontSize: 14, color: "var(--ink-800, #26231f)" }}>
{row.label}
</span>
<span
style={{
fontFamily: "'Instrument Serif', Georgia, serif",
fontSize: 18,
color: "var(--ink-900, #141312)",
}}
>
{v}
<span
style={{
fontSize: 11,
color: "var(--ink-500, #7a7269)",
marginLeft: 3,
fontFamily: "ui-monospace, monospace",
}}
>
{row.unit}
</span>
</span>
</div>
<div
style={{
height: 4,
background: "var(--ink-100, #f3efe7)",
borderRadius: 999,
overflow: "hidden",
}}
>
<div
style={{
width: `${pct}%`,
height: "100%",
background: barColor,
borderRadius: 999,
transition: "width 0.6s",
}}
/>
</div>
<div
style={{
fontSize: 10,
color: "var(--ink-500, #7a7269)",
marginTop: 4,
fontFamily: "ui-monospace, monospace",
letterSpacing: "0.04em",
}}
>
{pct}% VD
</div>
</div>
);
})}
</div>
{/* Ingredients */}
{product.ingredients_text && (
<div
style={{
marginTop: 40,
padding: 24,
background: "var(--ink-50, #f9f6f0)",
border: "1px solid var(--ink-200, #e4dfd7)",
borderRadius: 14,
}}
>
<h3
style={{
fontFamily: "'Instrument Serif', Georgia, serif",
fontSize: 20,
color: "var(--ink-900, #141312)",
marginBottom: 14,
}}
>
Lista de Ingredientes
</h3>
<p
style={{
fontSize: 14,
color: "var(--ink-600, #595149)",
lineHeight: 1.65,
}}
>
{product.ingredients_text}
</p>
</div>
)}
</motion.div>
{/* ── Footer attribution ─────────────────────────────────────────── */}
<div
style={{
padding: "24px clamp(20px, 5vw, 40px)",
background: "var(--ink-50, #f9f6f0)",
borderTop: "1px solid var(--ink-200, #e4dfd7)",
}}
>
<div
style={{
maxWidth: 1100,
margin: "0 auto",
fontSize: 12,
color: "var(--ink-500, #7a7269)",
fontFamily: "ui-monospace, monospace",
letterSpacing: "0.04em",
lineHeight: 1.6,
}}
>
Dados fornecidos pela comunidade Open Food Facts sob licença ODbL.
{product.countries && ` · Região: ${product.countries}`}
</div>
</div>
</div>
);
}
}