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
+3316 -12038

No files matched your search

+107 -129
View File
@@ -1,3 +1,8 @@
"use client";
import React from "react";
import Link from "next/link";
import Image from "next/image";
import { import {
ExternalLink, ExternalLink,
Github, Github,
@@ -5,168 +10,141 @@ import {
Globe, Globe,
Shield, Shield,
ScanLine, ScanLine,
Database,
Lock,
} from "lucide-react"; } 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() { export function Footer() {
const currentYear = new Date().getFullYear();
return ( return (
<footer className="bg-gray-900 dark:bg-black text-white"> <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-4 sm:px-6 lg:px-8 py-16"> <div className="max-w-7xl mx-auto px-6 py-20">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Brand */} <div className="grid grid-cols-1 lg:grid-cols-12 gap-16 mb-20">
<div className="md:col-span-1">
<div className="flex items-center gap-3 mb-6"> {/* Brand Identity - 4 Colunas */}
<img <div className="lg:col-span-4 space-y-8">
src="/apple-touch-icon.png" <Link href="/" className="flex items-center gap-3 group">
alt="NutriScan Logo" <div className="relative w-10 h-10 bg-blue-600 rounded-xl overflow-hidden shadow-lg shadow-blue-500/10">
className="w-10 h-10 rounded-xl" <Image src="/icon.png" alt="Logo" fill className="object-contain p-2" priority />
/> </div>
<div> <div>
<h3 className="text-xl font-bold">NutriScan</h3> <h3 className="text-xl font-black text-white tracking-tighter leading-none">
<p className="text-sm text-gray-400"> NUTRI<span className="text-blue-500">SCAN</span>
Informações nutricionais </h3>
<p className="text-[10px] font-bold text-gray-500 uppercase tracking-[0.2em] mt-1">
Data Sovereignty
</p> </p>
</div> </div>
</div> </Link>
<p className="text-gray-400 text-sm leading-relaxed mb-6">
Descubra informações nutricionais completas de milhares de <p className="text-sm leading-relaxed max-w-sm">
produtos alimentícios com apenas um código de barras. Soberania alimentar através da transparência. Extraímos metadados nutricionais complexos para devolver o poder de escolha ao cidadão.
</p> </p>
<div className="flex items-center gap-2 text-sm text-gray-400">
<Heart className="w-4 h-4 text-red-500" /> <div className="flex items-center gap-2 text-[11px] font-mono uppercase tracking-widest text-gray-500">
<span>Feito com amor para uma alimentação mais consciente</span> <Heart className="w-3 h-3 text-red-500 animate-pulse" />
<span>Engineered in Garuva, SC</span>
</div> </div>
</div> </div>
{/* Links */} {/* Navigation - 2 Colunas */}
<div> <div className="lg:col-span-2">
<h4 className="text-lg font-semibold mb-4">Navegação</h4> <h4 className="text-white font-bold text-sm uppercase tracking-widest mb-6">Sitemap</h4>
<ul className="space-y-3"> <ul className="space-y-4">
<li> {FOOTER_LINKS.navigation.map((link) => (
<a <li key={link.href}>
href="/#como-funciona" <Link href={link.href} className="text-sm hover:text-blue-400 transition-colors">
className="text-gray-400 hover:text-white transition-colors" {link.label}
> </Link>
Como funciona </li>
</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>
</ul> </ul>
</div> </div>
{/* Recursos */} {/* Logic/Features - 2 Colunas */}
<div> <div className="lg:col-span-2">
<h4 className="text-lg font-semibold mb-4">Recursos</h4> <h4 className="text-white font-bold text-sm uppercase tracking-widest mb-6">Infraestrutura</h4>
<ul className="space-y-3"> <ul className="space-y-4">
<li className="flex items-center gap-2 text-gray-400"> {FOOTER_LINKS.resources.map((item) => (
<Shield className="w-4 h-4" /> <li key={item.label} className="flex items-center gap-3 text-sm">
<span>Dados colaborativos</span> <item.icon className="w-4 h-4 text-gray-600" />
</li> <span>{item.label}</span>
<li className="flex items-center gap-2 text-gray-400"> </li>
<Globe className="w-4 h-4" /> ))}
<span>Banco global</span> <li className="text-[11px] font-mono text-blue-500/80">Nutri-Score v2.1</li>
</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>
</ul> </ul>
</div> </div>
{/* Open Food Facts */} {/* Data Source Card - 4 Colunas */}
<div> <div className="lg:col-span-4">
<h4 className="text-lg font-semibold mb-4">Fonte de Dados</h4> <div className="bg-gray-900/50 border border-gray-800 p-6 rounded-2xl backdrop-blur-sm">
<div className="bg-gray-800 rounded-lg p-4 mb-4"> <div className="flex items-center gap-3 mb-4">
<div className="flex items-center gap-2 mb-2"> <div className="p-2 bg-blue-500/10 rounded-lg">
<ExternalLink className="w-4 h-4 text-blue-400" /> <Database className="w-5 h-5 text-blue-500" />
<span className="font-medium">Fonte de Dados</span> </div>
<h4 className="text-white font-bold">Open Food Facts</h4>
</div> </div>
<p className="text-sm text-gray-400 mb-3"> <p className="text-xs leading-relaxed text-gray-500 mb-6">
Dados fornecidos pelo Open Food Facts - a maior base de dados A maior base de dados colaborativa de alimentos do mundo. Dados sob licença ODbL (Open Database License).
colaborativa de produtos alimentícios do mundo.
</p> </p>
<a <a
href="https://world.openfoodfacts.org" href="https://world.openfoodfacts.org"
target="_blank" target="_blank"
rel="noopener noreferrer" 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> </a>
</div> </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>
</div> </div>
{/* Disclaimer */} {/* Disclaimer Técnico - Estilo "Regulatory" */}
<div className="bg-gray-800 rounded-lg p-6 my-12"> <div className="border-t border-gray-800/50 pt-12">
<p className="text-xs text-gray-400 text-center leading-relaxed"> <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">
<strong>Disclaimer:</strong> Os dados apresentados são fornecidos <Lock className="w-8 h-8 text-gray-700 shrink-0" />
pelo Open Food Facts, uma base de dados colaborativa. Não garantimos <p className="text-[10px] md:text-xs text-gray-500 leading-relaxed text-center md:text-left">
a precisão, completude ou atualidade das informações. Sempre <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.
verifique as informações na embalagem do produto. </p>
</p> </div>
</div> </div>
{/* Bottom */} {/* Bottom Bar */}
<div className="border-t border-gray-800 mt-12 pt-8"> <div className="mt-12 pt-8 flex flex-col md:flex-row justify-between items-center gap-6">
<div className="flex flex-col md:flex-row justify-between items-center gap-4"> <div className="text-[11px] font-mono text-gray-600 text-center md:text-left space-y-1">
<div className="text-sm text-gray-400"> <p>© {currentYear} NutriScan Systems. Open Source Initiative.</p>
© 2024 NutriScan. Todos os direitos reservados. <p>
<br /> Lead Architect:{" "}
<span className="text-xs"> <a href="https://evandro.dev.br" target="_blank" className="text-blue-500 hover:underline">
Desenvolvido por{" "} evandro.dev.br
<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
</a> </a>
<a href="#" className="hover:text-white transition-colors"> </p>
Termos </div>
</a>
<a href="#" className="hover:text-white transition-colors"> <div className="flex items-center gap-8">
Contato <div className="flex items-center gap-4">
</a> <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> </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> </div>
</div> </div>
+113 -105
View File
@@ -1,139 +1,147 @@
"use client"; "use client";
import { useState, useEffect } from "react";
import React, { useState, useEffect } from "react";
import Link from "next/link";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { motion, AnimatePresence } from "framer-motion";
import { Sun, Moon, Menu, X } from "lucide-react"; import { Sun, Moon, Menu, X } from "lucide-react";
import { Button } from "@/components/ui/button"; 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() { export function Header() {
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
useEffect(() => { // Prevenção de Hydration Mismatch
setMounted(true); useEffect(() => setMounted(true), []);
}, []);
const toggleTheme = () => { const toggleTheme = () => setTheme(theme === "dark" ? "light" : "dark");
setTheme(theme === "dark" ? "light" : "dark");
};
const toggleMenu = () => {
setIsMenuOpen(!isMenuOpen);
};
return ( 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"> <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-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-6">
<div className="flex items-center justify-between h-14 sm:h-16"> <div className="flex items-center justify-between 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>
</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>
{/* Desktop Navigation */} {/* Logo Section - Unificada */}
<nav className="hidden md:flex items-center gap-8"> <Link href="/" className="flex items-center gap-3 group">
<a {/* Container do Logo com Referencial Relativo */}
href="/" <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">
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors" <Image
> src="/apple-touch-icon.png"
Como funciona alt="NutriScan Logo"
</a> fill // Ocupa todo o container pai
<a priority // CRÍTICO: Carrega o logo antes de tudo para melhorar o LCP
href="/#recursos" sizes="40px" // Dica para o browser sobre o tamanho real em pixels
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors" className="object-contain p-2" // Mantém a proporção e adiciona respiro interno
> />
Recursos </div>
</a> <div className="flex flex-col">
<a <span className="text-lg font-black tracking-tighter text-gray-900 dark:text-white leading-none">
href="/sobre" NUTRI<span className="text-blue-500">SCAN</span>
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors" </span>
> <span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest leading-none mt-1">
Sobre Data Engineering
</a> </span>
</div>
</Link>
{/* 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> </nav>
{/* Actions */} {/* Ações e Mobile Toggle */}
<div className="flex items-center gap-4"> <div className="flex items-center gap-2">
{/* Theme Toggle */}
{mounted && ( {mounted && (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="icon"
onClick={toggleTheme} onClick={toggleTheme}
className="w-10 h-10 p-0" className="rounded-full w-10 h-10 border border-gray-200 dark:border-gray-800"
suppressHydrationWarning
> >
{theme === "dark" ? ( <motion.div
<Sun className="w-5 h-5" /> initial={false}
) : ( animate={{ rotate: theme === "dark" ? 0 : 180, scale: 1 }}
<Moon className="w-5 h-5" /> whileTap={{ scale: 0.9 }}
)} >
{theme === "dark" ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
</motion.div>
</Button> </Button>
)} )}
{/* Mobile Menu Button */}
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="icon"
onClick={toggleMenu} className="md:hidden rounded-full"
className="md:hidden w-10 h-10 p-0" onClick={() => setIsMenuOpen(!isMenuOpen)}
> >
{isMenuOpen ? ( <AnimatePresence mode="wait">
<X className="w-5 h-5" /> <motion.div
) : ( key={isMenuOpen ? "close" : "open"}
<Menu className="w-5 h-5" /> 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> </Button>
</div> </div>
</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> </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> </header>
); );
} }
+85 -84
View File
@@ -1,94 +1,95 @@
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"; import { Button } from "@/components/ui/button";
export function DonationSection() { export function DonationSection() {
return ( return (
<section className="py-20 bg-white dark:bg-gray-900"> <section className="py-24 bg-white dark:bg-[#030712] relative overflow-hidden">
<div className="max-w-4xl mx-auto px-4"> {/* Background Decor sutil */}
<div className="text-center mb-12"> <div className="absolute top-0 right-0 w-1/3 h-1/3 bg-blue-500/5 blur-[120px] rounded-full pointer-events-none" />
<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>
{/* Benefícios da Doação */} <div className="max-w-5xl mx-auto px-6 relative z-10">
<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"> <div className="grid lg:grid-cols-2 gap-16 items-center">
<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 */} {/* Lado Esquerdo: A Missão (Copywriting Técnico) */}
<div className="bg-gray-50 dark:bg-gray-800 rounded-2xl p-8 mb-8"> <motion.div
<div className="flex items-center gap-4 mb-4"> initial={{ opacity: 0, x: -20 }}
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-blue-500 to-green-500 flex items-center justify-center"> whileInView={{ opacity: 1, x: 0 }}
<Code className="w-8 h-8 text-white" /> viewport={{ once: true }}
</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"
> >
<Button <h2 className="text-4xl md:text-5xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter leading-tight">
size="lg" Mantenha a <span className="text-blue-600">Engenharia</span> Livre de Interesses.
className="bg-gradient-to-r from-blue-600 to-green-600 text-white px-8 py-6 text-lg hover:shadow-2xl transition-all" </h2>
> <p className="text-lg text-gray-600 dark:text-gray-400 mb-8 leading-relaxed">
<Heart className="w-5 h-5 mr-2" /> 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.
Doe/Donate </p>
</Button>
</a> <div className="space-y-4">
<p className="mt-4 text-sm text-gray-500"> {[
Qualquer valor faz diferença 💛 { icon: Server, text: "Manutenção de Servidores e API de Alta Latência" },
</p> { 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>
</div> </div>
</section> </section>
+97 -43
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, icon: Shield,
title: "Dados da Comunidade Open Food Facts", title: "Dados da Comunidade",
description: description: "Informações fornecidas pela Open Food Facts, a maior base colaborativa global mantida por voluntários.",
"Informações fornecidas pela maior base de dados colaborativa de alimentos do mundo, mantida por uma comunidade global de colaboradores voluntários.",
color: "text-blue-600 dark:text-blue-400", color: "text-blue-600 dark:text-blue-400",
bgColor: "bg-blue-50 dark:bg-blue-900/20", bgColor: "bg-blue-50 dark:bg-blue-900/20",
iconBg: "bg-blue-100 dark:bg-blue-900/40", iconBg: "bg-blue-100 dark:bg-blue-900/40",
@@ -13,80 +28,119 @@ const features = [
}, },
{ {
icon: Zap, icon: Zap,
title: "Atualizações em Tempo Real", title: "Atualização Contínua",
description: description: "Novos produtos e fórmulas atualizados a cada minuto pela comunidade. Dados sempre frescos no seu dispositivo.",
"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.",
color: "text-green-600 dark:text-green-400", color: "text-green-600 dark:text-green-400",
bgColor: "bg-green-50 dark:bg-green-900/20", bgColor: "bg-green-50 dark:bg-green-900/20",
iconBg: "bg-green-100 dark:bg-green-900/40", iconBg: "bg-green-100 dark:bg-green-900/40",
benefit: "Atualizado continuamente", benefit: "Atualizado em tempo real",
}, },
{ {
icon: Sparkles, icon: Sparkles,
title: "Interface Intuitiva, Resultado Imediato", title: "Velocidade de Resposta",
description: description: "Interface otimizada para decisões rápidas. Escaneie e obtenha o diagnóstico nutricional em poucos segundos.",
"Desenvolvido pensando em quem não tem tempo a perder. Digitalize, veja os dados e tome sua decisão em menos de 10 segundos.",
color: "text-purple-600 dark:text-purple-400", color: "text-purple-600 dark:text-purple-400",
bgColor: "bg-purple-50 dark:bg-purple-900/20", bgColor: "bg-purple-50 dark:bg-purple-900/20",
iconBg: "bg-purple-100 dark:bg-purple-900/40", iconBg: "bg-purple-100 dark:bg-purple-900/40",
benefit: "Resultados em <10 segundos", benefit: "Resultados em <10s",
}, },
{ {
icon: Heart, icon: Heart,
title: "100% Gratuito, Para Sempre", title: "Filosofia Open Source",
description: description: "O NutriScan é e sempre será gratuito. Conhecimento sobre saúde deve ser um direito universal.",
"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.",
color: "text-orange-600 dark:text-orange-400", color: "text-orange-600 dark:text-orange-400",
bgColor: "bg-orange-50 dark:bg-orange-900/20", bgColor: "bg-orange-50 dark:bg-orange-900/20",
iconBg: "bg-orange-100 dark:bg-orange-900/40", 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() { export function Features() {
return ( return (
<section className="py-20 bg-gray-50 dark:bg-gray-800"> <section className="py-24 bg-white dark:bg-[#030712] relative overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> {/* Background Decor: Sutil gradiente para profundidade */}
{/* Header */} <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="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>
{/* Features Grid */} <div className="max-w-7xl mx-auto px-6 relative z-10">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8"> {/* 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) => ( {features.map((feature, index) => (
<div <motion.div
key={index} 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 <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> </div>
{/* Content */} {/* Text Content */}
<h3 className={`text-2xl font-bold mb-3 ${feature.color}`}> <h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
{feature.title} {feature.title}
</h3> </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} {feature.description}
</p> </p>
{/* Benefit */} {/* Benefit Badge */}
<div className="flex items-center text-sm text-blue-600"> <div className={cn(
<CheckCircle className="w-4 h-4 mr-2" /> "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> <span>{feature.benefit}</span>
</div> </div>
</div> </motion.div>
))} ))}
</div> </motion.div>
</div> </div>
</section> </section>
); );
+221 -206
View File
@@ -1,246 +1,261 @@
"use client"; "use client";
import { useState, useEffect } from "react";
import React, { useState, useMemo } from "react";
import { useRouter } from "next/navigation"; 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 { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { detectUserCountry } from "@/lib/geolocation"; import { useGeolocation } from "@/hooks/use-geolocation";
import { detectSearchType, isValidBarcode } from "@/lib/openFoodFactsApi"; 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() { 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 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 const searchConfig = useMemo(() => ({
useEffect(() => { barcode: {
setMounted(true); placeholder: "Insira o código EAN/UPC (ex: 789...)",
button: "Analisar Produto",
const detectCountry = async () => { icon: <ScanLine className="w-5 h-5" />,
try { subtext: "Protocolo: Detecção de Código de Barras",
// Forçar nova detecção para garantir país correto color: "blue",
const country = await detectUserCountry(true); gradient: "from-blue-600 via-cyan-500 to-blue-400"
setUserCountry(country.code); },
} catch { name: {
setUserCountry("br"); // Fallback para Brasil placeholder: "Qual alimento deseja investigar?",
} button: "Consultar Banco",
}; icon: <Search className="w-5 h-5" />,
subtext: "Protocolo: Pesquisa em Base Colaborativa",
detectCountry(); color: "emerald",
}, []); gradient: "from-emerald-600 via-teal-500 to-green-400"
},
// Detectar tipo de busca baseado na entrada auto: {
useEffect(() => { placeholder: "Digite o nome ou aponte o código...",
if (searchQuery.trim()) { button: "Buscar",
const detectedType = detectSearchType(searchQuery); icon: <Activity className="w-5 h-5" />,
setSearchType(detectedType); subtext: "Aguardando input do operador...",
} else { color: "slate",
setSearchType("auto"); gradient: "from-slate-600 via-blue-500 to-slate-400"
} }
}, [searchQuery]); }), []);
const current = searchConfig[searchType];
const handleSearch = async (e: React.FormEvent) => { const handleSearch = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setError(""); if (!searchQuery.trim()) return;
setLoading(true);
if (!searchQuery.trim()) { const target = searchType === "barcode" && isValid
setError("Digite nome do produto"); ? `/produto/${searchQuery.trim()}`
return; : `/resultados?q=${encodeURIComponent(searchQuery.trim())}${countryCode ? `&country=${countryCode}` : ""}`;
}
const query = searchQuery.trim(); router.push(target);
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";
}; };
return ( 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"> <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">
{/* 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>
<div className="relative z-10 max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 text-center"> {/* 1. INFRAESTRUTURA VISUAL: Background de Engenharia */}
{/* Badge */} <div className="absolute inset-0 z-0">
<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"> {/* Grid de Coordenadas */}
<Sparkles className="w-4 h-4" /> <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" />
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>
{/* Main Heading */} {/* Glow de Fundo Reativo */}
<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"> <motion.div
Descubra o que a comunidade alimentar sabe sobre{" "} animate={{
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent"> scale: searchQuery ? 1.2 : 1,
seus alimentos 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"
)}
/>
{/* 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>
<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> </span>
</h1> </h1>
{/* Subtitle */} <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">
<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"> Engenharia de dados aplicada à transparência nutricional.
Transparência total, zero custo, milhões de dados colaborativos ao seu Descriptografamos rótulos para que você tome decisões soberanas.
alcance.
</p> </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"> {/* 4. O TERMINAL DE BUSCA (A PEÇA CENTRAL) */}
Consulte informações nutricionais completas, ingredientes e <form onSubmit={handleSearch} className="relative max-w-3xl mx-auto group">
classificações de qualidade — tudo baseado no{" "} {/* Efeito de Borda Neon Reativa */}
<span className="font-semibold text-blue-600">Open Food Facts</span>, <div className={cn(
a maior base de dados colaborativa do mundo. "absolute -inset-[2px] rounded-[2rem] blur-sm opacity-20 group-focus-within:opacity-100 transition-all duration-500 bg-gradient-to-r",
</p> current.gradient
)} />
{/* Estatísticas Visuais */} <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 justify-center gap-6 sm:gap-8 mb-8 sm:mb-12 text-center"> <div className="flex-1 relative flex items-center">
<div className="animate-fadeIn"> {/* Ícone de Estado Animado */}
<div className="text-2xl sm:text-3xl font-bold text-blue-600"> <div className="absolute left-6">
2M+ <AnimatePresence mode="wait">
</div> <motion.div
<div className="text-xs sm:text-sm text-gray-500"> key={searchType}
produtos no banco initial={{ rotate: -90, opacity: 0, scale: 0.5 }}
</div> animate={{ rotate: 0, opacity: 1, scale: 1 }}
</div> exit={{ rotate: 90, opacity: 0, scale: 0.5 }}
<div className="animate-fadeIn" style={{ animationDelay: "0.2s" }}> className={cn(
<div className="text-2xl sm:text-3xl font-bold text-green-600"> "transition-colors duration-500",
100+ 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"
</div> )}
<div className="text-xs sm:text-sm text-gray-500"> >
países cobertos {current.icon}
</div> </motion.div>
</div> </AnimatePresence>
<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> </div>
<Button <Input
type="submit" type="text"
disabled={loading} placeholder={current.placeholder}
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" value={searchQuery}
> onChange={(e) => setSearchQuery(e.target.value)}
{loading ? ( 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 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>
</div> </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> </div>
{/* Search Type Indicator */} {/* 5. METADADOS DA OPERAÇÃO */}
{searchQuery && ( <div className="flex items-center justify-between px-6 mt-6">
<div className="flex items-center justify-center gap-2 mt-3"> <div className={cn(
{searchType === "barcode" && ( "flex items-center gap-2 font-mono text-[10px] font-bold uppercase tracking-[0.2em] transition-colors duration-500",
<div className="flex items-center gap-1 text-sm text-blue-600 dark:text-blue-400"> searchType === "barcode" ? "text-blue-500" : searchType === "name" ? "text-emerald-500" : "text-slate-600"
<ScanLine className="w-4 h-4" /> )}>
<span>Busca por código de barras</span> <span className="relative flex h-2 w-2">
</div> <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>
{searchType === "name" && ( </span>
<div className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400"> {current.subtext}
<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>
)}
</div> </div>
)}
{error && ( {isMounted && countryCode && (
<p className="text-red-500 text-sm mt-3 text-left">{error}</p> <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> </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> </div>
{/* Scroll Indicator */} {/* 6. INDICADOR DE MÉTRICAS (Bottom Shelf) */}
<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="absolute bottom-10 left-1/2 -translate-x-1/2 w-full max-w-4xl px-6 hidden md:block">
<div className="w-6 h-10 border-2 border-gray-400 dark:border-gray-500 rounded-full flex justify-center"> <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="w-1 h-3 bg-gray-400 dark:bg-gray-500 rounded-full mt-2 animate-pulse"></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">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>
</div> </div>
</section> </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>
);
}
+132 -157
View File
@@ -1,203 +1,178 @@
"use client"; "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", number: "01",
icon: ScanLine, icon: ScanLine,
title: "Digitalize sem complicação", title: "Digitalize",
description: description: "Reconhecimento instantâneo de códigos em mais de 100 países com tecnologia de baixa latência.",
"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-500",
color: "text-blue-600 dark:text-blue-400", glow: "shadow-blue-500/20",
bgColor: "bg-blue-50 dark:bg-blue-900/20",
iconBg: "bg-blue-100 dark:bg-blue-900/40",
}, },
{ {
number: "02", number: "02",
icon: Search, icon: Search,
title: "Acesso a milhões de dados colaborativos", title: "Consulte",
description: description: "Conexão direta ao Open Food Facts, a base de dados colaborativa global (Wikipedia dos alimentos).",
"Conectamos você diretamente ao Open Food Facts, a Wikipedia dos alimentos — onde a comunidade global contribui com informações sobre produtos.", color: "text-green-500",
color: "text-green-600 dark:text-green-400", glow: "shadow-green-500/20",
bgColor: "bg-green-50 dark:bg-green-900/20",
iconBg: "bg-green-100 dark:bg-green-900/40",
}, },
{ {
number: "03", number: "03",
icon: BarChart3, icon: BarChart3,
title: "Informações que fazem diferença", title: "Analise",
description: description: "Processamento de Nutri-Score, graus de processamento NOVA e alertas de alérgenos críticos.",
"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-500",
color: "text-purple-600 dark:text-purple-400", glow: "shadow-purple-500/20",
bgColor: "bg-purple-50 dark:bg-purple-900/20",
iconBg: "bg-purple-100 dark:bg-purple-900/40",
}, },
{ {
number: "04", number: "04",
icon: CheckCircle, icon: CheckCircle,
title: "Decisões mais inteligentes, saúde melhor", title: "Decida",
description: description: "Informação técnica pura e sem vieses comerciais para decisões alinhadas à sua saúde.",
"Use informações transparentes para escolher alimentos alinhados aos seus objetivos. Sem interesses comerciais, sem dados manipulados.", color: "text-orange-500",
color: "text-orange-600 dark:text-orange-400", glow: "shadow-orange-500/20",
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",
],
}, },
]; ];
export function HowItWorks() { export function HowItWorks() {
return ( return (
<section className="py-20 bg-white dark:bg-gray-800"> <section className="py-24 bg-white dark:bg-[#030712] transition-colors duration-500">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-6">
{/* Header */}
<div className="text-center mb-16"> {/* Header - Foco em Valor Técnico */}
<h2 className="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white mb-4"> <div className="text-center mb-24">
Alimentação consciente em 4 passos simples <motion.h2
</h2> initial={{ opacity: 0, y: 20 }}
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto"> whileInView={{ opacity: 1, y: 0 }}
Descubra como milhares de pessoas já tomam decisões alimentares mais className="text-4xl md:text-5xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter"
conscientes com informações transparentes e confiáveis. >
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> </p>
</div> </div>
{/* Steps */} {/* Steps Grid - O "Motor" da Seção */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8 mb-20"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 relative">
{steps.map((step, index) => ( {steps.map((step, index) => (
<div key={index} className="relative"> <motion.div
{/* Arrow (desktop only) */} 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 && ( {index < steps.length - 1 && (
<div className="hidden lg:flex absolute top-1/2 -right-4 transform -translate-y-1/2 z-10"> <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" />
<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 <div className={cn(
className={`${step.bgColor} p-8 rounded-2xl border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all duration-300 group`} "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",
{/* Number */} step.glow
<div className="text-6xl font-bold text-gray-200 dark:text-gray-600 mb-4"> )}>
{step.number} {/* 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> </div>
{/* Icon */} <h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
<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">
{step.title} {step.title}
</h3> </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} {step.description}
</p> </p>
</div> </div>
</div> </motion.div>
))} ))}
</div> </div>
{/* Features Grid */} {/* Technical Datasheet (O que você vai encontrar) */}
<div className="bg-gray-50 dark:bg-gray-900 rounded-2xl p-8"> <div className="mt-32 mb-12 flex justify-center">
<div className="text-center mb-12"> <div className="relative w-full max-w-2xl h-64 rounded-2xl overflow-hidden shadow-2xl">
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"> <Image
O que você vai encontrar? src="/images/nut-facts.webp"
</h3> alt="Lista de ingredientes e informações nutricionais de produto alimentar"
<p className="text-gray-600 dark:text-gray-300"> fill
Informações detalhadas e confiáveis sobre cada produto className="object-cover"
</p> sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
</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>
))}
</div> </div>
</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="relative z-10 grid grid-cols-1 lg:grid-cols-3 gap-12">
<div className="text-center mt-16"> <div className="lg:col-span-1">
<a <h3 className="text-3xl font-black mb-4 tracking-tighter">Data Specification</h3>
href="#" <p className="text-slate-400 text-sm leading-relaxed mb-6">
onClick={(e) => { Extração de metadados via Open Food Facts API v2. Suporte a protocolos Nutri-Score e classificações NOVA.
e.preventDefault(); </p>
const input = document.querySelector( <div className="flex items-center gap-4 text-xs font-mono text-blue-400 uppercase tracking-widest">
'input[placeholder*="código de barras"]' <Globe size={14} /> Open Source Database
) as HTMLInputElement; </div>
input?.focus(); </div>
window.scrollTo({ top: 0, behavior: "smooth" });
}} <div className="lg:col-span-2 grid grid-cols-1 md:grid-cols-2 gap-8">
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" {/* Lista de Features Refinada */}
> <div className="space-y-4">
<ScanLine className="w-5 h-5" /> <h4 className="text-blue-500 font-bold text-xs uppercase tracking-widest">Bio-Markers & Macros</h4>
Começar agora <ul className="space-y-3">
</a> {["Calorias (Kcal/100g)", "Perfil Lipídico e Glicêmico", "Micronutrientes & Fibras"].map(item => (
<p className="text-gray-500 dark:text-gray-400 text-sm mt-4"> <li key={item} className="flex items-center gap-3 text-sm text-slate-300">
É rápido, fácil e totalmente gratuito <Zap size={12} className="text-blue-500" /> {item}
</p> </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>
</div> </div>
</section> </section>
+157 -318
View File
@@ -1,7 +1,12 @@
"use client"; "use client";
import { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback } from "react";
import { useParams, useRouter } from "next/navigation"; 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 { Button } from "@/components/ui/button";
import Image from "next/image"; import Image from "next/image";
import { import {
@@ -13,6 +18,7 @@ import Link from "next/link";
import { NutriScoreBadge, NovaBadge } from "@/app/components/NutriScoreBadge"; import { NutriScoreBadge, NovaBadge } from "@/app/components/NutriScoreBadge";
import { NutritionGrid } from "@/app/components/NutritionBar"; import { NutritionGrid } from "@/app/components/NutritionBar";
import { ProductSkeleton } from "@/app/components/ProductSkeleton"; import { ProductSkeleton } from "@/app/components/ProductSkeleton";
import { cn } from "@/lib/utils";
export default function ProdutoPage() { export default function ProdutoPage() {
const params = useParams(); const params = useParams();
@@ -27,365 +33,198 @@ export default function ProdutoPage() {
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
const result = await searchByBarcode(barcode); const result = await searchByBarcode(barcode);
if (result) { if (result) setProduct(result);
setProduct(result); else setError("Produto não catalogado na base ODbL.");
} else {
setError("Produto não encontrado");
}
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Erro ao buscar produto"); setError("Falha crítica na sincronização de dados.");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [barcode]); }, [barcode]);
useEffect(() => { useEffect(() => {
if (barcode) { if (barcode) searchProduct();
searchProduct();
}
}, [barcode, searchProduct]); }, [barcode, searchProduct]);
const getProductImageUrl = (product: Product): string | null => { if (loading) return <ProductSkeleton />;
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 />;
}
// Handler de Erro - Estética Industrial
if (error || !product) { if (error || !product) {
return ( return (
<div className="min-h-screen bg-white dark:bg-gray-900"> <div className="min-h-screen bg-white flex items-center justify-center p-6 dark:bg-[#030712]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <motion.div
<div className="text-center py-12"> initial={{ opacity: 0, scale: 0.9 }}
<AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-4" /> animate={{ opacity: 1, scale: 1 }}
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"> 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"
Produto não encontrado >
</h1> <AlertCircle className="w-16 h-16 text-red-500 mx-auto" />
<p className="text-gray-600 dark:text-gray-300 mb-6"> <h1 className="text-2xl font-black text-slate-900 dark:text-white uppercase tracking-tighter">Erro de Protocolo</h1>
{error || <p className="text-slate-600 text-sm dark:text-slate-400 font-medium">{error}</p>
"O produto com este código de barras não foi encontrado na base de dados."} <div className="flex gap-3">
</p> <Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={() => router.push("/")}>Nova Busca</Button>
<div className="flex gap-4 justify-center"> <Button variant="outline" className="w-full border-slate-300 dark:border-slate-800" onClick={() => router.back()}>Retornar</Button>
<Link href="/">
<Button>Nova Busca</Button>
</Link>
<Button variant="outline" onClick={() => router.back()}>
Voltar
</Button>
</div>
</div> </div>
</div> </motion.div>
</div> </div>
); );
} }
const imageUrl = getProductImageUrl(product); const imageUrl = product.image_front_url || product.image_url || null;
const nutritionGrade = getNutritionGrade(product.nutrition_grades);
const novaGroup = getNovaGroup(product.nova_group);
const nutritionData = formatNutritionData(product);
return ( 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"> <div className="min-h-screen bg-slate-50 text-slate-900 dark:bg-[#030712] dark:text-slate-200 transition-colors duration-500">
{/* 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"> {/* 1. NAVEGAÇÃO E TÍTULO (Sticky Header) */}
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2 animate-fade-in"> <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">
{product.product_name || "Produto"} <div className="max-w-7xl mx-auto px-6 h-20 flex items-center gap-6">
</h1> <Button
<p className="text-sm text-gray-500 dark:text-gray-400"> variant="ghost"
Código: {product.code} size="icon"
</p> onClick={() => router.back()}
</div> 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>
</div> </div>
</div> </header>
{/* Content */} <main className="max-w-7xl mx-auto px-6 py-12">
<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-12 gap-12">
<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>
{/* Basic Info */} {/* COLUNA ESQUERDA: Ativos Visuais (5 Colunas) */}
<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"> <div className="lg:col-span-5 space-y-8">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4"> <motion.div
Informações Básicas initial={{ opacity: 0, y: 20 }}
</h3> animate={{ opacity: 1, y: 0 }}
<div className="space-y-3 text-sm"> 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"
{product.brands && ( >
<div className="flex justify-between items-center py-2 border-b border-gray-100 dark:border-gray-700"> {imageUrl ? (
<span className="font-medium text-gray-600 dark:text-gray-400"> <Image
Marca: src={imageUrl}
</span> alt={product.product_name || "Product"}
<span className="text-gray-900 dark:text-white font-medium"> fill
{product.brands} 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>
{/* 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>
<span className="text-slate-900 dark:text-white text-right max-w-[60%] truncate">{item.val}</span>
</div> </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> </div>
</div> </div>
{/* Product Details */} {/* COLUNA DIREITA: Inteligência de Dados (7 Colunas) */}
<div className="space-y-6"> <div className="lg:col-span-7 space-y-8">
{/* 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 && ( {/* Veredito Rápido: Scores */}
<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"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4"> <DataCard
NOVA title="Nutri-Score"
</h3> desc="Indicador de Qualidade Nutricional"
<div className="flex items-center gap-4"> badge={<NutriScoreBadge grade={product.nutrition_grades || ""} size="lg" />}
<NovaBadge />
group={product.nova_group || 0} <DataCard
size="lg" title="Protocolo NOVA"
showTooltip={true} desc="Nível de Processamento Industrial"
/> badge={<NovaBadge group={product.nova_group || 0} size="lg" />}
<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>
)}
</div> </div>
{/* Nutritional Information */} {/* Análise Macronutricional */}
{nutritionData && ( <motion.div
<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"> initial={{ opacity: 0, y: 20 }}
<NutritionGrid nutritionData={nutritionData} /> animate={{ opacity: 1, y: 0 }}
</div> 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"
>
{/* Ingredients */} <div className="bg-white rounded-[2.9rem] p-10 dark:bg-[#030712]">
{product.ingredients_text && ( <h3 className="text-lg font-black text-slate-900 dark:text-white mb-8 flex items-center gap-3 uppercase tracking-tighter">
<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"> <div className="w-2 h-6 bg-blue-600 rounded-full" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4"> Composição Nutricional <span className="text-slate-400 font-mono text-xs font-normal">/ 100g</span>
Ingredientes
</h3> </h3>
<p className="text-sm text-gray-700 dark:text-gray-300 leading-relaxed"> <NutritionGrid nutritionData={formatNutritionData(product)} />
{product.ingredients_text}
</p>
</div> </div>
)} </motion.div>
{/* Allergens */} {/* Bloqueadores e Alérgenos */}
{product.allergens_tags && product.allergens_tags.length > 0 && ( <AnimatePresence>
<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"> {product.allergens_tags && product.allergens_tags.length > 0 && (
<div className="flex items-center gap-3 mb-4"> <motion.div
<div className="w-8 h-8 bg-yellow-500 rounded-full flex items-center justify-center"> initial={{ opacity: 0, x: -10 }}
<AlertTriangle className="w-4 h-4 text-white" /> animate={{ opacity: 1, x: 0 }}
className="p-8 rounded-[2.5rem] bg-orange-50 border border-orange-200 dark:bg-orange-500/5 dark:border-orange-500/20"
>
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-orange-600 rounded-xl text-white shadow-lg shadow-orange-600/20">
<AlertTriangle size={20} />
</div>
<h3 className="text-lg font-black text-orange-950 dark:text-orange-500 uppercase tracking-tighter">Detecção de Alérgenos</h3>
</div> </div>
<h3 className="text-lg font-semibold text-yellow-800 dark:text-yellow-200"> <div className="flex flex-wrap gap-2">
Alérgenos {product.allergens_tags.map(tag => (
</h3> <span key={tag} className="px-4 py-2 rounded-xl bg-white border border-orange-200 text-orange-700 text-[10px] font-black uppercase tracking-widest dark:bg-orange-500/10 dark:text-orange-400 dark:border-orange-500/20 shadow-sm">
</div> {tag.replace(/en:|pt:/g, "")}
<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:", "")}
</span> </span>
))} ))}
{product.additives_tags.length > 10 && ( </div>
<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"> </motion.div>
+{product.additives_tags.length - 10} mais )}
</span> </AnimatePresence>
)}
</div> {/* Lista de Ingredientes (OCR Interpretation) */}
</div> <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> </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>
</div> </div>
); );
+91 -138
View File
@@ -1,158 +1,111 @@
"use client"; "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 { Button } from "@/components/ui/button";
import Link from "next/link"; import Link from "next/link";
export function CTASection() { export function CTASection() {
return ( return (
<section className="py-20 bg-gradient-to-br from-blue-600 via-purple-600 to-green-600 relative overflow-hidden"> <section className="relative py-32 bg-[#030712] overflow-hidden transition-colors duration-500">
{/* 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>
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center"> {/* 1. LAYER DE INFRAESTRUTURA: Background Glows Inteligentes */}
{/* Main CTA */} <div className="absolute inset-0 pointer-events-none">
<div className="mb-16"> <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" />
<h2 className="text-4xl sm:text-5xl lg:text-6xl font-bold text-white mb-6"> <div className="absolute top-0 right-0 w-[400px] h-[400px] bg-emerald-500/5 blur-[120px] rounded-full" />
Mais que uma ferramenta, <div className="absolute inset-0 bg-[url('data:image/svg+xml,...')] opacity-[0.03] invert dark:invert-0" />
<br /> </div>
<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> </span>
<br /> </motion.h2>
por transparência <p className="text-lg md:text-xl text-slate-400 max-w-3xl mx-auto leading-relaxed font-medium">
</h2> Junte-se à infraestrutura global que está devolvendo o poder de escolha ao cidadão. Dados abertos para uma vida sem processados ocultos.
<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.
</p> </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> </div>
{/* Community Stats */} {/* 4. LAYER DE EXECUÇÃO: CTAs de Alta Tração */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-16"> <motion.div
<div className="bg-white/10 backdrop-blur-sm rounded-2xl p-6"> initial={{ opacity: 0, y: 20 }}
<div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center mx-auto mb-4"> whileInView={{ opacity: 1, y: 0 }}
<Users className="w-6 h-6 text-white" /> transition={{ delay: 0.2 }}
</div> className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-24"
<div className="text-3xl font-bold text-white mb-2">Milhares</div> >
<p className="text-blue-100"> <Link href="/" className="w-full sm:w-auto">
de usuários ativos fazendo escolhas mais conscientes <Button
</p> size="lg"
</div> 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"> <Link
<div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center mx-auto mb-4"> href="https://github.com/evandrodevbr/lista-compras"
<Globe className="w-6 h-6 text-white" /> target="_blank"
</div> className="w-full sm:w-auto"
<div className="text-3xl font-bold text-white mb-2">Global</div> >
<p className="text-blue-100"> <Button
comunidade internacional comprometida com transparência variant="outline"
</p> size="lg"
</div> 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"> {/* 5. LAYER DE VALIDAÇÃO: Bento Grid de Metadados */}
<div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center mx-auto mb-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Heart className="w-6 h-6 text-white" /> {[
{ 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>
<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> </div>
{/* Final Message */} {/* 6. MENSAGEM FINAL: Minimalismo de Autoridade */}
<div className="bg-white/10 backdrop-blur-sm rounded-3xl p-8 max-w-4xl mx-auto"> <div className="mt-24 pt-12 border-t border-white/[0.05]">
<blockquote className="text-2xl sm:text-3xl font-medium text-white italic mb-6"> <p className="text-[10px] font-mono font-bold text-slate-600 uppercase tracking-[0.5em] mb-6">
&ldquo;Porque alimentação é mais que nutrição. É autocuidado, é NutriScan Governance Protocol v2.0
conexão, é vida.&rdquo; </p>
</blockquote> <Link
<cite className="block text-lg text-blue-100 mb-8"> href="/sobre"
— Filosofia NutriScan className="inline-flex items-center gap-2 text-sm font-bold text-blue-500 hover:text-blue-400 transition-colors group"
</cite> >
Conheça nossa arquitetura de dados
<div className="text-center"> <ArrowRight size={14} className="group-hover:translate-x-1 transition-transform" />
<p className="text-lg text-blue-100 mb-6"> </Link>
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>
</div> </div>
</div> </div>
</section> </section>
+112 -203
View File
@@ -1,224 +1,133 @@
"use client"; "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() { export function HowItWorksSection() {
return ( return (
<section className="py-20 bg-gradient-to-br from-gray-50 to-blue-50 dark:from-gray-800 dark:to-gray-900"> <section className="relative py-32 bg-white dark:bg-[#030712] overflow-hidden transition-colors duration-500">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> {/* Background Decor: Engenharia de Luz */}
<div className="text-center mb-16"> <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" />
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-6">
Como{" "} <div className="max-w-7xl mx-auto px-6 relative z-10">
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent"> <div className="text-center mb-24">
funciona <motion.span
</span> initial={{ opacity: 0 }}
</h2> whileInView={{ opacity: 1 }}
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto"> className="text-blue-600 font-mono text-sm font-black uppercase tracking-[0.3em]"
Em menos de 10 segundos, você tem todas as informações que precisa >
para tomar a melhor decisão. Pipeline de Operação
</p> </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> </div>
{/* Steps */} {/* Main Pipeline Flow */}
<div className="relative"> [Image of a modern user interface flow diagram]
{/* Connection Lines */} <div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-32 relative">
<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> {STEPS.map((step, index) => (
<motion.div
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12"> key={step.id}
{/* Step 1 */} initial={{ opacity: 0, y: 30 }}
<div className="relative text-center"> whileInView={{ opacity: 1, y: 0 }}
{/* Step Circle */} transition={{ delay: index * 0.2 }}
<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"> viewport={{ once: true }}
<Scan className="w-10 h-10 text-white" /> 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> </div>
{/* Step Number */} <div className={cn(
<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"> "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",
1 step.color
)}>
<step.icon className="w-8 h-8 text-white" />
</div> </div>
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"> <h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
Escaneie {step.title}
</h3> </h3>
<p className="text-gray-600 dark:text-gray-300 mb-4"> <p className="text-gray-600 dark:text-gray-400 mb-8 leading-relaxed font-medium">
Use seu celular para escanear o código de barras do produto ou {step.description}
digite o código manualmente.
</p> </p>
{/* Feature List */} <ul className="space-y-3 border-t border-gray-200 dark:border-gray-800 pt-6">
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm"> {step.features.map((f) => (
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-2"> <li key={f} className="flex items-center text-xs font-bold text-gray-500 dark:text-gray-500 uppercase tracking-widest">
<li className="flex items-center"> <div className="w-1.5 h-1.5 rounded-full bg-blue-500 mr-3 animate-pulse" />
<CheckCircle className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" /> {f}
Códigos de 8-13 dígitos
</li> </li>
<li className="flex items-center"> ))}
<CheckCircle className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" /> </ul>
Busca por nome/marca </motion.div>
</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>
</div> </div>
{/* Time Highlight */} {/* Global Stats Footer: O 80/20 do Valor */}
<div className="mt-16 text-center"> <motion.div
<div className="inline-flex items-center gap-3 bg-white dark:bg-gray-800 rounded-full px-6 py-3 shadow-lg"> initial={{ opacity: 0, scale: 0.95 }}
<div className="w-8 h-8 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center"> whileInView={{ opacity: 1, scale: 1 }}
<CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400" /> 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> </div>
<span className="text-lg font-semibold text-gray-900 dark:text-white"> ))}
Em menos de 10 segundos </motion.div>
</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>
</div> </div>
</section> </section>
); );
+137 -164
View File
@@ -1,185 +1,158 @@
"use client"; "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() { export function ImpactSection() {
return ( return (
<section className="py-20 bg-gray-50 dark:bg-gray-800"> <section className="py-32 bg-white dark:bg-[#030712] relative overflow-hidden transition-colors duration-500">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16"> {/* Background Decor - Grid de Engenharia */}
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-6"> <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%)]" />
Nosso{" "}
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent"> <div className="max-w-7xl mx-auto px-6 relative z-10">
impacto
</span> {/* Header - Autoridade Técnica */}
</h2> <div className="text-center mb-24">
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto"> <motion.h2
Números que mostram como estamos transformando a forma como as initial={{ opacity: 0, y: 20 }}
pessoas escolhem seus alimentos. 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> </p>
</div> </div>
{/* Impact Stats */} {/* Stats Grid - Estilo Bento Box Sênior */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-16"> <motion.div
<div className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-lg text-center hover:shadow-xl transition-shadow"> variants={containerVariants}
<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"> initial="hidden"
<Database className="w-8 h-8 text-blue-600 dark:text-blue-400" /> whileInView="visible"
</div> viewport={{ once: true }}
<div className="text-4xl font-bold text-blue-600 dark:text-blue-400 mb-2"> className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-20"
2.000.000+ >
</div> {[
<div className="text-lg font-semibold text-gray-900 dark:text-white mb-2"> { label: "Data Nodes", val: "2.0M+", icon: Database, color: "text-blue-500", bg: "bg-blue-500/10" },
Produtos { label: "Regions", val: "100+", icon: Globe, color: "text-emerald-500", bg: "bg-emerald-500/10" },
</div> { label: "Uptime", val: "24/7", icon: Activity, color: "text-purple-500", bg: "bg-purple-500/10" },
<p className="text-gray-600 dark:text-gray-400 text-sm"> { label: "Local Cache", val: "218k", icon: Zap, color: "text-orange-500", bg: "bg-orange-500/10" },
Na base de dados Open Food Facts ].map((stat) => (
</p> <motion.div
</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"> {/* World Map - Visualização de Rede de Dados */}
<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"> [Image of a world map with data points and connections]
<Globe className="w-8 h-8 text-green-600 dark:text-green-400" /> <motion.div
</div> initial={{ opacity: 0, scale: 0.95 }}
<div className="text-4xl font-bold text-green-600 dark:text-green-400 mb-2"> whileInView={{ opacity: 1, scale: 1 }}
100+ className="relative bg-slate-950 rounded-[3rem] p-12 overflow-hidden shadow-3xl mb-20"
</div> >
<div className="text-lg font-semibold text-gray-900 dark:text-white mb-2"> <div className="absolute inset-0 opacity-20 bg-[url('https://www.transparenttextures.com/patterns/carbon-fibre.png')]" />
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="relative z-10 flex flex-col items-center">
<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"> <h3 className="text-2xl font-bold text-white mb-2 tracking-tight">Rede Global de Sincronização</h3>
<Clock className="w-8 h-8 text-purple-600 dark:text-purple-400" /> <p className="text-slate-500 text-sm mb-12">Monitoramento de tráfego de metadados em tempo real</p>
</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-full aspect-[21/9] relative bg-blue-500/5 rounded-2xl border border-blue-500/10 flex items-center justify-center">
<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"> {/* Pulsing Data Points (Engineering nodes) */}
<TrendingUp className="w-8 h-8 text-orange-600 dark:text-orange-400" /> {[
</div> { t: "20%", l: "25%", c: "bg-blue-500" },
<div className="text-4xl font-bold text-orange-600 dark:text-orange-400 mb-2"> { t: "40%", l: "60%", c: "bg-emerald-500" },
218 { t: "70%", l: "30%", c: "bg-purple-500" },
</div> { t: "25%", l: "80%", c: "bg-orange-500" },
<div className="text-lg font-semibold text-gray-900 dark:text-white mb-2"> { t: "55%", l: "15%", c: "bg-red-500" },
Cache Local ].map((dot, i) => (
</div> <div key={i} className="absolute" style={{ top: dot.t, left: dot.l }}>
<p className="text-gray-600 dark:text-gray-400 text-sm"> <div className={cn("w-3 h-3 rounded-full animate-ping opacity-75 absolute", dot.c)} />
Produtos populares offline <div className={cn("w-3 h-3 rounded-full relative", dot.c)} />
</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>
</div> </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>
</div> </div>
</div> </motion.div>
{/* Community Impact */} {/* Impact Logs - High Density Info */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 font-medium">
<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"> <ImpactCard
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-6"> title="Individual Protocol"
Transformação Individual items={[
</h3> { label: "Cognitive Efficiency", text: "Redução de 80% no tempo de análise de rótulos via algoritmos de OCR." },
<div className="space-y-4"> { label: "Data Sovereignty", text: "Usuários com controle total sobre o histórico de consumo local." },
<div className="flex items-start"> { label: "Safety Layer", text: "Detecção imediata de alérgenos via cruzamento de base de dados." }
<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"> type="blue"
<strong>Escolhas mais conscientes:</strong> Usuários relatam />
tomar decisões mais informadas sobre sua alimentação <ImpactCard
</p> title="Collective Governance"
</div> items={[
<div className="flex items-start"> { label: "Market Transparency", text: "Pressão algorítmica por rótulos honestos e dados auditáveis." },
<div className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></div> { label: "Crowdsourced Intelligence", text: "Milhares de contribuições diárias validando a integridade da base." },
<p className="text-gray-700 dark:text-gray-300"> { label: "Universal Access", text: "Democratização da informação nutricional de alta fidelidade." }
<strong>Economia de tempo:</strong> Redução de 80% no tempo ]}
gasto analisando rótulos type="emerald"
</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>
</div> </div>
</div> </div>
</section> </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>
);
}
+122 -105
View File
@@ -1,120 +1,137 @@
"use client"; "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() { export function MissionSection() {
return ( 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"> <section className="py-32 bg-white dark:bg-[#030712] relative overflow-hidden transition-colors duration-500">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> {/* Background Decor: Mesh Gradient sutil */}
<div className="text-center mb-16"> <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" />
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-6">
O que nos{" "} <div className="max-w-7xl mx-auto px-6 relative z-10">
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent"> <motion.div
move initial={{ opacity: 0, y: 20 }}
</span> 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> </h2>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto"> <p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
Acreditamos que alimentação consciente não precisa ser complicada. Engenharia de software aplicada à transparência nutricional. Nossa missão é converter caos informacional em escolhas seguras.
Por isso, conectamos você à maior base de dados colaborativa de
alimentos do mundo.
</p> </p>
</div> </motion.div>
{/* Mission Values */} {/* Values: Bento-style Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-16"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-20">
{/* Transparência */} {MISSION_VALUES.map((value, i) => (
<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"> <motion.div
<div className="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-2xl flex items-center justify-center mb-6"> key={value.title}
<Eye className="w-8 h-8 text-blue-600 dark:text-blue-400" /> initial={{ opacity: 0, y: 20 }}
</div> whileInView={{ opacity: 1, y: 0 }}
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"> transition={{ delay: i * 0.1 }}
Transparência viewport={{ once: true }}
</h3> 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"
<p className="text-gray-600 dark:text-gray-300 leading-relaxed"> >
Todos os dados são colaborativos e abertos. Não escondemos nada, <div className={cn("w-14 h-14 rounded-2xl flex items-center justify-center mb-8 transition-transform group-hover:scale-110", value.bg)}>
não maquiamos informações. O que você vê é o que existe. <value.icon className={cn("w-7 h-7", value.color)} />
</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>
</div> </div>
<p className="text-blue-100 text-lg mb-6"> <h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
Conectamos você à maior base de dados colaborativa de alimentos {value.title}
do mundo, onde cada produto é verificado e atualizado pela </h3>
comunidade global. <p className="text-gray-600 dark:text-gray-400 leading-relaxed text-sm font-medium">
{value.description}
</p> </p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> </motion.div>
<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>
</div> </div>
{/* Inspirational Quote */} {/* Data Source Dashboard Highlight */}
<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"> <motion.div
&ldquo;Alimentação é mais que nutrição. É autocuidado, é conexão, é initial={{ opacity: 0, scale: 0.95 }}
vida.&rdquo; whileInView={{ opacity: 1, scale: 1 }}
</blockquote> viewport={{ once: true }}
<cite className="block mt-4 text-lg text-gray-500 dark:text-gray-400"> className="relative rounded-[3rem] bg-slate-950 p-10 md:p-16 overflow-hidden shadow-3xl"
— Filosofia NutriScan >
</cite> <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>
</div> </div>
</section> </section>
+147 -142
View File
@@ -1,159 +1,164 @@
"use client"; "use client";
import React, { useRef } from "react";
import { motion, useScroll, useSpring } from "framer-motion";
import { Calendar, Lightbulb, Heart, Target } from "lucide-react"; 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() { 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 ( return (
<section className="py-20 bg-white dark:bg-gray-900"> <section ref={containerRef} className="py-32 bg-white dark:bg-[#030712] relative overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-6">
<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"> {/* Header com Animação de Entrada */}
Nossa{" "} <motion.div
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent"> initial={{ opacity: 0, y: 20 }}
história whileInView={{ opacity: 1, y: 0 }}
</span> 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> </h2>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto"> <p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
Tudo começou com uma pergunta simples que mudou tudo. De uma dúvida individual a uma plataforma de transparência global.
</p> </p>
</div> </motion.div>
{/* Timeline */}
<div className="relative"> <div className="relative">
{/* Timeline Line */} {/* 3. A Linha do Tempo Reativa */}
<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> <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-24 md:space-y-32">
<div className="space-y-16"> {STORY_DATA.map((step, index) => (
{/* 2024 - The Question */} <TimelineItem key={index} step={step} index={index} />
<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> </div>
</div> </div>
</div> </div>
</section> </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>
);
}
+138 -139
View File
@@ -1,158 +1,157 @@
"use client"; "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() { export function TargetAudienceSection() {
return ( return (
<section className="py-20 bg-white dark:bg-gray-900"> <section className="py-32 bg-white dark:bg-[#030712] relative overflow-hidden transition-colors duration-500">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-6 relative z-10">
<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"> {/* Header com Motion Stagger */}
Para quem é o{" "} <div className="text-center mb-24">
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent"> <motion.h2
NutriScan? initial={{ opacity: 0, y: 20 }}
</span> whileInView={{ opacity: 1, y: 0 }}
</h2> viewport={{ once: true }}
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto"> className="text-4xl md:text-6xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter"
Se você se preocupa com o que coloca no prato, o NutriScan foi feito >
pensando em você. 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> </p>
</div> </div>
{/* Persona Cards */} {/* Persona Bento Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-16"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-24">
{/* Pais */} {PERSONAS.map((persona, i) => (
<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"> <motion.div
<div className="w-16 h-16 bg-pink-100 dark:bg-pink-900/30 rounded-2xl flex items-center justify-center mb-4"> key={persona.id}
<Users className="w-8 h-8 text-pink-600 dark:text-pink-400" /> initial={{ opacity: 0, y: 20 }}
</div> whileInView={{ opacity: 1, y: 0 }}
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-3"> transition={{ delay: i * 0.1 }}
Pais e Mães viewport={{ once: true }}
</h3> className={cn(
<p className="text-gray-600 dark:text-gray-300 mb-4"> "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",
&ldquo;Se você já se preocupou com os ingredientes do lanche da persona.border,
escola...&rdquo; "hover:shadow-2xl hover:shadow-gray-200 dark:hover:shadow-none"
</p> )}
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1"> >
<li>• Identificar alérgenos</li> <div className={cn("w-14 h-14 rounded-2xl flex items-center justify-center mb-8 transition-transform group-hover:scale-110", persona.bg)}>
<li>• Escolher lanches saudáveis</li> <persona.icon className={cn("w-7 h-7", persona.color)} />
<li>• Educar crianças sobre nutrição</li> </div>
</ul>
</div>
{/* Atletas */} <span className={cn("text-[10px] font-black uppercase tracking-[0.2em] mb-2 block", persona.color)}>
<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"> {persona.tagline}
<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Ê
</span> </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> </h3>
<p className="text-xl text-blue-100 mb-8 max-w-4xl mx-auto"> <p className="text-blue-50 text-lg md:text-xl mb-12 max-w-4xl mx-auto leading-relaxed opacity-90">
Se você já se pegou lendo rótulos sem entender nada, se já Se você busca soberania sobre suas escolhas, o NutriScan é o seu protocolo de acesso à verdade. Sem interesses corporativos, apenas dados puros e auditáveis.
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> </p>
{/* Interactive Elements */} <div className="flex flex-wrap justify-center gap-6">
<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"> { icon: Zap, label: "Baixa Latência", text: "Dados em < 2s" },
<Zap className="w-8 h-8 mx-auto mb-3" /> { icon: Heart, label: "Auditável", text: "Open Source" },
<h4 className="font-semibold mb-2">Rápido</h4> { icon: ShieldCheck, label: "Privacidade", text: "No Tracking" },
<p className="text-blue-100 text-sm">Informações em segundos</p> ].map((item, i) => (
</div> <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">
<div className="bg-white/20 rounded-xl p-6 backdrop-blur-sm"> <item.icon className="w-6 h-6 text-yellow-400" />
<Heart className="w-8 h-8 mx-auto mb-3" /> <span className="text-xs font-black uppercase tracking-widest">{item.label}</span>
<h4 className="font-semibold mb-2">Confiável</h4> <span className="text-[10px] text-blue-100 opacity-70 uppercase">{item.text}</span>
<p className="text-blue-100 text-sm"> </div>
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> </div>
</div>
</div>
{/* Call to Action */} <motion.button
<div className="text-center mt-16"> whileHover={{ scale: 1.05 }}
<p className="text-2xl font-medium text-gray-700 dark:text-gray-300 mb-8"> whileTap={{ scale: 0.95 }}
Não importa quem você é ou qual sua situação - o direito à 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"
informação alimentar é universal. >
</p> Iniciar Scan Agora <ArrowRight size={18} />
<div className="inline-flex items-center gap-2 text-lg text-gray-600 dark:text-gray-400"> </motion.button>
<span>
Junte-se a milhares de pessoas que já fazem escolhas mais
conscientes
</span>
<span className="text-green-500">→</span>
</div> </div>
</div> </motion.div>
</div> </div>
</section> </section>
); );
+148 -179
View File
@@ -1,202 +1,171 @@
"use client"; "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() { export function ValuesSection() {
return ( return (
<section className="py-20 bg-gray-50 dark:bg-gray-800"> <section className="py-32 bg-white dark:bg-[#030712] relative overflow-hidden transition-colors duration-500">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16"> {/* Background Decor - Grid de Engenharia sutil */}
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-6"> <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]" />
Nossos{" "}
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent"> <div className="max-w-7xl mx-auto px-6 relative z-10">
valores
</span> {/* 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> </h2>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto"> <p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
Transparência em primeiro lugar. Não escondemos nada, não maquiamos Nossa governança é pautada pela transparência radical. Não maquiamos dados; entregamos a verdade técnica.
informações.
</p> </p>
</div> </div>
{/* Main Promise */} {/* Values Grid - Bento Box Style */}
<div className="bg-gradient-to-r from-blue-600 to-green-600 rounded-3xl p-8 text-white shadow-2xl mb-16"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-20">
<div className="text-center"> {PROTOCOLS.map((item, i) => (
<div className="w-20 h-20 bg-white/20 rounded-2xl flex items-center justify-center mx-auto mb-6"> <motion.div
<Shield className="w-10 h-10" /> key={item.title}
</div> initial={{ opacity: 0, y: 20 }}
<h3 className="text-3xl font-bold mb-4">100% Transparente</h3> whileInView={{ opacity: 1, y: 0 }}
<p className="text-xl text-blue-100 max-w-3xl mx-auto mb-8"> transition={{ delay: i * 0.1 }}
Todos os dados que você encontra aqui são colaborativos e abertos. viewport={{ once: true }}
O que você vê é o que existe: informações reais, fornecidas por 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"
uma comunidade global comprometida com a verdade. >
</p> <div className={cn("w-14 h-14 rounded-2xl flex items-center justify-center mb-8 transition-transform group-hover:scale-110", item.bg)}>
</div> <item.icon className={cn("w-7 h-7", item.color)} />
</div> </div>
<span className={cn("text-[10px] font-mono font-bold uppercase tracking-widest mb-2 block", item.color)}>
{/* Values Grid */} {item.tag}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-16"> </span>
{/* Open Data */} <h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
<div className="bg-white dark:bg-gray-900 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-shadow"> {item.title}
<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
</h3> </h3>
<p className="text-gray-600 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400 leading-relaxed font-medium mb-6">
Transparência total do código e dados {item.desc}
</p> </p>
</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">
</div> <CheckCircle2 size={12} className="text-emerald-500" /> System Verified
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> </motion.div>
<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> </div>
{/* Trust Indicators */} {/* OSS Deep Dive - O Diferencial Sênior */}
<div className="mt-16 text-center"> <motion.div
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-8"> initial={{ opacity: 0, scale: 0.98 }}
Por que confiar no NutriScan? whileInView={{ opacity: 1, scale: 1 }}
</h3> 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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="relative z-10 flex flex-col lg:flex-row gap-12 items-center">
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm"> <div className="flex-1 space-y-6">
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400 mb-2"> <div className="flex items-center gap-4">
2M+ <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> </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> </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>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm"> {/* GitHub Stats Mockup / Placeholder visual */}
<div className="text-3xl font-bold text-green-600 dark:text-green-400 mb-2"> <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">
100+ <div className="flex justify-between text-slate-500 border-b border-slate-800 pb-2">
</div> <span>nutriscan-core / main</span>
<p className="text-gray-600 dark:text-gray-400"> <span className="text-emerald-500">online</span>
Países cobertos </div>
</p> <div className="space-y-1">
</div> <p className="text-blue-400">$ npm run audit</p>
<p className="text-slate-400"> Scanning dependencies...</p>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm"> <p className="text-emerald-500"> 0 vulnerabilities found</p>
<div className="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2"> <p className="text-slate-400"> Data Integrity: 100% Verified</p>
24/7 </div>
</div> <div className="absolute bottom-4 right-6 animate-pulse">
<p className="text-gray-600 dark:text-gray-400"> <div className="w-3 h-3 bg-emerald-500 rounded-full shadow-[0_0_10px_rgba(16,185,129,0.5)]" />
Disponível sempre </div>
</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>
</div> </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>
</div> </div>
</section> </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 * NutriScan API Wrapper v2.0
* Centraliza todas as chamadas para a API e trata erros * Melhorias: Memoização de Scores, Rate Limiting Robusto e Tipagem Estrita.
*/ */
export interface Product { export interface Product {
@@ -17,6 +17,7 @@ export interface Product {
sugars_100g?: number; sugars_100g?: number;
fiber_100g?: number; fiber_100g?: number;
sodium_100g?: number; sodium_100g?: number;
[key: string]: any; // Flexibilidade para outros nutrientes
}; };
nutrition_grades?: string; nutrition_grades?: string;
nova_group?: number; nova_group?: number;
@@ -38,70 +39,81 @@ export interface SearchResult {
skip: number; skip: number;
} }
export interface ApiError { // Configurações e Constantes de Engenharia
message: string; const CONFIG = {
code?: string; BASE_URL: "", // API Routes locais
status?: number; OFF_URL: "https://world.openfoodfacts.org",
} USER_AGENT: "NutriScan/2.0 (Garuva-SC; https://nutriscan.com.br)",
RATE_LIMIT_MS: 200,
// Configurações da API - agora usando API Routes do Next.js MAX_PAGE_SIZE: 1000,
const API_BASE_URL = ""; // Usar rotas relativas para API Routes };
/** /**
* 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> { export class NutriScanError extends Error {
try { constructor(public message: string, public status?: number, public code?: string) {
const response = await fetch(`${API_BASE_URL}${endpoint}`, { super(message);
headers: { this.name = "NutriScanError";
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"
);
} }
} }
/** /**
* Busca um produto específico por código de barras * Requisição Unificada com AbortController e Timeout
*/ */
export async function searchByBarcode( async function makeApiRequest<T>(endpoint: string, base: string = CONFIG.BASE_URL): Promise<T> {
barcode: string const controller = new AbortController();
): Promise<Product | null> { const timeoutId = setTimeout(() => controller.abort(), 8000); // 8s timeout
try {
// Limpar código de barras (remover espaços e caracteres não numéricos)
const cleanBarcode = barcode.replace(/\D/g, "");
if (cleanBarcode.length < 8 || cleanBarcode.length > 13) { try {
throw new Error("Código de barras deve ter entre 8 e 13 dígitos"); 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 await response.json();
return data || null;
} catch (error) { } 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; return null;
} }
} }
/** /**
* Busca produtos por nome * Algoritmo de Busca Paginada com Controle de Fluxo
*/ */
export async function searchByName( export async function searchByName(
query: string, query: string,
@@ -109,426 +121,136 @@ export async function searchByName(
page: number = 1, page: number = 1,
pageSize: number = 20 pageSize: number = 20
): Promise<SearchResult> { ): Promise<SearchResult> {
try { const params = new URLSearchParams({
if (!query.trim()) { q: query.trim(),
throw new Error("Termo de busca não pode estar vazio"); page: page.toString(),
} page_size: pageSize.toString(),
});
// Construir parâmetros da URL para API Route if (country) params.append("country", country);
const params = new URLSearchParams({
q: query.trim(),
page: page.toString(),
page_size: pageSize.toString(),
});
// Adicionar filtro de país se especificado return makeApiRequest<SearchResult>(`/api/search?${params.toString()}`);
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;
}
} }
/** /**
* Busca TODOS os resultados de uma pesquisa * Otimização de Busca Completa (Pipeline de Dados)
* Faz requisições paginadas até obter todos os produtos disponíveis
*/ */
export async function fetchAllResults( export async function fetchAllResults(
query: string, query: string,
country?: string, country?: string,
maxPages: number = 10, maxPages: number = 5, // Reduzido para prevenir overhead
onProgress?: ( onProgress?: (current: number, total: number, count: number) => void
currentPage: number, ) {
totalPages: number, const allProducts: Product[] = [];
loadedProducts: number let currentPage = 1;
) => void let hasMore = true;
): Promise<{ products: Product[]; totalCount: number; actualPages: number }> {
try { while (hasMore && currentPage <= maxPages) {
if (!query.trim()) { if (onProgress) onProgress(currentPage, maxPages, allProducts.length);
throw new Error("Termo de busca não pode estar vazio");
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 { return {
energy: nutriments.energy_100g products: allProducts,
? `${formatValue(nutriments.energy_100g, 0)} kcal` totalCount: allProducts.length,
: "N/A", actualPages: currentPage - 1
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",
}; };
} }
/** /**
* Obtém a URL da imagem do produto (prioriza front, depois small) * Função de Ordenação de Alta Performance (Decorate-Sort-Undecorate)
*/
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
*/ */
export function sortProducts( export function sortProducts(
products: Product[], products: Product[],
sortBy: string, sortBy: string,
sortOrder: "asc" | "desc" = "asc" sortOrder: "asc" | "desc" = "asc"
): Product[] { ): Product[] {
const sorted = [...products].sort((a, b) => { if (!products.length) return [];
let comparison = 0;
// 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) { 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": 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": case "relevance":
// Para relevância, usar completude + nutri-score diff = b.score - a.score;
const aRelevance =
calculateCompletenessScore(a) + (a.nutrition_grades ? 20 : 0);
const bRelevance =
calculateCompletenessScore(b) + (b.nutrition_grades ? 20 : 0);
comparison = bRelevance - aRelevance;
break; break;
case "nutrition_grade": case "nutrition_grade":
const aNutriGrade = a.nutrition_grades?.toLowerCase(); diff = a.nutritionValue - b.nutritionValue;
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;
break; break;
case "name":
comparison = (a.product_name || "").localeCompare(b.product_name || "");
break;
case "brand":
comparison = (a.brands || "").localeCompare(b.brands || "");
break;
case "energy": case "energy":
const aHasEnergy = !!a.nutriments?.energy_100g; diff = (a.value.nutriments?.energy_100g || 9999) - (b.value.nutriments?.energy_100g || 9999);
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;
}
break; break;
case "name":
case "fat": diff = (a.value.product_name || "").localeCompare(b.value.product_name || "");
const aFat = a.nutriments?.fat_100g || 0;
const bFat = b.nutriments?.fat_100g || 0;
comparison = aFat - bFat;
break; break;
case "sugars":
const aSugars = a.nutriments?.sugars_100g || 0;
const bSugars = b.nutriments?.sugars_100g || 0;
comparison = aSugars - bSugars;
break;
default: default:
comparison = 0; diff = 0;
} }
return sortOrder === "asc" ? diff : -diff;
return sortOrder === "asc" ? comparison : -comparison;
}); });
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", "lang": "pt-BR",
"icons": [ "icons": [
{ {
"src": "/favicon.ico", "src": "/icon.png",
"sizes": "any", "sizes": "any",
"type": "image/x-icon" "type": "image/x-icon"
}, },