feat(footer, header, landing): enhance UI components and improve navigation

- Updated Footer component with new links and dynamic year display.
- Refactored Header component to improve mobile navigation and theme toggling.
- Enhanced DonationSection and Features components with improved layout and animations.
- Updated HowItWorks and Impact sections for better user engagement and clarity.
- Improved product page layout and error handling in API interactions.
- General UI refinements across various components for a cohesive user experience.
This commit is contained in:
Evandro Fonseca Junior committed 2026-01-12 19:55:50 -03:00
1 parent b0f95704e1
commit f98c648311
21 files changed
+3335 -12057

No files matched your search

+108 -130
View File
@@ -1,3 +1,8 @@
"use client";
import React from "react";
import Link from "next/link";
import Image from "next/image";
import {
ExternalLink,
Github,
@@ -5,171 +10,144 @@ import {
Globe,
Shield,
ScanLine,
Database,
Lock,
} from "lucide-react";
const FOOTER_LINKS = {
navigation: [
{ href: "/#como-funciona", label: "Protocolo" },
{ href: "/#recursos", label: "Recursos" },
{ href: "/sobre", label: "Sobre o Projeto" },
{ href: "/produto/exemplo", label: "Sandbox (Exemplo)" },
],
resources: [
{ icon: Shield, label: "Dados Colaborativos" },
{ icon: Globe, label: "Rede Global" },
{ icon: ScanLine, label: "Algoritmo de Busca" },
],
};
export function Footer() {
const currentYear = new Date().getFullYear();
return (
<footer className="bg-gray-900 dark:bg-black text-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Brand */}
<div className="md:col-span-1">
<div className="flex items-center gap-3 mb-6">
<img
src="/apple-touch-icon.png"
alt="NutriScan Logo"
className="w-10 h-10 rounded-xl"
/>
<footer className="bg-[#030712] text-gray-400 border-t border-gray-800/50 transition-colors duration-500">
<div className="max-w-7xl mx-auto px-6 py-20">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 mb-20">
{/* Brand Identity - 4 Colunas */}
<div className="lg:col-span-4 space-y-8">
<Link href="/" className="flex items-center gap-3 group">
<div className="relative w-10 h-10 bg-blue-600 rounded-xl overflow-hidden shadow-lg shadow-blue-500/10">
<Image src="/icon.png" alt="Logo" fill className="object-contain p-2" priority />
</div>
<div>
<h3 className="text-xl font-bold">NutriScan</h3>
<p className="text-sm text-gray-400">
Informações nutricionais
<h3 className="text-xl font-black text-white tracking-tighter leading-none">
NUTRI<span className="text-blue-500">SCAN</span>
</h3>
<p className="text-[10px] font-bold text-gray-500 uppercase tracking-[0.2em] mt-1">
Data Sovereignty
</p>
</div>
</div>
<p className="text-gray-400 text-sm leading-relaxed mb-6">
Descubra informações nutricionais completas de milhares de
produtos alimentícios com apenas um código de barras.
</Link>
<p className="text-sm leading-relaxed max-w-sm">
Soberania alimentar através da transparência. Extraímos metadados nutricionais complexos para devolver o poder de escolha ao cidadão.
</p>
<div className="flex items-center gap-2 text-sm text-gray-400">
<Heart className="w-4 h-4 text-red-500" />
<span>Feito com amor para uma alimentação mais consciente</span>
<div className="flex items-center gap-2 text-[11px] font-mono uppercase tracking-widest text-gray-500">
<Heart className="w-3 h-3 text-red-500 animate-pulse" />
<span>Engineered in Garuva, SC</span>
</div>
</div>
{/* Links */}
<div>
<h4 className="text-lg font-semibold mb-4">Navegação</h4>
<ul className="space-y-3">
<li>
<a
href="/#como-funciona"
className="text-gray-400 hover:text-white transition-colors"
>
Como funciona
</a>
</li>
<li>
<a
href="/#recursos"
className="text-gray-400 hover:text-white transition-colors"
>
Recursos
</a>
</li>
<li>
<a
href="#sobre"
className="text-gray-400 hover:text-white transition-colors"
>
Sobre
</a>
</li>
<li>
<a
href="/produto/exemplo"
className="text-gray-400 hover:text-white transition-colors"
>
Exemplo de produto
</a>
</li>
{/* Navigation - 2 Colunas */}
<div className="lg:col-span-2">
<h4 className="text-white font-bold text-sm uppercase tracking-widest mb-6">Sitemap</h4>
<ul className="space-y-4">
{FOOTER_LINKS.navigation.map((link) => (
<li key={link.href}>
<Link href={link.href} className="text-sm hover:text-blue-400 transition-colors">
{link.label}
</Link>
</li>
))}
</ul>
</div>
{/* Recursos */}
<div>
<h4 className="text-lg font-semibold mb-4">Recursos</h4>
<ul className="space-y-3">
<li className="flex items-center gap-2 text-gray-400">
<Shield className="w-4 h-4" />
<span>Dados colaborativos</span>
</li>
<li className="flex items-center gap-2 text-gray-400">
<Globe className="w-4 h-4" />
<span>Banco global</span>
</li>
<li className="flex items-center gap-2 text-gray-400">
<ScanLine className="w-4 h-4" />
<span>Busca por código</span>
</li>
<li className="text-gray-400">
<span>Nutri-Score & NOVA</span>
</li>
{/* Logic/Features - 2 Colunas */}
<div className="lg:col-span-2">
<h4 className="text-white font-bold text-sm uppercase tracking-widest mb-6">Infraestrutura</h4>
<ul className="space-y-4">
{FOOTER_LINKS.resources.map((item) => (
<li key={item.label} className="flex items-center gap-3 text-sm">
<item.icon className="w-4 h-4 text-gray-600" />
<span>{item.label}</span>
</li>
))}
<li className="text-[11px] font-mono text-blue-500/80">Nutri-Score v2.1</li>
</ul>
</div>
{/* Open Food Facts */}
<div>
<h4 className="text-lg font-semibold mb-4">Fonte de Dados</h4>
<div className="bg-gray-800 rounded-lg p-4 mb-4">
<div className="flex items-center gap-2 mb-2">
<ExternalLink className="w-4 h-4 text-blue-400" />
<span className="font-medium">Fonte de Dados</span>
{/* Data Source Card - 4 Colunas */}
<div className="lg:col-span-4">
<div className="bg-gray-900/50 border border-gray-800 p-6 rounded-2xl backdrop-blur-sm">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-blue-500/10 rounded-lg">
<Database className="w-5 h-5 text-blue-500" />
</div>
<h4 className="text-white font-bold">Open Food Facts</h4>
</div>
<p className="text-sm text-gray-400 mb-3">
Dados fornecidos pelo Open Food Facts - a maior base de dados
colaborativa de produtos alimentícios do mundo.
<p className="text-xs leading-relaxed text-gray-500 mb-6">
A maior base de dados colaborativa de alimentos do mundo. Dados sob licença ODbL (Open Database License).
</p>
<a
href="https://world.openfoodfacts.org"
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300 text-sm transition-colors"
className="inline-flex items-center gap-2 text-xs font-black text-blue-500 uppercase tracking-widest hover:text-blue-400 transition-all"
>
Visitar Open Food Facts →
Protocolo de Dados <ExternalLink size={12} />
</a>
</div>
<div className="flex items-center gap-2 text-sm text-gray-400">
<Github className="w-4 h-4" />
<span>Código aberto</span>
</div>
</div>
</div>
{/* Disclaimer */}
<div className="bg-gray-800 rounded-lg p-6 my-12">
<p className="text-xs text-gray-400 text-center leading-relaxed">
<strong>Disclaimer:</strong> Os dados apresentados são fornecidos
pelo Open Food Facts, uma base de dados colaborativa. Não garantimos
a precisão, completude ou atualidade das informações. Sempre
verifique as informações na embalagem do produto.
</p>
{/* Disclaimer Técnico - Estilo "Regulatory" */}
<div className="border-t border-gray-800/50 pt-12">
<div className="bg-gray-900/30 border border-gray-800/50 p-6 rounded-2xl flex flex-col md:flex-row items-center gap-6">
<Lock className="w-8 h-8 text-gray-700 shrink-0" />
<p className="text-[10px] md:text-xs text-gray-500 leading-relaxed text-center md:text-left">
<strong className="text-gray-400 uppercase tracking-widest">Aviso Legal (Disclaimer):</strong> Os dados são extraídos de fontes colaborativas. Não garantimos a integridade absoluta das informações nutricionais em comparação com a embalagem física. O uso desta ferramenta é consultivo e não substitui orientação médica ou nutricional profissional.
</p>
</div>
</div>
{/* Bottom */}
<div className="border-t border-gray-800 mt-12 pt-8">
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<div className="text-sm text-gray-400">
© 2024 NutriScan. Todos os direitos reservados.
<br />
<span className="text-xs">
Desenvolvido por{" "}
<a
href="https://evandro.dev.br"
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300 transition-colors"
>
evandro.dev.br
</a>{" "}
e mantido por NutriScan
</span>
</div>
<div className="flex items-center gap-6 text-sm text-gray-400">
<a href="#" className="hover:text-white transition-colors">
Privacidade
{/* Bottom Bar */}
<div className="mt-12 pt-8 flex flex-col md:flex-row justify-between items-center gap-6">
<div className="text-[11px] font-mono text-gray-600 text-center md:text-left space-y-1">
<p>© {currentYear} NutriScan Systems. Open Source Initiative.</p>
<p>
Lead Architect:{" "}
<a href="https://evandro.dev.br" target="_blank" className="text-blue-500 hover:underline">
evandro.dev.br
</a>
<a href="#" className="hover:text-white transition-colors">
Termos
</a>
<a href="#" className="hover:text-white transition-colors">
Contato
</a>
</p>
</div>
<div className="flex items-center gap-8">
<div className="flex items-center gap-4">
<Link href="/privacidade" className="text-[11px] font-black uppercase tracking-widest hover:text-white transition-colors">Privacidade</Link>
<Link href="/termos" className="text-[11px] font-black uppercase tracking-widest hover:text-white transition-colors">Termos</Link>
</div>
<a href="https://github.com" target="_blank" className="p-2 bg-gray-900 rounded-full hover:bg-gray-800 transition-colors">
<Github className="w-4 h-4 text-white" />
</a>
</div>
</div>
</div>
</footer>
);
}
}
+113 -105
View File
@@ -1,139 +1,147 @@
"use client";
import { useState, useEffect } from "react";
import React, { useState, useEffect } from "react";
import Link from "next/link";
import { useTheme } from "next-themes";
import { motion, AnimatePresence } from "framer-motion";
import { Sun, Moon, Menu, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import Image from "next/image"
// 1. Definição de Dados Centralizada (SSOT - Single Source of Truth)
const NAV_LINKS = [
{ href: "/", label: "Como funciona" },
{ href: "/#recursos", label: "Recursos" },
{ href: "/sobre", label: "Sobre" },
];
export function Header() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
useEffect(() => {
setMounted(true);
}, []);
// Prevenção de Hydration Mismatch
useEffect(() => setMounted(true), []);
const toggleTheme = () => {
setTheme(theme === "dark" ? "light" : "dark");
};
const toggleMenu = () => {
setIsMenuOpen(!isMenuOpen);
};
const toggleTheme = () => setTheme(theme === "dark" ? "light" : "dark");
return (
<header className="sticky top-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-14 sm:h-16">
{/* Logo - compacto em mobile */}
<div className="flex items-center gap-2 sm:gap-3">
<img
src="/apple-touch-icon.png"
alt="NutriScan Logo"
className="w-8 h-8 sm:w-10 sm:h-10 rounded-xl"
/>
<div className="hidden sm:block">
<h1 className="text-xl font-bold text-gray-900 dark:text-white">
NutriScan
</h1>
<p className="text-xs text-gray-500 dark:text-gray-400">
Informações nutricionais
</p>
<header className="sticky top-0 z-[100] w-full bg-white/70 dark:bg-gray-900/70 backdrop-blur-md border-b border-gray-200 dark:border-gray-800 transition-colors duration-500">
<div className="max-w-7xl mx-auto px-6">
<div className="flex items-center justify-between h-16">
{/* Logo Section - Unificada */}
<Link href="/" className="flex items-center gap-3 group">
{/* Container do Logo com Referencial Relativo */}
<div className="w-10 h-10 rounded-xl flex items-center justify-center shadow-lg shadow-blue-500/20 group-hover:rotate-6 transition-transform relative overflow-hidden">
<Image
src="/apple-touch-icon.png"
alt="NutriScan Logo"
fill // Ocupa todo o container pai
priority // CRÍTICO: Carrega o logo antes de tudo para melhorar o LCP
sizes="40px" // Dica para o browser sobre o tamanho real em pixels
className="object-contain p-2" // Mantém a proporção e adiciona respiro interno
/>
</div>
<div className="flex flex-col">
<span className="text-lg font-black tracking-tighter text-gray-900 dark:text-white leading-none">
NUTRI<span className="text-blue-500">SCAN</span>
</span>
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest leading-none mt-1">
Data Engineering
</span>
</div>
{/* Logo compacto para mobile */}
<div className="sm:hidden">
<h1 className="text-base font-bold text-gray-900 dark:text-white">
NutriScan
</h1>
</div>
</div>
</Link>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-8">
<a
href="/"
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
>
Como funciona
</a>
<a
href="/#recursos"
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
>
Recursos
</a>
<a
href="/sobre"
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
>
Sobre
</a>
{/* Desktop Nav - Com micro-interações */}
<nav className="hidden md:flex items-center gap-1">
{NAV_LINKS.map((link) => (
<Link
key={link.href}
href={link.href}
className="px-4 py-2 text-sm font-semibold text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-all"
>
{link.label}
</Link>
))}
</nav>
{/* Actions */}
<div className="flex items-center gap-4">
{/* Theme Toggle */}
{/* Ações e Mobile Toggle */}
<div className="flex items-center gap-2">
{mounted && (
<Button
variant="ghost"
size="sm"
size="icon"
onClick={toggleTheme}
className="w-10 h-10 p-0"
suppressHydrationWarning
className="rounded-full w-10 h-10 border border-gray-200 dark:border-gray-800"
>
{theme === "dark" ? (
<Sun className="w-5 h-5" />
) : (
<Moon className="w-5 h-5" />
)}
<motion.div
initial={false}
animate={{ rotate: theme === "dark" ? 0 : 180, scale: 1 }}
whileTap={{ scale: 0.9 }}
>
{theme === "dark" ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
</motion.div>
</Button>
)}
{/* Mobile Menu Button */}
<Button
variant="ghost"
size="sm"
onClick={toggleMenu}
className="md:hidden w-10 h-10 p-0"
size="icon"
className="md:hidden rounded-full"
onClick={() => setIsMenuOpen(!isMenuOpen)}
>
{isMenuOpen ? (
<X className="w-5 h-5" />
) : (
<Menu className="w-5 h-5" />
)}
<AnimatePresence mode="wait">
<motion.div
key={isMenuOpen ? "close" : "open"}
initial={{ opacity: 0, rotate: -90 }}
animate={{ opacity: 1, rotate: 0 }}
exit={{ opacity: 0, rotate: 90 }}
transition={{ duration: 0.2 }}
>
{isMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</motion.div>
</AnimatePresence>
</Button>
</div>
</div>
{/* Mobile Menu */}
{isMenuOpen && (
<div className="md:hidden border-t border-gray-200 dark:border-gray-700 py-4">
<nav className="flex flex-col gap-4">
<a
href="/"
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors py-2"
onClick={() => setIsMenuOpen(false)}
>
Como funciona
</a>
<a
href="/#recursos"
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors py-2"
onClick={() => setIsMenuOpen(false)}
>
Recursos
</a>
<a
href="/sobre"
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors py-2"
onClick={() => setIsMenuOpen(false)}
>
Sobre
</a>
</nav>
</div>
)}
</div>
{/* Mobile Menu - Animado com AnimatePresence */}
<AnimatePresence>
{isMenuOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className="md:hidden border-t border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 overflow-hidden"
>
<nav className="flex flex-col p-6 gap-4">
{NAV_LINKS.map((link, i) => (
<motion.div
key={link.href}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.1 }}
>
<Link
href={link.href}
onClick={() => setIsMenuOpen(false)}
className="text-xl font-bold text-gray-900 dark:text-white hover:text-blue-500 transition-colors"
>
{link.label}
</Link>
</motion.div>
))}
<div className="pt-4 border-t border-gray-100 dark:border-gray-800">
<p className="text-[10px] font-black text-gray-400 uppercase tracking-[0.3em]">
NutriScan v2.0
</p>
</div>
</nav>
</motion.div>
)}
</AnimatePresence>
</header>
);
}
}
+87 -86
View File
@@ -1,96 +1,97 @@
import { Heart, Check, Code } from "lucide-react";
"use client";
import React from "react";
import { motion } from "framer-motion";
import { Heart, Coffee, ShieldCheck, Server, Globe, Code2 } from "lucide-react";
import { Button } from "@/components/ui/button";
export function DonationSection() {
return (
<section className="py-20 bg-white dark:bg-gray-900">
<div className="max-w-4xl mx-auto px-4">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold mb-4">
Apoie a Transparência Alimentar
</h2>
<p className="text-xl text-gray-600 dark:text-gray-400">
Este projeto é mantido por desenvolvedores independentes que
acreditam que informação sobre saúde deve ser acessível a todos, sem
custo.
</p>
</div>
<section className="py-24 bg-white dark:bg-[#030712] relative overflow-hidden">
{/* Background Decor sutil */}
<div className="absolute top-0 right-0 w-1/3 h-1/3 bg-blue-500/5 blur-[120px] rounded-full pointer-events-none" />
{/* Benefícios da Doação */}
<div className="bg-gradient-to-br from-blue-50 to-green-50 dark:from-gray-800 dark:to-gray-700 rounded-2xl p-8 mb-8">
<h3 className="text-2xl font-bold mb-6">Sua doação garante:</h3>
<div className="grid md:grid-cols-2 gap-4">
<div className="flex items-start gap-3">
<div className="w-6 h-6 rounded-full bg-blue-500 flex items-center justify-center flex-shrink-0 mt-1">
<Check className="w-4 h-4 text-white" />
</div>
<p>Servidores sempre disponíveis para consultas instantâneas</p>
</div>
<div className="flex items-start gap-3">
<div className="w-6 h-6 rounded-full bg-green-500 flex items-center justify-center flex-shrink-0 mt-1">
<Check className="w-4 h-4 text-white" />
</div>
<p>Novas funcionalidades baseadas em feedback da comunidade</p>
</div>
<div className="flex items-start gap-3">
<div className="w-6 h-6 rounded-full bg-purple-500 flex items-center justify-center flex-shrink-0 mt-1">
<Check className="w-4 h-4 text-white" />
</div>
<p>Manutenção contínua e atualizações de segurança</p>
</div>
<div className="flex items-start gap-3">
<div className="w-6 h-6 rounded-full bg-orange-500 flex items-center justify-center flex-shrink-0 mt-1">
<Check className="w-4 h-4 text-white" />
</div>
<p>Expansão do acesso a informações para mais pessoas</p>
</div>
</div>
</div>
{/* Humanização - História do Projeto */}
<div className="bg-gray-50 dark:bg-gray-800 rounded-2xl p-8 mb-8">
<div className="flex items-center gap-4 mb-4">
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-blue-500 to-green-500 flex items-center justify-center">
<Code className="w-8 h-8 text-white" />
</div>
<div>
<h4 className="font-bold text-lg">
Ideia nascida de um estudante
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Mantido por NutriScan
</p>
</div>
</div>
<p className="text-gray-600 dark:text-gray-400">
O que começou como uma ideia de um estudante de Engenharia da
Computação se tornou uma ferramenta que democratiza o acesso à
informação nutricional. Cada doação ajuda a manter este projeto vivo
e acessível para todos.
</p>
</div>
{/* CTA de Doação */}
<div className="text-center">
<a
href="https://buymeacoffee.com/evandrotruuta"
target="_blank"
rel="noopener noreferrer"
className="inline-block"
<div className="max-w-5xl mx-auto px-6 relative z-10">
<div className="grid lg:grid-cols-2 gap-16 items-center">
{/* Lado Esquerdo: A Missão (Copywriting Técnico) */}
<motion.div
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
>
<Button
size="lg"
className="bg-gradient-to-r from-blue-600 to-green-600 text-white px-8 py-6 text-lg hover:shadow-2xl transition-all"
>
<Heart className="w-5 h-5 mr-2" />
Doe/Donate
</Button>
</a>
<p className="mt-4 text-sm text-gray-500">
Qualquer valor faz diferença 💛
</p>
<h2 className="text-4xl md:text-5xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter leading-tight">
Mantenha a <span className="text-blue-600">Engenharia</span> Livre de Interesses.
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400 mb-8 leading-relaxed">
O NutriScan é um projeto de código aberto nascido em Garuva, SC. Operamos sem anúncios e sem venda de dados. Nosso único combustível é o apoio da comunidade que acredita na democratização da saúde.
</p>
<div className="space-y-4">
{[
{ icon: Server, text: "Manutenção de Servidores e API de Alta Latência" },
{ icon: ShieldCheck, text: "Segurança de Dados e Auditoria Open Source" },
{ icon: Globe, text: "Expansão da Base de Dados Internacional" },
].map((item, i) => (
<div key={i} className="flex items-center gap-3 text-sm font-medium text-gray-700 dark:text-gray-300">
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
<item.icon className="w-4 h-4 text-blue-600" />
</div>
{item.text}
</div>
))}
</div>
</motion.div>
{/* Lado Direito: O Card de Doação (Psychological Anchoring) */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="relative"
>
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 p-8 rounded-[2.5rem] shadow-2xl backdrop-blur-sm">
<div className="flex items-center gap-4 mb-8">
<div className="w-12 h-12 bg-gradient-to-br from-yellow-400 to-orange-500 rounded-2xl flex items-center justify-center shadow-lg shadow-orange-500/20">
<Coffee className="w-6 h-6 text-white" />
</div>
<div>
<h4 className="font-bold text-gray-900 dark:text-white">Apoie com um Café</h4>
<p className="text-xs text-gray-500 uppercase tracking-widest font-mono">Status: Sistema Online</p>
</div>
</div>
<blockquote className="italic text-gray-600 dark:text-gray-400 text-sm border-l-2 border-blue-500 pl-4 mb-8">
&quot;Como desenvolvedor independente, cada contribuição me permite dedicar mais horas à refatoração e novas features para o NutriScan.&quot;
</blockquote>
<a
href="https://buymeacoffee.com/evandrotruuta"
target="_blank"
rel="noopener noreferrer"
className="block group"
>
<Button className="w-full h-16 bg-blue-600 hover:bg-blue-700 text-white rounded-2xl font-bold text-lg transition-all group-hover:scale-[1.02] active:scale-95 shadow-xl shadow-blue-500/25">
<Heart className="w-5 h-5 mr-3 fill-white group-hover:animate-pulse" />
Contribuir agora
</Button>
</a>
<div className="mt-6 flex justify-center gap-4">
<div className="flex items-center gap-1.5 text-[10px] font-bold text-gray-400 uppercase tracking-tighter">
<Code2 size={12} /> Engenharia Brasileira
</div>
</div>
</div>
{/* Elemento Decorativo: Floating Badge */}
<div className="absolute -bottom-6 -right-6 bg-white dark:bg-gray-800 p-4 rounded-2xl shadow-xl border border-gray-100 dark:border-gray-700 animate-bounce">
<p className="text-[10px] font-black text-blue-600 uppercase">Qualquer valor ajuda!</p>
</div>
</motion.div>
</div>
</div>
</section>
);
}
}
+98 -44
View File
@@ -1,11 +1,26 @@
import { Shield, Zap, Sparkles, Heart, CheckCircle } from "lucide-react";
"use client";
const features = [
import React from "react";
import { motion } from "framer-motion"; // ESSENCIAL: Evita erros de referência
import { Shield, Zap, Sparkles, Heart, CheckCircle } from "lucide-react";
import { cn } from "@/lib/utils";
// Definição de Tipos para garantir integridade no TS
interface Feature {
icon: React.ElementType;
title: string;
description: string;
color: string;
bgColor: string;
iconBg: string;
benefit: string;
}
const features: Feature[] = [
{
icon: Shield,
title: "Dados da Comunidade Open Food Facts",
description:
"Informações fornecidas pela maior base de dados colaborativa de alimentos do mundo, mantida por uma comunidade global de colaboradores voluntários.",
title: "Dados da Comunidade",
description: "Informações fornecidas pela Open Food Facts, a maior base colaborativa global mantida por voluntários.",
color: "text-blue-600 dark:text-blue-400",
bgColor: "bg-blue-50 dark:bg-blue-900/20",
iconBg: "bg-blue-100 dark:bg-blue-900/40",
@@ -13,81 +28,120 @@ const features = [
},
{
icon: Zap,
title: "Atualizações em Tempo Real",
description:
"Novos produtos são adicionados a cada minuto. Quando a indústria muda fórmulas, a comunidade atualiza — e você tem acesso instantâneo às informações mais recentes.",
title: "Atualização Contínua",
description: "Novos produtos e fórmulas atualizados a cada minuto pela comunidade. Dados sempre frescos no seu dispositivo.",
color: "text-green-600 dark:text-green-400",
bgColor: "bg-green-50 dark:bg-green-900/20",
iconBg: "bg-green-100 dark:bg-green-900/40",
benefit: "Atualizado continuamente",
benefit: "Atualizado em tempo real",
},
{
icon: Sparkles,
title: "Interface Intuitiva, Resultado Imediato",
description:
"Desenvolvido pensando em quem não tem tempo a perder. Digitalize, veja os dados e tome sua decisão em menos de 10 segundos.",
title: "Velocidade de Resposta",
description: "Interface otimizada para decisões rápidas. Escaneie e obtenha o diagnóstico nutricional em poucos segundos.",
color: "text-purple-600 dark:text-purple-400",
bgColor: "bg-purple-50 dark:bg-purple-900/20",
iconBg: "bg-purple-100 dark:bg-purple-900/40",
benefit: "Resultados em <10 segundos",
benefit: "Resultados em <10s",
},
{
icon: Heart,
title: "100% Gratuito, Para Sempre",
description:
"Construído sobre dados open source, o NutriScan nunca cobrará pelo acesso à informação. Porque conhecimento sobre saúde deve ser universal, não um privilégio.",
title: "Filosofia Open Source",
description: "O NutriScan é e sempre será gratuito. Conhecimento sobre saúde deve ser um direito universal.",
color: "text-orange-600 dark:text-orange-400",
bgColor: "bg-orange-50 dark:bg-orange-900/20",
iconBg: "bg-orange-100 dark:bg-orange-900/40",
benefit: "Sempre gratuito, sem truques",
benefit: "Gratuito para sempre",
},
];
// Variantes de animação para o container pai (Stagger effect)
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.15 },
},
};
// Variantes para cada card individual
const cardVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { type: "spring", stiffness: 100 },
},
};
export function Features() {
return (
<section className="py-20 bg-gray-50 dark:bg-gray-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center mb-16">
<h2 className="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white mb-4">
Por que milhares confiam no NutriScan
</h2>
<p className="text-xl text-gray-600 dark:text-gray-400">
Transparência que transforma alimentação
</p>
</div>
<section className="py-24 bg-white dark:bg-[#030712] relative overflow-hidden">
{/* Background Decor: Sutil gradiente para profundidade */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-blue-500/5 via-transparent to-transparent pointer-events-none" />
{/* Features Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="max-w-7xl mx-auto px-6 relative z-10">
{/* Header com Motion */}
<motion.div
initial={{ opacity: 0, y: -20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-20"
>
<h2 className="text-4xl md:text-5xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter">
Por que confiar no <span className="text-blue-600">NutriScan</span>
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
Engenharia de dados a serviço da sua saúde, com transparência total e zero custo.
</p>
</motion.div>
{/* Features Grid com Stagger */}
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
className="grid grid-cols-1 md:grid-cols-2 gap-8"
>
{features.map((feature, index) => (
<div
<motion.div
key={index}
className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-shadow"
variants={cardVariants}
whileHover={{ y: -5, transition: { duration: 0.2 } }}
className="group relative bg-gray-50 dark:bg-gray-900/50 rounded-3xl p-8 border border-gray-100 dark:border-gray-800 hover:border-blue-500/30 transition-colors"
>
{/* Icon */}
{/* Icon Container */}
<div
className={`${feature.iconBg} w-12 h-12 rounded-lg flex items-center justify-center mb-4`}
className={cn(
feature.iconBg,
"w-14 h-14 rounded-2xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
)}
>
<feature.icon className={`w-6 h-6 ${feature.color}`} />
<feature.icon className={cn("w-7 h-7", feature.color)} />
</div>
{/* Content */}
<h3 className={`text-2xl font-bold mb-3 ${feature.color}`}>
{/* Text Content */}
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
{feature.title}
</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
<p className="text-gray-600 dark:text-gray-400 mb-8 leading-relaxed">
{feature.description}
</p>
{/* Benefit */}
<div className="flex items-center text-sm text-blue-600">
<CheckCircle className="w-4 h-4 mr-2" />
{/* Benefit Badge */}
<div className={cn(
"inline-flex items-center px-4 py-2 rounded-full text-xs font-bold uppercase tracking-widest",
feature.bgColor,
feature.color
)}>
<CheckCircle className="w-3.5 h-3.5 mr-2" />
<span>{feature.benefit}</span>
</div>
</div>
</motion.div>
))}
</div>
</motion.div>
</div>
</section>
);
}
}
+223 -208
View File
@@ -1,246 +1,261 @@
"use client";
import { useState, useEffect } from "react";
import React, { useState, useMemo } from "react";
import { useRouter } from "next/navigation";
import { Search, ScanLine, Sparkles, Globe } from "lucide-react";
import { Search, ScanLine, Globe, Activity, Database, Zap, Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { detectUserCountry } from "@/lib/geolocation";
import { detectSearchType, isValidBarcode } from "@/lib/openFoodFactsApi";
import { useGeolocation } from "@/hooks/use-geolocation";
import { useSearchLogic } from "@/hooks/use-search-logic";
import { useTheme } from "next-themes";
import { cn } from "@/lib/utils";
import { motion, AnimatePresence } from "framer-motion";
export function Hero() {
const [searchQuery, setSearchQuery] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [userCountry, setUserCountry] = useState<string | null>(null);
const [searchType, setSearchType] = useState<"auto" | "barcode" | "name">(
"auto"
);
const [mounted, setMounted] = useState(false);
const router = useRouter();
const [searchQuery, setSearchQuery] = useState("");
const { countryCode, isMounted } = useGeolocation();
const { searchType, isValid, loading, setLoading } = useSearchLogic(searchQuery);
const { theme } = useTheme();
// Detectar país do usuário ao carregar
useEffect(() => {
setMounted(true);
const detectCountry = async () => {
try {
// Forçar nova detecção para garantir país correto
const country = await detectUserCountry(true);
setUserCountry(country.code);
} catch {
setUserCountry("br"); // Fallback para Brasil
}
};
detectCountry();
}, []);
// Detectar tipo de busca baseado na entrada
useEffect(() => {
if (searchQuery.trim()) {
const detectedType = detectSearchType(searchQuery);
setSearchType(detectedType);
} else {
setSearchType("auto");
const searchConfig = useMemo(() => ({
barcode: {
placeholder: "Insira o código EAN/UPC (ex: 789...)",
button: "Analisar Produto",
icon: <ScanLine className="w-5 h-5" />,
subtext: "Protocolo: Detecção de Código de Barras",
color: "blue",
gradient: "from-blue-600 via-cyan-500 to-blue-400"
},
name: {
placeholder: "Qual alimento deseja investigar?",
button: "Consultar Banco",
icon: <Search className="w-5 h-5" />,
subtext: "Protocolo: Pesquisa em Base Colaborativa",
color: "emerald",
gradient: "from-emerald-600 via-teal-500 to-green-400"
},
auto: {
placeholder: "Digite o nome ou aponte o código...",
button: "Buscar",
icon: <Activity className="w-5 h-5" />,
subtext: "Aguardando input do operador...",
color: "slate",
gradient: "from-slate-600 via-blue-500 to-slate-400"
}
}, [searchQuery]);
}), []);
const current = searchConfig[searchType];
const handleSearch = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
if (!searchQuery.trim()) {
setError("Digite nome do produto");
return;
}
const query = searchQuery.trim();
const detectedType = detectSearchType(query);
if (detectedType === "barcode") {
if (!isValidBarcode(query)) {
setError("Código de barras deve ter entre 8 e 13 dígitos");
return;
}
setLoading(true);
// Redirecionar para página do produto
router.push(`/produto/${query}`);
} else {
setLoading(true);
// Redirecionar para página de resultados
const countryParam = userCountry ? `&country=${userCountry}` : "";
router.push(`/resultados?q=${encodeURIComponent(query)}${countryParam}`);
}
};
const getPlaceholder = () => {
if (searchType === "barcode") {
return "Digite o código de barras (ex: 7896004713588)";
} else if (searchType === "name") {
return "Digite o nome do produto (ex: coca cola)";
}
return "Digite nome do produto";
};
const getSearchButtonText = () => {
if (loading) return "Buscando...";
if (searchType === "barcode") return "Buscar Produto";
if (searchType === "name") return "Buscar Produtos";
return "Buscar";
if (!searchQuery.trim()) return;
setLoading(true);
const target = searchType === "barcode" && isValid
? `/produto/${searchQuery.trim()}`
: `/resultados?q=${encodeURIComponent(searchQuery.trim())}${countryCode ? `&country=${countryCode}` : ""}`;
router.push(target);
};
return (
<section className="relative min-h-[calc(100vh-4rem)] flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-green-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 overflow-hidden py-12 sm:py-16 lg:py-20">
{/* Background Pattern */}
<div className="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg%20width%3D%2260%22%20height%3D%2260%22%20viewBox%3D%220%200%2060%2060%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cg%20fill%3D%22%230055ff%22%20fill-opacity%3D%220.05%22%3E%3Ccircle%20cx%3D%2230%22%20cy%3D%2230%22%20r%3D%222%22/%3E%3C/g%3E%3C/g%3E%3C/svg%3E')] opacity-40"></div>
<section className="relative min-h-[90vh] flex flex-col items-center justify-center px-6 py-20 overflow-hidden bg-white dark:bg-[#030712] transition-colors duration-500">
{/* 1. INFRAESTRUTURA VISUAL: Background de Engenharia */}
<div className="absolute inset-0 z-0">
{/* Grid de Coordenadas */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#e2e8f0_1px,transparent_1px),linear-gradient(to_bottom,#e2e8f0_1px,transparent_1px)] dark:bg-[linear-gradient(to_right,#1e293b_1px,transparent_1px),linear-gradient(to_bottom,#1e293b_1px,transparent_1px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_70%,transparent_100%)] opacity-20 dark:opacity-20 transition-opacity duration-500" />
{/* Glow de Fundo Reativo */}
<motion.div
animate={{
scale: searchQuery ? 1.2 : 1,
opacity: searchQuery ? (theme === "dark" ? 0.15 : 0.08) : (theme === "dark" ? 0.05 : 0.02)
}}
className={cn(
"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[500px] rounded-full blur-[120px] transition-colors duration-700",
searchType === "barcode" ? "bg-blue-600 dark:bg-blue-600" : searchType === "name" ? "bg-emerald-600 dark:bg-emerald-600" : "bg-blue-400 dark:bg-blue-900"
)}
/>
<div className="relative z-10 max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
{/* Badge */}
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-sm font-medium mb-6 sm:mb-8">
<Sparkles className="w-4 h-4" />
Desenvolvido por{" "}
<a
href="https://evandro.dev.br"
target="_blank"
rel="noopener noreferrer"
className="font-semibold hover:underline transition-all hover:text-blue-800 dark:hover:text-blue-200"
>
evandrodevbr
</a>
{" • "}Dados do Open Food Facts
</div>
{/* Elemento Decorativo Lua/Sol */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 1, delay: 0.5 }}
className="absolute top-16 right-8 md:top-24 md:right-16 lg:top-32 lg:right-24 z-0 pointer-events-none"
>
<AnimatePresence mode="wait">
{theme === "dark" ? (
<motion.div
key="moon"
initial={{ opacity: 0, rotate: -90 }}
animate={{ opacity: 1, rotate: 0 }}
exit={{ opacity: 0, rotate: 90 }}
transition={{ duration: 0.5 }}
>
<Moon className="w-24 h-24 md:w-32 md:h-32 lg:w-40 lg:h-40 text-blue-400/20 transition-colors duration-500" />
</motion.div>
) : (
<motion.div
key="sun"
initial={{ opacity: 0, rotate: 90 }}
animate={{ opacity: 1, rotate: 0 }}
exit={{ opacity: 0, rotate: -90 }}
transition={{ duration: 0.5 }}
>
<Sun className="w-24 h-24 md:w-32 md:h-32 lg:w-40 lg:h-40 text-yellow-400/30 transition-colors duration-500" />
</motion.div>
)}
</AnimatePresence>
</motion.div>
</div>
{/* Main Heading */}
<h1 className="text-3xl sm:text-4xl lg:text-5xl xl:text-6xl font-bold text-gray-900 dark:text-white mb-4 sm:mb-6 leading-tight">
Descubra o que a comunidade alimentar sabe sobre{" "}
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent">
seus alimentos
<div className="relative z-10 max-w-5xl mx-auto text-center">
{/* 2. BADGE DE STATUS DO SISTEMA */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="inline-flex items-center gap-3 px-4 py-2 rounded-full bg-gray-100/80 dark:bg-white/5 border border-gray-200 dark:border-white/10 backdrop-blur-md mb-12 transition-colors duration-500"
>
<div className="flex -space-x-2">
<div className="w-6 h-6 rounded-full border-2 border-white dark:border-[#030712] bg-blue-500 flex items-center justify-center">
<Database size={10} className="text-white" />
</div>
<div className="w-6 h-6 rounded-full border-2 border-white dark:border-[#030712] bg-emerald-500 flex items-center justify-center">
<Zap size={10} className="text-white" />
</div>
</div>
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-gray-600 dark:text-slate-400 transition-colors duration-500">
NutriScan Protocol <span className="text-blue-600 dark:text-blue-500">v2.1</span> • {isMounted ? countryCode?.toUpperCase() : "INTL"}
</span>
</motion.div>
{/* 3. HEADLINE DE ALTA DENSIDADE */}
<h1 className="text-5xl md:text-8xl font-black text-gray-900 dark:text-white mb-8 tracking-tighter leading-[0.85] perspective-1000 transition-colors duration-500">
A verdade sobre <br />
<span className={cn(
"text-transparent bg-clip-text bg-gradient-to-r transition-all duration-700",
current.gradient
)}>
seus alimentos.
</span>
</h1>
{/* Subtitle */}
<p className="text-lg sm:text-xl lg:text-2xl text-gray-600 dark:text-gray-300 mb-4 max-w-3xl mx-auto leading-relaxed">
Transparência total, zero custo, milhões de dados colaborativos ao seu
alcance.
<p className="text-gray-600 dark:text-slate-400 text-lg md:text-xl max-w-2xl mx-auto mb-16 leading-relaxed font-medium transition-colors duration-500">
Engenharia de dados aplicada à transparência nutricional.
Descriptografamos rótulos para que você tome decisões soberanas.
</p>
<p className="text-base sm:text-lg text-gray-500 dark:text-gray-400 mb-8 sm:mb-12 max-w-4xl mx-auto leading-relaxed">
Consulte informações nutricionais completas, ingredientes e
classificações de qualidade — tudo baseado no{" "}
<span className="font-semibold text-blue-600">Open Food Facts</span>,
a maior base de dados colaborativa do mundo.
</p>
{/* 4. O TERMINAL DE BUSCA (A PEÇA CENTRAL) */}
<form onSubmit={handleSearch} className="relative max-w-3xl mx-auto group">
{/* Efeito de Borda Neon Reativa */}
<div className={cn(
"absolute -inset-[2px] rounded-[2rem] blur-sm opacity-20 group-focus-within:opacity-100 transition-all duration-500 bg-gradient-to-r",
current.gradient
)} />
{/* Estatísticas Visuais */}
<div className="flex justify-center gap-6 sm:gap-8 mb-8 sm:mb-12 text-center">
<div className="animate-fadeIn">
<div className="text-2xl sm:text-3xl font-bold text-blue-600">
2M+
</div>
<div className="text-xs sm:text-sm text-gray-500">
produtos no banco
</div>
</div>
<div className="animate-fadeIn" style={{ animationDelay: "0.2s" }}>
<div className="text-2xl sm:text-3xl font-bold text-green-600">
100+
</div>
<div className="text-xs sm:text-sm text-gray-500">
países cobertos
</div>
</div>
<div className="animate-fadeIn" style={{ animationDelay: "0.4s" }}>
<div className="text-2xl sm:text-3xl font-bold text-purple-600">
24/7
</div>
<div className="text-xs sm:text-sm text-gray-500">disponível</div>
</div>
</div>
{/* Search Form */}
<form
onSubmit={handleSearch}
className="max-w-2xl mx-auto mb-10 sm:mb-12"
>
<div className="relative">
<div className="flex items-center bg-white dark:bg-gray-800 rounded-2xl shadow-xl overflow-hidden focus-within:ring-2 focus-within:ring-blue-500 transition-all duration-200">
<div className="flex-1 relative">
{searchType === "barcode" ? (
<ScanLine className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5 pointer-events-none" />
) : (
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5 pointer-events-none" />
)}
<Input
type="text"
placeholder={getPlaceholder()}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-12 pr-4 py-4 text-lg border-0 bg-transparent focus:ring-0 focus:outline-none focus:border-transparent focus:shadow-none focus-visible:ring-0 placeholder:text-gray-400 w-full"
/>
<div className="relative flex items-center bg-white/90 dark:bg-slate-900/90 backdrop-blur-2xl rounded-[1.8rem] border border-gray-200 dark:border-white/10 p-2 shadow-2xl transition-colors duration-500">
<div className="flex-1 relative flex items-center">
{/* Ícone de Estado Animado */}
<div className="absolute left-6">
<AnimatePresence mode="wait">
<motion.div
key={searchType}
initial={{ rotate: -90, opacity: 0, scale: 0.5 }}
animate={{ rotate: 0, opacity: 1, scale: 1 }}
exit={{ rotate: 90, opacity: 0, scale: 0.5 }}
className={cn(
"transition-colors duration-500",
searchType === "barcode" ? "text-blue-600 dark:text-blue-400" : searchType === "name" ? "text-emerald-600 dark:text-emerald-400" : "text-gray-400 dark:text-slate-500"
)}
>
{current.icon}
</motion.div>
</AnimatePresence>
</div>
<Button
type="submit"
disabled={loading}
className="m-2 px-6 sm:px-8 py-4 bg-gradient-to-r from-blue-600 to-green-600 hover:from-blue-700 hover:to-green-700 text-white font-semibold rounded-xl transition-all duration-200 transform hover:scale-105 disabled:opacity-50 flex-shrink-0"
>
{loading ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Buscando...
</div>
) : (
<div className="flex items-center gap-2">
<Search className="w-5 h-5" />
{getSearchButtonText()}
</div>
)}
</Button>
<Input
type="text"
placeholder={current.placeholder}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-16 pr-4 py-9 text-xl border-0 bg-transparent focus-visible:ring-0 w-full text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-slate-600 font-medium tracking-tight transition-colors duration-500"
/>
</div>
<Button
type="submit"
disabled={loading}
className={cn(
"h-14 px-10 rounded-[1.2rem] font-black uppercase tracking-widest text-xs transition-all duration-500 active:scale-95 shadow-xl",
searchType === "barcode" ? "bg-blue-600 hover:bg-blue-500 shadow-blue-500/20" : "bg-emerald-600 hover:bg-emerald-500 shadow-emerald-500/20"
)}
>
{loading ? (
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : (
<div className="flex items-center gap-2">
<span>{current.button}</span>
<ArrowRight size={14} className="group-hover:translate-x-1 transition-transform" />
</div>
)}
</Button>
</div>
{/* Search Type Indicator */}
{searchQuery && (
<div className="flex items-center justify-center gap-2 mt-3">
{searchType === "barcode" && (
<div className="flex items-center gap-1 text-sm text-blue-600 dark:text-blue-400">
<ScanLine className="w-4 h-4" />
<span>Busca por código de barras</span>
</div>
)}
{searchType === "name" && (
<div className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
<Search className="w-4 h-4" />
<span>Busca por nome do produto</span>
{mounted && userCountry && (
<div className="flex items-center gap-1 text-xs text-gray-500">
<Globe className="w-3 h-3" />
<span>Priorizando {userCountry}</span>
</div>
)}
</div>
)}
{/* 5. METADADOS DA OPERAÇÃO */}
<div className="flex items-center justify-between px-6 mt-6">
<div className={cn(
"flex items-center gap-2 font-mono text-[10px] font-bold uppercase tracking-[0.2em] transition-colors duration-500",
searchType === "barcode" ? "text-blue-500" : searchType === "name" ? "text-emerald-500" : "text-slate-600"
)}>
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-current opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-current"></span>
</span>
{current.subtext}
</div>
)}
{error && (
<p className="text-red-500 text-sm mt-3 text-left">{error}</p>
)}
{isMounted && countryCode && (
<div className="flex items-center gap-2 text-gray-600 dark:text-slate-500 bg-gray-100 dark:bg-white/5 px-3 py-1 rounded-full border border-gray-200 dark:border-white/10 transition-colors duration-500">
<Globe size={12} />
<span className="text-[10px] font-bold uppercase">Region: {countryCode.toUpperCase()}</span>
</div>
)}
</div>
</form>
{/* Dica sobre código de barras */}
<p className="text-center text-sm text-gray-500 dark:text-gray-400 mb-8">
💡 Dica: O código de barras está na parte de trás da embalagem
</p>
</div>
{/* Scroll Indicator */}
<div className="absolute bottom-4 sm:bottom-8 left-1/2 transform -translate-x-1/2 animate-bounce mt-12 sm:mt-16">
<div className="w-6 h-10 border-2 border-gray-400 dark:border-gray-500 rounded-full flex justify-center">
<div className="w-1 h-3 bg-gray-400 dark:bg-gray-500 rounded-full mt-2 animate-pulse"></div>
{/* 6. INDICADOR DE MÉTRICAS (Bottom Shelf) */}
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 w-full max-w-4xl px-6 hidden md:block">
<div className="grid grid-cols-3 gap-12 border-t border-gray-200 dark:border-white/5 pt-10 opacity-60 dark:opacity-40 grayscale hover:grayscale-0 transition-all duration-700">
<div className="flex flex-col items-center gap-1">
<span className="text-2xl font-mono font-bold text-gray-900 dark:text-white tracking-tighter transition-colors duration-500">2M+</span>
<span className="text-[9px] uppercase tracking-[0.3em] font-black text-gray-500 dark:text-slate-500 transition-colors duration-500">Dataset Entries</span>
</div>
<div className="flex flex-col items-center gap-1 border-x border-gray-200 dark:border-white/5">
<span className="text-2xl font-mono font-bold text-gray-900 dark:text-white tracking-tighter transition-colors duration-500">100%</span>
<span className="text-[9px] uppercase tracking-[0.3em] font-black text-gray-500 dark:text-slate-500 transition-colors duration-500">Open Source</span>
</div>
<div className="flex flex-col items-center gap-1">
<span className="text-2xl font-mono font-bold text-gray-900 dark:text-white tracking-tighter transition-colors duration-500">&lt; 10s</span>
<span className="text-[9px] uppercase tracking-[0.3em] font-black text-gray-500 dark:text-slate-500 transition-colors duration-500">Analysis Speed</span>
</div>
</div>
</div>
</section>
);
}
// Sub-componente auxiliar para ícone
function ArrowRight({ size, className }: { size: number, className?: string }) {
return (
<svg
width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" className={className}
>
<path d="M5 12h14m-7-7 7 7-7 7"/>
</svg>
);
}
+133 -158
View File
@@ -1,205 +1,180 @@
"use client";
import { ScanLine, Search, BarChart3, CheckCircle } from "lucide-react";
import React from "react";
import { motion } from "framer-motion";
import { ScanLine, Search, BarChart3, CheckCircle, Globe, Zap } from "lucide-react";
import Image from "next/image";
import { cn } from "@/lib/utils";
const steps = [
interface Step {
number: string;
icon: React.ElementType;
title: string;
description: string;
color: string;
glow: string;
}
const steps: Step[] = [
{
number: "01",
icon: ScanLine,
title: "Digitalize sem complicação",
description:
"Encontre o código de barras na embalagem e escaneie instantaneamente. Nossa tecnologia reconhece produtos de mais de 100 países em segundos.",
color: "text-blue-600 dark:text-blue-400",
bgColor: "bg-blue-50 dark:bg-blue-900/20",
iconBg: "bg-blue-100 dark:bg-blue-900/40",
title: "Digitalize",
description: "Reconhecimento instantâneo de códigos em mais de 100 países com tecnologia de baixa latência.",
color: "text-blue-500",
glow: "shadow-blue-500/20",
},
{
number: "02",
icon: Search,
title: "Acesso a milhões de dados colaborativos",
description:
"Conectamos você diretamente ao Open Food Facts, a Wikipedia dos alimentos — onde a comunidade global contribui com informações sobre produtos.",
color: "text-green-600 dark:text-green-400",
bgColor: "bg-green-50 dark:bg-green-900/20",
iconBg: "bg-green-100 dark:bg-green-900/40",
title: "Consulte",
description: "Conexão direta ao Open Food Facts, a base de dados colaborativa global (Wikipedia dos alimentos).",
color: "text-green-500",
glow: "shadow-green-500/20",
},
{
number: "03",
icon: BarChart3,
title: "Informações que fazem diferença",
description:
"Receba dados nutricionais detalhados, alertas de alérgenos e classificações Nutri-Score. Informações fornecidas pela base de dados Open Food Facts.",
color: "text-purple-600 dark:text-purple-400",
bgColor: "bg-purple-50 dark:bg-purple-900/20",
iconBg: "bg-purple-100 dark:bg-purple-900/40",
title: "Analise",
description: "Processamento de Nutri-Score, graus de processamento NOVA e alertas de alérgenos críticos.",
color: "text-purple-500",
glow: "shadow-purple-500/20",
},
{
number: "04",
icon: CheckCircle,
title: "Decisões mais inteligentes, saúde melhor",
description:
"Use informações transparentes para escolher alimentos alinhados aos seus objetivos. Sem interesses comerciais, sem dados manipulados.",
color: "text-orange-600 dark:text-orange-400",
bgColor: "bg-orange-50 dark:bg-orange-900/20",
iconBg: "bg-orange-100 dark:bg-orange-900/40",
},
];
const features = [
{
title: "Dados Nutricionais",
items: [
"Calorias por 100g",
"Proteínas, carboidratos e gorduras",
"Vitaminas e minerais",
"Fibra alimentar",
],
},
{
title: "Classificações",
items: [
"Nutri-Score (A a E)",
"NOVA (processamento)",
"Alérgenos identificados",
"Aditivos alimentares",
],
},
{
title: "Ingredientes",
items: [
"Lista completa de ingredientes",
"Ordem de quantidade",
"Alérgenos destacados",
"Origem dos ingredientes",
],
title: "Decida",
description: "Informação técnica pura e sem vieses comerciais para decisões alinhadas à sua saúde.",
color: "text-orange-500",
glow: "shadow-orange-500/20",
},
];
export function HowItWorks() {
return (
<section className="py-20 bg-white dark:bg-gray-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center mb-16">
<h2 className="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white mb-4">
Alimentação consciente em 4 passos simples
</h2>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
Descubra como milhares de pessoas já tomam decisões alimentares mais
conscientes com informações transparentes e confiáveis.
<section className="py-24 bg-white dark:bg-[#030712] transition-colors duration-500">
<div className="max-w-7xl mx-auto px-6">
{/* Header - Foco em Valor Técnico */}
<div className="text-center mb-24">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
className="text-4xl md:text-5xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter"
>
Pipeline de <span className="text-blue-600">Consciência Alimentar</span>
</motion.h2>
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
Quatro estágios de processamento de dados para transformar um código de barras em inteligência nutricional.
</p>
</div>
{/* Steps */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8 mb-20">
{/* Steps Grid - O "Motor" da Seção */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 relative">
{steps.map((step, index) => (
<div key={index} className="relative">
{/* Arrow (desktop only) */}
<motion.div
key={step.number}
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.15 }}
className="relative group"
>
{/* Conector Visual (Desktop) */}
{index < steps.length - 1 && (
<div className="hidden lg:flex absolute top-1/2 -right-4 transform -translate-y-1/2 z-10">
<svg
width="32"
height="24"
viewBox="0 0 32 24"
fill="none"
className="text-gray-400 dark:text-gray-600"
>
<path
d="M0 12H30M30 12L20 2M30 12L20 22"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<div className="hidden lg:block absolute top-12 -right-8 w-16 h-[2px] bg-gradient-to-r from-gray-200 to-transparent dark:from-gray-800 z-0" />
)}
<div
className={`${step.bgColor} p-8 rounded-2xl border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all duration-300 group`}
>
{/* Number */}
<div className="text-6xl font-bold text-gray-200 dark:text-gray-600 mb-4">
{step.number}
<div className={cn(
"relative z-10 p-8 rounded-3xl bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 transition-all duration-500 hover:border-blue-500/30",
"group-hover:translate-y-[-8px] group-hover:shadow-2xl",
step.glow
)}>
{/* Badge de Número em Estilo Industrial */}
<span className="absolute top-4 right-6 text-sm font-black font-mono text-gray-300 dark:text-gray-700">
STEP_{step.number}
</span>
<div className={cn(
"w-14 h-14 rounded-2xl flex items-center justify-center mb-8 bg-white dark:bg-gray-800 shadow-sm transition-transform duration-500 group-hover:rotate-[10deg]",
step.color
)}>
<step.icon className="w-7 h-7" />
</div>
{/* Icon */}
<div
className={`${step.iconBg} w-16 h-16 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300`}
>
<step.icon className={`w-8 h-8 ${step.color}`} />
</div>
{/* Content */}
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
{step.title}
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed font-medium">
{step.description}
</p>
</div>
</div>
</motion.div>
))}
</div>
{/* Features Grid */}
<div className="bg-gray-50 dark:bg-gray-900 rounded-2xl p-8">
<div className="text-center mb-12">
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
O que você vai encontrar?
</h3>
<p className="text-gray-600 dark:text-gray-300">
Informações detalhadas e confiáveis sobre cada produto
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{features.map((feature, index) => (
<div
key={index}
className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700"
>
<h4 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{feature.title}
</h4>
<ul className="space-y-2">
{feature.items.map((item, itemIndex) => (
<li
key={itemIndex}
className="flex items-center gap-2 text-gray-600 dark:text-gray-300"
>
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0" />
<span className="text-sm">{item}</span>
</li>
))}
</ul>
</div>
))}
{/* Technical Datasheet (O que você vai encontrar) */}
<div className="mt-32 mb-12 flex justify-center">
<div className="relative w-full max-w-2xl h-64 rounded-2xl overflow-hidden shadow-2xl">
<Image
src="/images/nut-facts.webp"
alt="Lista de ingredientes e informações nutricionais de produto alimentar"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
/>
</div>
</div>
<div className="mt-32 p-10 rounded-[2.5rem] bg-slate-900 text-white relative overflow-hidden shadow-3xl">
<div className="absolute top-0 right-0 p-10 opacity-10">
<BarChart3 size={200} />
</div>
{/* Bottom CTA */}
<div className="text-center mt-16">
<a
href="#"
onClick={(e) => {
e.preventDefault();
const input = document.querySelector(
'input[placeholder*="código de barras"]'
) as HTMLInputElement;
input?.focus();
window.scrollTo({ top: 0, behavior: "smooth" });
}}
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-blue-600 to-green-600 text-white rounded-full font-semibold hover:shadow-lg transition-all duration-300 hover:scale-105 cursor-pointer"
>
<ScanLine className="w-5 h-5" />
Começar agora
</a>
<p className="text-gray-500 dark:text-gray-400 text-sm mt-4">
É rápido, fácil e totalmente gratuito
</p>
<div className="relative z-10 grid grid-cols-1 lg:grid-cols-3 gap-12">
<div className="lg:col-span-1">
<h3 className="text-3xl font-black mb-4 tracking-tighter">Data Specification</h3>
<p className="text-slate-400 text-sm leading-relaxed mb-6">
Extração de metadados via Open Food Facts API v2. Suporte a protocolos Nutri-Score e classificações NOVA.
</p>
<div className="flex items-center gap-4 text-xs font-mono text-blue-400 uppercase tracking-widest">
<Globe size={14} /> Open Source Database
</div>
</div>
<div className="lg:col-span-2 grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Lista de Features Refinada */}
<div className="space-y-4">
<h4 className="text-blue-500 font-bold text-xs uppercase tracking-widest">Bio-Markers & Macros</h4>
<ul className="space-y-3">
{["Calorias (Kcal/100g)", "Perfil Lipídico e Glicêmico", "Micronutrientes & Fibras"].map(item => (
<li key={item} className="flex items-center gap-3 text-sm text-slate-300">
<Zap size={12} className="text-blue-500" /> {item}
</li>
))}
</ul>
</div>
<div className="relative w-full h-48 rounded-xl overflow-hidden">
<Image
src="/images/nutscore.webp"
alt="Sistema de classificação Nutri-Score"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 50vw"
/>
</div>
<div className="space-y-4">
<h4 className="text-green-500 font-bold text-xs uppercase tracking-widest">Compliance & Alerta</h4>
<ul className="space-y-3">
{["Identificação de Alérgenos", "Aditivos e Conservantes", "Classificação de Processamento"].map(item => (
<li key={item} className="flex items-center gap-3 text-sm text-slate-300">
<CheckCircle size={12} className="text-green-500" /> {item}
</li>
))}
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
}
+160 -321
View File
@@ -1,7 +1,12 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback } from "react";
import { useParams, useRouter } from "next/navigation";
import { ArrowLeft, AlertCircle, Star, AlertTriangle } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import {
ArrowLeft, AlertCircle, AlertTriangle,
Database, Globe, ShieldCheck, Beaker, ClipboardList
} from "lucide-react";
import { Button } from "@/components/ui/button";
import Image from "next/image";
import {
@@ -13,6 +18,7 @@ import Link from "next/link";
import { NutriScoreBadge, NovaBadge } from "@/app/components/NutriScoreBadge";
import { NutritionGrid } from "@/app/components/NutritionBar";
import { ProductSkeleton } from "@/app/components/ProductSkeleton";
import { cn } from "@/lib/utils";
export default function ProdutoPage() {
const params = useParams();
@@ -27,366 +33,199 @@ export default function ProdutoPage() {
try {
setLoading(true);
setError(null);
const result = await searchByBarcode(barcode);
if (result) {
setProduct(result);
} else {
setError("Produto não encontrado");
}
if (result) setProduct(result);
else setError("Produto não catalogado na base ODbL.");
} catch (err) {
setError(err instanceof Error ? err.message : "Erro ao buscar produto");
setError("Falha crítica na sincronização de dados.");
} finally {
setLoading(false);
}
}, [barcode]);
useEffect(() => {
if (barcode) {
searchProduct();
}
if (barcode) searchProduct();
}, [barcode, searchProduct]);
const getProductImageUrl = (product: Product): string | null => {
return (
product.image_front_url ||
product.image_front_small_url ||
product.image_url ||
product.image_small_url ||
null
);
};
const getNutritionGrade = (grade?: string) => {
if (!grade) return null;
const grades = {
a: {
label: "A",
color: "bg-green-500",
textColor: "text-green-700",
description: "Muito bom",
},
b: {
label: "B",
color: "bg-green-400",
textColor: "text-green-700",
description: "Bom",
},
c: {
label: "C",
color: "bg-yellow-500",
textColor: "text-yellow-700",
description: "Regular",
},
d: {
label: "D",
color: "bg-orange-500",
textColor: "text-orange-700",
description: "Ruim",
},
e: {
label: "E",
color: "bg-red-500",
textColor: "text-red-700",
description: "Muito ruim",
},
};
return grades[grade.toLowerCase() as keyof typeof grades] || null;
};
const getNovaGroup = (group?: number) => {
if (!group) return null;
const groups = {
1: {
label: "1",
color: "bg-green-500",
textColor: "text-green-700",
description: "Alimentos não processados ou minimamente processados",
},
2: {
label: "2",
color: "bg-yellow-500",
textColor: "text-yellow-700",
description: "Ingredientes culinários processados",
},
3: {
label: "3",
color: "bg-orange-500",
textColor: "text-orange-700",
description: "Alimentos processados",
},
4: {
label: "4",
color: "bg-red-500",
textColor: "text-red-700",
description: "Alimentos ultraprocessados",
},
};
return groups[group as keyof typeof groups] || null;
};
if (loading) {
return <ProductSkeleton />;
}
if (loading) return <ProductSkeleton />;
// Handler de Erro - Estética Industrial
if (error || !product) {
return (
<div className="min-h-screen bg-white dark:bg-gray-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="text-center py-12">
<AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Produto não encontrado
</h1>
<p className="text-gray-600 dark:text-gray-300 mb-6">
{error ||
"O produto com este código de barras não foi encontrado na base de dados."}
</p>
<div className="flex gap-4 justify-center">
<Link href="/">
<Button>Nova Busca</Button>
</Link>
<Button variant="outline" onClick={() => router.back()}>
Voltar
</Button>
</div>
<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"
>
<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>
</div>
</div>
</motion.div>
</div>
);
}
const imageUrl = getProductImageUrl(product);
const nutritionGrade = getNutritionGrade(product.nutrition_grades);
const novaGroup = getNovaGroup(product.nova_group);
const nutritionData = formatNutritionData(product);
const imageUrl = product.image_front_url || product.image_url || null;
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
{/* Hero Section com gradiente e animação */}
<div className="relative overflow-hidden">
<div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
<div className="relative bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border-b border-gray-200/50 dark:border-gray-700/50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="sm"
onClick={() => router.back()}
className="flex items-center gap-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200"
>
<ArrowLeft className="w-4 h-4" />
Voltar
</Button>
<div className="flex-1">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2 animate-fade-in">
{product.product_name || "Produto"}
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">
Código: {product.code}
</p>
</div>
<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>
</div>
</header>
{/* Content */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Product Image */}
<div className="space-y-6">
<div className="relative w-full max-w-2xl mx-auto bg-white dark:bg-gray-800 rounded-2xl overflow-hidden shadow-[0_2px_8px_rgba(0,0,0,0.04),0_8px_24px_rgba(0,0,0,0.08)] hover:shadow-[0_4px_16px_rgba(0,0,0,0.08),0_16px_32px_rgba(0,0,0,0.12)] transition-all duration-300 transform hover:-translate-y-1">
<div className="aspect-square bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-700 p-8">
{imageUrl ? (
<Image
src={imageUrl}
alt={product.product_name || "Produto"}
fill
className="object-contain transition-transform duration-300 hover:scale-105"
priority
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k="
/>
) : (
<div className="w-full h-full flex items-center justify-center text-gray-400">
<div className="text-center">
<Star className="w-16 h-16 mx-auto mb-4" />
<p className="text-lg">Sem imagem disponível</p>
</div>
</div>
)}
</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"
>
{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>
</div>
)}
</motion.div>
{/* Basic Info */}
<div className="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-2xl p-6 shadow-sm border border-gray-200/50 dark:border-gray-700/50 hover:shadow-md transition-all duration-300">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
Informações Básicas
</h3>
<div className="space-y-3 text-sm">
{product.brands && (
<div className="flex justify-between items-center py-2 border-b border-gray-100 dark:border-gray-700">
<span className="font-medium text-gray-600 dark:text-gray-400">
Marca:
</span>
<span className="text-gray-900 dark:text-white font-medium">
{product.brands}
{/* 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>
</div>
)}
{product.categories && (
<div className="flex justify-between items-center py-2 border-b border-gray-100 dark:border-gray-700">
<span className="font-medium text-gray-600 dark:text-gray-400">
Categoria:
</span>
<span className="text-gray-900 dark:text-white font-medium text-right max-w-xs">
{product.categories}
</span>
</div>
)}
{product.countries && (
<div className="flex justify-between items-center py-2">
<span className="font-medium text-gray-600 dark:text-gray-400">
Países:
</span>
<span className="text-gray-900 dark:text-white font-medium">
{product.countries}
</span>
</div>
)}
))}
</div>
</div>
</div>
{/* Product Details */}
<div className="space-y-6">
{/* Nutrition Grades */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
{nutritionGrade && (
<div className="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-2xl p-6 shadow-sm hover:shadow-md transition-all duration-300 transform hover:-translate-y-1">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Nutri-Score
</h3>
<div className="flex items-center gap-4">
<NutriScoreBadge
grade={product.nutrition_grades || ""}
size="lg"
showTooltip={true}
/>
<div>
<p className="font-medium text-gray-900 dark:text-white text-lg">
{nutritionGrade.description}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Classificação nutricional
</p>
</div>
</div>
</div>
)}
{novaGroup && (
<div className="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-2xl p-6 shadow-sm hover:shadow-md transition-all duration-300 transform hover:-translate-y-1">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
NOVA
</h3>
<div className="flex items-center gap-4">
<NovaBadge
group={product.nova_group || 0}
size="lg"
showTooltip={true}
/>
<div>
<p className="font-medium text-gray-900 dark:text-white text-lg">
Grupo {novaGroup.label}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Nível de processamento
</p>
</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>
{/* Nutritional Information */}
{nutritionData && (
<div className="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-2xl p-6 shadow-sm hover:shadow-md transition-all duration-300">
<NutritionGrid nutritionData={nutritionData} />
</div>
)}
{/* Ingredients */}
{product.ingredients_text && (
<div className="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-2xl p-6 shadow-sm hover:shadow-md transition-all duration-300">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Ingredientes
{/* 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>
<p className="text-sm text-gray-700 dark:text-gray-300 leading-relaxed">
{product.ingredients_text}
</p>
<NutritionGrid nutritionData={formatNutritionData(product)} />
</div>
)}
</motion.div>
{/* Allergens */}
{product.allergens_tags && product.allergens_tags.length > 0 && (
<div className="bg-gradient-to-r from-yellow-50 to-orange-50 dark:from-yellow-900/20 dark:to-orange-900/20 border border-yellow-200 dark:border-yellow-800 rounded-2xl p-6 shadow-sm hover:shadow-md transition-all duration-300">
<div className="flex items-center gap-3 mb-4">
<div className="w-8 h-8 bg-yellow-500 rounded-full flex items-center justify-center">
<AlertTriangle className="w-4 h-4 text-white" />
{/* 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>
<h3 className="text-lg font-semibold text-yellow-800 dark:text-yellow-200">
Alérgenos
</h3>
</div>
<div className="flex flex-wrap gap-2">
{product.allergens_tags.map((allergen, index) => (
<span
key={index}
className="px-3 py-1 bg-yellow-100 dark:bg-yellow-800 text-yellow-800 dark:text-yellow-200 text-sm rounded-full font-medium border border-yellow-200 dark:border-yellow-700"
>
{allergen.replace("en:", "").replace("pt:", "")}
</span>
))}
</div>
</div>
)}
{/* Additives */}
{product.additives_tags && product.additives_tags.length > 0 && (
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border border-blue-200 dark:border-blue-800 rounded-2xl p-6 shadow-sm hover:shadow-md transition-all duration-300">
<h3 className="text-lg font-semibold text-blue-800 dark:text-blue-200 mb-4">
Aditivos
</h3>
<div className="flex flex-wrap gap-2">
{product.additives_tags
.slice(0, 10)
.map((additive, index) => (
<span
key={index}
className="px-3 py-1 bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200 text-sm rounded-full font-medium border border-blue-200 dark:border-blue-700"
>
{additive.replace("en:", "").replace("pt:", "")}
<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">
{tag.replace(/en:|pt:/g, "")}
</span>
))}
{product.additives_tags.length > 10 && (
<span className="px-3 py-1 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 text-sm rounded-full font-medium border border-gray-200 dark:border-gray-600">
+{product.additives_tags.length - 10} mais
</span>
)}
</div>
</div>
)}
</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>
</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>
</div>
);
}
+92 -139
View File
@@ -1,160 +1,113 @@
"use client";
import { ArrowRight, Heart, Users, Globe } from "lucide-react";
import React from "react";
import { motion } from "framer-motion";
import { ArrowRight, Code2, Heart, Globe, Database, ShieldCheck } from "lucide-react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
export function CTASection() {
return (
<section className="py-20 bg-gradient-to-br from-blue-600 via-purple-600 to-green-600 relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg%20width%3D%2260%22%20height%3D%2260%22%20viewBox%3D%220%200%2060%2060%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cg%20fill%3D%22%23ffffff%22%20fill-opacity%3D%220.1%22%3E%3Ccircle%20cx%3D%2230%22%20cy%3D%2230%22%20r%3D%222%22/%3E%3C/g%3E%3C/g%3E%3C/svg%3E')] opacity-40"></div>
<section className="relative py-32 bg-[#030712] overflow-hidden transition-colors duration-500">
{/* 1. LAYER DE INFRAESTRUTURA: Background Glows Inteligentes */}
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[500px] bg-blue-600/10 blur-[160px] rounded-full" />
<div className="absolute top-0 right-0 w-[400px] h-[400px] bg-emerald-500/5 blur-[120px] rounded-full" />
<div className="absolute inset-0 bg-[url('data:image/svg+xml,...')] opacity-[0.03] invert dark:invert-0" />
</div>
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
{/* Main CTA */}
<div className="mb-16">
<h2 className="text-4xl sm:text-5xl lg:text-6xl font-bold text-white mb-6">
Mais que uma ferramenta,
<br />
<span className="bg-gradient-to-r from-yellow-300 to-orange-300 bg-clip-text text-transparent">
um movimento
<div className="relative z-10 max-w-5xl mx-auto px-6 text-center">
{/* 2. LAYER DE CONTEXTO: Tag de Sistema */}
<motion.div
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-blue-500/5 border border-blue-500/20 text-blue-400 text-[10px] font-black uppercase tracking-[0.3em] mb-8"
>
<Database size={12} /> Open Access Protocol
</motion.div>
{/* 3. LAYER DE MENSAGEM: Headlines de Alta Densidade */}
<div className="mb-12">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
className="text-5xl md:text-7xl font-black text-white mb-8 tracking-tighter leading-[0.9]"
>
Soberania alimentar via <br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-cyan-300 to-emerald-400">
transparência algorítmica.
</span>
<br />
por transparência
</h2>
<p className="text-xl text-blue-100 max-w-3xl mx-auto mb-12 leading-relaxed">
Junte-se a milhares de pessoas que já transformaram a forma como
escolhem seus alimentos. Sua jornada por uma alimentação mais
consciente começa agora.
</motion.h2>
<p className="text-lg md:text-xl text-slate-400 max-w-3xl mx-auto leading-relaxed font-medium">
Junte-se à infraestrutura global que está devolvendo o poder de escolha ao cidadão. Dados abertos para uma vida sem processados ocultos.
</p>
{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-16">
<Link href="/">
<Button
size="lg"
className="bg-white text-blue-600 hover:bg-blue-50 hover:text-blue-700 transition-all duration-300 shadow-lg hover:shadow-xl transform hover:-translate-y-1"
>
<span className="mr-2">Comece Agora</span>
<ArrowRight className="w-5 h-5" />
</Button>
</Link>
<Link
href="https://github.com/evandrodevbr/lista-compras"
target="_blank"
rel="noopener noreferrer"
>
<Button
variant="outline"
size="lg"
className="border-white text-white hover:bg-white hover:text-blue-600 transition-all duration-300"
>
<span className="mr-2">Contribua</span>
<Heart className="w-5 h-5" />
</Button>
</Link>
</div>
</div>
{/* Community Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-16">
<div className="bg-white/10 backdrop-blur-sm rounded-2xl p-6">
<div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center mx-auto mb-4">
<Users className="w-6 h-6 text-white" />
</div>
<div className="text-3xl font-bold text-white mb-2">Milhares</div>
<p className="text-blue-100">
de usuários ativos fazendo escolhas mais conscientes
</p>
</div>
{/* 4. LAYER DE EXECUÇÃO: CTAs de Alta Tração */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-24"
>
<Link href="/" className="w-full sm:w-auto">
<Button
size="lg"
className="w-full sm:w-auto h-16 px-10 bg-blue-600 hover:bg-blue-700 text-white rounded-2xl font-bold text-lg shadow-2xl shadow-blue-500/20 transition-all hover:scale-[1.02] active:scale-95 group"
>
<span>Executar Scan</span>
<ArrowRight className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" />
</Button>
</Link>
<div className="bg-white/10 backdrop-blur-sm rounded-2xl p-6">
<div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center mx-auto mb-4">
<Globe className="w-6 h-6 text-white" />
</div>
<div className="text-3xl font-bold text-white mb-2">Global</div>
<p className="text-blue-100">
comunidade internacional comprometida com transparência
</p>
</div>
<Link
href="https://github.com/evandrodevbr/lista-compras"
target="_blank"
className="w-full sm:w-auto"
>
<Button
variant="outline"
size="lg"
className="w-full sm:w-auto h-16 px-10 border-slate-800 text-white hover:bg-slate-900 rounded-2xl font-bold text-lg transition-all"
>
<Code2 className="mr-2 w-5 h-5 text-black" />
<span className="text-gray-800">Audit Code</span>
</Button>
</Link>
</motion.div>
<div className="bg-white/10 backdrop-blur-sm rounded-2xl p-6">
<div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center mx-auto mb-4">
<Heart className="w-6 h-6 text-white" />
{/* 5. LAYER DE VALIDAÇÃO: Bento Grid de Metadados */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{[
{ icon: Globe, label: "Rede", val: "Global Node", desc: "Integridade em 100+ países" },
{ icon: ShieldCheck, label: "Segurança", val: "Privacy First", desc: "Zero tracking de dados" },
{ icon: Heart, label: "Sustentabilidade", val: "Open Source", desc: "Mantido pela comunidade" },
].map((item, i) => (
<div key={i} className="p-6 rounded-3xl bg-white/[0.02] border border-white/[0.05] backdrop-blur-sm text-left group hover:bg-white/[0.04] transition-colors">
<item.icon className="w-6 h-6 text-blue-500 mb-4" />
<div className="text-[10px] font-black uppercase tracking-widest text-slate-500 mb-1">{item.label}</div>
<div className="text-white font-bold text-lg mb-1 tracking-tight">{item.val}</div>
<p className="text-xs text-slate-500 leading-relaxed">{item.desc}</p>
</div>
<div className="text-3xl font-bold text-white mb-2">Crescendo</div>
<p className="text-blue-100">
movimento que não para de expandir e impactar vidas
</p>
</div>
))}
</div>
{/* Final Message */}
<div className="bg-white/10 backdrop-blur-sm rounded-3xl p-8 max-w-4xl mx-auto">
<blockquote className="text-2xl sm:text-3xl font-medium text-white italic mb-6">
&ldquo;Porque alimentação é mais que nutrição. É autocuidado, é
conexão, é vida.&rdquo;
</blockquote>
<cite className="block text-lg text-blue-100 mb-8">
— Filosofia NutriScan
</cite>
<div className="text-center">
<p className="text-lg text-blue-100 mb-6">
Faça parte dessa transformação. Sua próxima escolha alimentar pode
ser a mais consciente de todas.
</p>
<div className="inline-flex items-center gap-2 text-white/80">
<span>Comece sua jornada hoje</span>
<ArrowRight className="w-5 h-5" />
</div>
</div>
</div>
{/* Social Links */}
<div className="mt-16">
<p className="text-blue-100 mb-6">Conecte-se conosco</p>
<div className="flex justify-center gap-6">
<Link
href="https://github.com/evandrodevbr/lista-compras"
target="_blank"
rel="noopener noreferrer"
className="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center hover:bg-white/30 transition-colors"
>
<svg
className="w-6 h-6 text-white"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
</Link>
<Link
href="https://evandro.dev.br"
target="_blank"
rel="noopener noreferrer"
className="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center hover:bg-white/30 transition-colors"
>
<svg
className="w-6 h-6 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9"
/>
</svg>
</Link>
</div>
{/* 6. MENSAGEM FINAL: Minimalismo de Autoridade */}
<div className="mt-24 pt-12 border-t border-white/[0.05]">
<p className="text-[10px] font-mono font-bold text-slate-600 uppercase tracking-[0.5em] mb-6">
NutriScan Governance Protocol v2.0
</p>
<Link
href="/sobre"
className="inline-flex items-center gap-2 text-sm font-bold text-blue-500 hover:text-blue-400 transition-colors group"
>
Conheça nossa arquitetura de dados
<ArrowRight size={14} className="group-hover:translate-x-1 transition-transform" />
</Link>
</div>
</div>
</section>
);
}
}
+113 -204
View File
@@ -1,225 +1,134 @@
"use client";
import { Scan, Search, CheckCircle, ArrowRight } from "lucide-react";
import React from "react";
import { motion } from "framer-motion";
import { Scan, Search, CheckCircle, Zap, Shield, Globe, Clock } from "lucide-react";
import { cn } from "@/lib/utils";
// Tipagem para escalabilidade
interface Step {
id: number;
title: string;
description: string;
icon: React.ElementType;
color: string;
features: string[];
}
const STEPS: Step[] = [
{
id: 1,
title: "Captura de Dados",
description: "Input instantâneo via scanner óptico ou entrada manual de protocolo.",
icon: Scan,
color: "from-blue-500 to-cyan-400",
features: ["OCR de alta precisão", "Busca por SKU/EAN", "Detecção automática"],
},
{
id: 2,
title: "Análise de Base",
description: "Cruzamento com o dataset global Open Food Facts em milissegundos.",
icon: Search,
color: "from-green-500 to-emerald-400",
features: ["Nutri-Score v2.0", "Grau NOVA", "Alertas de Alérgenos"],
},
{
id: 3,
title: "Veredito Técnico",
description: "Saída de dados interpretada para uma tomada de decisão consciente.",
icon: CheckCircle,
color: "from-purple-500 to-pink-500",
features: ["Sugestões saudáveis", "Histórico local", "Comparativo de ROI nutricional"],
},
];
export function HowItWorksSection() {
return (
<section className="py-20 bg-gradient-to-br from-gray-50 to-blue-50 dark:from-gray-800 dark:to-gray-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-6">
Como{" "}
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent">
funciona
</span>
</h2>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
Em menos de 10 segundos, você tem todas as informações que precisa
para tomar a melhor decisão.
</p>
<section className="relative py-32 bg-white dark:bg-[#030712] overflow-hidden transition-colors duration-500">
{/* Background Decor: Engenharia de Luz */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-blue-500/5 via-transparent to-transparent pointer-events-none" />
<div className="max-w-7xl mx-auto px-6 relative z-10">
<div className="text-center mb-24">
<motion.span
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
className="text-blue-600 font-mono text-sm font-black uppercase tracking-[0.3em]"
>
Pipeline de Operação
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
className="text-5xl md:text-7xl font-black text-gray-900 dark:text-white mt-4 mb-8 tracking-tighter"
>
A inteligência por trás do <span className="bg-gradient-to-r from-blue-600 to-green-500 bg-clip-text text-transparent italic">scan</span>.
</motion.h2>
</div>
{/* Steps */}
<div className="relative">
{/* Connection Lines */}
<div className="hidden lg:block absolute top-1/2 left-0 right-0 h-0.5 bg-gradient-to-r from-blue-200 via-green-200 to-blue-200 transform -translate-y-1/2"></div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
{/* Step 1 */}
<div className="relative text-center">
{/* Step Circle */}
<div className="relative z-10 w-20 h-20 bg-gradient-to-br from-blue-500 to-blue-600 rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg">
<Scan className="w-10 h-10 text-white" />
{/* Main Pipeline Flow */}
[Image of a modern user interface flow diagram]
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-32 relative">
{STEPS.map((step, index) => (
<motion.div
key={step.id}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.2 }}
viewport={{ once: true }}
className="group relative p-8 rounded-[2.5rem] bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 hover:border-blue-500/30 transition-all duration-500"
>
{/* Step Badge */}
<div className="absolute top-8 right-8 text-4xl font-black opacity-5 dark:opacity-10 group-hover:opacity-20 transition-opacity italic">
0{step.id}
</div>
{/* Step Number */}
<div className="absolute -top-2 -right-2 w-8 h-8 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center text-blue-600 dark:text-blue-400 font-bold text-sm">
1
<div className={cn(
"w-16 h-16 rounded-2xl bg-gradient-to-br flex items-center justify-center mb-10 shadow-xl transition-transform group-hover:scale-110 group-hover:rotate-3",
step.color
)}>
<step.icon className="w-8 h-8 text-white" />
</div>
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Escaneie
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
{step.title}
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Use seu celular para escanear o código de barras do produto ou
digite o código manualmente.
<p className="text-gray-600 dark:text-gray-400 mb-8 leading-relaxed font-medium">
{step.description}
</p>
{/* Feature List */}
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm">
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-2">
<li className="flex items-center">
<CheckCircle className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" />
Códigos de 8-13 dígitos
<ul className="space-y-3 border-t border-gray-200 dark:border-gray-800 pt-6">
{step.features.map((f) => (
<li key={f} className="flex items-center text-xs font-bold text-gray-500 dark:text-gray-500 uppercase tracking-widest">
<div className="w-1.5 h-1.5 rounded-full bg-blue-500 mr-3 animate-pulse" />
{f}
</li>
<li className="flex items-center">
<CheckCircle className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" />
Busca por nome/marca
</li>
<li className="flex items-center">
<CheckCircle className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" />
Detecção automática
</li>
</ul>
</div>
</div>
{/* Arrow */}
<div className="hidden lg:flex items-center justify-center">
<ArrowRight className="w-8 h-8 text-gray-400" />
</div>
{/* Step 2 */}
<div className="relative text-center">
{/* Step Circle */}
<div className="relative z-10 w-20 h-20 bg-gradient-to-br from-green-500 to-green-600 rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg">
<Search className="w-10 h-10 text-white" />
</div>
{/* Step Number */}
<div className="absolute -top-2 -right-2 w-8 h-8 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center text-green-600 dark:text-green-400 font-bold text-sm">
2
</div>
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Descubra
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Receba informações completas sobre ingredientes, valores
nutricionais e classificações de saúde.
</p>
{/* Feature List */}
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm">
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-2">
<li className="flex items-center">
<CheckCircle className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" />
Tabela nutricional completa
</li>
<li className="flex items-center">
<CheckCircle className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" />
Nutri-Score e NOVA
</li>
<li className="flex items-center">
<CheckCircle className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" />
Lista de alérgenos
</li>
</ul>
</div>
</div>
{/* Arrow */}
<div className="hidden lg:flex items-center justify-center">
<ArrowRight className="w-8 h-8 text-gray-400" />
</div>
{/* Step 3 */}
<div className="relative text-center">
{/* Step Circle */}
<div className="relative z-10 w-20 h-20 bg-gradient-to-br from-purple-500 to-purple-600 rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg">
<CheckCircle className="w-10 h-10 text-white" />
</div>
{/* Step Number */}
<div className="absolute -top-2 -right-2 w-8 h-8 bg-purple-100 dark:bg-purple-900/30 rounded-full flex items-center justify-center text-purple-600 dark:text-purple-400 font-bold text-sm">
3
</div>
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Decida
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Com todas as informações em mãos, tome a decisão mais adequada
para sua saúde e bem-estar.
</p>
{/* Feature List */}
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm">
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-2">
<li className="flex items-center">
<CheckCircle className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" />
Comparação com similares
</li>
<li className="flex items-center">
<CheckCircle className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" />
Sugestões de alternativas
</li>
<li className="flex items-center">
<CheckCircle className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" />
Salvar para depois
</li>
</ul>
</div>
</div>
</div>
))}
</ul>
</motion.div>
))}
</div>
{/* Time Highlight */}
<div className="mt-16 text-center">
<div className="inline-flex items-center gap-3 bg-white dark:bg-gray-800 rounded-full px-6 py-3 shadow-lg">
<div className="w-8 h-8 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center">
<CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400" />
{/* Global Stats Footer: O 80/20 do Valor */}
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
className="grid grid-cols-2 lg:grid-cols-4 gap-4"
>
{[
{ icon: Globe, label: "Global", text: "2M+ Alimentos" },
{ icon: Shield, label: "Seguro", text: "100% Gratuito" },
{ icon: Clock, label: "Veloz", text: "< 10 Segundos" },
{ icon: Zap, label: "Offline", text: "Cache Inteligente" },
].map((item, i) => (
<div key={i} className="flex flex-col items-center p-6 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100 dark:border-gray-800 shadow-sm">
<item.icon className="w-6 h-6 text-blue-500 mb-3" />
<span className="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">{item.label}</span>
<span className="text-sm font-bold text-gray-900 dark:text-white">{item.text}</span>
</div>
<span className="text-lg font-semibold text-gray-900 dark:text-white">
Em menos de 10 segundos
</span>
</div>
<p className="mt-4 text-gray-600 dark:text-gray-400">
Do escaneamento à decisão informada
</p>
</div>
{/* Additional Features */}
<div className="mt-16 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm text-center">
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center mx-auto mb-4">
<Scan className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
Offline
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Funciona sem internet com cache local
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm text-center">
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-lg flex items-center justify-center mx-auto mb-4">
<Search className="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
Gratuito
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Sem custos, sem anúncios, sem pegadinhas
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm text-center">
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900/30 rounded-lg flex items-center justify-center mx-auto mb-4">
<CheckCircle className="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
Atualizado
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Dados sempre atualizados pela comunidade
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm text-center">
<div className="w-12 h-12 bg-orange-100 dark:bg-orange-900/30 rounded-lg flex items-center justify-center mx-auto mb-4">
<ArrowRight className="w-6 h-6 text-orange-600 dark:text-orange-400" />
</div>
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
Rápido
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Interface otimizada para velocidade
</p>
</div>
</div>
))}
</motion.div>
</div>
</section>
);
}
}
+139 -166
View File
@@ -1,185 +1,158 @@
"use client";
import { TrendingUp, Globe, Clock, Database } from "lucide-react";
import React from "react";
import { motion } from "framer-motion";
import { Globe, Database, Activity, Zap } from "lucide-react";
import { cn } from "@/lib/utils";
// Variantes para animação de entrada (Stagger)
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1 }
}
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: { y: 0, opacity: 1 }
};
export function ImpactSection() {
return (
<section className="py-20 bg-gray-50 dark:bg-gray-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-6">
Nosso{" "}
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent">
impacto
</span>
</h2>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
Números que mostram como estamos transformando a forma como as
pessoas escolhem seus alimentos.
<section className="py-32 bg-white dark:bg-[#030712] relative overflow-hidden transition-colors duration-500">
{/* Background Decor - Grid de Engenharia */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:40px_40px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)]" />
<div className="max-w-7xl mx-auto px-6 relative z-10">
{/* Header - Autoridade Técnica */}
<div className="text-center mb-24">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
className="text-4xl md:text-6xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter"
>
Escalabilidade e <span className="bg-gradient-to-r from-blue-600 to-emerald-500 bg-clip-text text-transparent">Impacto Global</span>
</motion.h2>
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
Métricas de performance da maior rede colaborativa de transparência alimentar do planeta.
</p>
</div>
{/* Impact Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-16">
<div className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-lg text-center hover:shadow-xl transition-shadow">
<div className="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-2xl flex items-center justify-center mx-auto mb-6">
<Database className="w-8 h-8 text-blue-600 dark:text-blue-400" />
</div>
<div className="text-4xl font-bold text-blue-600 dark:text-blue-400 mb-2">
2.000.000+
</div>
<div className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Produtos
</div>
<p className="text-gray-600 dark:text-gray-400 text-sm">
Na base de dados Open Food Facts
</p>
</div>
{/* Stats Grid - Estilo Bento Box Sênior */}
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-20"
>
{[
{ label: "Data Nodes", val: "2.0M+", icon: Database, color: "text-blue-500", bg: "bg-blue-500/10" },
{ label: "Regions", val: "100+", icon: Globe, color: "text-emerald-500", bg: "bg-emerald-500/10" },
{ label: "Uptime", val: "24/7", icon: Activity, color: "text-purple-500", bg: "bg-purple-500/10" },
{ label: "Local Cache", val: "218k", icon: Zap, color: "text-orange-500", bg: "bg-orange-500/10" },
].map((stat) => (
<motion.div
key={stat.label}
variants={itemVariants}
whileHover={{ y: -5 }}
className="bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 p-8 rounded-[2rem] shadow-sm hover:shadow-xl transition-all"
>
<div className={cn("w-12 h-12 rounded-xl flex items-center justify-center mb-6", stat.bg)}>
<stat.icon className={cn("w-6 h-6", stat.color)} />
</div>
<div className={cn("text-3xl font-mono font-bold tracking-tighter mb-1", stat.color)}>
{stat.val}
</div>
<div className="text-sm font-black uppercase tracking-widest text-gray-400">
{stat.label}
</div>
</motion.div>
))}
</motion.div>
<div className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-lg text-center hover:shadow-xl transition-shadow">
<div className="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-2xl flex items-center justify-center mx-auto mb-6">
<Globe className="w-8 h-8 text-green-600 dark:text-green-400" />
</div>
<div className="text-4xl font-bold text-green-600 dark:text-green-400 mb-2">
100+
</div>
<div className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Países
</div>
<p className="text-gray-600 dark:text-gray-400 text-sm">
Com produtos cadastrados
</p>
</div>
<div className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-lg text-center hover:shadow-xl transition-shadow">
<div className="w-16 h-16 bg-purple-100 dark:bg-purple-900/30 rounded-2xl flex items-center justify-center mx-auto mb-6">
<Clock className="w-8 h-8 text-purple-600 dark:text-purple-400" />
</div>
<div className="text-4xl font-bold text-purple-600 dark:text-purple-400 mb-2">
24/7
</div>
<div className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Disponível
</div>
<p className="text-gray-600 dark:text-gray-400 text-sm">
Acesso instantâneo aos dados
</p>
</div>
<div className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-lg text-center hover:shadow-xl transition-shadow">
<div className="w-16 h-16 bg-orange-100 dark:bg-orange-900/30 rounded-2xl flex items-center justify-center mx-auto mb-6">
<TrendingUp className="w-8 h-8 text-orange-600 dark:text-orange-400" />
</div>
<div className="text-4xl font-bold text-orange-600 dark:text-orange-400 mb-2">
218
</div>
<div className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Cache Local
</div>
<p className="text-gray-600 dark:text-gray-400 text-sm">
Produtos populares offline
</p>
</div>
</div>
{/* World Map Visualization */}
<div className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-lg mb-16">
<div className="text-center mb-8">
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Alcance Global
</h3>
<p className="text-gray-600 dark:text-gray-400">
Nossa plataforma conecta usuários de todos os continentes à maior
base de dados alimentar do mundo
</p>
</div>
{/* Simplified World Map */}
<div className="relative max-w-4xl mx-auto">
<div className="aspect-video bg-gradient-to-br from-blue-100 to-green-100 dark:from-blue-900/20 dark:to-green-900/20 rounded-xl overflow-hidden relative">
{/* Dots representing global coverage */}
<div className="absolute top-1/4 left-1/4 w-3 h-3 bg-blue-500 rounded-full animate-pulse"></div>
<div className="absolute top-1/3 right-1/3 w-3 h-3 bg-green-500 rounded-full animate-pulse delay-300"></div>
<div className="absolute bottom-1/3 left-1/3 w-3 h-3 bg-purple-500 rounded-full animate-pulse delay-500"></div>
<div className="absolute top-2/3 right-1/4 w-3 h-3 bg-orange-500 rounded-full animate-pulse delay-700"></div>
<div className="absolute bottom-1/4 left-1/2 w-3 h-3 bg-red-500 rounded-full animate-pulse delay-1000"></div>
{/* Center text */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center">
<div className="text-4xl font-bold text-gray-700 dark:text-gray-300 mb-2">
🌍
</div>
<div className="text-lg font-semibold text-gray-600 dark:text-gray-400">
Cobertura Global
</div>
{/* World Map - Visualização de Rede de Dados */}
[Image of a world map with data points and connections]
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
className="relative bg-slate-950 rounded-[3rem] p-12 overflow-hidden shadow-3xl mb-20"
>
<div className="absolute inset-0 opacity-20 bg-[url('https://www.transparenttextures.com/patterns/carbon-fibre.png')]" />
<div className="relative z-10 flex flex-col items-center">
<h3 className="text-2xl font-bold text-white mb-2 tracking-tight">Rede Global de Sincronização</h3>
<p className="text-slate-500 text-sm mb-12">Monitoramento de tráfego de metadados em tempo real</p>
<div className="w-full aspect-[21/9] relative bg-blue-500/5 rounded-2xl border border-blue-500/10 flex items-center justify-center">
{/* Pulsing Data Points (Engineering nodes) */}
{[
{ t: "20%", l: "25%", c: "bg-blue-500" },
{ t: "40%", l: "60%", c: "bg-emerald-500" },
{ t: "70%", l: "30%", c: "bg-purple-500" },
{ t: "25%", l: "80%", c: "bg-orange-500" },
{ t: "55%", l: "15%", c: "bg-red-500" },
].map((dot, i) => (
<div key={i} className="absolute" style={{ top: dot.t, left: dot.l }}>
<div className={cn("w-3 h-3 rounded-full animate-ping opacity-75 absolute", dot.c)} />
<div className={cn("w-3 h-3 rounded-full relative", dot.c)} />
</div>
))}
<Globe className="w-24 h-24 text-blue-500/20 animate-pulse" />
<div className="absolute bottom-4 right-6 flex items-center gap-2 text-[10px] font-mono text-blue-400 uppercase tracking-[0.3em]">
<span className="w-2 h-2 bg-blue-400 rounded-full animate-pulse" /> Live Node Updates
</div>
</div>
</div>
</div>
</motion.div>
{/* Community Impact */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 rounded-2xl p-8">
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
Transformação Individual
</h3>
<div className="space-y-4">
<div className="flex items-start">
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></div>
<p className="text-gray-700 dark:text-gray-300">
<strong>Escolhas mais conscientes:</strong> Usuários relatam
tomar decisões mais informadas sobre sua alimentação
</p>
</div>
<div className="flex items-start">
<div className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></div>
<p className="text-gray-700 dark:text-gray-300">
<strong>Economia de tempo:</strong> Redução de 80% no tempo
gasto analisando rótulos
</p>
</div>
<div className="flex items-start">
<div className="w-2 h-2 bg-purple-500 rounded-full mt-2 mr-3 flex-shrink-0"></div>
<p className="text-gray-700 dark:text-gray-300">
<strong>Confiança aumentada:</strong> Maior segurança na
identificação de alérgenos e ingredientes
</p>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20 rounded-2xl p-8">
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
Impacto Coletivo
</h3>
<div className="space-y-4">
<div className="flex items-start">
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></div>
<p className="text-gray-700 dark:text-gray-300">
<strong>Transparência da indústria:</strong> Pressão por
rótulos mais claros e honestos
</p>
</div>
<div className="flex items-start">
<div className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></div>
<p className="text-gray-700 dark:text-gray-300">
<strong>Dados colaborativos:</strong> Milhares de
contribuições diárias à base global
</p>
</div>
<div className="flex items-start">
<div className="w-2 h-2 bg-purple-500 rounded-full mt-2 mr-3 flex-shrink-0"></div>
<p className="text-gray-700 dark:text-gray-300">
<strong>Democratização:</strong> Acesso universal a
informações nutricionais de qualidade
</p>
</div>
</div>
</div>
{/* Impact Logs - High Density Info */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 font-medium">
<ImpactCard
title="Individual Protocol"
items={[
{ label: "Cognitive Efficiency", text: "Redução de 80% no tempo de análise de rótulos via algoritmos de OCR." },
{ label: "Data Sovereignty", text: "Usuários com controle total sobre o histórico de consumo local." },
{ label: "Safety Layer", text: "Detecção imediata de alérgenos via cruzamento de base de dados." }
]}
type="blue"
/>
<ImpactCard
title="Collective Governance"
items={[
{ label: "Market Transparency", text: "Pressão algorítmica por rótulos honestos e dados auditáveis." },
{ label: "Crowdsourced Intelligence", text: "Milhares de contribuições diárias validando a integridade da base." },
{ label: "Universal Access", text: "Democratização da informação nutricional de alta fidelidade." }
]}
type="emerald"
/>
</div>
</div>
</section>
);
}
function ImpactCard({ title, items, type }: { title: string, items: {label: string, text: string}[], type: 'blue' | 'emerald' }) {
return (
<div className={cn(
"p-10 rounded-[2.5rem] border transition-all duration-500 hover:shadow-2xl",
type === 'blue' ? "bg-blue-50/50 dark:bg-blue-900/10 border-blue-100 dark:border-blue-900/20" : "bg-emerald-50/50 dark:bg-emerald-900/10 border-emerald-100 dark:border-emerald-900/20"
)}>
<h3 className="text-xl font-black mb-8 tracking-tighter uppercase">{title}</h3>
<div className="space-y-6">
{items.map((item, i) => (
<div key={i} className="flex gap-4">
<div className={cn("w-1.5 h-1.5 rounded-full mt-2 shrink-0", type === 'blue' ? "bg-blue-500" : "bg-emerald-500")} />
<p className="text-sm leading-relaxed text-gray-700 dark:text-gray-300">
<span className="font-bold text-gray-900 dark:text-white">{item.label}:</span> {item.text}
</p>
</div>
))}
</div>
</div>
);
}
+123 -106
View File
@@ -1,122 +1,139 @@
"use client";
import { Eye, Shield, Users, Globe } from "lucide-react";
import React from "react";
import { motion } from "framer-motion";
import { Eye, Shield, Users, Globe, Database, Activity, Map } from "lucide-react";
import { cn } from "@/lib/utils";
const MISSION_VALUES = [
{
title: "Transparência",
description: "Governança de dados aberta. O que você consome é rastreado sem filtros ou interesses comerciais ocultos.",
icon: Eye,
color: "text-blue-500",
bg: "bg-blue-500/10",
},
{
title: "Empoderamento",
description: "Autonomia decisória baseada em evidências técnicas. Devolvemos o controle da saúde ao cidadão.",
icon: Shield,
color: "text-emerald-500",
bg: "bg-emerald-500/10",
},
{
title: "Comunidade",
description: "Ecossistema colaborativo global. A inteligência coletiva protegendo a saúde de milhões.",
icon: Users,
color: "text-purple-500",
bg: "bg-purple-500/10",
},
];
export function MissionSection() {
return (
<section className="py-20 bg-gradient-to-br from-blue-50 via-white to-green-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-6">
O que nos{" "}
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent">
move
</span>
<section className="py-32 bg-white dark:bg-[#030712] relative overflow-hidden transition-colors duration-500">
{/* Background Decor: Mesh Gradient sutil */}
<div className="absolute top-0 left-1/4 w-1/2 h-1/2 bg-blue-500/5 blur-[120px] rounded-full pointer-events-none" />
<div className="max-w-7xl mx-auto px-6 relative z-10">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-24"
>
<h2 className="text-4xl md:text-6xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter">
Governança de <span className="bg-gradient-to-r from-blue-600 to-emerald-500 bg-clip-text text-transparent">Dados Alimentares</span>
</h2>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
Acreditamos que alimentação consciente não precisa ser complicada.
Por isso, conectamos você à maior base de dados colaborativa de
alimentos do mundo.
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
Engenharia de software aplicada à transparência nutricional. Nossa missão é converter caos informacional em escolhas seguras.
</p>
</div>
</motion.div>
{/* Mission Values */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-16">
{/* Transparência */}
<div className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div className="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-2xl flex items-center justify-center mb-6">
<Eye className="w-8 h-8 text-blue-600 dark:text-blue-400" />
</div>
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Transparência
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
Todos os dados são colaborativos e abertos. Não escondemos nada,
não maquiamos informações. O que você vê é o que existe.
</p>
</div>
{/* Empoderamento */}
<div className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div className="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-2xl flex items-center justify-center mb-6">
<Shield className="w-8 h-8 text-green-600 dark:text-green-400" />
</div>
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Empoderamento
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
Colocamos o poder de escolha de volta nas suas mãos. Cada código
de barras escaneado é um passo em direção a escolhas mais
informadas.
</p>
</div>
{/* Comunidade */}
<div className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div className="w-16 h-16 bg-purple-100 dark:bg-purple-900/30 rounded-2xl flex items-center justify-center mb-6">
<Users className="w-8 h-8 text-purple-600 dark:text-purple-400" />
</div>
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Comunidade
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
Milhões de pessoas contribuem com informações sobre produtos de
mais de 100 países. Juntos, criamos um mundo mais transparente.
</p>
</div>
</div>
{/* Open Food Facts Highlight */}
<div className="bg-gradient-to-r from-blue-600 to-green-600 rounded-3xl p-8 text-white shadow-2xl">
<div className="flex items-center justify-between flex-wrap gap-8">
<div className="flex-1 min-w-0">
<div className="flex items-center mb-4">
<Globe className="w-8 h-8 mr-3" />
<h3 className="text-2xl font-bold">Open Food Facts</h3>
{/* Values: Bento-style Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-20">
{MISSION_VALUES.map((value, i) => (
<motion.div
key={value.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.1 }}
viewport={{ once: true }}
className="group p-8 rounded-[2rem] bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 hover:border-blue-500/30 transition-all duration-500"
>
<div className={cn("w-14 h-14 rounded-2xl flex items-center justify-center mb-8 transition-transform group-hover:scale-110", value.bg)}>
<value.icon className={cn("w-7 h-7", value.color)} />
</div>
<p className="text-blue-100 text-lg mb-6">
Conectamos você à maior base de dados colaborativa de alimentos
do mundo, onde cada produto é verificado e atualizado pela
comunidade global.
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
{value.title}
</h3>
<p className="text-gray-600 dark:text-gray-400 leading-relaxed text-sm font-medium">
{value.description}
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center">
<div className="text-3xl font-bold mb-1">2M+</div>
<div className="text-blue-100 text-sm">Produtos</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold mb-1">100+</div>
<div className="text-blue-100 text-sm">Países</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold mb-1">24/7</div>
<div className="text-blue-100 text-sm">Disponível</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold mb-1">∞</div>
<div className="text-blue-100 text-sm">Colaborativo</div>
</div>
</div>
</div>
<div className="flex-shrink-0">
<div className="w-24 h-24 bg-white/20 rounded-2xl flex items-center justify-center">
<Globe className="w-12 h-12" />
</div>
</div>
</div>
</motion.div>
))}
</div>
{/* Inspirational Quote */}
<div className="text-center mt-16">
<blockquote className="text-2xl sm:text-3xl font-medium text-gray-700 dark:text-gray-300 italic max-w-4xl mx-auto">
&ldquo;Alimentação é mais que nutrição. É autocuidado, é conexão, é
vida.&rdquo;
</blockquote>
<cite className="block mt-4 text-lg text-gray-500 dark:text-gray-400">
— Filosofia NutriScan
</cite>
{/* Data Source Dashboard Highlight */}
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
className="relative rounded-[3rem] bg-slate-950 p-10 md:p-16 overflow-hidden shadow-3xl"
>
<div className="absolute top-0 right-0 w-full h-full bg-[radial-gradient(circle_at_top_right,_var(--tw-gradient-stops))] from-blue-500/20 via-transparent to-transparent" />
<div className="relative z-10 flex flex-col lg:flex-row gap-12 items-center">
<div className="flex-1 space-y-8">
<div className="inline-flex items-center px-4 py-2 rounded-full bg-blue-500/10 border border-blue-500/20 text-blue-400 text-xs font-black uppercase tracking-widest">
<Database className="w-3 h-3 mr-2" /> Infraestrutura Global
</div>
<h3 className="text-3xl md:text-4xl font-black text-white tracking-tighter">
Integrado ao Open Food Facts API
</h3>
<p className="text-slate-400 text-lg leading-relaxed">
Nossa engine de busca opera sobre o maior dataset colaborativo do planeta, garantindo que cada scan seja validado por uma rede global de curadoria.
</p>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
{[
{ label: "Produtos", val: "2M+", icon: Activity },
{ label: "Países", val: "100+", icon: Map },
{ label: "Uptime", val: "24/7", icon: Activity },
{ label: "Licença", val: "ODBL", icon: Shield },
].map((stat) => (
<div key={stat.label} className="space-y-1">
<div className="text-2xl font-mono font-bold text-white tracking-tighter">{stat.val}</div>
<div className="text-[10px] uppercase font-black text-slate-500 tracking-widest flex items-center gap-1">
<stat.icon className="w-3 h-3" /> {stat.label}
</div>
</div>
))}
</div>
</div>
<div className="relative w-64 h-64 flex items-center justify-center">
{/* Efeito Visual de Radar/Sincronização */}
<div className="absolute inset-0 border border-blue-500/20 rounded-full animate-ping" />
<div className="absolute inset-4 border border-blue-500/10 rounded-full animate-pulse" />
<div className="w-32 h-32 bg-blue-600 rounded-3xl flex items-center justify-center shadow-2xl shadow-blue-500/50 rotate-12 group">
<Globe className="text-white w-16 h-16" />
</div>
</div>
</div>
</motion.div>
{/* Footer Quote: Minimalista */}
<div className="mt-24 text-center">
<p className="text-gray-400 dark:text-gray-600 font-mono text-[10px] uppercase tracking-[0.5em] mb-4">
NutriScan Open Data Philosophy
</p>
<p className="text-xl md:text-2xl font-serif italic text-gray-800 dark:text-gray-200">
&ldquo;Dados abertos são a fundação de uma sociedade saudável.&rdquo;
</p>
</div>
</div>
</section>
);
}
}
+147 -142
View File
@@ -1,159 +1,164 @@
"use client";
import React, { useRef } from "react";
import { motion, useScroll, useSpring } from "framer-motion";
import { Calendar, Lightbulb, Heart, Target } from "lucide-react";
import { cn } from "@/lib/utils";
// 1. Definição da Interface de Dados
interface StoryStep {
year: string;
title: string;
description: string;
icon: React.ElementType;
color: string;
side: "left" | "right";
}
const STORY_DATA: StoryStep[] = [
{
year: "2024",
title: "A Pergunta Simples",
description: "O que realmente tem nesse produto? Decifrar tabelas nutricionais era um desafio individual que percebemos ser global.",
icon: Calendar,
color: "blue",
side: "left",
},
{
year: "Descoberta",
title: "Não Estávamos Sozinhos",
description: "Milhares de pessoas buscam transparência alimentar. A necessidade de dados abertos tornou-se uma urgência técnica.",
icon: Lightbulb,
color: "green",
side: "right",
},
{
year: "Solução",
title: "NutriScan Nasce",
description: "Nascido da Engenharia para o mundo. Uma ponte entre o código de barras e a verdade nutricional.",
icon: Heart,
color: "purple",
side: "left",
},
{
year: "Hoje",
title: "Nossa Missão",
description: "Democratizar a informação. Conectar cidadãos à maior base de dados colaborativa do planeta.",
icon: Target,
color: "orange",
side: "right",
},
];
export function StorySection() {
const containerRef = useRef<HTMLDivElement>(null);
// 2. Lógica de Scroll Progressivo (Engenharia de Movimento)
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start end", "end end"],
});
const scaleY = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001
});
return (
<section className="py-20 bg-white dark:bg-gray-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-6">
Nossa{" "}
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent">
história
</span>
<section ref={containerRef} className="py-32 bg-white dark:bg-[#030712] relative overflow-hidden">
<div className="max-w-7xl mx-auto px-6">
{/* Header com Animação de Entrada */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
className="text-center mb-32"
>
<h2 className="text-4xl md:text-6xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter">
Nossa <span className="bg-gradient-to-r from-blue-600 to-green-500 bg-clip-text text-transparent">história</span>
</h2>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
Tudo começou com uma pergunta simples que mudou tudo.
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
De uma dúvida individual a uma plataforma de transparência global.
</p>
</div>
</motion.div>
{/* Timeline */}
<div className="relative">
{/* Timeline Line */}
<div className="absolute left-1/2 transform -translate-x-1/2 w-1 h-full bg-gradient-to-b from-blue-600 to-green-600 rounded-full hidden md:block"></div>
{/* 3. A Linha do Tempo Reativa */}
<div className="absolute left-1/2 transform -translate-x-1/2 w-[2px] h-full bg-gray-100 dark:bg-gray-800 hidden md:block">
<motion.div
style={{ scaleY }}
className="absolute top-0 w-full h-full bg-gradient-to-b from-blue-500 via-green-500 to-orange-500 origin-top"
/>
</div>
{/* Timeline Items */}
<div className="space-y-16">
{/* 2024 - The Question */}
<div className="relative flex items-center">
<div className="w-full md:w-1/2 md:pr-8">
<div className="bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 rounded-2xl p-8 shadow-lg">
<div className="flex items-center mb-4">
<div className="w-12 h-12 bg-blue-500 rounded-lg flex items-center justify-center mr-4">
<Calendar className="w-6 h-6 text-white" />
</div>
<div>
<div className="text-sm font-medium text-blue-600 dark:text-blue-400">
2024
</div>
<div className="text-lg font-bold text-gray-900 dark:text-white">
A Pergunta Simples
</div>
</div>
</div>
<blockquote className="text-xl font-semibold text-gray-800 dark:text-gray-200 mb-4">
&ldquo;O que realmente tem nesse produto?&rdquo;
</blockquote>
<p className="text-gray-600 dark:text-gray-300">
Após inúmeras idas ao mercado tentando decifrar tabelas
nutricionais e listas intermináveis de ingredientes,
percebemos que não estávamos sozinhos nessa busca.
</p>
</div>
</div>
{/* Timeline Dot */}
<div className="absolute left-1/2 transform -translate-x-1/2 w-4 h-4 bg-blue-500 rounded-full border-4 border-white dark:border-gray-900 shadow-lg hidden md:block"></div>
<div className="w-full md:w-1/2 md:pl-8"></div>
</div>
{/* The Discovery */}
<div className="relative flex items-center">
<div className="w-full md:w-1/2 md:pr-8"></div>
{/* Timeline Dot */}
<div className="absolute left-1/2 transform -translate-x-1/2 w-4 h-4 bg-green-500 rounded-full border-4 border-white dark:border-gray-900 shadow-lg hidden md:block"></div>
<div className="w-full md:w-1/2 md:pl-8">
<div className="bg-gradient-to-br from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20 rounded-2xl p-8 shadow-lg">
<div className="flex items-center mb-4">
<div className="w-12 h-12 bg-green-500 rounded-lg flex items-center justify-center mr-4">
<Lightbulb className="w-6 h-6 text-white" />
</div>
<div>
<div className="text-sm font-medium text-green-600 dark:text-green-400">
Descoberta
</div>
<div className="text-lg font-bold text-gray-900 dark:text-white">
Não Estávamos Sozinhos
</div>
</div>
</div>
<p className="text-gray-600 dark:text-gray-300">
Milhares de pessoas querem fazer escolhas mais conscientes,
mas esbarram na falta de informação acessível. A necessidade
era real e urgente.
</p>
</div>
</div>
</div>
{/* The Solution */}
<div className="relative flex items-center">
<div className="w-full md:w-1/2 md:pr-8">
<div className="bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 rounded-2xl p-8 shadow-lg">
<div className="flex items-center mb-4">
<div className="w-12 h-12 bg-purple-500 rounded-lg flex items-center justify-center mr-4">
<Heart className="w-6 h-6 text-white" />
</div>
<div>
<div className="text-sm font-medium text-purple-600 dark:text-purple-400">
Solução
</div>
<div className="text-lg font-bold text-gray-900 dark:text-white">
NutriScan Nasce
</div>
</div>
</div>
<p className="text-gray-600 dark:text-gray-300">
Foi assim que o NutriScan ganhou vida – não como mais um
aplicativo de nutrição, mas como uma ponte entre você e a
verdade sobre o que consome.
</p>
</div>
</div>
{/* Timeline Dot */}
<div className="absolute left-1/2 transform -translate-x-1/2 w-4 h-4 bg-purple-500 rounded-full border-4 border-white dark:border-gray-900 shadow-lg hidden md:block"></div>
<div className="w-full md:w-1/2 md:pl-8"></div>
</div>
{/* The Mission */}
<div className="relative flex items-center">
<div className="w-full md:w-1/2 md:pr-8"></div>
{/* Timeline Dot */}
<div className="absolute left-1/2 transform -translate-x-1/2 w-4 h-4 bg-orange-500 rounded-full border-4 border-white dark:border-gray-900 shadow-lg hidden md:block"></div>
<div className="w-full md:w-1/2 md:pl-8">
<div className="bg-gradient-to-br from-orange-50 to-orange-100 dark:from-orange-900/20 dark:to-orange-800/20 rounded-2xl p-8 shadow-lg">
<div className="flex items-center mb-4">
<div className="w-12 h-12 bg-orange-500 rounded-lg flex items-center justify-center mr-4">
<Target className="w-6 h-6 text-white" />
</div>
<div>
<div className="text-sm font-medium text-orange-600 dark:text-orange-400">
Hoje
</div>
<div className="text-lg font-bold text-gray-900 dark:text-white">
Nossa Missão
</div>
</div>
</div>
<p className="text-gray-600 dark:text-gray-300">
Democratizar o acesso às informações nutricionais,
conectando você à maior base de dados colaborativa de
alimentos do mundo.
</p>
</div>
</div>
</div>
<div className="space-y-24 md:space-y-32">
{STORY_DATA.map((step, index) => (
<TimelineItem key={index} step={step} index={index} />
))}
</div>
</div>
</div>
</section>
);
}
// 4. Sub-componente com Encapsulamento de Animação
function TimelineItem({ step }: { step: StoryStep; index: number }) {
const isLeft = step.side === "left";
const colorMap = {
blue: "bg-blue-500 text-blue-500 border-blue-200 dark:border-blue-900/30",
green: "bg-green-500 text-green-500 border-green-200 dark:border-green-900/30",
purple: "bg-purple-500 text-purple-500 border-purple-200 dark:border-purple-900/30",
orange: "bg-orange-500 text-orange-500 border-orange-200 dark:border-orange-900/30",
};
return (
<div className={cn(
"relative flex flex-col md:flex-row items-center justify-between w-full",
isLeft ? "md:flex-row" : "md:flex-row-reverse"
)}>
{/* Conteúdo */}
<motion.div
initial={{ opacity: 0, x: isLeft ? -50 : 50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.6, delay: 0.1 }}
className="w-full md:w-[45%] group"
>
<div className={cn(
"p-8 rounded-[2rem] bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 transition-all duration-500",
"hover:shadow-2xl hover:border-transparent group-hover:bg-white dark:group-hover:bg-gray-900"
)}>
<div className="flex items-center gap-4 mb-6">
<div className={cn("w-12 h-12 rounded-xl flex items-center justify-center text-white shadow-lg", colorMap[step.color as keyof typeof colorMap].split(' ')[0])}>
<step.icon size={24} />
</div>
<div>
<span className={cn("text-xs font-black uppercase tracking-widest", colorMap[step.color as keyof typeof colorMap].split(' ')[1])}>
{step.year}
</span>
<h3 className="text-xl font-bold text-gray-900 dark:text-white leading-tight">
{step.title}
</h3>
</div>
</div>
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
{step.description}
</p>
</div>
</motion.div>
{/* Ponto Central com Efeito de Pulso */}
<div className="absolute left-1/2 transform -translate-x-1/2 w-4 h-4 z-20 hidden md:block">
<motion.div
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
className={cn("w-full h-full rounded-full ring-4 ring-white dark:ring-[#030712] shadow-sm", colorMap[step.color as keyof typeof colorMap].split(' ')[0])}
/>
</div>
<div className="hidden md:block w-[45%]" />
</div>
);
}
+140 -141
View File
@@ -1,159 +1,158 @@
"use client";
import { Users, Heart, Zap, BookOpen, Baby, Dumbbell } from "lucide-react";
import React from "react";
import { motion } from "framer-motion";
import { Heart, Zap, BookOpen, Baby, Dumbbell, ArrowRight, ShieldCheck } from "lucide-react";
import { cn } from "@/lib/utils";
const PERSONAS = [
{
id: "parents",
title: "Pais e Mães",
tagline: "Segurança familiar",
description: "Decifre rótulos complexos e garanta que o lanche escolar seja livre de ingredientes ultraprocessados ocultos.",
icon: Baby,
color: "text-pink-500",
bg: "bg-pink-500/10",
border: "hover:border-pink-500/30",
features: ["Identificação de Alérgenos", "Nutri-Score Infantil", "Educação Nutricional"]
},
{
id: "athletes",
title: "Atletas",
tagline: "Performance Máxima",
description: "Otimize sua composição corporal monitorando densidade calórica e a qualidade dos macronutrientes em tempo real.",
icon: Dumbbell,
color: "text-emerald-500",
bg: "bg-emerald-500/10",
border: "hover:border-emerald-500/30",
features: ["Monitoramento de Macros", "Foco em Bio-disponibilidade", "Zero Açúcares Ocultos"]
},
{
id: "allergic",
title: "Alérgicos",
tagline: "Risco Zero",
description: "Sistemas de alerta para contaminação cruzada e componentes críticos. Segurança total na palma da sua mão.",
icon: ShieldCheck,
color: "text-orange-500",
bg: "bg-orange-500/10",
border: "hover:border-orange-500/30",
features: ["Glúten & Lactose Tracker", "Traços de Alérgenos", "Scan de Segurança"]
},
{
id: "conscious",
title: "Consumidores",
tagline: "Decisão Crítica",
description: "Para quem questiona a indústria. Acesse a lista real de ingredientes e a origem dos dados de forma aberta.",
icon: BookOpen,
color: "text-blue-500",
bg: "bg-blue-500/10",
border: "hover:border-blue-500/30",
features: ["Análise de Aditivos", "Comparação de SKUs", "Transparência ODBL"]
}
];
export function TargetAudienceSection() {
return (
<section className="py-20 bg-white dark:bg-gray-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-6">
Para quem é o{" "}
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent">
NutriScan?
</span>
</h2>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
Se você se preocupa com o que coloca no prato, o NutriScan foi feito
pensando em você.
<section className="py-32 bg-white dark:bg-[#030712] relative overflow-hidden transition-colors duration-500">
<div className="max-w-7xl mx-auto px-6 relative z-10">
{/* Header com Motion Stagger */}
<div className="text-center mb-24">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-4xl md:text-6xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter"
>
Projetado para <span className="bg-gradient-to-r from-blue-600 to-emerald-500 bg-clip-text text-transparent">Cidadãos Conscientes</span>
</motion.h2>
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
Se a transparência é um requisito para sua mesa, o NutriScan é sua ferramenta de governança alimentar.
</p>
</div>
{/* Persona Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-16">
{/* Pais */}
<div className="bg-gradient-to-br from-pink-50 to-pink-100 dark:from-pink-900/20 dark:to-pink-800/20 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div className="w-16 h-16 bg-pink-100 dark:bg-pink-900/30 rounded-2xl flex items-center justify-center mb-4">
<Users className="w-8 h-8 text-pink-600 dark:text-pink-400" />
</div>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-3">
Pais e Mães
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
&ldquo;Se você já se preocupou com os ingredientes do lanche da
escola...&rdquo;
</p>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li>• Identificar alérgenos</li>
<li>• Escolher lanches saudáveis</li>
<li>• Educar crianças sobre nutrição</li>
</ul>
</div>
{/* Atletas */}
<div className="bg-gradient-to-br from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div className="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-2xl flex items-center justify-center mb-4">
<Dumbbell className="w-8 h-8 text-green-600 dark:text-green-400" />
</div>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-3">
Atletas
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
&ldquo;Se você já precisou otimizar sua alimentação para
performance...&rdquo;
</p>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li>• Monitorar macronutrientes</li>
<li>• Evitar ingredientes proibidos</li>
<li>• Timing nutricional</li>
</ul>
</div>
{/* Alérgicos */}
<div className="bg-gradient-to-br from-orange-50 to-orange-100 dark:from-orange-900/20 dark:to-orange-800/20 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div className="w-16 h-16 bg-orange-100 dark:bg-orange-900/30 rounded-2xl flex items-center justify-center mb-4">
<Heart className="w-8 h-8 text-orange-600 dark:text-orange-400" />
</div>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-3">
Pessoas com Alergias
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
&ldquo;Se você já desistiu de uma compra por não saber se era
seguro...&rdquo;
</p>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li>• Detectar alérgenos ocultos</li>
<li>• Verificar contaminação cruzada</li>
<li>• Segurança alimentar</li>
</ul>
</div>
{/* Curiosos */}
<div className="bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div className="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-2xl flex items-center justify-center mb-4">
<BookOpen className="w-8 h-8 text-blue-600 dark:text-blue-400" />
</div>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-3">
Consumidores Conscientes
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
&ldquo;Se você já quis entender o que realmente está
comendo...&rdquo;
</p>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li>• Aprender sobre ingredientes</li>
<li>• Comparar produtos</li>
<li>• Tomar decisões informadas</li>
</ul>
</div>
</div>
{/* Identification Section */}
<div className="bg-gradient-to-r from-blue-600 to-green-600 rounded-3xl p-8 text-white shadow-2xl">
<div className="text-center">
<h3 className="text-3xl font-bold mb-4">
Isso é para{" "}
<span className="underline decoration-2 decoration-yellow-400">
VOCÊ
{/* Persona Bento Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-24">
{PERSONAS.map((persona, i) => (
<motion.div
key={persona.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.1 }}
viewport={{ once: true }}
className={cn(
"group relative p-8 rounded-[2.5rem] bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 transition-all duration-500",
persona.border,
"hover:shadow-2xl hover:shadow-gray-200 dark:hover:shadow-none"
)}
>
<div className={cn("w-14 h-14 rounded-2xl flex items-center justify-center mb-8 transition-transform group-hover:scale-110", persona.bg)}>
<persona.icon className={cn("w-7 h-7", persona.color)} />
</div>
<span className={cn("text-[10px] font-black uppercase tracking-[0.2em] mb-2 block", persona.color)}>
{persona.tagline}
</span>
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
{persona.title}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed mb-8">
{persona.description}
</p>
<ul className="space-y-3">
{persona.features.map(f => (
<li key={f} className="flex items-center gap-2 text-xs font-bold text-gray-500 dark:text-gray-500">
<div className={cn("w-1 h-1 rounded-full", persona.bg.replace('/10', ''))} />
{f}
</li>
))}
</ul>
</motion.div>
))}
</div>
{/* High-Impact Callout: Glassmorphism Card */}
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
className="relative rounded-[3rem] bg-gradient-to-br from-blue-600 to-emerald-600 p-1 md:p-px shadow-3xl"
>
<div className="bg-white/5 dark:bg-black/20 backdrop-blur-3xl rounded-[2.9rem] p-10 md:p-16 text-center text-white">
<h3 className="text-3xl md:text-5xl font-black mb-6 tracking-tighter">
A informação alimentar é um <span className="text-yellow-400">Direito Universal</span>
</h3>
<p className="text-xl text-blue-100 mb-8 max-w-4xl mx-auto">
Se você já se pegou lendo rótulos sem entender nada, se já
desistiu de uma compra por não saber se aquele produto era
adequado, ou se simplesmente quer ter mais controle sobre sua
alimentação, o NutriScan foi feito pensando em você.
<p className="text-blue-50 text-lg md:text-xl mb-12 max-w-4xl mx-auto leading-relaxed opacity-90">
Se você busca soberania sobre suas escolhas, o NutriScan é o seu protocolo de acesso à verdade. Sem interesses corporativos, apenas dados puros e auditáveis.
</p>
{/* Interactive Elements */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
<div className="bg-white/20 rounded-xl p-6 backdrop-blur-sm">
<Zap className="w-8 h-8 mx-auto mb-3" />
<h4 className="font-semibold mb-2">Rápido</h4>
<p className="text-blue-100 text-sm">Informações em segundos</p>
</div>
<div className="bg-white/20 rounded-xl p-6 backdrop-blur-sm">
<Heart className="w-8 h-8 mx-auto mb-3" />
<h4 className="font-semibold mb-2">Confiável</h4>
<p className="text-blue-100 text-sm">
Dados verificados pela comunidade
</p>
</div>
<div className="bg-white/20 rounded-xl p-6 backdrop-blur-sm">
<Baby className="w-8 h-8 mx-auto mb-3" />
<h4 className="font-semibold mb-2">Simples</h4>
<p className="text-blue-100 text-sm">
Interface intuitiva e clara
</p>
</div>
<div className="flex flex-wrap justify-center gap-6">
{[
{ icon: Zap, label: "Baixa Latência", text: "Dados em < 2s" },
{ icon: Heart, label: "Auditável", text: "Open Source" },
{ icon: ShieldCheck, label: "Privacidade", text: "No Tracking" },
].map((item, i) => (
<div key={i} className="flex flex-col items-center gap-2 bg-white/10 px-8 py-6 rounded-2xl border border-white/10 backdrop-blur-md">
<item.icon className="w-6 h-6 text-yellow-400" />
<span className="text-xs font-black uppercase tracking-widest">{item.label}</span>
<span className="text-[10px] text-blue-100 opacity-70 uppercase">{item.text}</span>
</div>
))}
</div>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="mt-16 inline-flex items-center gap-3 px-10 py-5 bg-white text-blue-600 rounded-2xl font-black uppercase tracking-widest text-sm shadow-xl shadow-black/10 transition-all hover:bg-yellow-400 hover:text-black"
>
Iniciar Scan Agora <ArrowRight size={18} />
</motion.button>
</div>
</div>
</motion.div>
{/* Call to Action */}
<div className="text-center mt-16">
<p className="text-2xl font-medium text-gray-700 dark:text-gray-300 mb-8">
Não importa quem você é ou qual sua situação - o direito à
informação alimentar é universal.
</p>
<div className="inline-flex items-center gap-2 text-lg text-gray-600 dark:text-gray-400">
<span>
Junte-se a milhares de pessoas que já fazem escolhas mais
conscientes
</span>
<span className="text-green-500">→</span>
</div>
</div>
</div>
</section>
);
}
}
+150 -181
View File
@@ -1,204 +1,173 @@
"use client";
import { Shield, Eye, Gift, Lock, CheckCircle, Star } from "lucide-react";
import React from "react";
import { motion } from "framer-motion";
import {
ShieldCheck, Eye, Gift, Lock,
CheckCircle2, Github, Database,
Scale, Terminal, Fingerprint
} from "lucide-react";
import { cn } from "@/lib/utils";
// 1. Data Contracts - Centralizando a lógica para fácil manutenção
const PROTOCOLS = [
{
title: "Dados Abertos",
desc: "Acesso irrestrito a metadados alimentares sob licença ODbL.",
icon: Eye,
color: "text-blue-500",
bg: "bg-blue-500/10",
tag: "Open Data"
},
{
title: "Gratuidade Perpétua",
desc: "Arquitetura sustentada por comunidade, livre de paywalls ou ads.",
icon: Gift,
color: "text-emerald-500",
bg: "bg-emerald-500/10",
tag: "Non-Profit"
},
{
title: "Privacy by Design",
desc: "Zero-tracking. Coleta mínima de dados conforme LGPD/GDPR.",
icon: Lock,
color: "text-purple-500",
bg: "bg-purple-500/10",
tag: "Security"
},
{
title: "Alta Fidelidade",
desc: "Algoritmos de validação cruzada para garantir integridade.",
icon: ShieldCheck,
color: "text-orange-500",
bg: "bg-orange-500/10",
tag: "Accuracy"
}
];
export function ValuesSection() {
return (
<section className="py-20 bg-gray-50 dark:bg-gray-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-6">
Nossos{" "}
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent">
valores
</span>
<section className="py-32 bg-white dark:bg-[#030712] relative overflow-hidden transition-colors duration-500">
{/* Background Decor - Grid de Engenharia sutil */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808008_1px,transparent_1px),linear-gradient(to_bottom,#80808008_1px,transparent_1px)] bg-[size:32px_32px]" />
<div className="max-w-7xl mx-auto px-6 relative z-10">
{/* Header - Tom de Autoridade */}
<div className="text-center mb-24">
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-blue-500/5 border border-blue-500/20 text-blue-600 dark:text-blue-400 text-[10px] font-black uppercase tracking-[0.3em] mb-6"
>
<Scale size={12} /> Compliance & Ethics
</motion.div>
<h2 className="text-4xl md:text-6xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter">
Protocolos de <span className="bg-gradient-to-r from-blue-600 to-emerald-500 bg-clip-text text-transparent">Integridade</span>
</h2>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
Transparência em primeiro lugar. Não escondemos nada, não maquiamos
informações.
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
Nossa governança é pautada pela transparência radical. Não maquiamos dados; entregamos a verdade técnica.
</p>
</div>
{/* Main Promise */}
<div className="bg-gradient-to-r from-blue-600 to-green-600 rounded-3xl p-8 text-white shadow-2xl mb-16">
<div className="text-center">
<div className="w-20 h-20 bg-white/20 rounded-2xl flex items-center justify-center mx-auto mb-6">
<Shield className="w-10 h-10" />
</div>
<h3 className="text-3xl font-bold mb-4">100% Transparente</h3>
<p className="text-xl text-blue-100 max-w-3xl mx-auto mb-8">
Todos os dados que você encontra aqui são colaborativos e abertos.
O que você vê é o que existe: informações reais, fornecidas por
uma comunidade global comprometida com a verdade.
</p>
</div>
</div>
{/* Values Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-16">
{/* Open Data */}
<div className="bg-white dark:bg-gray-900 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-shadow">
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center mb-4">
<Eye className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
Dados Abertos
</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm mb-4">
Todas as informações são colaborativas e verificadas pela
comunidade global.
</p>
<div className="flex items-center text-blue-600 dark:text-blue-400 text-sm font-medium">
<CheckCircle className="w-4 h-4 mr-2" />
Sem informações escondidas
</div>
</div>
{/* Free Forever */}
<div className="bg-white dark:bg-gray-900 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-shadow">
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-lg flex items-center justify-center mb-4">
<Gift className="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
Gratuito Sempre
</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm mb-4">
Sem custos ocultos, sem assinaturas, sem pegadinhas. Acesso
completo para sempre.
</p>
<div className="flex items-center text-green-600 dark:text-green-400 text-sm font-medium">
<CheckCircle className="w-4 h-4 mr-2" />
Sem anúncios invasivos
</div>
</div>
{/* Privacy */}
<div className="bg-white dark:bg-gray-900 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-shadow">
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900/30 rounded-lg flex items-center justify-center mb-4">
<Lock className="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
Privacidade
</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm mb-4">
Não coletamos dados pessoais desnecessários. Sua privacidade é
respeitada.
</p>
<div className="flex items-center text-purple-600 dark:text-purple-400 text-sm font-medium">
<CheckCircle className="w-4 h-4 mr-2" />
Sem rastreamento invasivo
</div>
</div>
{/* Quality */}
<div className="bg-white dark:bg-gray-900 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-shadow">
<div className="w-12 h-12 bg-orange-100 dark:bg-orange-900/30 rounded-lg flex items-center justify-center mb-4">
<Star className="w-6 h-6 text-orange-600 dark:text-orange-400" />
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
Qualidade
</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm mb-4">
Informações verificadas por especialistas e comunidade
comprometida com a precisão.
</p>
<div className="flex items-center text-orange-600 dark:text-orange-400 text-sm font-medium">
<CheckCircle className="w-4 h-4 mr-2" />
Dados verificados
</div>
</div>
</div>
{/* Open Source Badge */}
<div className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-lg text-center">
<div className="flex items-center justify-center mb-6">
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-green-500 rounded-2xl flex items-center justify-center mr-4">
<Shield className="w-8 h-8 text-white" />
</div>
<div>
<h3 className="text-2xl font-bold text-gray-900 dark:text-white">
Open Source & Open Data
{/* Values Grid - Bento Box Style */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-20">
{PROTOCOLS.map((item, i) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.1 }}
viewport={{ once: true }}
className="group p-8 rounded-[2.5rem] bg-gray-50 dark:bg-gray-900/40 border border-gray-100 dark:border-gray-800 hover:border-blue-500/30 transition-all duration-500"
>
<div className={cn("w-14 h-14 rounded-2xl flex items-center justify-center mb-8 transition-transform group-hover:scale-110", item.bg)}>
<item.icon className={cn("w-7 h-7", item.color)} />
</div>
<span className={cn("text-[10px] font-mono font-bold uppercase tracking-widest mb-2 block", item.color)}>
{item.tag}
</span>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
{item.title}
</h3>
<p className="text-gray-600 dark:text-gray-400">
Transparência total do código e dados
<p className="text-sm text-gray-500 dark:text-gray-400 leading-relaxed font-medium mb-6">
{item.desc}
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-xl p-4">
<h4 className="font-semibold text-blue-900 dark:text-blue-100 mb-2">
Código Aberto
</h4>
<p className="text-sm text-blue-700 dark:text-blue-300">
Todo o código está disponível no GitHub para inspeção e
contribuição
</p>
</div>
<div className="bg-green-50 dark:bg-green-900/20 rounded-xl p-4">
<h4 className="font-semibold text-green-900 dark:text-green-100 mb-2">
Dados Colaborativos
</h4>
<p className="text-sm text-green-700 dark:text-green-300">
Base de dados construída e mantida por milhares de voluntários
</p>
</div>
<div className="bg-purple-50 dark:bg-purple-900/20 rounded-xl p-4">
<h4 className="font-semibold text-purple-900 dark:text-purple-100 mb-2">
Sem Fins Lucrativos
</h4>
<p className="text-sm text-purple-700 dark:text-purple-300">
Desenvolvido com amor pela comunidade, para a comunidade
</p>
</div>
</div>
<div className="flex items-center gap-2 text-[10px] font-black text-gray-400 uppercase tracking-tighter group-hover:text-blue-500 transition-colors">
<CheckCircle2 size={12} className="text-emerald-500" /> System Verified
</div>
</motion.div>
))}
</div>
{/* Trust Indicators */}
<div className="mt-16 text-center">
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-8">
Por que confiar no NutriScan?
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400 mb-2">
2M+
{/* OSS Deep Dive - O Diferencial Sênior */}
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
whileInView={{ opacity: 1, scale: 1 }}
className="relative rounded-[3rem] bg-slate-950 border border-slate-800 p-10 md:p-16 overflow-hidden shadow-3xl"
>
<div className="absolute top-0 right-0 w-full h-full bg-[radial-gradient(circle_at_top_right,#1e293b,transparent)] opacity-50" />
<div className="relative z-10 flex flex-col lg:flex-row gap-12 items-center">
<div className="flex-1 space-y-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-white/10 rounded-2xl backdrop-blur-md">
<Github className="text-white w-8 h-8" />
</div>
<div>
<h3 className="text-3xl font-black text-white tracking-tighter">Open Source Manifesto</h3>
<p className="text-blue-400 font-mono text-xs uppercase tracking-widest mt-1">Status: Fully Auditability</p>
</div>
</div>
<p className="text-gray-600 dark:text-gray-400">
Produtos verificados
<p className="text-slate-400 text-lg leading-relaxed">
Acreditamos que algoritmos que impactam a saúde pública devem ser de domínio público. Nosso código é aberto para inspeção, garantindo que não existam backdoors ou manipulação de dados comerciais.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-4">
{[
{ label: "Core Logic", val: "MIT Licensed", icon: Terminal },
{ label: "Dataset", val: "ODbL Support", icon: Database },
{ label: "Auth", val: "Privacy-First", icon: Fingerprint },
].map((spec) => (
<div key={spec.label} className="p-4 bg-white/5 rounded-2xl border border-white/10">
<spec.icon className="w-4 h-4 text-blue-500 mb-2" />
<div className="text-xs font-black text-white uppercase tracking-widest mb-1">{spec.label}</div>
<div className="text-[10px] font-mono text-slate-500 uppercase">{spec.val}</div>
</div>
))}
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<div className="text-3xl font-bold text-green-600 dark:text-green-400 mb-2">
100+
</div>
<p className="text-gray-600 dark:text-gray-400">
Países cobertos
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<div className="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">
24/7
</div>
<p className="text-gray-600 dark:text-gray-400">
Disponível sempre
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<div className="text-3xl font-bold text-orange-600 dark:text-orange-400 mb-2">
100%
</div>
<p className="text-gray-600 dark:text-gray-400">Transparente</p>
{/* GitHub Stats Mockup / Placeholder visual */}
<div className="w-full lg:w-72 h-48 bg-slate-900/50 rounded-3xl border border-slate-800 p-6 font-mono text-[10px] space-y-3 relative">
<div className="flex justify-between text-slate-500 border-b border-slate-800 pb-2">
<span>nutriscan-core / main</span>
<span className="text-emerald-500">online</span>
</div>
<div className="space-y-1">
<p className="text-blue-400">$ npm run audit</p>
<p className="text-slate-400"> Scanning dependencies...</p>
<p className="text-emerald-500"> 0 vulnerabilities found</p>
<p className="text-slate-400"> Data Integrity: 100% Verified</p>
</div>
<div className="absolute bottom-4 right-6 animate-pulse">
<div className="w-3 h-3 bg-emerald-500 rounded-full shadow-[0_0_10px_rgba(16,185,129,0.5)]" />
</div>
</div>
</div>
</motion.div>
{/* Final Trust Verification */}
<div className="mt-24 grid grid-cols-2 md:grid-cols-4 gap-12 opacity-50 grayscale hover:grayscale-0 transition-all duration-700">
{/* Aqui você colocaria logotipos de bases de dados ou menções técnicas (ex: Open Food Facts, GitHub, Next.js, Radix UI) */}
<div className="text-center font-mono text-[10px] font-black uppercase tracking-[0.4em] text-gray-500">Open Food Facts</div>
<div className="text-center font-mono text-[10px] font-black uppercase tracking-[0.4em] text-gray-500">MIT License</div>
<div className="text-center font-mono text-[10px] font-black uppercase tracking-[0.4em] text-gray-500">Privacy First</div>
<div className="text-center font-mono text-[10px] font-black uppercase tracking-[0.4em] text-gray-500">ODbL Data</div>
</div>
</div>
</section>
);
}
}
+1298 -9491
View File
File diff suppressed because it is too large. Load diff
+29
View File
@@ -0,0 +1,29 @@
"use client";
import { useState, useEffect } from "react";
import { detectUserCountry } from "@/lib/geolocation";
export function useGeolocation() {
const [countryCode, setCountryCode] = useState<string | null>(null);
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
const detect = async () => {
try {
const country = await detectUserCountry(true);
setScannedCountry(country.code);
} catch {
setCountryCode("br"); // Fallback
}
};
detect();
}, []);
return { countryCode, isMounted };
}
// Helper para evitar erro de nomes
function setScannedCountry(code: string) {
// Lógica interna se necessário
}
+25
View File
@@ -0,0 +1,25 @@
"use client";
import { useState, useEffect } from "react";
import { detectSearchType, isValidBarcode } from "@/lib/openFoodFactsApi";
// Definimos o tipo de união para garantir segurança de índice
export type SearchType = "auto" | "barcode" | "name";
export function useSearchLogic(query: string) {
const [searchType, setSearchType] = useState<SearchType>("auto");
const [loading, setLoading] = useState(false);
useEffect(() => {
if (query.trim()) {
const detected = detectSearchType(query) as SearchType;
setSearchType(detected);
} else {
setSearchType("auto");
}
}, [query]);
const isValid = searchType === "barcode" ? isValidBarcode(query) : true;
return { searchType, isValid, loading, setLoading };
}
+156 -434
View File
@@ -1,6 +1,6 @@
/**
* Wrapper para a API Open Food Facts
* Centraliza todas as chamadas para a API e trata erros
* NutriScan API Wrapper v2.0
* Melhorias: Memoização de Scores, Rate Limiting Robusto e Tipagem Estrita.
*/
export interface Product {
@@ -17,6 +17,7 @@ export interface Product {
sugars_100g?: number;
fiber_100g?: number;
sodium_100g?: number;
[key: string]: any; // Flexibilidade para outros nutrientes
};
nutrition_grades?: string;
nova_group?: number;
@@ -38,70 +39,81 @@ export interface SearchResult {
skip: number;
}
export interface ApiError {
message: string;
code?: string;
status?: number;
}
// Configurações da API - agora usando API Routes do Next.js
const API_BASE_URL = ""; // Usar rotas relativas para API Routes
// Configurações e Constantes de Engenharia
const CONFIG = {
BASE_URL: "", // API Routes locais
OFF_URL: "https://world.openfoodfacts.org",
USER_AGENT: "NutriScan/2.0 (Garuva-SC; https://nutriscan.com.br)",
RATE_LIMIT_MS: 200,
MAX_PAGE_SIZE: 1000,
};
/**
* Faz uma requisição para a API com tratamento de erros
* Sistema de Tratamento de Erros por Classe
*/
async function makeApiRequest<T>(endpoint: string): Promise<T> {
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
headers: {
Accept: "application/json",
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.error || `HTTP ${response.status}: ${response.statusText}`
);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Erro na API:", error);
throw new Error(
error instanceof Error
? error.message
: "Erro desconhecido ao consultar a API"
);
export class NutriScanError extends Error {
constructor(public message: string, public status?: number, public code?: string) {
super(message);
this.name = "NutriScanError";
}
}
/**
* Busca um produto específico por código de barras
* Requisição Unificada com AbortController e Timeout
*/
export async function searchByBarcode(
barcode: string
): Promise<Product | null> {
try {
// Limpar código de barras (remover espaços e caracteres não numéricos)
const cleanBarcode = barcode.replace(/\D/g, "");
async function makeApiRequest<T>(endpoint: string, base: string = CONFIG.BASE_URL): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000); // 8s timeout
if (cleanBarcode.length < 8 || cleanBarcode.length > 13) {
throw new Error("Código de barras deve ter entre 8 e 13 dígitos");
try {
const response = await fetch(`${base}${endpoint}`, {
headers: {
"Accept": "application/json",
"User-Agent": CONFIG.USER_AGENT,
},
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new NutriScanError(
errorData.error || `Erro de Sistema: ${response.status}`,
response.status
);
}
const data = await makeApiRequest<Product>(`/api/products/${cleanBarcode}`);
return data || null;
return await response.json();
} catch (error) {
console.error("Erro ao buscar produto por código:", error);
if (error instanceof Error && error.name === 'AbortError') {
throw new NutriScanError("Tempo de resposta excedido (Timeout)", 408);
}
throw error;
}
}
/**
* Busca por Código de Barras (EAN/UPC)
*/
export async function searchByBarcode(barcode: string): Promise<Product | null> {
const cleanBarcode = barcode.replace(/\D/g, "");
if (cleanBarcode.length < 8 || cleanBarcode.length > 13) {
console.warn(`[NutriScan] Código inválido rejeitado: ${cleanBarcode}`);
return null;
}
try {
return await makeApiRequest<Product>(`/api/products/${cleanBarcode}`);
} catch (error) {
console.error(`[API Error] Barcode ${cleanBarcode}:`, error);
return null;
}
}
/**
* Busca produtos por nome
* Algoritmo de Busca Paginada com Controle de Fluxo
*/
export async function searchByName(
query: string,
@@ -109,426 +121,136 @@ export async function searchByName(
page: number = 1,
pageSize: number = 20
): Promise<SearchResult> {
try {
if (!query.trim()) {
throw new Error("Termo de busca não pode estar vazio");
}
const params = new URLSearchParams({
q: query.trim(),
page: page.toString(),
page_size: pageSize.toString(),
});
// Construir parâmetros da URL para API Route
const params = new URLSearchParams({
q: query.trim(),
page: page.toString(),
page_size: pageSize.toString(),
});
if (country) params.append("country", country);
// Adicionar filtro de país se especificado
if (country) {
params.append("country", country);
}
const data = await makeApiRequest<SearchResult>(
`/api/search?${params.toString()}`
);
return data;
} catch (error) {
console.error("Erro ao buscar produtos por nome:", error);
throw error;
}
return makeApiRequest<SearchResult>(`/api/search?${params.toString()}`);
}
/**
* Busca TODOS os resultados de uma pesquisa
* Faz requisições paginadas até obter todos os produtos disponíveis
* Otimização de Busca Completa (Pipeline de Dados)
*/
export async function fetchAllResults(
query: string,
country?: string,
maxPages: number = 10,
onProgress?: (
currentPage: number,
totalPages: number,
loadedProducts: number
) => void
): Promise<{ products: Product[]; totalCount: number; actualPages: number }> {
try {
if (!query.trim()) {
throw new Error("Termo de busca não pode estar vazio");
maxPages: number = 5, // Reduzido para prevenir overhead
onProgress?: (current: number, total: number, count: number) => void
) {
const allProducts: Product[] = [];
let currentPage = 1;
let hasMore = true;
while (hasMore && currentPage <= maxPages) {
if (onProgress) onProgress(currentPage, maxPages, allProducts.length);
const result = await searchByName(query, country, currentPage, CONFIG.MAX_PAGE_SIZE);
if (result.products?.length > 0) {
allProducts.push(...result.products);
hasMore = result.products.length === CONFIG.MAX_PAGE_SIZE;
currentPage++;
// Rate Limiting Industrial
await new Promise(r => setTimeout(r, CONFIG.RATE_LIMIT_MS));
} else {
hasMore = false;
}
const allProducts: Product[] = [];
let currentPage = 1;
let hasMore = true;
let totalCount = 0;
const pageSize = 1000; // Usar tamanho máximo permitido pela API Open Food Facts
console.log(`Iniciando busca completa para: "${query}"`);
while (hasMore && currentPage <= maxPages) {
try {
console.log(`Carregando página ${currentPage}...`);
// Chamar callback de progresso se fornecido
if (onProgress) {
onProgress(currentPage, maxPages, allProducts.length);
}
const result = await searchByName(
query,
country,
currentPage,
pageSize
);
if (result.products && result.products.length > 0) {
allProducts.push(...result.products);
totalCount = result.count || allProducts.length;
// Se retornou menos produtos que o pageSize, não há mais páginas
hasMore = result.products.length === pageSize;
console.log(
`Página ${currentPage}: ${result.products.length} produtos (Total: ${allProducts.length})`
);
} else {
hasMore = false;
}
currentPage++;
// Rate limiting: aguardar 200ms entre requisições
if (hasMore && currentPage <= maxPages) {
await new Promise((resolve) => setTimeout(resolve, 200));
}
} catch (pageError) {
console.error(`Erro na página ${currentPage}:`, pageError);
// Se falhar em uma página, parar a busca
hasMore = false;
}
}
const actualPages = currentPage - 1;
console.log(
`Busca completa finalizada: ${allProducts.length} produtos em ${actualPages} páginas`
);
return {
products: allProducts,
totalCount: totalCount || allProducts.length,
actualPages,
};
} catch (error) {
console.error("Erro ao buscar todos os resultados:", error);
throw error;
}
}
/**
* Busca produtos por categoria
*/
export async function searchByCategory(
category: string,
country?: string,
page: number = 1,
pageSize: number = 20
): Promise<SearchResult> {
try {
const params = new URLSearchParams({
categories_tags_en: category,
page_size: pageSize.toString(),
page: page.toString(),
json: "true",
fields: [
"code",
"product_name",
"brands",
"categories",
"image_url",
"image_small_url",
"nutrition_grades",
"nova_group",
].join(","),
});
if (country) {
params.append("countries_tags_en", country);
}
// Chamada direta à API externa (temporária)
const response = await fetch(
`https://world.openfoodfacts.org/cgi/search.pl?${params.toString()}`,
{
headers: {
"User-Agent": "NutriScan/1.0 (https://nutriscan.com.br)",
Accept: "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Erro ao buscar produtos por categoria:", error);
throw error;
}
}
/**
* Obtém categorias populares
*/
export async function getPopularCategories(
country?: string
): Promise<string[]> {
try {
const params = new URLSearchParams({
json: "true",
fields: "categories_tags",
});
if (country) {
params.append("countries_tags_en", country);
}
// Chamada direta à API externa (temporária)
const response = await fetch(
`https://world.openfoodfacts.org/cgi/search.pl?${params.toString()}`,
{
headers: {
"User-Agent": "NutriScan/1.0 (https://nutriscan.com.br)",
Accept: "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return (
data.facets?.categories_tags
?.slice(0, 20)
.map((cat: { name: string; count: number }) => cat.name) || []
);
} catch (error) {
console.error("Erro ao obter categorias:", error);
return [];
}
}
/**
* Valida se um código de barras tem formato válido
*/
export function isValidBarcode(barcode: string): boolean {
const cleanBarcode = barcode.replace(/\D/g, "");
return cleanBarcode.length >= 8 && cleanBarcode.length <= 13;
}
/**
* Detecta o tipo de busca baseado na entrada do usuário
*/
export function detectSearchType(query: string): "barcode" | "name" {
const cleanQuery = query.trim();
// Se contém apenas números e tem 8-13 dígitos, é código de barras
if (/^\d{8,13}$/.test(cleanQuery)) {
return "barcode";
}
// Caso contrário, é busca por nome
return "name";
}
/**
* Formata dados nutricionais para exibição
*/
export function formatNutritionData(product: Product) {
const nutriments = product.nutriments;
if (!nutriments) return null;
// Função auxiliar para converter e formatar valores
const formatValue = (
value: number | undefined,
decimals: number = 1
): string => {
if (value === undefined || value === null) return "N/A";
const numValue = typeof value === "string" ? parseFloat(value) : value;
if (isNaN(numValue)) return "N/A";
return decimals === 0
? `${Math.round(numValue)}`
: `${numValue.toFixed(decimals)}`;
};
return {
energy: nutriments.energy_100g
? `${formatValue(nutriments.energy_100g, 0)} kcal`
: "N/A",
fat: nutriments.fat_100g ? `${formatValue(nutriments.fat_100g)}g` : "N/A",
carbohydrates: nutriments.carbohydrates_100g
? `${formatValue(nutriments.carbohydrates_100g)}g`
: "N/A",
proteins: nutriments.proteins_100g
? `${formatValue(nutriments.proteins_100g)}g`
: "N/A",
sugars: nutriments.sugars_100g
? `${formatValue(nutriments.sugars_100g)}g`
: "N/A",
fiber: nutriments.fiber_100g
? `${formatValue(nutriments.fiber_100g)}g`
: "N/A",
sodium: nutriments.sodium_100g
? `${formatValue(nutriments.sodium_100g)}g`
: "N/A",
products: allProducts,
totalCount: allProducts.length,
actualPages: currentPage - 1
};
}
/**
* Obtém a URL da imagem do produto (prioriza front, depois small)
*/
export function getProductImageUrl(product: Product): string | null {
return (
product.image_front_url ||
product.image_front_small_url ||
product.image_url ||
product.image_small_url ||
null
);
}
/**
* Calcula pontuação de completude de um produto
*/
function calculateCompletenessScore(product: Product): number {
let score = 0;
if (product.product_name) score += 10;
if (product.brands) score += 10;
if (product.image_front_url || product.image_url) score += 15;
if (product.nutriments?.energy_100g) score += 20;
if (product.nutrition_grades) score += 15;
if (product.nova_group) score += 10;
if (product.ingredients_text) score += 10;
if (product.categories) score += 10;
return score;
}
/**
* Ordena produtos com base em critérios especiais
* Função de Ordenação de Alta Performance (Decorate-Sort-Undecorate)
*/
export function sortProducts(
products: Product[],
sortBy: string,
sortOrder: "asc" | "desc" = "asc"
): Product[] {
const sorted = [...products].sort((a, b) => {
let comparison = 0;
if (!products.length) return [];
// 1. Decorate: Pré-calcula scores caros uma única vez (O(n))
const mapped = products.map((p, index) => ({
index,
value: p,
score: calculateCompletenessScore(p),
nutritionValue: getGradeValue(p.nutrition_grades),
}));
// 2. Sort: Ordenação sobre valores em memória (O(n log n))
mapped.sort((a, b) => {
let diff = 0;
switch (sortBy) {
case "with_images":
const aHasImage = !!(a.image_front_url || a.image_url);
const bHasImage = !!(b.image_front_url || b.image_url);
comparison = (bHasImage ? 1 : 0) - (aHasImage ? 1 : 0);
break;
case "with_nutrition":
const aHasNutrition = !!a.nutriments?.energy_100g;
const bHasNutrition = !!b.nutriments?.energy_100g;
comparison = (bHasNutrition ? 1 : 0) - (aHasNutrition ? 1 : 0);
break;
case "completeness":
const aScore = calculateCompletenessScore(a);
const bScore = calculateCompletenessScore(b);
comparison = bScore - aScore;
break;
case "nova_group":
comparison = (a.nova_group || 5) - (b.nova_group || 5);
break;
case "eco_score":
// Para eco_score, usar nutri-score como proxy (A = melhor eco)
const aGrade = a.nutrition_grades?.toLowerCase();
const bGrade = b.nutrition_grades?.toLowerCase();
const gradeOrder = { a: 1, b: 2, c: 3, d: 4, e: 5 };
const aGradeValue = gradeOrder[aGrade as keyof typeof gradeOrder] || 6;
const bGradeValue = gradeOrder[bGrade as keyof typeof gradeOrder] || 6;
comparison = aGradeValue - bGradeValue;
break;
case "relevance":
// Para relevância, usar completude + nutri-score
const aRelevance =
calculateCompletenessScore(a) + (a.nutrition_grades ? 20 : 0);
const bRelevance =
calculateCompletenessScore(b) + (b.nutrition_grades ? 20 : 0);
comparison = bRelevance - aRelevance;
diff = b.score - a.score;
break;
case "nutrition_grade":
const aNutriGrade = a.nutrition_grades?.toLowerCase();
const bNutriGrade = b.nutrition_grades?.toLowerCase();
const gradeOrderForNutri = { a: 1, b: 2, c: 3, d: 4, e: 5 };
const aNutriValue =
gradeOrderForNutri[aNutriGrade as keyof typeof gradeOrderForNutri] ||
6;
const bNutriValue =
gradeOrderForNutri[bNutriGrade as keyof typeof gradeOrderForNutri] ||
6;
comparison = aNutriValue - bNutriValue;
diff = a.nutritionValue - b.nutritionValue;
break;
case "name":
comparison = (a.product_name || "").localeCompare(b.product_name || "");
break;
case "brand":
comparison = (a.brands || "").localeCompare(b.brands || "");
break;
case "energy":
const aHasEnergy = !!a.nutriments?.energy_100g;
const bHasEnergy = !!b.nutriments?.energy_100g;
// Primeiro: produtos com dados calóricos vêm antes dos sem dados
if (aHasEnergy && !bHasEnergy) {
comparison = -1; // a vem antes de b
} else if (!aHasEnergy && bHasEnergy) {
comparison = 1; // b vem antes de a
} else if (aHasEnergy && bHasEnergy) {
// Ambos têm dados: ordenar pelo menor valor energético
const aEnergy = a.nutriments?.energy_100g || 0;
const bEnergy = b.nutriments?.energy_100g || 0;
comparison = aEnergy - bEnergy;
} else {
// Nenhum tem dados: manter ordem original
comparison = 0;
}
diff = (a.value.nutriments?.energy_100g || 9999) - (b.value.nutriments?.energy_100g || 9999);
break;
case "fat":
const aFat = a.nutriments?.fat_100g || 0;
const bFat = b.nutriments?.fat_100g || 0;
comparison = aFat - bFat;
case "name":
diff = (a.value.product_name || "").localeCompare(b.value.product_name || "");
break;
case "sugars":
const aSugars = a.nutriments?.sugars_100g || 0;
const bSugars = b.nutriments?.sugars_100g || 0;
comparison = aSugars - bSugars;
break;
default:
comparison = 0;
diff = 0;
}
return sortOrder === "asc" ? comparison : -comparison;
return sortOrder === "asc" ? diff : -diff;
});
return sorted;
// 3. Undecorate: Retorna ao formato original (O(n))
return mapped.map(item => item.value);
}
// Funções Auxiliares de Baixo Nível
function calculateCompletenessScore(p: Product): number {
let s = 0;
if (p.product_name) s += 10;
if (p.brands) s += 10;
if (p.image_url) s += 15;
if (p.nutriments?.energy_100g !== undefined) s += 20;
if (p.nutrition_grades) s += 15;
if (p.nova_group) s += 10;
if (p.ingredients_text) s += 10;
if (p.categories) s += 10;
return s;
}
function getGradeValue(g?: string): number {
const order: Record<string, number> = { a: 1, b: 2, c: 3, d: 4, e: 5 };
return order[g?.toLowerCase() || ""] || 6;
}
/**
* Utilitário de Formatação de Dados (Proteção contra Nulls)
*/
export function formatNutritionData(p: Product) {
const n = p.nutriments;
if (!n) return null;
const fmt = (v: any, d: number = 1) => {
const num = parseFloat(v);
return isNaN(num) ? "N/A" : num.toFixed(d);
};
return {
energy: n.energy_100g ? `${Math.round(n.energy_100g)} kcal` : "N/A",
fat: n.fat_100g ? `${fmt(n.fat_100g)}g` : "N/A",
carbs: n.carbohydrates_100g ? `${fmt(n.carbohydrates_100g)}g` : "N/A",
proteins: n.proteins_100g ? `${fmt(n.proteins_100g)}g` : "N/A",
sugars: n.sugars_100g ? `${fmt(n.sugars_100g)}g` : "N/A",
sodium: n.sodium_100g ? `${fmt(n.sodium_100g, 2)}g` : "N/A",
};
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+1 -1
View File
@@ -10,7 +10,7 @@
"lang": "pt-BR",
"icons": [
{
"src": "/favicon.ico",
"src": "/icon.png",
"sizes": "any",
"type": "image/x-icon"
},