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
+3163 -11885

No files matched your search

+104 -126
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
</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> </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">
<Globe className="w-4 h-4" />
<span>Banco global</span>
</li>
<li className="flex items-center gap-2 text-gray-400">
<ScanLine className="w-4 h-4" />
<span>Busca por código</span>
</li>
<li className="text-gray-400">
<span>Nutri-Score & NOVA</span>
</li> </li>
))}
<li className="text-[11px] font-mono text-blue-500/80">Nutri-Score v2.1</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> </div>
<p className="text-sm text-gray-400 mb-3"> <h4 className="text-white font-bold">Open Food Facts</h4>
Dados fornecidos pelo Open Food Facts - a maior base de dados </div>
colaborativa de produtos alimentícios do mundo. <p className="text-xs leading-relaxed text-gray-500 mb-6">
A maior base de dados colaborativa de alimentos do mundo. Dados sob licença ODbL (Open Database License).
</p> </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>
</div>
</div>
{/* Bottom Bar */}
<div className="mt-12 pt-8 flex flex-col md:flex-row justify-between items-center gap-6">
<div className="text-[11px] font-mono text-gray-600 text-center md:text-left space-y-1">
<p>© {currentYear} NutriScan Systems. Open Source Initiative.</p>
<p>
Lead Architect:{" "}
<a href="https://evandro.dev.br" target="_blank" className="text-blue-500 hover:underline">
evandro.dev.br
</a>
</p> </p>
</div> </div>
{/* Bottom */} <div className="flex items-center gap-8">
<div className="border-t border-gray-800 mt-12 pt-8"> <div className="flex items-center gap-4">
<div className="flex flex-col md:flex-row justify-between items-center gap-4"> <Link href="/privacidade" className="text-[11px] font-black uppercase tracking-widest hover:text-white transition-colors">Privacidade</Link>
<div className="text-sm text-gray-400"> <Link href="/termos" className="text-[11px] font-black uppercase tracking-widest hover:text-white transition-colors">Termos</Link>
© 2024 NutriScan. Todos os direitos reservados.
<br />
<span className="text-xs">
Desenvolvido por{" "}
<a
href="https://evandro.dev.br"
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300 transition-colors"
>
evandro.dev.br
</a>{" "}
e mantido por NutriScan
</span>
</div> </div>
<div className="flex items-center gap-6 text-sm text-gray-400"> <a href="https://github.com" target="_blank" className="p-2 bg-gray-900 rounded-full hover:bg-gray-800 transition-colors">
<a href="#" className="hover:text-white transition-colors"> <Github className="w-4 h-4 text-white" />
Privacidade
</a> </a>
<a href="#" className="hover:text-white transition-colors">
Termos
</a>
<a href="#" className="hover:text-white transition-colors">
Contato
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
+100 -92
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"> {/* Logo Section - Unificada */}
<img <Link href="/" className="flex items-center gap-3 group">
{/* Container do Logo com Referencial Relativo */}
<div className="w-10 h-10 rounded-xl flex items-center justify-center shadow-lg shadow-blue-500/20 group-hover:rotate-6 transition-transform relative overflow-hidden">
<Image
src="/apple-touch-icon.png" src="/apple-touch-icon.png"
alt="NutriScan Logo" alt="NutriScan Logo"
className="w-8 h-8 sm:w-10 sm:h-10 rounded-xl" fill // Ocupa todo o container pai
priority // CRÍTICO: Carrega o logo antes de tudo para melhorar o LCP
sizes="40px" // Dica para o browser sobre o tamanho real em pixels
className="object-contain p-2" // Mantém a proporção e adiciona respiro interno
/> />
<div 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>
<div className="flex flex-col">
<span className="text-lg font-black tracking-tighter text-gray-900 dark:text-white leading-none">
NUTRI<span className="text-blue-500">SCAN</span>
</span>
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest leading-none mt-1">
Data Engineering
</span>
</div> </div>
</Link>
{/* Desktop Navigation */} {/* Desktop Nav - Com micro-interações */}
<nav className="hidden md:flex items-center gap-8"> <nav className="hidden md:flex items-center gap-1">
<a {NAV_LINKS.map((link) => (
href="/" <Link
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors" 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"
> >
Como funciona {link.label}
</a> </Link>
<a ))}
href="/#recursos"
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
>
Recursos
</a>
<a
href="/sobre"
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
>
Sobre
</a>
</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>
</div>
{/* Mobile Menu */} {/* Mobile Menu - Animado com AnimatePresence */}
<AnimatePresence>
{isMenuOpen && ( {isMenuOpen && (
<div className="md:hidden border-t border-gray-200 dark:border-gray-700 py-4"> <motion.div
<nav className="flex flex-col gap-4"> initial={{ opacity: 0, height: 0 }}
<a animate={{ opacity: 1, height: "auto" }}
href="/" exit={{ opacity: 0, height: 0 }}
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors py-2" className="md:hidden border-t border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 overflow-hidden"
onClick={() => setIsMenuOpen(false)}
> >
Como funciona <nav className="flex flex-col p-6 gap-4">
</a> {NAV_LINKS.map((link, i) => (
<a <motion.div
href="/#recursos" key={link.href}
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors py-2" initial={{ opacity: 0, x: -20 }}
onClick={() => setIsMenuOpen(false)} animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.1 }}
> >
Recursos <Link
</a> href={link.href}
<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)} onClick={() => setIsMenuOpen(false)}
className="text-xl font-bold text-gray-900 dark:text-white hover:text-blue-500 transition-colors"
> >
Sobre {link.label}
</a> </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> </nav>
</div> </motion.div>
)} )}
</div> </AnimatePresence>
</header> </header>
); );
} }
+69 -68
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 <div className="max-w-5xl mx-auto px-6 relative z-10">
<div className="grid lg:grid-cols-2 gap-16 items-center">
{/* Lado Esquerdo: A Missão (Copywriting Técnico) */}
<motion.div
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
>
<h2 className="text-4xl md:text-5xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter leading-tight">
Mantenha a <span className="text-blue-600">Engenharia</span> Livre de Interesses.
</h2> </h2>
<p className="text-xl text-gray-600 dark:text-gray-400"> <p className="text-lg text-gray-600 dark:text-gray-400 mb-8 leading-relaxed">
Este projeto é mantido por desenvolvedores independentes que 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.
acreditam que informação sobre saúde deve ser acessível a todos, sem
custo.
</p> </p>
</div>
{/* Benefícios da Doação */} <div className="space-y-4">
<div className="bg-gradient-to-br from-blue-50 to-green-50 dark:from-gray-800 dark:to-gray-700 rounded-2xl p-8 mb-8"> {[
<h3 className="text-2xl font-bold mb-6">Sua doação garante:</h3> { icon: Server, text: "Manutenção de Servidores e API de Alta Latência" },
<div className="grid md:grid-cols-2 gap-4"> { icon: ShieldCheck, text: "Segurança de Dados e Auditoria Open Source" },
<div className="flex items-start gap-3"> { icon: Globe, text: "Expansão da Base de Dados Internacional" },
<div className="w-6 h-6 rounded-full bg-blue-500 flex items-center justify-center flex-shrink-0 mt-1"> ].map((item, i) => (
<Check className="w-4 h-4 text-white" /> <div key={i} className="flex items-center gap-3 text-sm font-medium text-gray-700 dark:text-gray-300">
</div> <div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
<p>Servidores sempre disponíveis para consultas instantâneas</p> <item.icon className="w-4 h-4 text-blue-600" />
</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>
{item.text}
</div> </div>
))}
</div> </div>
</motion.div>
{/* Humanização - História do Projeto */} {/* Lado Direito: O Card de Doação (Psychological Anchoring) */}
<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, y: 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, y: 0 }}
<Code className="w-8 h-8 text-white" /> 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>
<div> <div>
<h4 className="font-bold text-lg"> <h4 className="font-bold text-gray-900 dark:text-white">Apoie com um Café</h4>
Ideia nascida de um estudante <p className="text-xs text-gray-500 uppercase tracking-widest font-mono">Status: Sistema Online</p>
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Mantido por NutriScan
</p>
</div> </div>
</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 */} <blockquote className="italic text-gray-600 dark:text-gray-400 text-sm border-l-2 border-blue-500 pl-4 mb-8">
<div className="text-center"> &quot;Como desenvolvedor independente, cada contribuição me permite dedicar mais horas à refatoração e novas features para o NutriScan.&quot;
</blockquote>
<a <a
href="https://buymeacoffee.com/evandrotruuta" href="https://buymeacoffee.com/evandrotruuta"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-block" className="block group"
> >
<Button <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">
size="lg" <Heart className="w-5 h-5 mr-3 fill-white group-hover:animate-pulse" />
className="bg-gradient-to-r from-blue-600 to-green-600 text-white px-8 py-6 text-lg hover:shadow-2xl transition-all" Contribuir agora
>
<Heart className="w-5 h-5 mr-2" />
Doe/Donate
</Button> </Button>
</a> </a>
<p className="mt-4 text-sm text-gray-500">
Qualquer valor faz diferença 💛 <div className="mt-6 flex justify-center gap-4">
</p> <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>
+95 -41
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"> <div className="max-w-7xl mx-auto px-6 relative z-10">
Por que milhares confiam no NutriScan {/* 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> </h2>
<p className="text-xl text-gray-600 dark:text-gray-400"> <p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
Transparência que transforma alimentação Engenharia de dados a serviço da sua saúde, com transparência total e zero custo.
</p> </p>
</div> </motion.div>
{/* Features Grid */} {/* Features Grid com Stagger */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8"> <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>
); );
+200 -185
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",
color: "emerald",
gradient: "from-emerald-600 via-teal-500 to-green-400"
},
auto: {
placeholder: "Digite o nome ou aponte o código...",
button: "Buscar",
icon: <Activity className="w-5 h-5" />,
subtext: "Aguardando input do operador...",
color: "slate",
gradient: "from-slate-600 via-blue-500 to-slate-400"
} }
}; }), []);
detectCountry(); const current = searchConfig[searchType];
}, []);
// Detectar tipo de busca baseado na entrada
useEffect(() => {
if (searchQuery.trim()) {
const detectedType = detectSearchType(searchQuery);
setSearchType(detectedType);
} else {
setSearchType("auto");
}
}, [searchQuery]);
const handleSearch = async (e: React.FormEvent) => { const handleSearch = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setError(""); if (!searchQuery.trim()) return;
if (!searchQuery.trim()) {
setError("Digite nome do produto");
return;
}
const query = searchQuery.trim();
const detectedType = detectSearchType(query);
if (detectedType === "barcode") {
if (!isValidBarcode(query)) {
setError("Código de barras deve ter entre 8 e 13 dígitos");
return;
}
setLoading(true); 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 = () => { const target = searchType === "barcode" && isValid
if (searchType === "barcode") { ? `/produto/${searchQuery.trim()}`
return "Digite o código de barras (ex: 7896004713588)"; : `/resultados?q=${encodeURIComponent(searchQuery.trim())}${countryCode ? `&country=${countryCode}` : ""}`;
} else if (searchType === "name") {
return "Digite o nome do produto (ex: coca cola)";
}
return "Digite nome do produto";
};
const getSearchButtonText = () => { router.push(target);
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 {/* Glow de Fundo Reativo */}
href="https://evandro.dev.br" <motion.div
target="_blank" animate={{
rel="noopener noreferrer" scale: searchQuery ? 1.2 : 1,
className="font-semibold hover:underline transition-all hover:text-blue-800 dark:hover:text-blue-200" 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"
> >
evandrodevbr <AnimatePresence mode="wait">
</a> {theme === "dark" ? (
{" • "}Dados do Open Food Facts <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>
{/* Main Heading */} <div className="relative z-10 max-w-5xl mx-auto text-center">
<h1 className="text-3xl sm:text-4xl lg:text-5xl xl:text-6xl font-bold text-gray-900 dark:text-white mb-4 sm:mb-6 leading-tight">
Descubra o que a comunidade alimentar sabe sobre{" "} {/* 2. BADGE DE STATUS DO SISTEMA */}
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent"> <motion.div
seus alimentos 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
</div>
</div>
<div className="animate-fadeIn" style={{ animationDelay: "0.4s" }}>
<div className="text-2xl sm:text-3xl font-bold text-purple-600">
24/7
</div>
<div className="text-xs sm:text-sm text-gray-500">disponível</div>
</div>
</div>
{/* Search Form */}
<form
onSubmit={handleSearch}
className="max-w-2xl mx-auto mb-10 sm:mb-12"
>
<div className="relative">
<div className="flex items-center bg-white dark:bg-gray-800 rounded-2xl shadow-xl overflow-hidden focus-within:ring-2 focus-within:ring-blue-500 transition-all duration-200">
<div className="flex-1 relative">
{searchType === "barcode" ? (
<ScanLine className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5 pointer-events-none" />
) : (
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5 pointer-events-none" />
)} )}
>
{current.icon}
</motion.div>
</AnimatePresence>
</div>
<Input <Input
type="text" type="text"
placeholder={getPlaceholder()} placeholder={current.placeholder}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} 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" 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> </div>
<Button <Button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="m-2 px-6 sm:px-8 py-4 bg-gradient-to-r from-blue-600 to-green-600 hover:from-blue-700 hover:to-green-700 text-white font-semibold rounded-xl transition-all duration-200 transform hover:scale-105 disabled:opacity-50 flex-shrink-0" 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 ? ( {loading ? (
<div className="flex items-center gap-2"> <div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
<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"> <div className="flex items-center gap-2">
<Search className="w-5 h-5" /> <span>{current.button}</span>
{getSearchButtonText()} <ArrowRight size={14} className="group-hover:translate-x-1 transition-transform" />
</div> </div>
)} )}
</Button> </Button>
</div> </div>
{/* 5. METADADOS DA OPERAÇÃO */}
<div className="flex items-center justify-between px-6 mt-6">
<div className={cn(
"flex items-center gap-2 font-mono text-[10px] font-bold uppercase tracking-[0.2em] transition-colors duration-500",
searchType === "barcode" ? "text-blue-500" : searchType === "name" ? "text-emerald-500" : "text-slate-600"
)}>
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-current opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-current"></span>
</span>
{current.subtext}
</div> </div>
{/* Search Type Indicator */} {isMounted && countryCode && (
{searchQuery && ( <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">
<div className="flex items-center justify-center gap-2 mt-3"> <Globe size={12} />
{searchType === "barcode" && ( <span className="text-[10px] font-bold uppercase">Region: {countryCode.toUpperCase()}</span>
<div className="flex items-center gap-1 text-sm text-blue-600 dark:text-blue-400">
<ScanLine className="w-4 h-4" />
<span>Busca por código de barras</span>
</div>
)}
{searchType === "name" && (
<div className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
<Search className="w-4 h-4" />
<span>Busca por nome do produto</span>
{mounted && userCountry && (
<div className="flex items-center gap-1 text-xs text-gray-500">
<Globe className="w-3 h-3" />
<span>Priorizando {userCountry}</span>
</div> </div>
)} )}
</div> </div>
)}
</div>
)}
{error && (
<p className="text-red-500 text-sm mt-3 text-left">{error}</p>
)}
</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>
);
}
+126 -151
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}
{index < steps.length - 1 && ( initial={{ opacity: 0, x: -20 }}
<div className="hidden lg:flex absolute top-1/2 -right-4 transform -translate-y-1/2 z-10"> whileInView={{ opacity: 1, x: 0 }}
<svg transition={{ delay: index * 0.15 }}
width="32" className="relative group"
height="24"
viewBox="0 0 32 24"
fill="none"
className="text-gray-400 dark:text-gray-600"
> >
<path {/* Conector Visual (Desktop) */}
d="M0 12H30M30 12L20 2M30 12L20 22" {index < steps.length - 1 && (
stroke="currentColor" <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" />
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>
<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> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8"> <div className="relative z-10 grid grid-cols-1 lg:grid-cols-3 gap-12">
{features.map((feature, index) => ( <div className="lg:col-span-1">
<div <h3 className="text-3xl font-black mb-4 tracking-tighter">Data Specification</h3>
key={index} <p className="text-slate-400 text-sm leading-relaxed mb-6">
className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700" Extração de metadados via Open Food Facts API v2. Suporte a protocolos Nutri-Score e classificações NOVA.
> </p>
<h4 className="text-lg font-semibold text-gray-900 dark:text-white mb-4"> <div className="flex items-center gap-4 text-xs font-mono text-blue-400 uppercase tracking-widest">
{feature.title} <Globe size={14} /> Open Source Database
</h4> </div>
<ul className="space-y-2"> </div>
{feature.items.map((item, itemIndex) => (
<li <div className="lg:col-span-2 grid grid-cols-1 md:grid-cols-2 gap-8">
key={itemIndex} {/* Lista de Features Refinada */}
className="flex items-center gap-2 text-gray-600 dark:text-gray-300" <div className="space-y-4">
> <h4 className="text-blue-500 font-bold text-xs uppercase tracking-widest">Bio-Markers & Macros</h4>
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0" /> <ul className="space-y-3">
<span className="text-sm">{item}</span> {["Calorias (Kcal/100g)", "Perfil Lipídico e Glicêmico", "Micronutrientes & Fibras"].map(item => (
<li key={item} className="flex items-center gap-3 text-sm text-slate-300">
<Zap size={12} className="text-blue-500" /> {item}
</li> </li>
))} ))}
</ul> </ul>
</div> </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>
{/* Bottom CTA */}
<div className="text-center mt-16">
<a
href="#"
onClick={(e) => {
e.preventDefault();
const input = document.querySelector(
'input[placeholder*="código de barras"]'
) as HTMLInputElement;
input?.focus();
window.scrollTo({ top: 0, behavior: "smooth" });
}}
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-blue-600 to-green-600 text-white rounded-full font-semibold hover:shadow-lg transition-all duration-300 hover:scale-105 cursor-pointer"
>
<ScanLine className="w-5 h-5" />
Começar agora
</a>
<p className="text-gray-500 dark:text-gray-400 text-sm mt-4">
É rápido, fácil e totalmente gratuito
</p>
</div> </div>
</div> </div>
</section> </section>
+135 -296
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"> {/* 1. NAVEGAÇÃO E TÍTULO (Sticky Header) */}
<div className="absolute inset-0 bg-grid-pattern opacity-5"></div> <header className="sticky top-0 z-50 bg-white/80 backdrop-blur-xl border-b border-slate-200 dark:bg-[#030712]/80 dark:border-white/5">
<div className="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-6 h-20 flex items-center gap-6">
<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 <Button
variant="ghost" variant="ghost"
size="sm" size="icon"
onClick={() => router.back()} onClick={() => router.back()}
className="flex items-center gap-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200" className="rounded-full hover:bg-slate-100 dark:hover:bg-white/5"
> >
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="w-5 h-5" />
Voltar
</Button> </Button>
<div className="flex-1 min-w-0">
<div className="flex-1"> <h1 className="text-xl font-black tracking-tighter text-slate-900 dark:text-white truncate uppercase">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2 animate-fade-in"> {product.product_name || "Unlabeled Product"}
{product.product_name || "Produto"}
</h1> </h1>
<p className="text-sm text-gray-500 dark:text-gray-400"> <div className="flex items-center gap-2 text-[10px] font-mono font-bold text-slate-500 uppercase tracking-widest">
Código: {product.code} <Database size={10} className="text-blue-500" /> {product.code}
</p>
</div>
</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 */} {/* COLUNA ESQUERDA: Ativos Visuais (5 Colunas) */}
<div className="space-y-6"> <div className="lg:col-span-5 space-y-8">
<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"> <motion.div
<div className="aspect-square bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-700 p-8"> initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="relative aspect-square bg-white rounded-[3rem] border border-slate-200 overflow-hidden shadow-2xl dark:bg-slate-900 dark:border-white/5 group"
>
{imageUrl ? ( {imageUrl ? (
<Image <Image
src={imageUrl} src={imageUrl}
alt={product.product_name || "Produto"} alt={product.product_name || "Product"}
fill fill
className="object-contain transition-transform duration-300 hover:scale-105" className="object-contain p-12 transition-transform duration-700 group-hover:scale-110"
priority priority
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" sizes="(max-width: 768px) 100vw, 500px"
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="h-full flex flex-col items-center justify-center text-slate-300 dark:text-slate-800">
<div className="text-center"> <Beaker size={80} className="mb-4 opacity-50" />
<Star className="w-16 h-16 mx-auto mb-4" /> <span className="text-xs font-mono font-black uppercase tracking-[0.4em]">No Visual Metadata</span>
<p className="text-lg">Sem imagem disponível</p>
</div>
</div> </div>
)} )}
</div> </motion.div>
</div>
{/* Basic Info */} {/* Ficha Técnica (Bento-style) */}
<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="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-xl font-semibold text-gray-900 dark:text-white mb-4"> <h3 className="text-[10px] font-black uppercase tracking-[0.4em] text-blue-600 dark:text-blue-400">SKU Datasheet</h3>
Informações Básicas <div className="space-y-4 font-medium">
</h3> {[
<div className="space-y-3 text-sm"> { label: "Fabricante", val: product.brands, icon: ShieldCheck },
{product.brands && ( { label: "Região", val: product.countries, icon: Globe },
<div className="flex justify-between items-center py-2 border-b border-gray-100 dark:border-gray-700"> { label: "Registro", val: "Auditado", icon: ClipboardList }
<span className="font-medium text-gray-600 dark:text-gray-400"> ].map((item, i) => item.val && (
Marca: <div key={i} className="flex justify-between items-center text-sm">
</span> <span className="text-slate-400 flex items-center gap-2 font-bold uppercase text-[11px] tracking-tight">
<span className="text-gray-900 dark:text-white font-medium"> <item.icon size={14} className="text-slate-300" /> {item.label}
{product.brands}
</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"> {/* Veredito Rápido: Scores */}
{nutritionGrade && ( <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<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"> <DataCard
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4"> title="Nutri-Score"
Nutri-Score desc="Indicador de Qualidade Nutricional"
</h3> badge={<NutriScoreBadge grade={product.nutrition_grades || ""} size="lg" />}
<div className="flex items-center gap-4">
<NutriScoreBadge
grade={product.nutrition_grades || ""}
size="lg"
showTooltip={true}
/> />
<div> <DataCard
<p className="font-medium text-gray-900 dark:text-white text-lg"> title="Protocolo NOVA"
{nutritionGrade.description} desc="Nível de Processamento Industrial"
</p> badge={<NovaBadge group={product.nova_group || 0} size="lg" />}
<p className="text-sm text-gray-600 dark:text-gray-400">
Classificação nutricional
</p>
</div>
</div>
</div>
)}
{novaGroup && (
<div className="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-2xl p-6 shadow-sm hover:shadow-md transition-all duration-300 transform hover:-translate-y-1">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
NOVA
</h3>
<div className="flex items-center gap-4">
<NovaBadge
group={product.nova_group || 0}
size="lg"
showTooltip={true}
/> />
<div>
<p className="font-medium text-gray-900 dark:text-white text-lg">
Grupo {novaGroup.label}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Nível de processamento
</p>
</div>
</div>
</div>
)}
</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 */}
<AnimatePresence>
{product.allergens_tags && product.allergens_tags.length > 0 && ( {product.allergens_tags && product.allergens_tags.length > 0 && (
<div className="bg-gradient-to-r from-yellow-50 to-orange-50 dark:from-yellow-900/20 dark:to-orange-900/20 border border-yellow-200 dark:border-yellow-800 rounded-2xl p-6 shadow-sm hover:shadow-md transition-all duration-300"> <motion.div
<div className="flex items-center gap-3 mb-4"> initial={{ opacity: 0, x: -10 }}
<div className="w-8 h-8 bg-yellow-500 rounded-full flex items-center justify-center"> animate={{ opacity: 1, x: 0 }}
<AlertTriangle className="w-4 h-4 text-white" /> 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> </div>
<h3 className="text-lg font-semibold text-yellow-800 dark:text-yellow-200"> <h3 className="text-lg font-black text-orange-950 dark:text-orange-500 uppercase tracking-tighter">Detecção de Alérgenos</h3>
Alérgenos
</h3>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{product.allergens_tags.map((allergen, index) => ( {product.allergens_tags.map(tag => (
<span <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">
key={index} {tag.replace(/en:|pt:/g, "")}
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> </span>
))} ))}
</div> </div>
</div> </motion.div>
)} )}
</AnimatePresence>
{/* Lista de Ingredientes (OCR Interpretation) */}
<div className="p-10 rounded-[2.5rem] bg-white border border-slate-200 shadow-sm dark:bg-white/[0.02] dark:border-white/5">
<h3 className="text-lg font-black text-slate-900 dark:text-white mb-6 uppercase tracking-tighter">Relatório de Ingredientes</h3>
<p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400 font-medium">
{product.ingredients_text || "O fabricante não forneceu a lista de ingredientes para este SKU."}
</p>
</div>
{/* 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>
))}
{product.additives_tags.length > 10 && (
<span className="px-3 py-1 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 text-sm rounded-full font-medium border border-gray-200 dark:border-gray-600">
+{product.additives_tags.length - 10} mais
</span>
)}
</div> </div>
</div> </div>
)} </main>
</div>
</div> </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>
); );
+74 -121
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>
</div>
{/* CTA Buttons */} {/* 4. LAYER DE EXECUÇÃO: CTAs de Alta Tração */}
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-16"> <motion.div
<Link href="/"> initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-24"
>
<Link href="/" className="w-full sm:w-auto">
<Button <Button
size="lg" 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" 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 className="mr-2">Comece Agora</span> <span>Executar Scan</span>
<ArrowRight className="w-5 h-5" /> <ArrowRight className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" />
</Button> </Button>
</Link> </Link>
<Link <Link
href="https://github.com/evandrodevbr/lista-compras" href="https://github.com/evandrodevbr/lista-compras"
target="_blank" target="_blank"
rel="noopener noreferrer" className="w-full sm:w-auto"
> >
<Button <Button
variant="outline" variant="outline"
size="lg" size="lg"
className="border-white text-white hover:bg-white hover:text-blue-600 transition-all duration-300" 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"
> >
<span className="mr-2">Contribua</span> <Code2 className="mr-2 w-5 h-5 text-black" />
<Heart className="w-5 h-5" /> <span className="text-gray-800">Audit Code</span>
</Button> </Button>
</Link> </Link>
</motion.div>
{/* 5. LAYER DE VALIDAÇÃO: Bento Grid de Metadados */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{[
{ icon: Globe, label: "Rede", val: "Global Node", desc: "Integridade em 100+ países" },
{ icon: ShieldCheck, label: "Segurança", val: "Privacy First", desc: "Zero tracking de dados" },
{ icon: Heart, label: "Sustentabilidade", val: "Open Source", desc: "Mantido pela comunidade" },
].map((item, i) => (
<div key={i} className="p-6 rounded-3xl bg-white/[0.02] border border-white/[0.05] backdrop-blur-sm text-left group hover:bg-white/[0.04] transition-colors">
<item.icon className="w-6 h-6 text-blue-500 mb-4" />
<div className="text-[10px] font-black uppercase tracking-widest text-slate-500 mb-1">{item.label}</div>
<div className="text-white font-bold text-lg mb-1 tracking-tight">{item.val}</div>
<p className="text-xs text-slate-500 leading-relaxed">{item.desc}</p>
</div> </div>
))}
</div> </div>
{/* Community Stats */} {/* 6. MENSAGEM FINAL: Minimalismo de Autoridade */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-16"> <div className="mt-24 pt-12 border-t border-white/[0.05]">
<div className="bg-white/10 backdrop-blur-sm rounded-2xl p-6"> <p className="text-[10px] font-mono font-bold text-slate-600 uppercase tracking-[0.5em] mb-6">
<div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center mx-auto mb-4"> NutriScan Governance Protocol v2.0
<Users className="w-6 h-6 text-white" />
</div>
<div className="text-3xl font-bold text-white mb-2">Milhares</div>
<p className="text-blue-100">
de usuários ativos fazendo escolhas mais conscientes
</p> </p>
</div>
<div className="bg-white/10 backdrop-blur-sm rounded-2xl p-6">
<div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center mx-auto mb-4">
<Globe className="w-6 h-6 text-white" />
</div>
<div className="text-3xl font-bold text-white mb-2">Global</div>
<p className="text-blue-100">
comunidade internacional comprometida com transparência
</p>
</div>
<div className="bg-white/10 backdrop-blur-sm rounded-2xl p-6">
<div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center mx-auto mb-4">
<Heart className="w-6 h-6 text-white" />
</div>
<div className="text-3xl font-bold text-white mb-2">Crescendo</div>
<p className="text-blue-100">
movimento que não para de expandir e impactar vidas
</p>
</div>
</div>
{/* Final Message */}
<div className="bg-white/10 backdrop-blur-sm rounded-3xl p-8 max-w-4xl mx-auto">
<blockquote className="text-2xl sm:text-3xl font-medium text-white italic mb-6">
&ldquo;Porque alimentação é mais que nutrição. É autocuidado, é
conexão, é vida.&rdquo;
</blockquote>
<cite className="block text-lg text-blue-100 mb-8">
— Filosofia NutriScan
</cite>
<div className="text-center">
<p className="text-lg text-blue-100 mb-6">
Faça parte dessa transformação. Sua próxima escolha alimentar pode
ser a mais consciente de todas.
</p>
<div className="inline-flex items-center gap-2 text-white/80">
<span>Comece sua jornada hoje</span>
<ArrowRight className="w-5 h-5" />
</div>
</div>
</div>
{/* Social Links */}
<div className="mt-16">
<p className="text-blue-100 mb-6">Conecte-se conosco</p>
<div className="flex justify-center gap-6">
<Link <Link
href="https://github.com/evandrodevbr/lista-compras" href="/sobre"
target="_blank" className="inline-flex items-center gap-2 text-sm font-bold text-blue-500 hover:text-blue-400 transition-colors group"
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 Conheça nossa arquitetura de dados
className="w-6 h-6 text-white" <ArrowRight size={14} className="group-hover:translate-x-1 transition-transform" />
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>
<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>
+111 -202
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 className="flex items-center">
<CheckCircle className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" />
Busca por nome/marca
</li>
<li className="flex items-center">
<CheckCircle className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" />
Detecção automática
</li> </li>
))}
</ul> </ul>
</div> </motion.div>
))}
</div> </div>
{/* Arrow */} {/* Global Stats Footer: O 80/20 do Valor */}
<div className="hidden lg:flex items-center justify-center"> <motion.div
<ArrowRight className="w-8 h-8 text-gray-400" /> initial={{ opacity: 0, scale: 0.95 }}
</div> whileInView={{ opacity: 1, scale: 1 }}
className="grid grid-cols-2 lg:grid-cols-4 gap-4"
{/* Step 2 */} >
<div className="relative text-center"> {[
{/* Step Circle */} { icon: Globe, label: "Global", text: "2M+ Alimentos" },
<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"> { icon: Shield, label: "Seguro", text: "100% Gratuito" },
<Search className="w-10 h-10 text-white" /> { icon: Clock, label: "Veloz", text: "< 10 Segundos" },
</div> { icon: Zap, label: "Offline", text: "Cache Inteligente" },
].map((item, i) => (
{/* Step Number */} <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">
<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"> <item.icon className="w-6 h-6 text-blue-500 mb-3" />
2 <span className="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">{item.label}</span>
</div> <span className="text-sm font-bold text-gray-900 dark:text-white">{item.text}</span>
<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>
{/* Time Highlight */}
<div className="mt-16 text-center">
<div className="inline-flex items-center gap-3 bg-white dark:bg-gray-800 rounded-full px-6 py-3 shadow-lg">
<div className="w-8 h-8 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center">
<CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400" />
</div>
<span className="text-lg font-semibold text-gray-900 dark:text-white">
Em menos de 10 segundos
</span>
</div>
<p className="mt-4 text-gray-600 dark:text-gray-400">
Do escaneamento à decisão informada
</p>
</div>
{/* Additional Features */}
<div className="mt-16 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm text-center">
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center mx-auto mb-4">
<Scan className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
Offline
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Funciona sem internet com cache local
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm text-center">
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-lg flex items-center justify-center mx-auto mb-4">
<Search className="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
Gratuito
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Sem custos, sem anúncios, sem pegadinhas
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm text-center">
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900/30 rounded-lg flex items-center justify-center mx-auto mb-4">
<CheckCircle className="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
Atualizado
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Dados sempre atualizados pela comunidade
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm text-center">
<div className="w-12 h-12 bg-orange-100 dark:bg-orange-900/30 rounded-lg flex items-center justify-center mx-auto mb-4">
<ArrowRight className="w-6 h-6 text-orange-600 dark:text-orange-400" />
</div>
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
Rápido
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Interface otimizada para velocidade
</p>
</div>
</div> </div>
))}
</motion.div>
</div> </div>
</section> </section>
); );
+135 -162
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"
viewport={{ once: true }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-20"
>
{[
{ label: "Data Nodes", val: "2.0M+", icon: Database, color: "text-blue-500", bg: "bg-blue-500/10" },
{ label: "Regions", val: "100+", icon: Globe, color: "text-emerald-500", bg: "bg-emerald-500/10" },
{ label: "Uptime", val: "24/7", icon: Activity, color: "text-purple-500", bg: "bg-purple-500/10" },
{ label: "Local Cache", val: "218k", icon: Zap, color: "text-orange-500", bg: "bg-orange-500/10" },
].map((stat) => (
<motion.div
key={stat.label}
variants={itemVariants}
whileHover={{ y: -5 }}
className="bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 p-8 rounded-[2rem] shadow-sm hover:shadow-xl transition-all"
>
<div className={cn("w-12 h-12 rounded-xl flex items-center justify-center mb-6", stat.bg)}>
<stat.icon className={cn("w-6 h-6", stat.color)} />
</div> </div>
<div className="text-4xl font-bold text-blue-600 dark:text-blue-400 mb-2"> <div className={cn("text-3xl font-mono font-bold tracking-tighter mb-1", stat.color)}>
2.000.000+ {stat.val}
</div> </div>
<div className="text-lg font-semibold text-gray-900 dark:text-white mb-2"> <div className="text-sm font-black uppercase tracking-widest text-gray-400">
Produtos {stat.label}
</div>
<p className="text-gray-600 dark:text-gray-400 text-sm">
Na base de dados Open Food Facts
</p>
</div> </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" /> {[
{ t: "20%", l: "25%", c: "bg-blue-500" },
{ t: "40%", l: "60%", c: "bg-emerald-500" },
{ t: "70%", l: "30%", c: "bg-purple-500" },
{ t: "25%", l: "80%", c: "bg-orange-500" },
{ t: "55%", l: "15%", c: "bg-red-500" },
].map((dot, i) => (
<div key={i} className="absolute" style={{ top: dot.t, left: dot.l }}>
<div className={cn("w-3 h-3 rounded-full animate-ping opacity-75 absolute", dot.c)} />
<div className={cn("w-3 h-3 rounded-full relative", dot.c)} />
</div> </div>
<div className="text-4xl font-bold text-orange-600 dark:text-orange-400 mb-2"> ))}
218 <Globe className="w-24 h-24 text-blue-500/20 animate-pulse" />
</div> <div className="absolute bottom-4 right-6 flex items-center gap-2 text-[10px] font-mono text-blue-400 uppercase tracking-[0.3em]">
<div className="text-lg font-semibold text-gray-900 dark:text-white mb-2"> <span className="w-2 h-2 bg-blue-400 rounded-full animate-pulse" /> Live Node Updates
Cache Local
</div>
<p className="text-gray-600 dark:text-gray-400 text-sm">
Produtos populares offline
</p>
</div> </div>
</div> </div>
</div>
</motion.div>
{/* World Map Visualization */} {/* Impact Logs - High Density Info */}
<div className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-lg mb-16"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 font-medium">
<div className="text-center mb-8"> <ImpactCard
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"> title="Individual Protocol"
Alcance Global items={[
</h3> { label: "Cognitive Efficiency", text: "Redução de 80% no tempo de análise de rótulos via algoritmos de OCR." },
<p className="text-gray-600 dark:text-gray-400"> { label: "Data Sovereignty", text: "Usuários com controle total sobre o histórico de consumo local." },
Nossa plataforma conecta usuários de todos os continentes à maior { label: "Safety Layer", text: "Detecção imediata de alérgenos via cruzamento de base de dados." }
base de dados alimentar do mundo ]}
</p> type="blue"
</div> />
<ImpactCard
{/* Simplified World Map */} title="Collective Governance"
<div className="relative max-w-4xl mx-auto"> items={[
<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"> { label: "Market Transparency", text: "Pressão algorítmica por rótulos honestos e dados auditáveis." },
{/* Dots representing global coverage */} { label: "Crowdsourced Intelligence", text: "Milhares de contribuições diárias validando a integridade da base." },
<div className="absolute top-1/4 left-1/4 w-3 h-3 bg-blue-500 rounded-full animate-pulse"></div> { label: "Universal Access", text: "Democratização da informação nutricional de alta fidelidade." }
<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> type="emerald"
<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>
</div>
</div>
</div>
{/* Community Impact */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 rounded-2xl p-8">
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
Transformação Individual
</h3>
<div className="space-y-4">
<div className="flex items-start">
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></div>
<p className="text-gray-700 dark:text-gray-300">
<strong>Escolhas mais conscientes:</strong> Usuários relatam
tomar decisões mais informadas sobre sua alimentação
</p>
</div>
<div className="flex items-start">
<div className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></div>
<p className="text-gray-700 dark:text-gray-300">
<strong>Economia de tempo:</strong> Redução de 80% no tempo
gasto analisando rótulos
</p>
</div>
<div className="flex items-start">
<div className="w-2 h-2 bg-purple-500 rounded-full mt-2 mr-3 flex-shrink-0"></div>
<p className="text-gray-700 dark:text-gray-300">
<strong>Confiança aumentada:</strong> Maior segurança na
identificação de alérgenos e ingredientes
</p>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20 rounded-2xl p-8">
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
Impacto Coletivo
</h3>
<div className="space-y-4">
<div className="flex items-start">
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></div>
<p className="text-gray-700 dark:text-gray-300">
<strong>Transparência da indústria:</strong> Pressão por
rótulos mais claros e honestos
</p>
</div>
<div className="flex items-start">
<div className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></div>
<p className="text-gray-700 dark:text-gray-300">
<strong>Dados colaborativos:</strong> Milhares de
contribuições diárias à base global
</p>
</div>
<div className="flex items-start">
<div className="w-2 h-2 bg-purple-500 rounded-full mt-2 mr-3 flex-shrink-0"></div>
<p className="text-gray-700 dark:text-gray-300">
<strong>Democratização:</strong> Acesso universal a
informações nutricionais de qualidade
</p>
</div>
</div>
</div>
</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>
);
}
+109 -92
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 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.1 }}
viewport={{ once: true }}
className="group p-8 rounded-[2rem] bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 hover:border-blue-500/30 transition-all duration-500"
>
<div className={cn("w-14 h-14 rounded-2xl flex items-center justify-center mb-8 transition-transform group-hover:scale-110", value.bg)}>
<value.icon className={cn("w-7 h-7", value.color)} />
</div> </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">
Transparência {value.title}
</h3> </h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed"> <p className="text-gray-600 dark:text-gray-400 leading-relaxed text-sm font-medium">
Todos os dados são colaborativos e abertos. Não escondemos nada, {value.description}
não maquiamos informações. O que você vê é o que existe.
</p> </p>
</motion.div>
))}
</div> </div>
{/* Empoderamento */} {/* Data Source Dashboard Highlight */}
<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"> <motion.div
<Shield className="w-8 h-8 text-green-600 dark:text-green-400" /> initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
className="relative rounded-[3rem] bg-slate-950 p-10 md:p-16 overflow-hidden shadow-3xl"
>
<div className="absolute top-0 right-0 w-full h-full bg-[radial-gradient(circle_at_top_right,_var(--tw-gradient-stops))] from-blue-500/20 via-transparent to-transparent" />
<div className="relative z-10 flex flex-col lg:flex-row gap-12 items-center">
<div className="flex-1 space-y-8">
<div className="inline-flex items-center px-4 py-2 rounded-full bg-blue-500/10 border border-blue-500/20 text-blue-400 text-xs font-black uppercase tracking-widest">
<Database className="w-3 h-3 mr-2" /> Infraestrutura Global
</div> </div>
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"> <h3 className="text-3xl md:text-4xl font-black text-white tracking-tighter">
Empoderamento Integrado ao Open Food Facts API
</h3> </h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed"> <p className="text-slate-400 text-lg leading-relaxed">
Colocamos o poder de escolha de volta nas suas mãos. Cada código Nossa engine de busca opera sobre o maior dataset colaborativo do planeta, garantindo que cada scan seja validado por uma rede global de curadoria.
de barras escaneado é um passo em direção a escolhas mais
informadas.
</p> </p>
</div>
{/* Comunidade */} <div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
<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"> { label: "Produtos", val: "2M+", icon: Activity },
<Users className="w-8 h-8 text-purple-600 dark:text-purple-400" /> { 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>
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"> </div>
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>
</div> </div>
{/* Open Food Facts Highlight */} <div className="relative w-64 h-64 flex items-center justify-center">
<div className="bg-gradient-to-r from-blue-600 to-green-600 rounded-3xl p-8 text-white shadow-2xl"> {/* Efeito Visual de Radar/Sincronização */}
<div className="flex items-center justify-between flex-wrap gap-8"> <div className="absolute inset-0 border border-blue-500/20 rounded-full animate-ping" />
<div className="flex-1 min-w-0"> <div className="absolute inset-4 border border-blue-500/10 rounded-full animate-pulse" />
<div className="flex items-center mb-4"> <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="w-8 h-8 mr-3" /> <Globe className="text-white w-16 h-16" />
<h3 className="text-2xl font-bold">Open Food Facts</h3>
</div>
<p className="text-blue-100 text-lg mb-6">
Conectamos você à maior base de dados colaborativa de alimentos
do mundo, onde cada produto é verificado e atualizado pela
comunidade global.
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center">
<div className="text-3xl font-bold mb-1">2M+</div>
<div className="text-blue-100 text-sm">Produtos</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold mb-1">100+</div>
<div className="text-blue-100 text-sm">Países</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold mb-1">24/7</div>
<div className="text-blue-100 text-sm">Disponível</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold mb-1">∞</div>
<div className="text-blue-100 text-sm">Colaborativo</div>
</div>
</div>
</div>
<div className="flex-shrink-0">
<div className="w-24 h-24 bg-white/20 rounded-2xl flex items-center justify-center">
<Globe className="w-12 h-12" />
</div>
</div> </div>
</div> </div>
</div> </div>
</motion.div>
{/* Inspirational Quote */} {/* Footer Quote: Minimalista */}
<div className="text-center mt-16"> <div className="mt-24 text-center">
<blockquote className="text-2xl sm:text-3xl font-medium text-gray-700 dark:text-gray-300 italic max-w-4xl mx-auto"> <p className="text-gray-400 dark:text-gray-600 font-mono text-[10px] uppercase tracking-[0.5em] mb-4">
&ldquo;Alimentação é mais que nutrição. É autocuidado, é conexão, é NutriScan Open Data Philosophy
vida.&rdquo; </p>
</blockquote> <p className="text-xl md:text-2xl font-serif italic text-gray-800 dark:text-gray-200">
<cite className="block mt-4 text-lg text-gray-500 dark:text-gray-400"> &ldquo;Dados abertos são a fundação de uma sociedade saudável.&rdquo;
— Filosofia NutriScan </p>
</cite>
</div> </div>
</div> </div>
</section> </section>
+146 -141
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
{/* Timeline Items */} style={{ scaleY }}
<div className="space-y-16"> className="absolute top-0 w-full h-full bg-gradient-to-b from-blue-500 via-green-500 to-orange-500 origin-top"
{/* 2024 - The Question */} />
<div className="relative flex items-center">
<div className="w-full md:w-1/2 md:pr-8">
<div className="bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 rounded-2xl p-8 shadow-lg">
<div className="flex items-center mb-4">
<div className="w-12 h-12 bg-blue-500 rounded-lg flex items-center justify-center mr-4">
<Calendar className="w-6 h-6 text-white" />
</div>
<div>
<div className="text-sm font-medium text-blue-600 dark:text-blue-400">
2024
</div>
<div className="text-lg font-bold text-gray-900 dark:text-white">
A Pergunta Simples
</div>
</div>
</div>
<blockquote className="text-xl font-semibold text-gray-800 dark:text-gray-200 mb-4">
&ldquo;O que realmente tem nesse produto?&rdquo;
</blockquote>
<p className="text-gray-600 dark:text-gray-300">
Após inúmeras idas ao mercado tentando decifrar tabelas
nutricionais e listas intermináveis de ingredientes,
percebemos que não estávamos sozinhos nessa busca.
</p>
</div>
</div> </div>
{/* Timeline Dot */} <div className="space-y-24 md:space-y-32">
<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> {STORY_DATA.map((step, index) => (
<TimelineItem key={index} step={step} index={index} />
<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>
);
}
+136 -137
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 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-4xl md:text-6xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter"
>
Projetado para <span className="bg-gradient-to-r from-blue-600 to-emerald-500 bg-clip-text text-transparent">Cidadãos Conscientes</span>
</motion.h2>
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
Se a transparência é um requisito para sua mesa, o NutriScan é sua ferramenta de governança alimentar.
</p>
</div>
{/* Persona Bento Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-24">
{PERSONAS.map((persona, i) => (
<motion.div
key={persona.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.1 }}
viewport={{ once: true }}
className={cn(
"group relative p-8 rounded-[2.5rem] bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 transition-all duration-500",
persona.border,
"hover:shadow-2xl hover:shadow-gray-200 dark:hover:shadow-none"
)}
>
<div className={cn("w-14 h-14 rounded-2xl flex items-center justify-center mb-8 transition-transform group-hover:scale-110", persona.bg)}>
<persona.icon className={cn("w-7 h-7", persona.color)} />
</div>
<span className={cn("text-[10px] font-black uppercase tracking-[0.2em] mb-2 block", persona.color)}>
{persona.tagline}
</span> </span>
</h2> <h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto"> {persona.title}
Se você se preocupa com o que coloca no prato, o NutriScan foi feito
pensando em você.
</p>
</div>
{/* Persona Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-16">
{/* Pais */}
<div className="bg-gradient-to-br from-pink-50 to-pink-100 dark:from-pink-900/20 dark:to-pink-800/20 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div className="w-16 h-16 bg-pink-100 dark:bg-pink-900/30 rounded-2xl flex items-center justify-center mb-4">
<Users className="w-8 h-8 text-pink-600 dark:text-pink-400" />
</div>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-3">
Pais e Mães
</h3> </h3>
<p className="text-gray-600 dark:text-gray-300 mb-4"> <p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed mb-8">
&ldquo;Se você já se preocupou com os ingredientes do lanche da {persona.description}
escola...&rdquo;
</p> </p>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li>• Identificar alérgenos</li> <ul className="space-y-3">
<li>• Escolher lanches saudáveis</li> {persona.features.map(f => (
<li>• Educar crianças sobre nutrição</li> <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> </ul>
</motion.div>
))}
</div> </div>
{/* Atletas */} {/* High-Impact Callout: Glassmorphism Card */}
<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"> <motion.div
<div className="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-2xl flex items-center justify-center mb-4"> initial={{ opacity: 0, scale: 0.98 }}
<Dumbbell className="w-8 h-8 text-green-600 dark:text-green-400" /> whileInView={{ opacity: 1, scale: 1 }}
</div> viewport={{ once: true }}
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-3"> className="relative rounded-[3rem] bg-gradient-to-br from-blue-600 to-emerald-600 p-1 md:p-px shadow-3xl"
Atletas >
<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-gray-600 dark:text-gray-300 mb-4"> <p className="text-blue-50 text-lg md:text-xl mb-12 max-w-4xl mx-auto leading-relaxed opacity-90">
&ldquo;Se você já precisou otimizar sua alimentação para Se você busca soberania sobre suas escolhas, o NutriScan é o seu protocolo de acesso à verdade. Sem interesses corporativos, apenas dados puros e auditáveis.
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>
</h3>
<p className="text-xl text-blue-100 mb-8 max-w-4xl mx-auto">
Se você já se pegou lendo rótulos sem entender nada, se já
desistiu de uma compra por não saber se aquele produto era
adequado, ou se simplesmente quer ter mais controle sobre sua
alimentação, o NutriScan foi feito pensando em você.
</p> </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">
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> </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>
); );
+144 -175
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}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.1 }}
viewport={{ once: true }}
className="group p-8 rounded-[2.5rem] bg-gray-50 dark:bg-gray-900/40 border border-gray-100 dark:border-gray-800 hover:border-blue-500/30 transition-all duration-500"
>
<div className={cn("w-14 h-14 rounded-2xl flex items-center justify-center mb-8 transition-transform group-hover:scale-110", item.bg)}>
<item.icon className={cn("w-7 h-7", item.color)} />
</div> </div>
<h3 className="text-3xl font-bold mb-4">100% Transparente</h3> <span className={cn("text-[10px] font-mono font-bold uppercase tracking-widest mb-2 block", item.color)}>
<p className="text-xl text-blue-100 max-w-3xl mx-auto mb-8"> {item.tag}
Todos os dados que você encontra aqui são colaborativos e abertos. </span>
O que você vê é o que existe: informações reais, fornecidas por <h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
uma comunidade global comprometida com a verdade. {item.title}
</p>
</div>
</div>
{/* Values Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-16">
{/* Open Data */}
<div className="bg-white dark:bg-gray-900 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-shadow">
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center mb-4">
<Eye className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
Dados Abertos
</h3> </h3>
<p className="text-gray-600 dark:text-gray-400 text-sm mb-4"> <p className="text-sm text-gray-500 dark:text-gray-400 leading-relaxed font-medium mb-6">
Todas as informações são colaborativas e verificadas pela {item.desc}
comunidade global.
</p> </p>
<div className="flex items-center text-blue-600 dark:text-blue-400 text-sm font-medium"> <div className="flex items-center gap-2 text-[10px] font-black text-gray-400 uppercase tracking-tighter group-hover:text-blue-500 transition-colors">
<CheckCircle className="w-4 h-4 mr-2" /> <CheckCircle2 size={12} className="text-emerald-500" /> System Verified
Sem informações escondidas
</div> </div>
</motion.div>
))}
</div> </div>
{/* Free Forever */} {/* OSS Deep Dive - O Diferencial Sênior */}
<div className="bg-white dark:bg-gray-900 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-shadow"> <motion.div
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-lg flex items-center justify-center mb-4"> initial={{ opacity: 0, scale: 0.98 }}
<Gift className="w-6 h-6 text-green-600 dark:text-green-400" /> whileInView={{ opacity: 1, scale: 1 }}
</div> className="relative rounded-[3rem] bg-slate-950 border border-slate-800 p-10 md:p-16 overflow-hidden shadow-3xl"
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3"> >
Gratuito Sempre <div className="absolute top-0 right-0 w-full h-full bg-[radial-gradient(circle_at_top_right,#1e293b,transparent)] opacity-50" />
</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="relative z-10 flex flex-col lg:flex-row gap-12 items-center">
<div className="bg-white dark:bg-gray-900 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-shadow"> <div className="flex-1 space-y-6">
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900/30 rounded-lg flex items-center justify-center mb-4"> <div className="flex items-center gap-4">
<Lock className="w-6 h-6 text-purple-600 dark:text-purple-400" /> <div className="p-3 bg-white/10 rounded-2xl backdrop-blur-md">
</div> <Github className="text-white w-8 h-8" />
<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>
<div> <div>
<h3 className="text-2xl font-bold text-gray-900 dark:text-white"> <h3 className="text-3xl font-black text-white tracking-tighter">Open Source Manifesto</h3>
Open Source & Open Data <p className="text-blue-400 font-mono text-xs uppercase tracking-widest mt-1">Status: Fully Auditability</p>
</h3> </div>
<p className="text-gray-600 dark:text-gray-400"> </div>
Transparência total do código e dados
<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>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> {/* GitHub Stats Mockup / Placeholder visual */}
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-xl p-4"> <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">
<h4 className="font-semibold text-blue-900 dark:text-blue-100 mb-2"> <div className="flex justify-between text-slate-500 border-b border-slate-800 pb-2">
Código Aberto <span>nutriscan-core / main</span>
</h4> <span className="text-emerald-500">online</span>
<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>
<div className="space-y-1">
<p className="text-blue-400">$ npm run audit</p>
<p className="text-slate-400"> Scanning dependencies...</p>
<p className="text-emerald-500"> 0 vulnerabilities found</p>
<p className="text-slate-400"> Data Integrity: 100% Verified</p>
</div>
<div className="absolute bottom-4 right-6 animate-pulse">
<div className="w-3 h-3 bg-emerald-500 rounded-full shadow-[0_0_10px_rgba(16,185,129,0.5)]" />
</div>
</div>
</div>
</motion.div>
<div className="bg-green-50 dark:bg-green-900/20 rounded-xl p-4"> {/* Final Trust Verification */}
<h4 className="font-semibold text-green-900 dark:text-green-100 mb-2"> <div className="mt-24 grid grid-cols-2 md:grid-cols-4 gap-12 opacity-50 grayscale hover:grayscale-0 transition-all duration-700">
Dados Colaborativos {/* Aqui você colocaria logotipos de bases de dados ou menções técnicas (ex: Open Food Facts, GitHub, Next.js, Radix UI) */}
</h4> <div className="text-center font-mono text-[10px] font-black uppercase tracking-[0.4em] text-gray-500">Open Food Facts</div>
<p className="text-sm text-green-700 dark:text-green-300"> <div className="text-center font-mono text-[10px] font-black uppercase tracking-[0.4em] text-gray-500">MIT License</div>
Base de dados construída e mantida por milhares de voluntários <div className="text-center font-mono text-[10px] font-black uppercase tracking-[0.4em] text-gray-500">Privacy First</div>
</p> <div className="text-center font-mono text-[10px] font-black uppercase tracking-[0.4em] text-gray-500">ODbL Data</div>
</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>
{/* Trust Indicators */}
<div className="mt-16 text-center">
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-8">
Por que confiar no NutriScan?
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400 mb-2">
2M+
</div>
<p className="text-gray-600 dark:text-gray-400">
Produtos verificados
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<div className="text-3xl font-bold text-green-600 dark:text-green-400 mb-2">
100+
</div>
<p className="text-gray-600 dark:text-gray-400">
Países cobertos
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<div className="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">
24/7
</div>
<p className="text-gray-600 dark:text-gray-400">
Disponível sempre
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<div className="text-3xl font-bold text-orange-600 dark:text-orange-400 mb-2">
100%
</div>
<p className="text-gray-600 dark:text-gray-400">Transparente</p>
</div>
</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 };
}
+126 -404
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 {
constructor(public message: string, public status?: number, public code?: string) {
super(message);
this.name = "NutriScanError";
}
}
/**
* Requisição Unificada com AbortController e Timeout
*/
async function makeApiRequest<T>(endpoint: string, base: string = CONFIG.BASE_URL): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000); // 8s timeout
try { try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, { const response = await fetch(`${base}${endpoint}`, {
headers: { headers: {
Accept: "application/json", "Accept": "application/json",
"User-Agent": CONFIG.USER_AGENT,
}, },
signal: controller.signal,
}); });
clearTimeout(timeoutId);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error( throw new NutriScanError(
errorData.error || `HTTP ${response.status}: ${response.statusText}` errorData.error || `Erro de Sistema: ${response.status}`,
response.status
); );
} }
const data = await response.json(); return await response.json();
return data;
} catch (error) { } catch (error) {
console.error("Erro na API:", error); if (error instanceof Error && error.name === 'AbortError') {
throw new Error( throw new NutriScanError("Tempo de resposta excedido (Timeout)", 408);
error instanceof Error }
? error.message throw error;
: "Erro desconhecido ao consultar a API"
);
} }
} }
/** /**
* Busca um produto específico por código de barras * Busca por Código de Barras (EAN/UPC)
*/ */
export async function searchByBarcode( export async function searchByBarcode(barcode: string): Promise<Product | null> {
barcode: string
): Promise<Product | null> {
try {
// Limpar código de barras (remover espaços e caracteres não numéricos)
const cleanBarcode = barcode.replace(/\D/g, ""); const cleanBarcode = barcode.replace(/\D/g, "");
if (cleanBarcode.length < 8 || cleanBarcode.length > 13) { if (cleanBarcode.length < 8 || cleanBarcode.length > 13) {
throw new Error("Código de barras deve ter entre 8 e 13 dígitos"); console.warn(`[NutriScan] Código inválido rejeitado: ${cleanBarcode}`);
return null;
} }
const data = await makeApiRequest<Product>(`/api/products/${cleanBarcode}`); try {
return await makeApiRequest<Product>(`/api/products/${cleanBarcode}`);
return data || null;
} catch (error) { } catch (error) {
console.error("Erro ao buscar produto por código:", 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 {
if (!query.trim()) {
throw new Error("Termo de busca não pode estar vazio");
}
// Construir parâmetros da URL para API Route
const params = new URLSearchParams({ const params = new URLSearchParams({
q: query.trim(), q: query.trim(),
page: page.toString(), page: page.toString(),
page_size: pageSize.toString(), page_size: pageSize.toString(),
}); });
// Adicionar filtro de país se especificado if (country) params.append("country", country);
if (country) {
params.append("country", country);
}
const data = await makeApiRequest<SearchResult>( return makeApiRequest<SearchResult>(`/api/search?${params.toString()}`);
`/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,
loadedProducts: number
) => void
): Promise<{ products: Product[]; totalCount: number; actualPages: number }> {
try {
if (!query.trim()) {
throw new Error("Termo de busca não pode estar vazio");
}
const allProducts: Product[] = []; const allProducts: Product[] = [];
let currentPage = 1; let currentPage = 1;
let hasMore = true; 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) { while (hasMore && currentPage <= maxPages) {
try { if (onProgress) onProgress(currentPage, maxPages, allProducts.length);
console.log(`Carregando página ${currentPage}...`);
// Chamar callback de progresso se fornecido const result = await searchByName(query, country, currentPage, CONFIG.MAX_PAGE_SIZE);
if (onProgress) {
onProgress(currentPage, maxPages, allProducts.length);
}
const result = await searchByName( if (result.products?.length > 0) {
query,
country,
currentPage,
pageSize
);
if (result.products && result.products.length > 0) {
allProducts.push(...result.products); allProducts.push(...result.products);
totalCount = result.count || allProducts.length; hasMore = result.products.length === CONFIG.MAX_PAGE_SIZE;
currentPage++;
// Se retornou menos produtos que o pageSize, não há mais páginas // Rate Limiting Industrial
hasMore = result.products.length === pageSize; await new Promise(r => setTimeout(r, CONFIG.RATE_LIMIT_MS));
console.log(
`Página ${currentPage}: ${result.products.length} produtos (Total: ${allProducts.length})`
);
} else { } else {
hasMore = false; 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 { return {
products: allProducts, products: allProducts,
totalCount: totalCount || allProducts.length, totalCount: allProducts.length,
actualPages, actualPages: currentPage - 1
};
} catch (error) {
console.error("Erro ao buscar todos os resultados:", error);
throw error;
}
}
/**
* Busca produtos por categoria
*/
export async function searchByCategory(
category: string,
country?: string,
page: number = 1,
pageSize: number = 20
): Promise<SearchResult> {
try {
const params = new URLSearchParams({
categories_tags_en: category,
page_size: pageSize.toString(),
page: page.toString(),
json: "true",
fields: [
"code",
"product_name",
"brands",
"categories",
"image_url",
"image_small_url",
"nutrition_grades",
"nova_group",
].join(","),
});
if (country) {
params.append("countries_tags_en", country);
}
// Chamada direta à API externa (temporária)
const response = await fetch(
`https://world.openfoodfacts.org/cgi/search.pl?${params.toString()}`,
{
headers: {
"User-Agent": "NutriScan/1.0 (https://nutriscan.com.br)",
Accept: "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Erro ao buscar produtos por categoria:", error);
throw error;
}
}
/**
* Obtém categorias populares
*/
export async function getPopularCategories(
country?: string
): Promise<string[]> {
try {
const params = new URLSearchParams({
json: "true",
fields: "categories_tags",
});
if (country) {
params.append("countries_tags_en", country);
}
// Chamada direta à API externa (temporária)
const response = await fetch(
`https://world.openfoodfacts.org/cgi/search.pl?${params.toString()}`,
{
headers: {
"User-Agent": "NutriScan/1.0 (https://nutriscan.com.br)",
Accept: "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return (
data.facets?.categories_tags
?.slice(0, 20)
.map((cat: { name: string; count: number }) => cat.name) || []
);
} catch (error) {
console.error("Erro ao obter categorias:", error);
return [];
}
}
/**
* Valida se um código de barras tem formato válido
*/
export function isValidBarcode(barcode: string): boolean {
const cleanBarcode = barcode.replace(/\D/g, "");
return cleanBarcode.length >= 8 && cleanBarcode.length <= 13;
}
/**
* Detecta o tipo de busca baseado na entrada do usuário
*/
export function detectSearchType(query: string): "barcode" | "name" {
const cleanQuery = query.trim();
// Se contém apenas números e tem 8-13 dígitos, é código de barras
if (/^\d{8,13}$/.test(cleanQuery)) {
return "barcode";
}
// Caso contrário, é busca por nome
return "name";
}
/**
* Formata dados nutricionais para exibição
*/
export function formatNutritionData(product: Product) {
const nutriments = product.nutriments;
if (!nutriments) return null;
// Função auxiliar para converter e formatar valores
const formatValue = (
value: number | undefined,
decimals: number = 1
): string => {
if (value === undefined || value === null) return "N/A";
const numValue = typeof value === "string" ? parseFloat(value) : value;
if (isNaN(numValue)) return "N/A";
return decimals === 0
? `${Math.round(numValue)}`
: `${numValue.toFixed(decimals)}`;
};
return {
energy: nutriments.energy_100g
? `${formatValue(nutriments.energy_100g, 0)} kcal`
: "N/A",
fat: nutriments.fat_100g ? `${formatValue(nutriments.fat_100g)}g` : "N/A",
carbohydrates: nutriments.carbohydrates_100g
? `${formatValue(nutriments.carbohydrates_100g)}g`
: "N/A",
proteins: nutriments.proteins_100g
? `${formatValue(nutriments.proteins_100g)}g`
: "N/A",
sugars: nutriments.sugars_100g
? `${formatValue(nutriments.sugars_100g)}g`
: "N/A",
fiber: nutriments.fiber_100g
? `${formatValue(nutriments.fiber_100g)}g`
: "N/A",
sodium: nutriments.sodium_100g
? `${formatValue(nutriments.sodium_100g)}g`
: "N/A",
}; };
} }
/** /**
* 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"
}, },