feat: complete NutriScan redesign
This commit is contained in:
1 parent
c1fe4af997
commit
f71cd167b3
163 files changed
+30802
-14169
No files matched your search
@@ -5,9 +5,9 @@ const USER_AGENT = "NutriScan/1.0 (https://nutriscan.com.br)";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { barcode: string } }
|
||||
{ params }: { params: Promise<{ barcode: string }> }
|
||||
) {
|
||||
const { barcode } = params;
|
||||
const { barcode } = await params;
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const cacheOnly = searchParams.get("cache_only") === "true";
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function CardList({ listaId }: CardListProps) {
|
||||
|
||||
if (produtos.length === 0) {
|
||||
return (
|
||||
<div className="text-center p-8 text-gray-500">
|
||||
<div className="text-center p-8" style={{ color: 'var(--fg-muted)' }}>
|
||||
Nenhum produto cadastrado ainda.
|
||||
</div>
|
||||
);
|
||||
@@ -47,7 +47,7 @@ export async function CardList({ listaId }: CardListProps) {
|
||||
|
||||
{produtosComprados.length > 0 && (
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold mb-4 text-green-700">
|
||||
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--accent)' }}>
|
||||
Produtos Comprados
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
|
||||
@@ -42,7 +42,6 @@ export function DeleteButton({ produtoId, produtoNome, listaId }: DeleteButtonPr
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="text-white hover:bg-red-600"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -59,7 +58,7 @@ export function DeleteButton({ produtoId, produtoNome, listaId }: DeleteButtonPr
|
||||
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? 'Excluindo...' : 'Excluir'}
|
||||
|
||||
+81
-126
@@ -1,8 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Heart, Check, Code } from "lucide-react";
|
||||
import { Heart, Check } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface DonateCardProps {
|
||||
className?: string;
|
||||
@@ -10,141 +9,97 @@ interface DonateCardProps {
|
||||
}
|
||||
|
||||
export function DonateCard({ className = "", variant = "default" }: DonateCardProps) {
|
||||
// Layout mobile horizontal
|
||||
/* ── Mobile horizontal ───────────────────────────────────────────────── */
|
||||
if (variant === "mobile") {
|
||||
return (
|
||||
<Card
|
||||
className={`donate-card group relative overflow-hidden hover:shadow-lg transition-all duration-200 bg-gradient-to-r from-blue-50 to-green-50 dark:from-blue-900/20 dark:to-green-900/20 border border-blue-200 dark:border-blue-800 ${className}`}
|
||||
<article
|
||||
className={cn(
|
||||
"card-warm rounded-xl overflow-hidden flex",
|
||||
"border-t-2 border-t-[var(--accent)]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-3 p-3">
|
||||
{/* Ícone - tamanho ajustado */}
|
||||
<div className="w-16 h-16 flex-shrink-0 relative bg-gradient-to-br from-blue-500 to-green-500 rounded-lg overflow-hidden flex items-center justify-center">
|
||||
<Heart className="w-8 h-8 text-white animate-pulse" />
|
||||
</div>
|
||||
|
||||
{/* Conteúdo - melhor espaçamento */}
|
||||
<div className="flex-1 min-w-0 flex flex-col">
|
||||
<div className="flex-1">
|
||||
{/* Título */}
|
||||
<h3 className="font-bold text-sm text-gray-900 dark:text-white mb-1">
|
||||
Ajude o Projeto
|
||||
</h3>
|
||||
|
||||
{/* Texto persuasivo */}
|
||||
<p className="text-xs text-gray-600 dark:text-gray-300 mb-2">
|
||||
Mantenha este serviço gratuito e acessível para todos
|
||||
</p>
|
||||
|
||||
{/* Benefício rápido */}
|
||||
<div className="flex items-center gap-1 text-xs text-green-600 dark:text-green-400">
|
||||
<Check className="w-3 h-3" />
|
||||
<span>Qualquer valor faz diferença</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botão CTA - menor */}
|
||||
<a
|
||||
href="https://buymeacoffee.com/evandrotruuta"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-2 inline-block"
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full bg-gradient-to-r from-blue-600 to-green-600 hover:from-blue-700 hover:to-green-700 text-white text-xs font-medium h-8"
|
||||
>
|
||||
<Heart className="w-3 h-3 mr-1" />
|
||||
Doar
|
||||
</Button>
|
||||
</a>
|
||||
<div className="flex-1 p-4 flex flex-col justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="font-sans font-semibold text-[var(--fg-primary)] text-sm mb-1">
|
||||
Apoie o Projeto
|
||||
</h3>
|
||||
<p className="label text-[var(--fg-muted)]">
|
||||
Mantenha o NutriScan gratuito e independente.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="https://buymeacoffee.com/evandrotruuta"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-accent self-start py-2 px-4 text-xs"
|
||||
>
|
||||
<Heart className="w-3 h-3" strokeWidth={1.5} />
|
||||
Contribuir
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
// Layout desktop padrão
|
||||
/* ── Desktop card ────────────────────────────────────────────────────── */
|
||||
return (
|
||||
<Card
|
||||
className={`donate-card group relative overflow-hidden hover:shadow-2xl transition-all duration-300 hover:-translate-y-1 bg-gradient-to-br from-blue-50 to-green-50 dark:from-blue-900/20 dark:to-green-900/20 border border-blue-200 dark:border-blue-800 ${className}`}
|
||||
<article
|
||||
className={cn(
|
||||
"card-warm hover-lift rounded-xl overflow-hidden flex flex-col h-full",
|
||||
"border-t-2 border-t-[var(--accent)]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Header com ícone animado */}
|
||||
<div className="relative h-32 bg-gradient-to-br from-blue-500 to-green-500 overflow-hidden">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Heart className="w-16 h-16 text-white animate-pulse" />
|
||||
<div className="p-8 flex flex-col flex-1 gap-6">
|
||||
{/* Eyebrow */}
|
||||
<span className="label text-[var(--accent)]">Apoie o Projeto</span>
|
||||
|
||||
{/* Headline */}
|
||||
<div>
|
||||
<h3 className="font-display text-display-sm text-[var(--fg-primary)] leading-tight mb-2">
|
||||
Transparência alimentar,{" "}
|
||||
<em className="font-display-i">para todos.</em>
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--fg-secondary)] leading-relaxed">
|
||||
O NutriScan opera sem anúncios e sem venda de dados. Sua
|
||||
contribuição mantém o projeto vivo.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Overlay sutil */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent" />
|
||||
|
||||
{/* Badge de destaque */}
|
||||
<div className="absolute top-3 right-3">
|
||||
<div className="bg-white/90 dark:bg-gray-800/90 text-blue-600 dark:text-blue-400 rounded-full px-3 py-1 text-xs font-bold">
|
||||
💛 Apoie
|
||||
</div>
|
||||
|
||||
{/* Benefits */}
|
||||
<ul className="space-y-2.5">
|
||||
{[
|
||||
"Servidores sempre disponíveis",
|
||||
"Novas funcionalidades",
|
||||
"Acesso gratuito para todos",
|
||||
].map((item) => (
|
||||
<li key={item} className="flex items-center gap-2.5">
|
||||
<Check
|
||||
className="w-3.5 h-3.5 text-[var(--accent)] shrink-0"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<span className="text-sm text-[var(--fg-secondary)]">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="mt-auto pt-2">
|
||||
<a
|
||||
href="https://buymeacoffee.com/evandrotruuta"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-accent w-full justify-center py-3.5 text-sm"
|
||||
>
|
||||
<Heart className="w-4 h-4" strokeWidth={1.5} />
|
||||
Contribuir agora
|
||||
</a>
|
||||
<p className="label text-center text-[var(--fg-faint)] mt-3">
|
||||
Qualquer valor ajuda
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="p-5 space-y-4">
|
||||
{/* Título principal */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-bold text-lg leading-tight text-gray-900 dark:text-white">
|
||||
Apoie a Transparência Alimentar
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Projeto mantido por um estudante dedicado
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Benefícios */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Check className="w-4 h-4 text-green-500 flex-shrink-0" />
|
||||
<span className="text-gray-600 dark:text-gray-400">Servidores sempre disponíveis</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Check className="w-4 h-4 text-green-500 flex-shrink-0" />
|
||||
<span className="text-gray-600 dark:text-gray-400">Novas funcionalidades</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Check className="w-4 h-4 text-green-500 flex-shrink-0" />
|
||||
<span className="text-gray-600 dark:text-gray-400">Acesso gratuito para todos</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mensagem humanizada */}
|
||||
<div className="bg-white/50 dark:bg-gray-800/50 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Code className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
Desenvolvido com amor
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
Cada doação ajuda a manter este projeto vivo e acessível
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Botão CTA principal */}
|
||||
<a
|
||||
href="https://buymeacoffee.com/evandrotruuta"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block"
|
||||
>
|
||||
<Button
|
||||
className="w-full bg-gradient-to-r from-blue-600 to-green-600 hover:from-blue-700 hover:to-green-700 text-white font-medium h-12 text-base"
|
||||
>
|
||||
<Heart className="w-5 h-5 mr-2" />
|
||||
Doar Qualquer Valor
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
{/* Texto de incentivo */}
|
||||
<p className="text-center text-xs text-gray-500 dark:text-gray-400">
|
||||
Qualquer valor faz diferença 💛
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
+221
-398
@@ -120,106 +120,37 @@ export function FilterSidebar({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`w-full max-w-sm space-y-6 ${className}`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="h-5 w-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold">Filtros</h3>
|
||||
<div className={`w-full space-y-5 ${className}`}>
|
||||
|
||||
{/* ── Header ────────────────────────────────────────────────────────── */}
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", paddingBottom: 12, borderBottom: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Filter style={{ width: 15, height: 15, color: "var(--accent, oklch(52% 0.11 155))" }} strokeWidth={1.75} />
|
||||
<span style={{ fontFamily: "ui-monospace, monospace", fontSize: 10, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--fg-muted, #9a8870)", fontWeight: 600 }}>
|
||||
Filtros avançados
|
||||
</span>
|
||||
</div>
|
||||
{hasActiveFilters && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
<button
|
||||
onClick={onClearFilters}
|
||||
className="text-xs text-muted-foreground hover:text-foreground"
|
||||
style={{ display: "inline-flex", alignItems: "center", gap: 4, fontSize: 11, color: "var(--accent, oklch(52% 0.11 155))", background: "none", border: "none", cursor: "pointer", fontFamily: "inherit", letterSpacing: "0.03em" }}
|
||||
>
|
||||
<X className="h-4 w-4 mr-1" />
|
||||
Limpar
|
||||
</Button>
|
||||
<X style={{ width: 12, height: 12 }} strokeWidth={2} />
|
||||
Limpar tudo
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Resultados */}
|
||||
{/* ── Result count ──────────────────────────────────────────────────── */}
|
||||
{totalResults > 0 && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{totalResults.toLocaleString()} produto{totalResults !== 1 ? "s" : ""}{" "}
|
||||
encontrado{totalResults !== 1 ? "s" : ""}
|
||||
<div style={{ fontFamily: "ui-monospace, monospace", fontSize: 11, color: "var(--fg-muted, #9a8870)", letterSpacing: "0.04em" }}>
|
||||
{totalResults.toLocaleString()} produto{totalResults !== 1 ? "s" : ""} encontrado{totalResults !== 1 ? "s" : ""}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Controles de Visualização e Cache */}
|
||||
<div className="space-y-4">
|
||||
{/* Modo de Visualização */}
|
||||
{onViewModeChange && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Modo de visualização</div>
|
||||
<div className="flex items-center border border-gray-100 dark:border-gray-800 rounded-lg">
|
||||
<Button
|
||||
variant={viewMode === "grid" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => onViewModeChange("grid")}
|
||||
className="rounded-r-none flex-1"
|
||||
>
|
||||
<Grid className="w-4 h-4 mr-2" />
|
||||
Grade
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === "list" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => onViewModeChange("list")}
|
||||
className="rounded-l-none flex-1"
|
||||
>
|
||||
<List className="w-4 h-4 mr-2" />
|
||||
Lista
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Controles de Cache */}
|
||||
{(onRefresh || onClearCache) && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Controles</div>
|
||||
<div className="flex gap-2">
|
||||
{onRefresh && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading}
|
||||
className="flex-1"
|
||||
title="Atualizar busca"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`w-4 h-4 mr-2 ${
|
||||
isLoading ? "animate-spin" : ""
|
||||
}`}
|
||||
/>
|
||||
Atualizar
|
||||
</Button>
|
||||
)}
|
||||
{onClearCache && isCached && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onClearCache}
|
||||
className="flex-1"
|
||||
title="Limpar cache e refazer busca"
|
||||
>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Limpar Cache
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Filtros Ativos */}
|
||||
{/* ── Active filter badges ───────────────────────────────────────────── */}
|
||||
{onRemoveFilter && hasActiveFilters && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Filtros ativos</div>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
|
||||
<FilterBadges
|
||||
filters={filters}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
@@ -228,183 +159,155 @@ export function FilterSidebar({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Accordion
|
||||
type="multiple"
|
||||
defaultValue={["sorting"]}
|
||||
className="space-y-4"
|
||||
>
|
||||
{/* Ordenação */}
|
||||
<AccordionItem
|
||||
value="sorting"
|
||||
className="border border-gray-100 dark:border-gray-800 rounded-lg"
|
||||
>
|
||||
<AccordionTrigger className="px-4 py-3 hover:no-underline">
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowUpDown className="h-4 w-4 text-indigo-500" />
|
||||
<span className="font-medium">Ordenação</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-4">
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium mb-3">Ordenar por</div>
|
||||
{onSortChange && (
|
||||
<SortDropdown
|
||||
value={sortBy}
|
||||
onValueChange={onSortChange}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
{/* ── Cache / Refresh controls ──────────────────────────────────────── */}
|
||||
{(onRefresh || onClearCache) && (
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
{onRefresh && (
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading}
|
||||
style={{ flex: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 6, padding: "7px 0", border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 8, background: "transparent", fontSize: 12, color: "var(--fg-secondary, #5e4e3c)", fontFamily: "inherit", cursor: isLoading ? "not-allowed" : "pointer", opacity: isLoading ? 0.5 : 1 }}
|
||||
>
|
||||
<RefreshCw style={{ width: 12, height: 12 }} className={isLoading ? "animate-spin" : ""} />
|
||||
Atualizar
|
||||
</button>
|
||||
)}
|
||||
{onClearCache && isCached && (
|
||||
<button
|
||||
onClick={onClearCache}
|
||||
style={{ flex: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 6, padding: "7px 0", border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 8, background: "transparent", fontSize: 12, color: "var(--fg-secondary, #5e4e3c)", fontFamily: "inherit", cursor: "pointer" }}
|
||||
>
|
||||
<X style={{ width: 12, height: 12 }} />
|
||||
Limpar cache
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Nutri-Score */}
|
||||
<AccordionItem
|
||||
value="nutrition"
|
||||
className="border border-gray-100 dark:border-gray-800 rounded-lg"
|
||||
>
|
||||
<AccordionTrigger className="px-4 py-3 hover:no-underline">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="h-4 w-4 text-green-500" />
|
||||
<span className="font-medium">Classificação Nutricional</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-4">
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium mb-3">Nutri-Score</div>
|
||||
<div className="space-y-2">
|
||||
{NUTRITION_GRADES.map((grade) => (
|
||||
<div
|
||||
key={grade.grade}
|
||||
className="flex items-center space-x-3"
|
||||
>
|
||||
<Checkbox
|
||||
id={`nutrition-${grade.grade}`}
|
||||
checked={
|
||||
filters.nutritionGrades?.includes(grade.grade) || false
|
||||
}
|
||||
onCheckedChange={(checked) =>
|
||||
updateArrayFilter(
|
||||
"nutritionGrades",
|
||||
grade.grade,
|
||||
checked as boolean
|
||||
)
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`nutrition-${grade.grade}`}
|
||||
className="flex items-center gap-2 text-sm cursor-pointer"
|
||||
>
|
||||
<Badge className={`${grade.color} text-white text-xs`}>
|
||||
{grade.label}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground">
|
||||
{grade.description}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<div className="text-sm font-medium mb-3">
|
||||
NOVA (Processamento)
|
||||
</div>
|
||||
<RadioGroup
|
||||
value={filters.novaGroups?.[0]?.toString() || ""}
|
||||
onValueChange={(value) =>
|
||||
updateFilter(
|
||||
"novaGroups",
|
||||
value ? [parseInt(value) as 1 | 2 | 3 | 4] : []
|
||||
)
|
||||
}
|
||||
className="space-y-2"
|
||||
{/* ── View mode ─────────────────────────────────────────────────────── */}
|
||||
{onViewModeChange && (
|
||||
<div>
|
||||
<div style={{ fontFamily: "ui-monospace, monospace", fontSize: 10, letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--fg-muted, #9a8870)", marginBottom: 8 }}>Visualização</div>
|
||||
<div style={{ display: "flex", border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 8, overflow: "hidden" }}>
|
||||
{(["grid", "list"] as const).map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
onClick={() => onViewModeChange(mode)}
|
||||
style={{ flex: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 6, padding: "7px 0", background: viewMode === mode ? "var(--accent-muted, oklch(92% 0.04 155))" : "transparent", color: viewMode === mode ? "var(--accent, oklch(52% 0.11 155))" : "var(--fg-muted, #9a8870)", border: "none", fontSize: 12, cursor: "pointer", fontFamily: "inherit", fontWeight: viewMode === mode ? 500 : 400 }}
|
||||
>
|
||||
{NOVA_GROUPS.map((group) => (
|
||||
<div
|
||||
key={group.group}
|
||||
className="flex items-center space-x-3"
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={group.group.toString()}
|
||||
id={`nova-${group.group}`}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`nova-${group.group}`}
|
||||
className="flex items-center gap-2 text-sm cursor-pointer flex-1"
|
||||
>
|
||||
<Badge className={`${group.color} text-white text-xs`}>
|
||||
{group.label}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{group.description}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
{mode === "grid" ? <Grid style={{ width: 13, height: 13 }} /> : <List style={{ width: 13, height: 13 }} />}
|
||||
{mode === "grid" ? "Grade" : "Lista"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Accordion sections ────────────────────────────────────────────── */}
|
||||
<Accordion type="multiple" defaultValue={["nutrition"]} className="space-y-1">
|
||||
|
||||
{/* Ordenação */}
|
||||
{onSortChange && (
|
||||
<AccordionItem value="sorting" style={{ border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 10, overflow: "hidden" }}>
|
||||
<AccordionTrigger className="px-4 py-3 hover:no-underline" style={{ background: "transparent" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<ArrowUpDown style={{ width: 14, height: 14, color: "var(--accent, oklch(52% 0.11 155))" }} strokeWidth={1.75} />
|
||||
<span style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-primary, #1e1810)" }}>Ordenação</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-4" style={{ borderTop: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
||||
<div style={{ paddingTop: 12 }}>
|
||||
<SortDropdown value={sortBy} onValueChange={onSortChange} className="w-full" />
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{/* Nutri-Score & NOVA */}
|
||||
<AccordionItem value="nutrition" style={{ border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 10, overflow: "hidden" }}>
|
||||
<AccordionTrigger className="px-4 py-3 hover:no-underline">
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Zap style={{ width: 14, height: 14, color: "var(--accent, oklch(52% 0.11 155))" }} strokeWidth={1.75} />
|
||||
<span style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-primary, #1e1810)" }}>Classificação Nutricional</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-4" style={{ borderTop: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
||||
<div style={{ paddingTop: 12, display: "flex", flexDirection: "column", gap: 14 }}>
|
||||
<div>
|
||||
<div style={{ fontFamily: "ui-monospace, monospace", fontSize: 10, letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--fg-muted, #9a8870)", marginBottom: 10 }}>Nutri-Score</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{NUTRITION_GRADES.map((grade) => (
|
||||
<div key={grade.grade} style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<Checkbox
|
||||
id={`nutrition-${grade.grade}`}
|
||||
checked={filters.nutritionGrades?.includes(grade.grade) || false}
|
||||
onCheckedChange={(checked) => updateArrayFilter("nutritionGrades", grade.grade, checked as boolean)}
|
||||
/>
|
||||
<label htmlFor={`nutrition-${grade.grade}`} style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, cursor: "pointer", flex: 1 }}>
|
||||
<Badge className={`${grade.color} text-white text-xs`}>{grade.label}</Badge>
|
||||
<span style={{ color: "var(--fg-secondary, #5e4e3c)", fontSize: 12 }}>{grade.description}</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 1, background: "var(--border-subtle, #e8e0d0)" }} />
|
||||
|
||||
<div>
|
||||
<div style={{ fontFamily: "ui-monospace, monospace", fontSize: 10, letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--fg-muted, #9a8870)", marginBottom: 10 }}>NOVA — Nível de processamento</div>
|
||||
<RadioGroup
|
||||
value={filters.novaGroups?.[0]?.toString() || ""}
|
||||
onValueChange={(value) => updateFilter("novaGroups", value ? [parseInt(value) as 1 | 2 | 3 | 4] : [])}
|
||||
className="space-y-2"
|
||||
>
|
||||
{NOVA_GROUPS.map((group) => (
|
||||
<div key={group.group} style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<RadioGroupItem value={group.group.toString()} id={`nova-${group.group}`} />
|
||||
<label htmlFor={`nova-${group.group}`} style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, cursor: "pointer", flex: 1 }}>
|
||||
<Badge className={`${group.color} text-white text-xs`}>{group.label}</Badge>
|
||||
<span style={{ color: "var(--fg-secondary, #5e4e3c)", fontSize: 11 }}>{group.description}</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Categorias */}
|
||||
<AccordionItem
|
||||
value="categories"
|
||||
className="border border-gray-100 dark:border-gray-800 rounded-lg"
|
||||
>
|
||||
<AccordionItem value="categories" style={{ border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 10, overflow: "hidden" }}>
|
||||
<AccordionTrigger className="px-4 py-3 hover:no-underline">
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutGrid className="h-4 w-4 text-blue-500" />
|
||||
<span className="font-medium">Categorias</span>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<LayoutGrid style={{ width: 14, height: 14, color: "var(--accent, oklch(52% 0.11 155))" }} strokeWidth={1.75} />
|
||||
<span style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-primary, #1e1810)" }}>Categorias</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-4">
|
||||
<div className="space-y-3">
|
||||
<AccordionContent className="px-4 pb-4" style={{ borderTop: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
||||
<div style={{ paddingTop: 12, display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<Input
|
||||
placeholder="Buscar categorias..."
|
||||
placeholder="Buscar categorias…"
|
||||
value={searchCategories}
|
||||
onChange={(e) => setSearchCategories(e.target.value)}
|
||||
className="h-8"
|
||||
style={{ height: 34, fontSize: 13, borderColor: "var(--border-subtle, #e8e0d0)", background: "var(--bg-base, #f9f6f0)" }}
|
||||
/>
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto">
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8, maxHeight: 192, overflowY: "auto" }}>
|
||||
{POPULAR_CATEGORIES_WITH_ICONS.filter(
|
||||
(cat) =>
|
||||
cat.label
|
||||
.toLowerCase()
|
||||
.includes(searchCategories.toLowerCase()) ||
|
||||
cat.value
|
||||
.toLowerCase()
|
||||
.includes(searchCategories.toLowerCase())
|
||||
cat.label.toLowerCase().includes(searchCategories.toLowerCase()) ||
|
||||
cat.value.toLowerCase().includes(searchCategories.toLowerCase())
|
||||
).map((category) => {
|
||||
const IconComponent = LucideIcons[
|
||||
category.icon as keyof typeof LucideIcons
|
||||
] as React.ComponentType<{ className?: string }> | undefined;
|
||||
const IconComponent = LucideIcons[category.icon as keyof typeof LucideIcons] as React.ComponentType<{ style?: React.CSSProperties }> | undefined;
|
||||
return (
|
||||
<div
|
||||
key={category.value}
|
||||
className="flex items-center space-x-3"
|
||||
>
|
||||
<div key={category.value} style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<Checkbox
|
||||
id={`category-${category.value}`}
|
||||
checked={
|
||||
filters.categories?.includes(category.value) || false
|
||||
}
|
||||
onCheckedChange={(checked) =>
|
||||
updateArrayFilter(
|
||||
"categories",
|
||||
category.value,
|
||||
checked as boolean
|
||||
)
|
||||
}
|
||||
checked={filters.categories?.includes(category.value) || false}
|
||||
onCheckedChange={(checked) => updateArrayFilter("categories", category.value, checked as boolean)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`category-${category.value}`}
|
||||
className="text-sm cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
{IconComponent && (
|
||||
<IconComponent className="w-4 h-4 text-gray-500" />
|
||||
)}
|
||||
<label htmlFor={`category-${category.value}`} style={{ display: "flex", alignItems: "center", gap: 7, fontSize: 13, cursor: "pointer", color: "var(--fg-secondary, #5e4e3c)" }}>
|
||||
{IconComponent && <IconComponent style={{ width: 13, height: 13, color: "var(--fg-muted, #9a8870)" } as React.CSSProperties} />}
|
||||
{category.label}
|
||||
</label>
|
||||
</div>
|
||||
@@ -416,70 +319,53 @@ export function FilterSidebar({
|
||||
</AccordionItem>
|
||||
|
||||
{/* Marcas */}
|
||||
<AccordionItem
|
||||
value="brands"
|
||||
className="border border-gray-100 dark:border-gray-800 rounded-lg"
|
||||
>
|
||||
<AccordionItem value="brands" style={{ border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 10, overflow: "hidden" }}>
|
||||
<AccordionTrigger className="px-4 py-3 hover:no-underline">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag className="h-4 w-4 text-purple-500" />
|
||||
<span className="font-medium">Marcas</span>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Tag style={{ width: 14, height: 14, color: "var(--accent, oklch(52% 0.11 155))" }} strokeWidth={1.75} />
|
||||
<span style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-primary, #1e1810)" }}>Marcas</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-4">
|
||||
<div className="space-y-3">
|
||||
<AccordionContent className="px-4 pb-4" style={{ borderTop: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
||||
<div style={{ paddingTop: 12, display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<Input
|
||||
placeholder="Buscar marcas..."
|
||||
placeholder="Nome da marca…"
|
||||
value={searchBrands}
|
||||
onChange={(e) => setSearchBrands(e.target.value)}
|
||||
className="h-8"
|
||||
style={{ height: 34, fontSize: 13, borderColor: "var(--border-subtle, #e8e0d0)", background: "var(--bg-base, #f9f6f0)" }}
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Digite o nome da marca para buscar
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: "var(--fg-muted, #9a8870)", letterSpacing: "0.02em" }}>Digite para filtrar por marca</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Alérgenos */}
|
||||
<AccordionItem
|
||||
value="allergens"
|
||||
className="border border-gray-100 dark:border-gray-800 rounded-lg"
|
||||
>
|
||||
<AccordionItem value="allergens" style={{ border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 10, overflow: "hidden" }}>
|
||||
<AccordionTrigger className="px-4 py-3 hover:no-underline">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-yellow-500" />
|
||||
<span className="font-medium">Alérgenos</span>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<AlertTriangle style={{ width: 14, height: 14, color: "var(--accent, oklch(52% 0.11 155))" }} strokeWidth={1.75} />
|
||||
<span style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-primary, #1e1810)" }}>Alérgenos</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-4">
|
||||
<div className="space-y-3">
|
||||
<AccordionContent className="px-4 pb-4" style={{ borderTop: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
||||
<div style={{ paddingTop: 12, display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<Input
|
||||
placeholder="Buscar alérgenos..."
|
||||
placeholder="Buscar alérgenos…"
|
||||
value={searchAllergens}
|
||||
onChange={(e) => setSearchAllergens(e.target.value)}
|
||||
className="h-8"
|
||||
style={{ height: 34, fontSize: 13, borderColor: "var(--border-subtle, #e8e0d0)", background: "var(--bg-base, #f9f6f0)" }}
|
||||
/>
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto">
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8, maxHeight: 192, overflowY: "auto" }}>
|
||||
{COMMON_ALLERGENS.filter((allergen) =>
|
||||
allergen.toLowerCase().includes(searchAllergens.toLowerCase())
|
||||
).map((allergen) => (
|
||||
<div key={allergen} className="flex items-center space-x-3">
|
||||
<div key={allergen} style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<Checkbox
|
||||
id={`allergen-${allergen}`}
|
||||
checked={filters.allergens?.includes(allergen) || false}
|
||||
onCheckedChange={(checked) =>
|
||||
updateArrayFilter(
|
||||
"allergens",
|
||||
allergen,
|
||||
checked as boolean
|
||||
)
|
||||
}
|
||||
onCheckedChange={(checked) => updateArrayFilter("allergens", allergen, checked as boolean)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`allergen-${allergen}`}
|
||||
className="text-sm cursor-pointer capitalize"
|
||||
>
|
||||
<label htmlFor={`allergen-${allergen}`} style={{ fontSize: 13, cursor: "pointer", textTransform: "capitalize", color: "var(--fg-secondary, #5e4e3c)" }}>
|
||||
{allergen}
|
||||
</label>
|
||||
</div>
|
||||
@@ -490,32 +376,26 @@ export function FilterSidebar({
|
||||
</AccordionItem>
|
||||
|
||||
{/* Aditivos */}
|
||||
<AccordionItem
|
||||
value="additives"
|
||||
className="border border-gray-100 dark:border-gray-800 rounded-lg"
|
||||
>
|
||||
<AccordionItem value="additives" style={{ border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 10, overflow: "hidden" }}>
|
||||
<AccordionTrigger className="px-4 py-3 hover:no-underline">
|
||||
<div className="flex items-center gap-2">
|
||||
<Beaker className="h-4 w-4 text-orange-500" />
|
||||
<span className="font-medium">Aditivos</span>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Beaker style={{ width: 14, height: 14, color: "var(--accent, oklch(52% 0.11 155))" }} strokeWidth={1.75} />
|
||||
<span style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-primary, #1e1810)" }}>Aditivos</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="no-additives" className="text-sm font-medium">
|
||||
<AccordionContent className="px-4 pb-4" style={{ borderTop: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
||||
<div style={{ paddingTop: 12, display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<label htmlFor="no-additives" style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-secondary, #5e4e3c)", cursor: "pointer" }}>
|
||||
Sem aditivos
|
||||
</label>
|
||||
<Switch
|
||||
id="no-additives"
|
||||
checked={filters.noAdditives || false}
|
||||
onCheckedChange={(checked) =>
|
||||
updateFilter("noAdditives", checked)
|
||||
}
|
||||
onCheckedChange={(checked) => updateFilter("noAdditives", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<div style={{ fontSize: 11, color: "var(--fg-muted, #9a8870)" }}>
|
||||
Mostrar apenas produtos sem aditivos
|
||||
</div>
|
||||
</div>
|
||||
@@ -523,119 +403,62 @@ export function FilterSidebar({
|
||||
</AccordionItem>
|
||||
|
||||
{/* Valores Nutricionais */}
|
||||
<AccordionItem
|
||||
value="nutrition-values"
|
||||
className="border border-gray-100 dark:border-gray-800 rounded-lg"
|
||||
>
|
||||
<AccordionItem value="nutrition-values" style={{ border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 10, overflow: "hidden" }}>
|
||||
<AccordionTrigger className="px-4 py-3 hover:no-underline">
|
||||
<div className="flex items-center gap-2">
|
||||
<BarChart3 className="h-4 w-4 text-green-500" />
|
||||
<span className="font-medium">Valores Nutricionais</span>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<BarChart3 style={{ width: 14, height: 14, color: "var(--accent, oklch(52% 0.11 155))" }} strokeWidth={1.75} />
|
||||
<span style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-primary, #1e1810)" }}>Valores Nutricionais</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-4">
|
||||
<div className="space-y-4">
|
||||
{/* Energia */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Energia (kcal/100g)
|
||||
</label>
|
||||
<div className="px-3">
|
||||
<Slider
|
||||
value={[
|
||||
filters.nutritionRanges?.energy?.min || 0,
|
||||
filters.nutritionRanges?.energy?.max || 500,
|
||||
]}
|
||||
onValueChange={([min, max]) => {
|
||||
updateNutritionRange("energy", "min", min);
|
||||
updateNutritionRange("energy", "max", max);
|
||||
}}
|
||||
max={500}
|
||||
step={10}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground mt-1">
|
||||
<span>
|
||||
{filters.nutritionRanges?.energy?.min || 0} kcal
|
||||
</span>
|
||||
<span>
|
||||
{filters.nutritionRanges?.energy?.max || 500} kcal
|
||||
</span>
|
||||
<AccordionContent className="px-4 pb-4" style={{ borderTop: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
||||
<div style={{ paddingTop: 12, display: "flex", flexDirection: "column", gap: 18 }}>
|
||||
{[
|
||||
{ key: "energy", label: "Energia", unit: "kcal", max: 500, step: 10 },
|
||||
{ key: "sugars", label: "Açúcares", unit: "g/100g", max: 50, step: 1 },
|
||||
{ key: "sodium", label: "Sódio", unit: "g/100g", max: 3, step: 0.1 },
|
||||
].map(({ key, label, unit, max, step }) => (
|
||||
<div key={key} style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
<label style={{ fontSize: 12, fontWeight: 500, color: "var(--fg-secondary, #5e4e3c)" }}>{label} <span style={{ color: "var(--fg-muted)", fontWeight: 400 }}>({unit})</span></label>
|
||||
<div style={{ paddingLeft: 4, paddingRight: 4 }}>
|
||||
<Slider
|
||||
value={[
|
||||
(filters.nutritionRanges as Record<string, { min?: number; max?: number }>)?.[key]?.min || 0,
|
||||
(filters.nutritionRanges as Record<string, { min?: number; max?: number }>)?.[key]?.max || max,
|
||||
]}
|
||||
onValueChange={([min, maxVal]) => {
|
||||
updateNutritionRange(key, "min", min);
|
||||
updateNutritionRange(key, "max", maxVal);
|
||||
}}
|
||||
max={max}
|
||||
step={step}
|
||||
className="w-full"
|
||||
/>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 4 }}>
|
||||
<span style={{ fontSize: 11, color: "var(--fg-muted, #9a8870)" }}>
|
||||
{(filters.nutritionRanges as Record<string, { min?: number; max?: number }>)?.[key]?.min || 0}{key === "energy" ? " kcal" : "g"}
|
||||
</span>
|
||||
<span style={{ fontSize: 11, color: "var(--fg-muted, #9a8870)" }}>
|
||||
{(filters.nutritionRanges as Record<string, { min?: number; max?: number }>)?.[key]?.max || max}{key === "energy" ? " kcal" : "g"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Açúcares */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Açúcares (g/100g)</label>
|
||||
<div className="px-3">
|
||||
<Slider
|
||||
value={[
|
||||
filters.nutritionRanges?.sugars?.min || 0,
|
||||
filters.nutritionRanges?.sugars?.max || 50,
|
||||
]}
|
||||
onValueChange={([min, max]) => {
|
||||
updateNutritionRange("sugars", "min", min);
|
||||
updateNutritionRange("sugars", "max", max);
|
||||
}}
|
||||
max={50}
|
||||
step={1}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground mt-1">
|
||||
<span>{filters.nutritionRanges?.sugars?.min || 0}g</span>
|
||||
<span>{filters.nutritionRanges?.sugars?.max || 50}g</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sódio */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Sódio (g/100g)</label>
|
||||
<div className="px-3">
|
||||
<Slider
|
||||
value={[
|
||||
filters.nutritionRanges?.sodium?.min || 0,
|
||||
filters.nutritionRanges?.sodium?.max || 3,
|
||||
]}
|
||||
onValueChange={([min, max]) => {
|
||||
updateNutritionRange("sodium", "min", min);
|
||||
updateNutritionRange("sodium", "max", max);
|
||||
}}
|
||||
max={3}
|
||||
step={0.1}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground mt-1">
|
||||
<span>{filters.nutritionRanges?.sodium?.min || 0}g</span>
|
||||
<span>{filters.nutritionRanges?.sodium?.max || 3}g</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* Botões de ação */}
|
||||
<div className="flex gap-2 pt-4 border-t border-gray-100 dark:border-gray-800">
|
||||
<Button
|
||||
{/* ── Footer action ─────────────────────────────────────────────────── */}
|
||||
<div style={{ paddingTop: 16, borderTop: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
||||
<button
|
||||
onClick={onClearFilters}
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
disabled={!hasActiveFilters}
|
||||
style={{ width: "100%", padding: "10px 0", border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 10, background: "transparent", fontSize: 13, color: hasActiveFilters ? "var(--fg-secondary, #5e4e3c)" : "var(--fg-muted, #9a8870)", fontFamily: "inherit", cursor: hasActiveFilters ? "pointer" : "not-allowed", transition: "all 0.15s" }}
|
||||
>
|
||||
Limpar Filtros
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
/* Aplicar filtros é automático */
|
||||
}}
|
||||
className="flex-1"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Aplicando..." : "Aplicar"}
|
||||
</Button>
|
||||
Limpar todos os filtros
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+110
-83
@@ -3,73 +3,75 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
ExternalLink,
|
||||
Github,
|
||||
Heart,
|
||||
Globe,
|
||||
Shield,
|
||||
ScanLine,
|
||||
Database,
|
||||
Lock,
|
||||
} from "lucide-react";
|
||||
import { ExternalLink, Github, Heart, Shield, Globe, ScanLine, Database, Lock } from "lucide-react";
|
||||
|
||||
const FOOTER_LINKS = {
|
||||
navigation: [
|
||||
{ href: "/#como-funciona", label: "Protocolo" },
|
||||
{ href: "/#recursos", label: "Recursos" },
|
||||
{ href: "/sobre", label: "Sobre o Projeto" },
|
||||
{ href: "/produto/exemplo", label: "Sandbox (Exemplo)" },
|
||||
{ href: "/#como-funciona", label: "Como funciona" },
|
||||
{ href: "/#recursos", label: "Recursos" },
|
||||
{ href: "/sobre", label: "Sobre o Projeto" },
|
||||
{ href: "/produto/exemplo", label: "Produto (demo)" },
|
||||
],
|
||||
resources: [
|
||||
{ icon: Shield, label: "Dados Colaborativos" },
|
||||
{ icon: Globe, label: "Rede Global" },
|
||||
infrastructure: [
|
||||
{ icon: Shield, label: "Dados Colaborativos" },
|
||||
{ icon: Globe, label: "Rede Global" },
|
||||
{ icon: ScanLine, label: "Algoritmo de Busca" },
|
||||
],
|
||||
};
|
||||
|
||||
export function Footer() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="bg-[#030712] text-gray-400 border-t border-gray-800/50 transition-colors duration-500">
|
||||
<div className="max-w-7xl mx-auto px-6 py-20">
|
||||
|
||||
<footer className="bg-[var(--fg-primary)] text-[var(--fg-faint)]">
|
||||
<div className="container-editorial pt-20 pb-10">
|
||||
|
||||
{/* ── Top grid ─────────────────────────────────────────────────── */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 mb-20">
|
||||
|
||||
{/* Brand Identity - 4 Colunas */}
|
||||
<div className="lg:col-span-4 space-y-8">
|
||||
<Link href="/" className="flex items-center gap-3 group">
|
||||
<div className="relative w-10 h-10 bg-blue-600 rounded-xl overflow-hidden shadow-lg shadow-blue-500/10">
|
||||
<Image src="/icon.png" alt="Logo" fill className="object-contain p-2" priority />
|
||||
</div>
|
||||
|
||||
{/* Brand */}
|
||||
<div className="lg:col-span-4 space-y-6">
|
||||
<Link href="/" className="flex items-center gap-3">
|
||||
<Image
|
||||
src="/icon.png"
|
||||
alt="NutriScan"
|
||||
width={36}
|
||||
height={36}
|
||||
className="rounded object-contain"
|
||||
priority
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-xl font-black text-white tracking-tighter leading-none">
|
||||
NUTRI<span className="text-blue-500">SCAN</span>
|
||||
</h3>
|
||||
<p className="text-[10px] font-bold text-gray-500 uppercase tracking-[0.2em] mt-1">
|
||||
Data Sovereignty
|
||||
<p className="font-sans font-semibold text-base text-[var(--bg-base)] leading-none">
|
||||
NutriScan
|
||||
</p>
|
||||
<p className="label text-[var(--fg-faint)] mt-1">
|
||||
Dados Nutricionais
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<p className="text-sm leading-relaxed max-w-sm">
|
||||
Soberania alimentar através da transparência. Extraímos metadados nutricionais complexos para devolver o poder de escolha ao cidadão.
|
||||
|
||||
<p className="text-sm leading-relaxed max-w-xs">
|
||||
Soberania alimentar através da transparência. Extraímos
|
||||
metadados nutricionais para devolver o poder de escolha ao
|
||||
cidadão.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-2 text-[11px] font-mono uppercase tracking-widest text-gray-500">
|
||||
<Heart className="w-3 h-3 text-red-500 animate-pulse" />
|
||||
<span>Engineered in Garuva, SC</span>
|
||||
<div className="flex items-center gap-1.5 label">
|
||||
<Heart className="w-3 h-3 text-red-400" strokeWidth={1.5} />
|
||||
<span>Feito em Garuva, SC</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation - 2 Colunas */}
|
||||
{/* Navigation */}
|
||||
<div className="lg:col-span-2">
|
||||
<h4 className="text-white font-bold text-sm uppercase tracking-widest mb-6">Sitemap</h4>
|
||||
<h4 className="label text-[var(--bg-surface)] mb-6">Páginas</h4>
|
||||
<ul className="space-y-4">
|
||||
{FOOTER_LINKS.navigation.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link href={link.href} className="text-sm hover:text-blue-400 transition-colors">
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-sm hover:text-[var(--bg-base)] transition-colors duration-200"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
@@ -77,77 +79,102 @@ export function Footer() {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Logic/Features - 2 Colunas */}
|
||||
{/* Infrastructure */}
|
||||
<div className="lg:col-span-2">
|
||||
<h4 className="text-white font-bold text-sm uppercase tracking-widest mb-6">Infraestrutura</h4>
|
||||
<h4 className="label text-[var(--bg-surface)] mb-6">Infraestrutura</h4>
|
||||
<ul className="space-y-4">
|
||||
{FOOTER_LINKS.resources.map((item) => (
|
||||
<li key={item.label} className="flex items-center gap-3 text-sm">
|
||||
<item.icon className="w-4 h-4 text-gray-600" />
|
||||
<span>{item.label}</span>
|
||||
{FOOTER_LINKS.infrastructure.map(({ icon: Icon, label }) => (
|
||||
<li key={label} className="flex items-center gap-3 text-sm">
|
||||
<Icon className="w-4 h-4 text-[var(--fg-faint)]" strokeWidth={1.5} />
|
||||
<span>{label}</span>
|
||||
</li>
|
||||
))}
|
||||
<li className="text-[11px] font-mono text-blue-500/80">Nutri-Score v2.1</li>
|
||||
<li className="label text-[var(--accent)]">Nutri-Score v2.1</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Data Source Card - 4 Colunas */}
|
||||
{/* Data source */}
|
||||
<div className="lg:col-span-4">
|
||||
<div className="bg-gray-900/50 border border-gray-800 p-6 rounded-2xl backdrop-blur-sm">
|
||||
<div
|
||||
className="rounded-xl p-6"
|
||||
style={{ border: "1px solid rgba(255,255,255,0.08)", background: "rgba(255,255,255,0.04)" }}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 bg-blue-500/10 rounded-lg">
|
||||
<Database className="w-5 h-5 text-blue-500" />
|
||||
</div>
|
||||
<h4 className="text-white font-bold">Open Food Facts</h4>
|
||||
<Database className="w-5 h-5 text-[var(--accent)]" strokeWidth={1.5} />
|
||||
<h4 className="font-sans font-semibold text-[var(--bg-base)] text-sm">
|
||||
Open Food Facts
|
||||
</h4>
|
||||
</div>
|
||||
<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 className="text-xs leading-relaxed mb-6">
|
||||
A maior base de dados colaborativa de alimentos do mundo.
|
||||
Licença ODbL (Open Database License).
|
||||
</p>
|
||||
<a
|
||||
href="https://world.openfoodfacts.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-xs font-black text-blue-500 uppercase tracking-widest hover:text-blue-400 transition-all"
|
||||
className="inline-flex items-center gap-1.5 label text-[var(--accent)] hover:opacity-80 transition-opacity"
|
||||
>
|
||||
Protocolo de Dados <ExternalLink size={12} />
|
||||
Ver base de dados
|
||||
<ExternalLink className="w-3 h-3" strokeWidth={1.5} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Disclaimer Técnico - Estilo "Regulatory" */}
|
||||
<div className="border-t border-gray-800/50 pt-12">
|
||||
<div className="bg-gray-900/30 border border-gray-800/50 p-6 rounded-2xl flex flex-col md:flex-row items-center gap-6">
|
||||
<Lock className="w-8 h-8 text-gray-700 shrink-0" />
|
||||
<p className="text-[10px] md:text-xs text-gray-500 leading-relaxed text-center md:text-left">
|
||||
<strong className="text-gray-400 uppercase tracking-widest">Aviso Legal (Disclaimer):</strong> Os dados são extraídos de fontes colaborativas. Não garantimos a integridade absoluta das informações nutricionais em comparação com a embalagem física. O uso desta ferramenta é consultivo e não substitui orientação médica ou nutricional profissional.
|
||||
{/* ── Hairline ─────────────────────────────────────────────────── */}
|
||||
<div style={{ borderTop: "1px solid rgba(255,255,255,0.08)" }} className="pt-10">
|
||||
{/* Legal disclaimer */}
|
||||
<div
|
||||
className="rounded-xl p-5 mb-10 flex gap-4 items-start"
|
||||
style={{ border: "1px solid rgba(255,255,255,0.06)", background: "rgba(255,255,255,0.02)" }}
|
||||
>
|
||||
<Lock className="w-4 h-4 shrink-0 mt-0.5 text-[var(--fg-faint)]" strokeWidth={1.5} />
|
||||
<p className="text-xs leading-relaxed">
|
||||
<strong className="text-[var(--fg-faint)] label mr-1 not-italic">Aviso Legal:</strong>
|
||||
Os dados são extraídos de fontes colaborativas. Não garantimos
|
||||
a integridade absoluta das informações nutricionais em
|
||||
comparação com a embalagem física. O uso desta ferramenta é
|
||||
consultivo e não substitui orientação médica ou nutricional
|
||||
profissional.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom 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">
|
||||
{/* Bottom bar */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<p className="label">
|
||||
© {year} NutriScan ·{" "}
|
||||
<a
|
||||
href="https://evandro.dev.br"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-[var(--bg-base)] transition-colors"
|
||||
>
|
||||
evandro.dev.br
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/privacidade" className="text-[11px] font-black uppercase tracking-widest hover:text-white transition-colors">Privacidade</Link>
|
||||
<Link href="/termos" className="text-[11px] font-black uppercase tracking-widest hover:text-white transition-colors">Termos</Link>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
<Link href="/privacidade" className="label hover:text-[var(--bg-base)] transition-colors">
|
||||
Privacidade
|
||||
</Link>
|
||||
<Link href="/termos" className="label hover:text-[var(--bg-base)] transition-colors">
|
||||
Termos
|
||||
</Link>
|
||||
<a
|
||||
href="https://github.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-8 h-8 flex items-center justify-center rounded"
|
||||
style={{ border: "1px solid rgba(255,255,255,0.10)" }}
|
||||
aria-label="GitHub"
|
||||
>
|
||||
<Github className="w-3.5 h-3.5 text-[var(--fg-faint)]" strokeWidth={1.5} />
|
||||
</a>
|
||||
</div>
|
||||
<a href="https://github.com" target="_blank" className="p-2 bg-gray-900 rounded-full hover:bg-gray-800 transition-colors">
|
||||
<Github className="w-4 h-4 text-white" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -38,13 +38,29 @@ export function IdInput() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
|
||||
<div className="max-w-md w-full space-y-8 p-8 bg-white dark:bg-gray-800 rounded-xl shadow-lg">
|
||||
<div
|
||||
className="min-h-screen flex items-center justify-center"
|
||||
style={{ background: 'var(--bg-base)' }}
|
||||
>
|
||||
<div
|
||||
className="max-w-md w-full space-y-8 p-8 rounded-xl"
|
||||
style={{
|
||||
background: 'var(--bg-elevated)',
|
||||
border: '1px solid var(--border-subtle)',
|
||||
boxShadow: '0 8px 32px -8px rgba(30,24,16,0.10)',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
|
||||
<h2
|
||||
className="mt-6 text-center text-3xl font-display"
|
||||
style={{ color: 'var(--fg-primary)' }}
|
||||
>
|
||||
Acesse sua lista
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<p
|
||||
className="mt-2 text-center text-sm"
|
||||
style={{ color: 'var(--fg-muted)' }}
|
||||
>
|
||||
Digite um ID para acessar ou criar uma nova lista
|
||||
</p>
|
||||
</div>
|
||||
@@ -56,19 +72,20 @@ export function IdInput() {
|
||||
placeholder="Digite o ID da lista"
|
||||
value={id}
|
||||
onChange={(e) => setId(e.target.value)}
|
||||
className="appearance-none rounded-lg relative block w-full px-3 py-2 border dark:bg-gray-700 dark:border-gray-600"
|
||||
className="appearance-none rounded-lg relative block w-full px-3 py-2"
|
||||
style={{ borderColor: 'var(--border-strong)', background: 'var(--bg-surface)' }}
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-red-500 text-sm text-center">{error}</p>
|
||||
<p className="text-sm text-center" style={{ color: 'var(--nutri-e)' }}>{error}</p>
|
||||
)}
|
||||
<Button
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
className="btn-accent w-full justify-center"
|
||||
>
|
||||
{loading ? 'Carregando...' : 'Acessar/Criar Lista'}
|
||||
</Button>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,21 +35,20 @@ export function ListaProductCard({ produto, listaId }: ListaProductCardProps) {
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`group relative overflow-hidden hover:shadow-lg transition-all duration-300 hover:-translate-y-1 ${
|
||||
produto.comprado
|
||||
? "bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800"
|
||||
: "bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700"
|
||||
}`}
|
||||
className="group relative overflow-hidden hover:shadow-lg transition-all duration-300 hover:-translate-y-1"
|
||||
style={{
|
||||
background: produto.comprado ? 'var(--accent-muted)' : 'var(--bg-elevated)',
|
||||
borderColor: produto.comprado ? 'var(--accent-light)' : 'var(--border-subtle)',
|
||||
}}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<h3
|
||||
className={`font-semibold text-lg leading-tight ${
|
||||
produto.comprado
|
||||
? "text-green-700 dark:text-green-300 line-through"
|
||||
: "text-gray-900 dark:text-white"
|
||||
}`}
|
||||
className={`font-semibold text-lg leading-tight${produto.comprado ? ' line-through' : ''}`}
|
||||
style={{
|
||||
color: produto.comprado ? 'var(--accent)' : 'var(--fg-primary)',
|
||||
}}
|
||||
>
|
||||
{produto.nome}
|
||||
</h3>
|
||||
@@ -57,7 +56,12 @@ export function ListaProductCard({ produto, listaId }: ListaProductCardProps) {
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-700"
|
||||
className="text-xs"
|
||||
style={{
|
||||
background: 'var(--bg-surface)',
|
||||
color: 'var(--fg-secondary)',
|
||||
borderColor: 'var(--border-subtle)',
|
||||
}}
|
||||
>
|
||||
{produto.qtd} {produto.tipoUN}
|
||||
</Badge>
|
||||
@@ -65,7 +69,12 @@ export function ListaProductCard({ produto, listaId }: ListaProductCardProps) {
|
||||
{produto.comprado && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 border-green-200 dark:border-green-700"
|
||||
className="text-xs"
|
||||
style={{
|
||||
background: 'var(--accent-muted)',
|
||||
color: 'var(--accent-dark)',
|
||||
borderColor: 'var(--accent-light)',
|
||||
}}
|
||||
>
|
||||
Comprado
|
||||
</Badge>
|
||||
@@ -79,11 +88,7 @@ export function ListaProductCard({ produto, listaId }: ListaProductCardProps) {
|
||||
onClick={handleToggleComprado}
|
||||
variant={produto.comprado ? "outline" : "default"}
|
||||
size="sm"
|
||||
className={`flex-1 ${
|
||||
produto.comprado
|
||||
? "hover:bg-red-50 hover:text-red-700 hover:border-red-300"
|
||||
: "hover:bg-green-50 hover:text-green-700"
|
||||
}`}
|
||||
className="flex-1"
|
||||
>
|
||||
{produto.comprado ? (
|
||||
<>
|
||||
@@ -101,7 +106,8 @@ export function ListaProductCard({ produto, listaId }: ListaProductCardProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="px-3 hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
className="px-3"
|
||||
style={{ ['--hover-bg' as string]: 'var(--bg-surface)' }}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
@@ -109,7 +115,7 @@ export function ListaProductCard({ produto, listaId }: ListaProductCardProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="px-3 hover:bg-red-50 hover:text-red-700 hover:border-red-300"
|
||||
className="px-3"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
+12
-7
@@ -47,7 +47,8 @@ export function Main({ listaId }: MainProps) {
|
||||
<Input
|
||||
required
|
||||
name="produtoNome"
|
||||
className="flex-1 min-w-[200px] text-base px-4 py-2 border-[#383838] focus:border-[#1118A7]"
|
||||
className="flex-1 min-w-[200px] text-base px-4 py-2"
|
||||
style={{ borderColor: 'var(--border-strong)', ['--tw-ring-color' as string]: 'var(--accent)' }}
|
||||
placeholder="Nome do produto"
|
||||
/>
|
||||
<Input
|
||||
@@ -56,11 +57,15 @@ export function Main({ listaId }: MainProps) {
|
||||
min="0"
|
||||
step="0.01"
|
||||
name="produtoQuantidade"
|
||||
className="w-32 text-base px-4 py-2 border-[#383838] focus:border-[#1118A7]"
|
||||
className="w-32 text-base px-4 py-2"
|
||||
style={{ borderColor: 'var(--border-strong)' }}
|
||||
placeholder="Quantidade"
|
||||
/>
|
||||
<Select name="produtoTipo" required>
|
||||
<SelectTrigger className="w-32 text-base border-[#383838] focus:border-[#1118A7]">
|
||||
<SelectTrigger
|
||||
className="w-32 text-base"
|
||||
style={{ borderColor: 'var(--border-strong)' }}
|
||||
>
|
||||
<SelectValue placeholder="Tipo" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -74,16 +79,16 @@ export function Main({ listaId }: MainProps) {
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
className="bg-[#1118A7] hover:bg-[#080C94] text-white min-w-[100px]"
|
||||
<button
|
||||
className="btn-accent min-w-[100px]"
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? 'Salvando...' : 'Adicionar'}
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-[#CC2121] text-sm">{error}</p>
|
||||
<p className="text-sm" style={{ color: 'var(--nutri-e)' }}>{error}</p>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface NutriScoreBadgeProps {
|
||||
@@ -10,126 +9,65 @@ interface NutriScoreBadgeProps {
|
||||
showTooltip?: boolean;
|
||||
}
|
||||
|
||||
const GRADE_INFO: Record<string, { desc: string; explanation: string }> = {
|
||||
a: { desc: "Muito bom", explanation: "Excelente qualidade nutricional" },
|
||||
b: { desc: "Bom", explanation: "Boa qualidade nutricional" },
|
||||
c: { desc: "Regular", explanation: "Qualidade nutricional moderada" },
|
||||
d: { desc: "Ruim", explanation: "Qualidade nutricional baixa" },
|
||||
e: { desc: "Muito ruim", explanation: "Qualidade nutricional muito baixa" },
|
||||
};
|
||||
|
||||
const GRADE_CLASS: Record<string, string> = {
|
||||
a: "grade-a", b: "grade-b", c: "grade-c", d: "grade-d", e: "grade-e",
|
||||
};
|
||||
|
||||
const SIZE_CLASS = {
|
||||
sm: "w-8 h-8 text-sm",
|
||||
md: "w-14 h-14 text-xl",
|
||||
lg: "w-20 h-20 text-2xl",
|
||||
};
|
||||
|
||||
export function NutriScoreBadge({
|
||||
grade,
|
||||
className,
|
||||
size = "md",
|
||||
showTooltip = true,
|
||||
}: NutriScoreBadgeProps) {
|
||||
const [showInfo, setShowInfo] = useState(false);
|
||||
|
||||
const getGradeInfo = (grade: string) => {
|
||||
const grades = {
|
||||
a: {
|
||||
label: "A",
|
||||
color: "bg-gradient-to-br from-green-400 to-green-600",
|
||||
textColor: "text-white",
|
||||
description: "Muito bom",
|
||||
explanation: "Produto com excelente qualidade nutricional",
|
||||
pulse: false,
|
||||
},
|
||||
b: {
|
||||
label: "B",
|
||||
color: "bg-gradient-to-br from-green-300 to-green-500",
|
||||
textColor: "text-white",
|
||||
description: "Bom",
|
||||
explanation: "Produto com boa qualidade nutricional",
|
||||
pulse: false,
|
||||
},
|
||||
c: {
|
||||
label: "C",
|
||||
color: "bg-gradient-to-br from-yellow-400 to-yellow-600",
|
||||
textColor: "text-white",
|
||||
description: "Regular",
|
||||
explanation: "Produto com qualidade nutricional regular",
|
||||
pulse: false,
|
||||
},
|
||||
d: {
|
||||
label: "D",
|
||||
color: "bg-gradient-to-br from-orange-400 to-orange-600",
|
||||
textColor: "text-white",
|
||||
description: "Ruim",
|
||||
explanation: "Produto com qualidade nutricional ruim",
|
||||
pulse: true,
|
||||
},
|
||||
e: {
|
||||
label: "E",
|
||||
color: "bg-gradient-to-br from-red-400 to-red-600",
|
||||
textColor: "text-white",
|
||||
description: "Muito ruim",
|
||||
explanation: "Produto com qualidade nutricional muito ruim",
|
||||
pulse: true,
|
||||
},
|
||||
};
|
||||
|
||||
return grades[grade.toLowerCase() as keyof typeof grades] || null;
|
||||
};
|
||||
|
||||
const gradeInfo = getGradeInfo(grade);
|
||||
if (!gradeInfo) return null;
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "w-8 h-8 text-sm",
|
||||
md: "w-16 h-16 text-xl",
|
||||
lg: "w-20 h-20 text-2xl",
|
||||
};
|
||||
const [hover, setHover] = useState(false);
|
||||
const key = grade?.toLowerCase();
|
||||
const info = GRADE_INFO[key];
|
||||
if (!info) return null;
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<div className="relative inline-block">
|
||||
<div
|
||||
className={cn(
|
||||
"relative rounded-full flex items-center justify-center shadow-lg transition-all duration-300 transform hover:scale-105",
|
||||
gradeInfo.color,
|
||||
sizeClasses[size],
|
||||
gradeInfo.pulse && "animate-pulse",
|
||||
"nutriscore-hero font-sans font-bold rounded-full flex items-center justify-center cursor-default",
|
||||
GRADE_CLASS[key],
|
||||
SIZE_CLASS[size],
|
||||
className
|
||||
)}
|
||||
onMouseEnter={() => setShowInfo(true)}
|
||||
onMouseLeave={() => setShowInfo(false)}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
<span className={cn("font-bold", gradeInfo.textColor)}>
|
||||
{gradeInfo.label}
|
||||
</span>
|
||||
|
||||
{/* Efeito de brilho para notas baixas */}
|
||||
{gradeInfo.pulse && (
|
||||
<div className="absolute inset-0 rounded-full bg-white opacity-20 animate-ping" />
|
||||
)}
|
||||
{key.toUpperCase()}
|
||||
</div>
|
||||
|
||||
{/* Tooltip informativo */}
|
||||
{showTooltip && showInfo && (
|
||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 z-50">
|
||||
<div className="bg-gray-900 text-white text-xs rounded-lg px-3 py-2 shadow-xl max-w-xs">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-semibold">
|
||||
Nutri-Score {gradeInfo.label}
|
||||
</span>
|
||||
{gradeInfo.pulse && (
|
||||
<AlertTriangle className="w-3 h-3 text-yellow-400" />
|
||||
)}
|
||||
</div>
|
||||
<p className="font-medium">{gradeInfo.description}</p>
|
||||
<p className="text-gray-300 mt-1">{gradeInfo.explanation}</p>
|
||||
|
||||
{/* Seta do tooltip */}
|
||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2">
|
||||
<div className="w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900" />
|
||||
</div>
|
||||
{showTooltip && hover && (
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 z-50 whitespace-nowrap">
|
||||
<div className="bg-[var(--fg-primary)] text-[var(--bg-base)] text-xs rounded-lg px-3 py-2 shadow-xl">
|
||||
<p className="font-semibold">Nutri-Score {key.toUpperCase()}</p>
|
||||
<p className="opacity-70">{info.desc}</p>
|
||||
<p className="opacity-50 mt-0.5">{info.explanation}</p>
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-[var(--fg-primary)]" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Badge de alerta para notas baixas */}
|
||||
{gradeInfo.pulse && (
|
||||
<div className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full flex items-center justify-center">
|
||||
<AlertTriangle className="w-2.5 h-2.5 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── NOVA Badge ───────────────────────────────────────────────────────────── */
|
||||
interface NovaBadgeProps {
|
||||
group: number;
|
||||
className?: string;
|
||||
@@ -137,112 +75,48 @@ interface NovaBadgeProps {
|
||||
showTooltip?: boolean;
|
||||
}
|
||||
|
||||
const NOVA_INFO: Record<number, { desc: string; explanation: string; bg: string; fg: string }> = {
|
||||
1: { desc: "Não processado", explanation: "Alimentos não ou minimamente processados", bg: "#1a7a4a", fg: "#fff" },
|
||||
2: { desc: "Ingrediente", explanation: "Ingredientes culinários processados", bg: "#e8c110", fg: "#1e1810" },
|
||||
3: { desc: "Processado", explanation: "Alimentos processados", bg: "#e07820", fg: "#fff" },
|
||||
4: { desc: "Ultraprocessado",explanation: "Alimentos ultraprocessados", bg: "#c0201a", fg: "#fff" },
|
||||
};
|
||||
|
||||
export function NovaBadge({
|
||||
group,
|
||||
className,
|
||||
size = "md",
|
||||
showTooltip = true,
|
||||
}: NovaBadgeProps) {
|
||||
const [showInfo, setShowInfo] = useState(false);
|
||||
|
||||
const getGroupInfo = (group: number) => {
|
||||
const groups = {
|
||||
1: {
|
||||
label: "1",
|
||||
color: "bg-gradient-to-br from-green-400 to-green-600",
|
||||
textColor: "text-white",
|
||||
description: "Não processado",
|
||||
explanation: "Alimentos não processados ou minimamente processados",
|
||||
pulse: false,
|
||||
},
|
||||
2: {
|
||||
label: "2",
|
||||
color: "bg-gradient-to-br from-yellow-400 to-yellow-600",
|
||||
textColor: "text-white",
|
||||
description: "Ingredientes",
|
||||
explanation: "Ingredientes culinários processados",
|
||||
pulse: false,
|
||||
},
|
||||
3: {
|
||||
label: "3",
|
||||
color: "bg-gradient-to-br from-orange-400 to-orange-600",
|
||||
textColor: "text-white",
|
||||
description: "Processado",
|
||||
explanation: "Alimentos processados",
|
||||
pulse: true,
|
||||
},
|
||||
4: {
|
||||
label: "4",
|
||||
color: "bg-gradient-to-br from-red-400 to-red-600",
|
||||
textColor: "text-white",
|
||||
description: "Ultraprocessado",
|
||||
explanation: "Alimentos ultraprocessados",
|
||||
pulse: true,
|
||||
},
|
||||
};
|
||||
|
||||
return groups[group as keyof typeof groups] || null;
|
||||
};
|
||||
|
||||
const groupInfo = getGroupInfo(group);
|
||||
if (!groupInfo) return null;
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "w-8 h-8 text-sm",
|
||||
md: "w-16 h-16 text-xl",
|
||||
lg: "w-20 h-20 text-2xl",
|
||||
};
|
||||
const [hover, setHover] = useState(false);
|
||||
const info = NOVA_INFO[group];
|
||||
if (!info) return null;
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<div className="relative inline-block">
|
||||
<div
|
||||
className={cn(
|
||||
"relative rounded-full flex items-center justify-center shadow-lg transition-all duration-300 transform hover:scale-105",
|
||||
groupInfo.color,
|
||||
sizeClasses[size],
|
||||
groupInfo.pulse && "animate-pulse",
|
||||
"nutriscore-hero font-sans font-bold rounded-full flex items-center justify-center cursor-default",
|
||||
SIZE_CLASS[size],
|
||||
className
|
||||
)}
|
||||
onMouseEnter={() => setShowInfo(true)}
|
||||
onMouseLeave={() => setShowInfo(false)}
|
||||
style={{ background: info.bg, color: info.fg, animation: "none" }}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
<span className={cn("font-bold", groupInfo.textColor)}>
|
||||
{groupInfo.label}
|
||||
</span>
|
||||
|
||||
{/* Efeito de brilho para grupos altos */}
|
||||
{groupInfo.pulse && (
|
||||
<div className="absolute inset-0 rounded-full bg-white opacity-20 animate-ping" />
|
||||
)}
|
||||
{group}
|
||||
</div>
|
||||
|
||||
{/* Tooltip informativo */}
|
||||
{showTooltip && showInfo && (
|
||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 z-50">
|
||||
<div className="bg-gray-900 text-white text-xs rounded-lg px-3 py-2 shadow-xl max-w-xs">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-semibold">NOVA {groupInfo.label}</span>
|
||||
{groupInfo.pulse && (
|
||||
<AlertTriangle className="w-3 h-3 text-yellow-400" />
|
||||
)}
|
||||
</div>
|
||||
<p className="font-medium">{groupInfo.description}</p>
|
||||
<p className="text-gray-300 mt-1">{groupInfo.explanation}</p>
|
||||
|
||||
{/* Seta do tooltip */}
|
||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2">
|
||||
<div className="w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900" />
|
||||
</div>
|
||||
{showTooltip && hover && (
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 z-50 whitespace-nowrap">
|
||||
<div className="bg-[var(--fg-primary)] text-[var(--bg-base)] text-xs rounded-lg px-3 py-2 shadow-xl">
|
||||
<p className="font-semibold">NOVA {group}</p>
|
||||
<p className="opacity-70">{info.desc}</p>
|
||||
<p className="opacity-50 mt-0.5">{info.explanation}</p>
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-[var(--fg-primary)]" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Badge de alerta para grupos altos */}
|
||||
{groupInfo.pulse && (
|
||||
<div className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full flex items-center justify-center">
|
||||
<AlertTriangle className="w-2.5 h-2.5 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+70
-298
@@ -1,6 +1,4 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { Info, TrendingUp, TrendingDown, AlertTriangle } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface NutritionBarProps {
|
||||
@@ -12,7 +10,6 @@ interface NutritionBarProps {
|
||||
isLow?: boolean;
|
||||
recommended?: string;
|
||||
className?: string;
|
||||
showTooltip?: boolean;
|
||||
}
|
||||
|
||||
export function NutritionBar({
|
||||
@@ -22,132 +19,41 @@ export function NutritionBar({
|
||||
percentage = 0,
|
||||
isHigh = false,
|
||||
isLow = false,
|
||||
recommended,
|
||||
className,
|
||||
showTooltip = true,
|
||||
}: NutritionBarProps) {
|
||||
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
|
||||
const barWidth = Math.min(Math.max(percentage, 2), 100);
|
||||
|
||||
// Calcular cor baseada no valor e se é alto/baixo
|
||||
const getBarColor = () => {
|
||||
if (isHigh) {
|
||||
return "bg-gradient-to-r from-red-400 to-red-600";
|
||||
}
|
||||
if (isLow) {
|
||||
return "bg-gradient-to-r from-green-400 to-green-600";
|
||||
}
|
||||
if (percentage > 80) {
|
||||
return "bg-gradient-to-r from-orange-400 to-orange-600";
|
||||
}
|
||||
if (percentage > 60) {
|
||||
return "bg-gradient-to-r from-yellow-400 to-yellow-600";
|
||||
}
|
||||
return "bg-gradient-to-r from-blue-400 to-blue-600";
|
||||
};
|
||||
const barColor = isHigh
|
||||
? "bg-[var(--score-e)]"
|
||||
: isLow
|
||||
? "bg-[var(--score-a)]"
|
||||
: percentage > 75
|
||||
? "bg-[var(--score-d)]"
|
||||
: percentage > 50
|
||||
? "bg-[var(--score-c)]"
|
||||
: "bg-[var(--accent)]";
|
||||
|
||||
// Calcular largura da barra baseada na porcentagem
|
||||
const barWidth = Math.min(Math.max(percentage, 5), 100);
|
||||
|
||||
// Determinar ícone baseado no status
|
||||
const getIcon = () => {
|
||||
if (isHigh) return <TrendingUp className="w-3 h-3" />;
|
||||
if (isLow) return <TrendingDown className="w-3 h-3" />;
|
||||
return null;
|
||||
};
|
||||
|
||||
// Determinar cor do texto baseada no status
|
||||
const getTextColor = () => {
|
||||
if (isHigh) return "text-red-600 dark:text-red-400";
|
||||
if (isLow) return "text-green-600 dark:text-green-400";
|
||||
return "text-gray-900 dark:text-white";
|
||||
};
|
||||
const valueColor = isHigh
|
||||
? "text-[var(--score-e)]"
|
||||
: isLow
|
||||
? "text-[var(--score-a)]"
|
||||
: "text-[var(--fg-primary)]";
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-3", className)}>
|
||||
{/* Linha principal: Informação nutricional à esquerda, porcentagem à direita */}
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Informação nutricional à esquerda */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
{label}:
|
||||
</span>
|
||||
<span className={cn("text-sm font-semibold", getTextColor())}>
|
||||
{value} {unit}
|
||||
</span>
|
||||
{getIcon() && (
|
||||
<div className={cn("flex items-center", getTextColor())}>
|
||||
{getIcon()}
|
||||
</div>
|
||||
)}
|
||||
{showTooltip && (
|
||||
<button
|
||||
onMouseEnter={() => setIsTooltipVisible(true)}
|
||||
onMouseLeave={() => setIsTooltipVisible(false)}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
>
|
||||
<Info className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Separador visual */}
|
||||
<div className="flex-1 mx-4 h-px bg-gray-200 dark:bg-gray-600"></div>
|
||||
|
||||
{/* Porcentagem à direita */}
|
||||
{percentage > 0 && (
|
||||
<div className="text-xs text-gray-600 dark:text-gray-300 font-medium bg-white dark:bg-gray-800 px-3 py-1 rounded-full shadow-sm border border-gray-200 dark:border-gray-600 flex-shrink-0">
|
||||
{Math.round(percentage)}%
|
||||
</div>
|
||||
)}
|
||||
<div className={cn("space-y-2", className)}>
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
<span className="label text-[var(--fg-muted)] truncate">{label}</span>
|
||||
<span className={cn("label shrink-0 font-medium", valueColor)}>
|
||||
{value} {unit}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Barra de progresso */}
|
||||
<div className="relative">
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full rounded-full transition-all duration-500 ease-out relative",
|
||||
getBarColor()
|
||||
)}
|
||||
style={{ width: `${barWidth}%` }}
|
||||
>
|
||||
{/* Efeito de brilho */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent opacity-30 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
{/* Hairline track */}
|
||||
<div className="w-full h-px bg-[var(--border-subtle)] relative overflow-hidden rounded-full">
|
||||
<div
|
||||
className={cn("h-full rounded-full transition-all duration-700", barColor)}
|
||||
style={{ width: `${barWidth}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tooltip informativo */}
|
||||
{showTooltip && isTooltipVisible && (
|
||||
<div className="absolute bottom-full left-0 mb-2 z-50">
|
||||
<div className="bg-gray-900 text-white text-xs rounded-lg px-3 py-2 shadow-xl max-w-xs">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-semibold">{label}</span>
|
||||
{isHigh && <AlertTriangle className="w-3 h-3 text-red-400" />}
|
||||
{isLow && <TrendingDown className="w-3 h-3 text-green-400" />}
|
||||
</div>
|
||||
<p className="text-gray-300">
|
||||
Valor: {value} {unit}
|
||||
</p>
|
||||
{recommended && (
|
||||
<p className="text-gray-300 mt-1">Recomendado: {recommended}</p>
|
||||
)}
|
||||
{isHigh && (
|
||||
<p className="text-red-300 mt-1">
|
||||
⚠️ Alto valor - consumir com moderação
|
||||
</p>
|
||||
)}
|
||||
{isLow && (
|
||||
<p className="text-green-300 mt-1">✅ Baixo valor - boa opção</p>
|
||||
)}
|
||||
|
||||
{/* Seta do tooltip */}
|
||||
<div className="absolute top-full left-4">
|
||||
<div className="w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -165,196 +71,62 @@ interface NutritionGridProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function NutritionGrid({
|
||||
nutritionData,
|
||||
className,
|
||||
}: NutritionGridProps) {
|
||||
// Valores de referência para calcular porcentagens (valores diários recomendados)
|
||||
const referenceValues = {
|
||||
energy: 2000, // kcal
|
||||
fat: 65, // g
|
||||
carbohydrates: 300, // g
|
||||
proteins: 50, // g
|
||||
sugars: 50, // g
|
||||
fiber: 25, // g
|
||||
sodium: 2.3, // g
|
||||
};
|
||||
const REFERENCE = {
|
||||
energy: 2000, fat: 65, carbohydrates: 300,
|
||||
proteins: 50, sugars: 50, fiber: 25, sodium: 2.3,
|
||||
};
|
||||
|
||||
// Extrair valor numérico da string
|
||||
const extractNumericValue = (value: string | undefined): number => {
|
||||
if (!value) return 0;
|
||||
const match = value.match(/(\d+(?:\.\d+)?)/);
|
||||
return match ? parseFloat(match[1]) : 0;
|
||||
};
|
||||
function extractNumber(val?: string): number {
|
||||
if (!val) return 0;
|
||||
const m = val.match(/(\d+(?:\.\d+)?)/);
|
||||
return m ? parseFloat(m[1]) : 0;
|
||||
}
|
||||
|
||||
// Calcular porcentagem baseada no valor de referência
|
||||
const calculatePercentage = (
|
||||
value: string | undefined,
|
||||
reference: number
|
||||
): number => {
|
||||
const numericValue = extractNumericValue(value);
|
||||
return (numericValue / reference) * 100;
|
||||
};
|
||||
function pct(val: string | undefined, ref: number): number {
|
||||
return (extractNumber(val) / ref) * 100;
|
||||
}
|
||||
|
||||
// Determinar se um valor é alto ou baixo
|
||||
const isHighValue = (
|
||||
value: string | undefined,
|
||||
reference: number
|
||||
): boolean => {
|
||||
const percentage = calculatePercentage(value, reference);
|
||||
return percentage > 80;
|
||||
};
|
||||
|
||||
const isLowValue = (
|
||||
value: string | undefined,
|
||||
reference: number
|
||||
): boolean => {
|
||||
const percentage = calculatePercentage(value, reference);
|
||||
return percentage < 20;
|
||||
};
|
||||
|
||||
const nutritionItems = [
|
||||
{
|
||||
label: "Energia",
|
||||
value: nutritionData.energy,
|
||||
unit: "kcal",
|
||||
percentage: calculatePercentage(
|
||||
nutritionData.energy,
|
||||
referenceValues.energy
|
||||
),
|
||||
isHigh: isHighValue(nutritionData.energy, referenceValues.energy),
|
||||
isLow: isLowValue(nutritionData.energy, referenceValues.energy),
|
||||
recommended: "2000 kcal/dia",
|
||||
},
|
||||
{
|
||||
label: "Gorduras",
|
||||
value: nutritionData.fat,
|
||||
unit: "g",
|
||||
percentage: calculatePercentage(nutritionData.fat, referenceValues.fat),
|
||||
isHigh: isHighValue(nutritionData.fat, referenceValues.fat),
|
||||
isLow: isLowValue(nutritionData.fat, referenceValues.fat),
|
||||
recommended: "65g/dia",
|
||||
},
|
||||
{
|
||||
label: "Carboidratos",
|
||||
value: nutritionData.carbohydrates,
|
||||
unit: "g",
|
||||
percentage: calculatePercentage(
|
||||
nutritionData.carbohydrates,
|
||||
referenceValues.carbohydrates
|
||||
),
|
||||
isHigh: isHighValue(
|
||||
nutritionData.carbohydrates,
|
||||
referenceValues.carbohydrates
|
||||
),
|
||||
isLow: isLowValue(
|
||||
nutritionData.carbohydrates,
|
||||
referenceValues.carbohydrates
|
||||
),
|
||||
recommended: "300g/dia",
|
||||
},
|
||||
{
|
||||
label: "Proteínas",
|
||||
value: nutritionData.proteins,
|
||||
unit: "g",
|
||||
percentage: calculatePercentage(
|
||||
nutritionData.proteins,
|
||||
referenceValues.proteins
|
||||
),
|
||||
isHigh: isHighValue(nutritionData.proteins, referenceValues.proteins),
|
||||
isLow: isLowValue(nutritionData.proteins, referenceValues.proteins),
|
||||
recommended: "50g/dia",
|
||||
},
|
||||
{
|
||||
label: "Açúcares",
|
||||
value: nutritionData.sugars,
|
||||
unit: "g",
|
||||
percentage: calculatePercentage(
|
||||
nutritionData.sugars,
|
||||
referenceValues.sugars
|
||||
),
|
||||
isHigh: isHighValue(nutritionData.sugars, referenceValues.sugars),
|
||||
isLow: isLowValue(nutritionData.sugars, referenceValues.sugars),
|
||||
recommended: "50g/dia",
|
||||
},
|
||||
{
|
||||
label: "Fibras",
|
||||
value: nutritionData.fiber,
|
||||
unit: "g",
|
||||
percentage: calculatePercentage(
|
||||
nutritionData.fiber,
|
||||
referenceValues.fiber
|
||||
),
|
||||
isHigh: isHighValue(nutritionData.fiber, referenceValues.fiber),
|
||||
isLow: isLowValue(nutritionData.fiber, referenceValues.fiber),
|
||||
recommended: "25g/dia",
|
||||
},
|
||||
{
|
||||
label: "Sódio",
|
||||
value: nutritionData.sodium,
|
||||
unit: "g",
|
||||
percentage: calculatePercentage(
|
||||
nutritionData.sodium,
|
||||
referenceValues.sodium
|
||||
),
|
||||
isHigh: isHighValue(nutritionData.sodium, referenceValues.sodium),
|
||||
isLow: isLowValue(nutritionData.sodium, referenceValues.sodium),
|
||||
recommended: "2.3g/dia",
|
||||
},
|
||||
export function NutritionGrid({ nutritionData, className }: NutritionGridProps) {
|
||||
const items = [
|
||||
{ label: "Energia", value: nutritionData.energy, unit: "kcal", ref: REFERENCE.energy },
|
||||
{ label: "Gorduras", value: nutritionData.fat, unit: "g", ref: REFERENCE.fat },
|
||||
{ label: "Carboidratos", value: nutritionData.carbohydrates, unit: "g", ref: REFERENCE.carbohydrates },
|
||||
{ label: "Proteínas", value: nutritionData.proteins, unit: "g", ref: REFERENCE.proteins },
|
||||
{ label: "Açúcares", value: nutritionData.sugars, unit: "g", ref: REFERENCE.sugars },
|
||||
{ label: "Fibras", value: nutritionData.fiber, unit: "g", ref: REFERENCE.fiber },
|
||||
{ label: "Sódio", value: nutritionData.sodium, unit: "g", ref: REFERENCE.sodium },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-4", className)}>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Informação Nutricional
|
||||
</h3>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded-full">
|
||||
Valores diários de referência
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{nutritionItems.map((item) => (
|
||||
<div className={cn("space-y-5", className)}>
|
||||
{items.map((item) => {
|
||||
const p = pct(item.value, item.ref);
|
||||
return (
|
||||
<NutritionBar
|
||||
key={item.label}
|
||||
label={item.label}
|
||||
value={item.value || "N/A"}
|
||||
value={item.value || "—"}
|
||||
unit={item.unit}
|
||||
percentage={item.percentage}
|
||||
isHigh={item.isHigh}
|
||||
isLow={item.isLow}
|
||||
recommended={item.recommended}
|
||||
showTooltip={true}
|
||||
percentage={p}
|
||||
isHigh={p > 80}
|
||||
isLow={p < 20 && p > 0}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Legenda */}
|
||||
<div className="mt-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-2">
|
||||
Como interpretar os valores:
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-green-400 rounded-full"></div>
|
||||
<span className="text-gray-600 dark:text-gray-400">
|
||||
Baixo (verde)
|
||||
</span>
|
||||
{/* Legend */}
|
||||
<div className="pt-4 border-t border-[var(--border-subtle)] flex flex-wrap gap-x-6 gap-y-2">
|
||||
{[
|
||||
{ color: "bg-[var(--score-a)]", label: "Baixo" },
|
||||
{ color: "bg-[var(--accent)]", label: "Moderado" },
|
||||
{ color: "bg-[var(--score-e)]", label: "Alto" },
|
||||
].map(({ color, label }) => (
|
||||
<div key={label} className="flex items-center gap-2">
|
||||
<div className={cn("w-2 h-2 rounded-full", color)} />
|
||||
<span className="label text-[var(--fg-muted)]">{label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-yellow-400 rounded-full"></div>
|
||||
<span className="text-gray-600 dark:text-gray-400">
|
||||
Moderado (amarelo)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-red-400 rounded-full"></div>
|
||||
<span className="text-gray-600 dark:text-gray-400">
|
||||
Alto (vermelho)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<span className="label text-[var(--fg-faint)] ml-auto">% do VDR</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+254
-312
@@ -1,350 +1,292 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { OptimizedImage } from "@/components/OptimizedImage";
|
||||
import { Product } from "@/lib/openFoodFactsApi";
|
||||
import { Heart, AlertTriangle, Zap, Plus, Eye } from "lucide-react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { formatNutritionData } from "@/lib/openFoodFactsApi";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ProductCardProps {
|
||||
product: Product;
|
||||
className?: string;
|
||||
priority?: number; // Posição na lista (0 = primeiro)
|
||||
currentPage?: number; // Página atual para controle de cancelamento
|
||||
variant?: "default" | "mobile"; // Variante de layout
|
||||
priority?: number;
|
||||
currentPage?: number;
|
||||
variant?: "default" | "mobile";
|
||||
}
|
||||
|
||||
const NUTRI_SCORE_CONFIG = {
|
||||
a: {
|
||||
label: "A",
|
||||
className: "bg-green-500 text-white border-green-500",
|
||||
description: "Muito bom",
|
||||
},
|
||||
b: {
|
||||
label: "B",
|
||||
className: "bg-green-400 text-white border-green-400",
|
||||
description: "Bom",
|
||||
},
|
||||
c: {
|
||||
label: "C",
|
||||
className: "bg-yellow-500 text-white border-yellow-500",
|
||||
description: "Regular",
|
||||
},
|
||||
d: {
|
||||
label: "D",
|
||||
className: "bg-orange-500 text-white border-orange-500",
|
||||
description: "Ruim",
|
||||
},
|
||||
e: {
|
||||
label: "E",
|
||||
className: "bg-red-500 text-white border-red-500",
|
||||
description: "Muito ruim",
|
||||
},
|
||||
const NUTRI_GRADE_CLASS: Record<string, string> = {
|
||||
a: "a", b: "b", c: "c", d: "d", e: "e",
|
||||
};
|
||||
|
||||
const NOVA_CONFIG = {
|
||||
1: {
|
||||
label: "1",
|
||||
className: "bg-green-500 text-white",
|
||||
description: "Minimamente processado",
|
||||
},
|
||||
2: {
|
||||
label: "2",
|
||||
className: "bg-yellow-500 text-white",
|
||||
description: "Ingrediente culinário",
|
||||
},
|
||||
3: {
|
||||
label: "3",
|
||||
className: "bg-orange-500 text-white",
|
||||
description: "Processado",
|
||||
},
|
||||
4: {
|
||||
label: "4",
|
||||
className: "bg-red-500 text-white",
|
||||
description: "Ultraprocessado",
|
||||
},
|
||||
};
|
||||
function getVerdict(grade?: string, nova?: number): { label: string; colorClass: string } {
|
||||
if (grade === "a" || grade === "b") return { label: "Recomendado", colorClass: "text-[var(--nutri-a)]" };
|
||||
if (grade === "c" || nova === 3) return { label: "Com moderação", colorClass: "text-[var(--nutri-c)]" };
|
||||
return { label: "Evitar", colorClass: "text-[var(--nutri-e)]" };
|
||||
}
|
||||
|
||||
export function ProductCard({
|
||||
product,
|
||||
className = "",
|
||||
priority = 999,
|
||||
currentPage = 1,
|
||||
variant = "default",
|
||||
}: ProductCardProps) {
|
||||
const router = useRouter();
|
||||
function getVerdictDotBg(grade?: string, nova?: number): string {
|
||||
if (grade === "a" || grade === "b") return "bg-[var(--nutri-a)]";
|
||||
if (grade === "c" || nova === 3) return "bg-[var(--nutri-c)]";
|
||||
return "bg-[var(--nutri-e)]";
|
||||
}
|
||||
|
||||
const getProductImageUrl = (): string => {
|
||||
return (
|
||||
product.image_front_url ||
|
||||
product.image_front_small_url ||
|
||||
product.image_url ||
|
||||
product.image_small_url ||
|
||||
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='400'%3E%3Crect fill='%23f3f4f6' width='400' height='400'/%3E%3Ctext fill='%239ca3af' font-family='sans-serif' font-size='48' x='50%25' y='50%25' text-anchor='middle' dominant-baseline='middle'%3ESem Imagem%3C/text%3E%3C/svg%3E"
|
||||
);
|
||||
};
|
||||
|
||||
const getNutritionGrade = () => {
|
||||
if (!product.nutrition_grades) return null;
|
||||
const grade = product.nutrition_grades.toLowerCase();
|
||||
return NUTRI_SCORE_CONFIG[grade as keyof typeof NUTRI_SCORE_CONFIG] || null;
|
||||
};
|
||||
|
||||
const getNovaGroup = () => {
|
||||
if (!product.nova_group) return null;
|
||||
return NOVA_CONFIG[product.nova_group as keyof typeof NOVA_CONFIG] || null;
|
||||
};
|
||||
|
||||
const nutritionGrade = getNutritionGrade();
|
||||
const novaGroup = getNovaGroup();
|
||||
const imageUrl = getProductImageUrl();
|
||||
const nutritionData = formatNutritionData(product);
|
||||
|
||||
// Verificar se tem alérgenos comuns
|
||||
const hasCommonAllergens =
|
||||
product.allergens_tags?.some((allergen) =>
|
||||
["gluten", "milk", "eggs", "nuts", "peanuts", "soy"].some((common) =>
|
||||
allergen.toLowerCase().includes(common)
|
||||
)
|
||||
) || false;
|
||||
|
||||
// Layout mobile horizontal
|
||||
if (variant === "mobile") {
|
||||
return (
|
||||
<Card
|
||||
className={`product-card group relative overflow-hidden hover:shadow-lg transition-all duration-200 bg-white dark:bg-gray-900 border border-gray-100 dark:border-gray-800 ${className}`}
|
||||
>
|
||||
<div className="flex gap-3 p-3">
|
||||
{/* Imagem - tamanho ajustado */}
|
||||
<div className="w-24 h-24 flex-shrink-0 relative bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-700 rounded-lg overflow-hidden">
|
||||
<OptimizedImage
|
||||
src={imageUrl}
|
||||
alt={product.product_name || "Produto"}
|
||||
fill
|
||||
priority={priority}
|
||||
onPageChange={currentPage}
|
||||
className="object-contain p-2"
|
||||
sizes="96px"
|
||||
placeholder="empty"
|
||||
/>
|
||||
|
||||
{/* Badges - tamanho ajustado */}
|
||||
{nutritionGrade && (
|
||||
<div className="absolute top-1 right-1">
|
||||
<Badge
|
||||
className={`${nutritionGrade.className} text-[9px] px-1.5 py-0.5 font-bold`}
|
||||
>
|
||||
{nutritionGrade.label}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasCommonAllergens && (
|
||||
<div className="absolute bottom-1 right-1">
|
||||
<div className="bg-yellow-500 text-white rounded-full p-0.5">
|
||||
<AlertTriangle className="w-2 h-2" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Conteúdo - melhor espaçamento */}
|
||||
<div className="flex-1 min-w-0 flex flex-col">
|
||||
<div className="flex-1">
|
||||
{/* Nome do produto */}
|
||||
<h3 className="font-semibold text-sm text-gray-900 dark:text-white line-clamp-2 mb-1">
|
||||
{product.product_name || "Produto sem nome"}
|
||||
</h3>
|
||||
|
||||
{/* Marca */}
|
||||
{product.brands && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||
{product.brands}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Info nutricional compacta */}
|
||||
<div className="flex gap-2 mt-2 text-xs">
|
||||
{product.nutriments?.energy_100g &&
|
||||
typeof product.nutriments.energy_100g === "number" && (
|
||||
<span className="text-gray-600 dark:text-gray-400">
|
||||
{Math.round(product.nutriments.energy_100g)} kcal
|
||||
</span>
|
||||
)}
|
||||
{product.nutriments?.fat_100g &&
|
||||
typeof product.nutriments.fat_100g === "number" && (
|
||||
<span className="text-gray-600 dark:text-gray-400">
|
||||
{product.nutriments.fat_100g.toFixed(1)}g gord.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botão Ver - menor */}
|
||||
<Link
|
||||
href={`/produto/${product.code}`}
|
||||
className="mt-2 inline-flex items-center justify-center gap-1 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium rounded-md transition-colors"
|
||||
>
|
||||
<Eye className="w-3 h-3" />
|
||||
Ver
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Layout desktop padrão
|
||||
function getImageUrl(product: Product): string {
|
||||
return (
|
||||
<Card
|
||||
className={`product-card group relative overflow-hidden hover:shadow-2xl transition-all duration-300 hover:-translate-y-2 bg-white dark:bg-gray-900 border border-gray-100 dark:border-gray-800 ${className}`}
|
||||
product.image_front_url ||
|
||||
product.image_front_small_url ||
|
||||
product.image_url ||
|
||||
product.image_small_url ||
|
||||
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='400'%3E%3Crect fill='%23f3efe7' width='400' height='400'/%3E%3Ctext fill='%23a39c93' font-family='Georgia,serif' font-size='16' x='50%25' y='50%25' text-anchor='middle' dominant-baseline='middle'%3ESem imagem%3C/text%3E%3C/svg%3E"
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Nutri-Letter hero circle ───────────────────────────────────────────── */
|
||||
function NutriLetter({ grade, size = 56 }: { grade: string; size?: number }) {
|
||||
const g = grade?.toLowerCase();
|
||||
if (!g || !NUTRI_GRADE_CLASS[g]) return null;
|
||||
return (
|
||||
<div
|
||||
className={`nutri-letter ${g}`}
|
||||
style={{ width: size, height: size, fontSize: Math.round(size * 0.62), flexShrink: 0 }}
|
||||
aria-label={`Nutri-Score ${g.toUpperCase()}`}
|
||||
>
|
||||
{/* Imagem do Produto */}
|
||||
<div className="relative h-56 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-700 overflow-hidden">
|
||||
{g.toUpperCase()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Verdict dot — pure Tailwind, no inline style ──────────────────────── */
|
||||
function VerdictDot({ grade, nova }: { grade?: string; nova?: number }) {
|
||||
const { label, colorClass } = getVerdict(grade, nova);
|
||||
const dotBg = getVerdictDotBg(grade, nova);
|
||||
return (
|
||||
<span className={cn("inline-flex items-center gap-1.5 text-[11px] font-semibold", colorClass)}>
|
||||
<span className={cn("w-1.5 h-1.5 rounded-full shrink-0", dotBg)} />
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
MOBILE VARIANT — horizontal row
|
||||
All hover/focus states are pure CSS via Tailwind so keyboard + touch work
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
||||
function MobileCard({ product, className, priority, currentPage }: Omit<ProductCardProps, "variant">) {
|
||||
const imageUrl = getImageUrl(product);
|
||||
const grade = product.nutrition_grades?.toLowerCase();
|
||||
const nova = product.nova_group;
|
||||
const allergenCount =
|
||||
product.allergens_tags?.filter((a) =>
|
||||
["gluten", "milk", "eggs", "nuts", "peanuts", "soy"].some((c) =>
|
||||
a.toLowerCase().includes(c)
|
||||
)
|
||||
).length ?? 0;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/produto/${product.code}`}
|
||||
className={cn(
|
||||
"product-card group flex gap-3.5 items-center p-3.5",
|
||||
"bg-[var(--ink-0)] border border-[var(--ink-200)] rounded-[14px]",
|
||||
/* Hover — CSS only, works for keyboard focus-visible too */
|
||||
"transition-all duration-200 ease-out",
|
||||
"hover:border-[var(--ink-400)] hover:-translate-y-px hover:shadow-[0_6px_20px_-10px_rgba(0,0,0,0.14)]",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink-0)]",
|
||||
"active:scale-[0.99] active:shadow-none",
|
||||
className
|
||||
)}
|
||||
aria-label={`${product.product_name || "Produto"} — ver detalhes`}
|
||||
>
|
||||
{/* Thumb */}
|
||||
<div className="relative shrink-0 w-[72px] h-[72px] rounded-[10px] overflow-hidden bg-[var(--ink-100)]">
|
||||
<OptimizedImage
|
||||
src={imageUrl}
|
||||
alt={product.product_name || "Produto"}
|
||||
fill
|
||||
priority={priority}
|
||||
onPageChange={currentPage}
|
||||
className="object-contain p-6 group-hover:scale-110 transition-transform duration-500"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
className="object-contain p-1.5"
|
||||
sizes="72px"
|
||||
placeholder="empty"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Overlay com badges */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
|
||||
{/* Badge Nutri-Score */}
|
||||
{nutritionGrade && (
|
||||
<div className="absolute top-3 right-3">
|
||||
<Badge
|
||||
className={`${nutritionGrade.className} shadow-lg text-xs font-bold px-2 py-1`}
|
||||
>
|
||||
<Zap className="w-3 h-3 mr-1" />
|
||||
{nutritionGrade.label}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Badge NOVA */}
|
||||
{novaGroup && (
|
||||
<div className="absolute top-3 left-3">
|
||||
<Badge
|
||||
className={`${novaGroup.className} shadow-lg text-xs font-bold px-2 py-1`}
|
||||
>
|
||||
NOVA {novaGroup.label}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Ícone de alérgenos */}
|
||||
{hasCommonAllergens && (
|
||||
<div className="absolute bottom-3 right-3">
|
||||
<div className="bg-yellow-500 text-white rounded-full p-1.5 shadow-lg">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Botão de favorito */}
|
||||
<div className="absolute bottom-3 left-3 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="h-8 w-8 p-0 rounded-full bg-white/90 hover:bg-white shadow-lg"
|
||||
>
|
||||
<Heart className="w-4 h-4" />
|
||||
</Button>
|
||||
{/* Center text */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="eyebrow-mono truncate mb-0.5">
|
||||
{product.brands || "Marca desconhecida"}
|
||||
</div>
|
||||
<div className="text-[15px] font-medium text-[var(--ink-900)] truncate mb-2 tracking-[-0.005em]">
|
||||
{product.product_name || "Produto sem nome"}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<VerdictDot grade={grade} nova={nova} />
|
||||
{nova && (
|
||||
<>
|
||||
<span className="text-[var(--ink-300)]">·</span>
|
||||
<span className="eyebrow-mono text-[var(--ink-500)]">NOVA {nova}</span>
|
||||
</>
|
||||
)}
|
||||
{allergenCount > 0 && (
|
||||
<>
|
||||
<span className="text-[var(--ink-300)]">·</span>
|
||||
<span className="inline-flex items-center gap-1 text-[11px] text-[var(--warn)] font-medium">
|
||||
<AlertTriangle className="w-2.5 h-2.5" strokeWidth={2} />
|
||||
{allergenCount} alérgeno{allergenCount > 1 ? "s" : ""}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="p-5 space-y-4">
|
||||
{/* Nome do Produto */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-lg leading-tight line-clamp-2 min-h-[2.5rem] text-gray-900 dark:text-white group-hover:text-primary transition-colors">
|
||||
{/* Nutri-Score letter */}
|
||||
{grade && NUTRI_GRADE_CLASS[grade] && <NutriLetter grade={grade} size={52} />}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
DEFAULT VARIANT — portrait card, image hero
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
||||
function DefaultCard({ product, className, priority, currentPage }: Omit<ProductCardProps, "variant">) {
|
||||
const imageUrl = getImageUrl(product);
|
||||
const nutritionData = formatNutritionData(product);
|
||||
const grade = product.nutrition_grades?.toLowerCase();
|
||||
const nova = product.nova_group;
|
||||
const allergenCount =
|
||||
product.allergens_tags?.filter((a) =>
|
||||
["gluten", "milk", "eggs", "nuts", "peanuts", "soy"].some((c) =>
|
||||
a.toLowerCase().includes(c)
|
||||
)
|
||||
).length ?? 0;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/produto/${product.code}`}
|
||||
className={cn(
|
||||
/* Layout */
|
||||
"product-card group flex flex-col h-full",
|
||||
/* Surface */
|
||||
"bg-[var(--ink-0)] border border-[var(--ink-200)] rounded-2xl overflow-hidden",
|
||||
/* Transitions — one rule governs border + lift + shadow */
|
||||
"transition-all duration-200 ease-out",
|
||||
/* Hover — CSS, no JS */
|
||||
"hover:border-[var(--ink-400)] hover:-translate-y-0.5 hover:shadow-[0_12px_32px_-14px_rgba(0,0,0,0.15)]",
|
||||
/* Keyboard focus ring — WCAG 2.4.7 compliant */
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-base)]",
|
||||
/* Press feedback */
|
||||
"active:scale-[0.99] active:shadow-none",
|
||||
className
|
||||
)}
|
||||
aria-label={`${product.product_name || "Produto"} — ver detalhes`}
|
||||
>
|
||||
{/* ── Hero image area ─────────────────────────────────────────────── */}
|
||||
<div className="relative aspect-[5/4] bg-[var(--ink-50)] border-b border-[var(--ink-200)] overflow-hidden">
|
||||
{/* Image zooms on hover because parent now HAS the `group` class */}
|
||||
<OptimizedImage
|
||||
src={imageUrl}
|
||||
alt={product.product_name || "Produto"}
|
||||
fill
|
||||
priority={priority}
|
||||
onPageChange={currentPage}
|
||||
className="object-contain p-6 transition-transform duration-500 ease-out group-hover:scale-105"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
/>
|
||||
|
||||
{/* Nutri-Score — top-right */}
|
||||
{grade && NUTRI_GRADE_CLASS[grade] && (
|
||||
<div className="absolute top-3.5 right-3.5 drop-shadow-md">
|
||||
<NutriLetter grade={grade} size={52} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Allergen chip — top-left */}
|
||||
{allergenCount > 0 && (
|
||||
<div className="absolute top-3.5 left-3.5">
|
||||
<span className="rd-chip warn" style={{ padding: "4px 9px", fontSize: 11 }}>
|
||||
<AlertTriangle className="w-3 h-3" strokeWidth={2} />
|
||||
{allergenCount}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Card body ──────────────────────────────────────────────────── */}
|
||||
<div className="flex flex-col gap-3.5 p-[18px] flex-1">
|
||||
|
||||
{/* Brand + product name */}
|
||||
<div>
|
||||
<div className="eyebrow-mono truncate mb-1">
|
||||
{product.brands || "Marca desconhecida"}
|
||||
</div>
|
||||
<h3
|
||||
className="font-display text-[22px] leading-[1.15] tracking-[-0.01em] text-[var(--ink-900)] line-clamp-2 m-0"
|
||||
>
|
||||
{product.product_name || "Nome não disponível"}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Marca */}
|
||||
{product.brands && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 font-medium">
|
||||
{product.brands}
|
||||
</p>
|
||||
{/* ── Nutrition mini-bar: 4 columns ─────────────────────────────── */}
|
||||
{nutritionData && (
|
||||
<div className="grid grid-cols-4 border-y border-[var(--ink-200)]">
|
||||
{[
|
||||
{ l: "kcal", v: nutritionData.energy },
|
||||
{ l: "Açúcar", v: nutritionData.sugars },
|
||||
{ l: "Gord.", v: nutritionData.fat },
|
||||
{ l: "Sódio", v: nutritionData.sodium },
|
||||
].map((n, i) =>
|
||||
n.v ? (
|
||||
<div
|
||||
key={n.l}
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-center py-3 px-1.5 min-w-0 text-center",
|
||||
i < 3 && "border-r border-[var(--ink-200)]"
|
||||
)}
|
||||
>
|
||||
<span className="font-display text-base text-[var(--ink-900)] leading-none truncate max-w-full">
|
||||
{n.v}
|
||||
</span>
|
||||
<span className="eyebrow-mono text-[9px] text-[var(--ink-500)] mt-1.5">
|
||||
{n.l}
|
||||
</span>
|
||||
</div>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer row: verdict + nova badge */}
|
||||
<div className="flex items-center justify-between mt-auto">
|
||||
<VerdictDot grade={grade} nova={nova} />
|
||||
{nova && (
|
||||
<span className="eyebrow-mono text-[var(--ink-500)]">
|
||||
NOVA {nova}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Informações nutricionais resumidas */}
|
||||
{nutritionData && (
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Energia:</span>
|
||||
<span className="font-medium">{nutritionData.energy}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Gorduras:</span>
|
||||
<span className="font-medium">{nutritionData.fat}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Açúcares:</span>
|
||||
<span className="font-medium">{nutritionData.sugars}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Sódio:</span>
|
||||
<span className="font-medium">{nutritionData.sodium}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Categorias */}
|
||||
{product.categories && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{product.categories
|
||||
.split(",")
|
||||
.slice(0, 2)
|
||||
.map((cat, i) => (
|
||||
<Badge
|
||||
key={i}
|
||||
variant="outline"
|
||||
className="text-xs font-normal bg-gray-50 dark:bg-gray-800 border-gray-100 dark:border-gray-800"
|
||||
>
|
||||
{cat.trim()}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Alérgenos */}
|
||||
{hasCommonAllergens && (
|
||||
<div className="flex items-center gap-1 text-xs text-yellow-600 dark:text-yellow-400">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
<span>Contém alérgenos</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Botões de ação */}
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 group-hover:bg-primary group-hover:text-primary-foreground group-hover:border-primary transition-all duration-200"
|
||||
onClick={() => router.push(`/produto/${product.code}`)}
|
||||
>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
Ver Detalhes
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="px-3 group-hover:bg-primary group-hover:text-primary-foreground transition-all duration-200"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
{/* CTA — styled as ghost button, inherits the card's hover context */}
|
||||
<div
|
||||
className={cn(
|
||||
"block text-center py-2.5 text-[13px] font-medium text-[var(--ink-700)] no-underline",
|
||||
"border border-[var(--ink-200)] rounded-[10px]",
|
||||
"transition-colors duration-150",
|
||||
/* Group-hover darkens the CTA border/text for free */
|
||||
"group-hover:border-[var(--ink-400)] group-hover:text-[var(--ink-900)]"
|
||||
)}
|
||||
aria-hidden="true" /* Link is on the <Link> wrapper above */
|
||||
tabIndex={-1}
|
||||
>
|
||||
Ver detalhes →
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Public export ──────────────────────────────────────────────────────── */
|
||||
export function ProductCard({ variant = "default", ...props }: ProductCardProps) {
|
||||
if (variant === "mobile") return <MobileCard {...props} />;
|
||||
return <DefaultCard {...props} />;
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SkeletonProps {
|
||||
className?: string;
|
||||
}
|
||||
interface SkeletonProps { className?: string; }
|
||||
|
||||
function Skeleton({ className }: SkeletonProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"animate-pulse rounded-md bg-gray-200 dark:bg-gray-700",
|
||||
"animate-pulse rounded bg-[var(--border-subtle)]",
|
||||
className
|
||||
)}
|
||||
/>
|
||||
@@ -18,124 +16,62 @@ function Skeleton({ className }: SkeletonProps) {
|
||||
|
||||
export function ProductSkeleton({ className }: SkeletonProps) {
|
||||
return (
|
||||
<div className={cn("space-y-4", className)}>
|
||||
{/* Header skeleton */}
|
||||
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Skeleton className="h-8 w-16" />
|
||||
<div className="flex-1">
|
||||
<Skeleton className="h-8 w-64 mb-2" />
|
||||
<Skeleton className="h-4 w-32" />
|
||||
</div>
|
||||
<div className={cn("min-h-screen bg-[var(--bg-base)]", className)}>
|
||||
{/* Sticky header skeleton */}
|
||||
<div className="sticky top-0 z-50 bg-[var(--bg-elevated)] border-b border-[var(--border-subtle)] h-20">
|
||||
<div className="container-editorial h-full flex items-center gap-6">
|
||||
<Skeleton className="w-9 h-9 rounded" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<Skeleton className="h-5 w-64" />
|
||||
<Skeleton className="h-3 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content skeleton */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Left column - Product image */}
|
||||
<div className="space-y-4">
|
||||
<div className="relative w-full max-w-2xl mx-auto bg-white dark:bg-gray-800 rounded-xl overflow-hidden shadow-lg">
|
||||
<div className="aspect-square bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-700 p-8">
|
||||
<Skeleton className="w-full h-full rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Basic info skeleton */}
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-xl p-4">
|
||||
<Skeleton className="h-5 w-32 mb-3" />
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="container-editorial py-12">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12">
|
||||
{/* Left column */}
|
||||
<div className="lg:col-span-5 space-y-6">
|
||||
<Skeleton className="aspect-square w-full rounded-2xl" />
|
||||
<div className="card-warm rounded-xl p-8 space-y-4">
|
||||
<Skeleton className="h-3 w-24" />
|
||||
{[1,2,3].map(i => (
|
||||
<div key={i} className="flex justify-between">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-4 w-32" />
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right column - Product details */}
|
||||
<div className="space-y-6">
|
||||
{/* Nutrition grades skeleton */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4">
|
||||
<Skeleton className="h-5 w-24 mb-3" />
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="w-12 h-12 rounded-full" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-3 w-32" />
|
||||
</div>
|
||||
{/* Right column */}
|
||||
<div className="lg:col-span-7 space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{[1,2].map(i => (
|
||||
<div key={i} className="card-warm rounded-xl p-8 space-y-4">
|
||||
<Skeleton className="h-3 w-24" />
|
||||
<Skeleton className="h-3 w-40" />
|
||||
<Skeleton className="w-20 h-20 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4">
|
||||
<Skeleton className="h-5 w-16 mb-3" />
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="w-12 h-12 rounded-full" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-3 w-28" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Nutritional information skeleton */}
|
||||
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4">
|
||||
<Skeleton className="h-5 w-48 mb-4" />
|
||||
<div className="card-warm rounded-xl p-8 space-y-4">
|
||||
<Skeleton className="h-5 w-48" />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{[...Array(7)].map((_, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-3 w-20" />
|
||||
<Skeleton className="h-3 w-16" />
|
||||
</div>
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<Skeleton className="h-1.5 w-full rounded-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ingredients skeleton */}
|
||||
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4">
|
||||
<Skeleton className="h-5 w-24 mb-3" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-4/5" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Allergens skeleton */}
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-xl p-4">
|
||||
<Skeleton className="h-5 w-20 mb-3" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<Skeleton key={i} className="h-6 w-16 rounded-full" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Additives skeleton */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-xl p-4">
|
||||
<Skeleton className="h-5 w-16 mb-3" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<Skeleton key={i} className="h-6 w-12 rounded-full" />
|
||||
))}
|
||||
<Skeleton className="h-6 w-12 rounded-full" />
|
||||
</div>
|
||||
<div className="card-warm rounded-xl p-8 space-y-3">
|
||||
<Skeleton className="h-5 w-36" />
|
||||
{[1,2,3,4].map(i => <Skeleton key={i} className="h-4 w-full" />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,26 +82,20 @@ export function ProductSkeleton({ className }: SkeletonProps) {
|
||||
|
||||
export function ProductCardSkeleton({ className }: SkeletonProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="aspect-square bg-gray-100 dark:bg-gray-700 p-4">
|
||||
<Skeleton className="w-full h-full rounded-lg" />
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
<div className={cn("card-warm rounded-xl overflow-hidden", className)}>
|
||||
<Skeleton className="aspect-square w-full" />
|
||||
<div className="p-5 space-y-3">
|
||||
<Skeleton className="h-5 w-3/4" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="w-8 h-8 rounded-full" />
|
||||
<Skeleton className="h-4 w-16" />
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-6 w-16 rounded-full" />
|
||||
<Skeleton className="h-3 w-1/2" />
|
||||
<div className="pt-3 border-t border-[var(--border-subtle)] space-y-2">
|
||||
{[1,2,3,4].map(i => (
|
||||
<div key={i} className="flex justify-between">
|
||||
<Skeleton className="h-3 w-16" />
|
||||
<Skeleton className="h-3 w-12" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Skeleton className="h-9 w-full rounded" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -174,23 +104,21 @@ export function ProductCardSkeleton({ className }: SkeletonProps) {
|
||||
export function NutritionBarSkeleton({ className }: SkeletonProps) {
|
||||
return (
|
||||
<div className={cn("space-y-2", className)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-4 w-16" />
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<Skeleton className="h-2 w-3/4 rounded-full" />
|
||||
<div className="flex justify-between">
|
||||
<Skeleton className="h-3 w-20" />
|
||||
<Skeleton className="h-3 w-12" />
|
||||
</div>
|
||||
<Skeleton className="h-1.5 w-full rounded-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BadgeSkeleton({ className }: SkeletonProps) {
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2", className)}>
|
||||
<Skeleton className="w-12 h-12 rounded-full" />
|
||||
<div className="space-y-1">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<div className={cn("flex items-center gap-3", className)}>
|
||||
<Skeleton className="w-14 h-14 rounded-full" />
|
||||
<div className="space-y-1.5">
|
||||
<Skeleton className="h-3 w-16" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,32 +2,29 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Search } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Search, ScanLine } from "lucide-react";
|
||||
import { detectSearchType, isValidBarcode } from "@/lib/openFoodFactsApi";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SearchBarProps {
|
||||
initialQuery?: string;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
compact?: boolean; // Modo compacto para mobile
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export function SearchBar({
|
||||
initialQuery = "",
|
||||
className = "",
|
||||
placeholder = "Buscar produtos...",
|
||||
placeholder = "Buscar produtos ou código de barras…",
|
||||
compact = false,
|
||||
}: SearchBarProps) {
|
||||
const [query, setQuery] = useState(initialQuery);
|
||||
const [searchType, setSearchType] = useState<"barcode" | "name" | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
// Detectar tipo de busca em tempo real
|
||||
useEffect(() => {
|
||||
const cleaned = query.replace(/\D/g, ""); // Remove tudo que não é número
|
||||
|
||||
const cleaned = query.replace(/\D/g, "");
|
||||
if (cleaned.length >= 8 && cleaned.length <= 13) {
|
||||
setSearchType("barcode");
|
||||
} else if (query.trim()) {
|
||||
@@ -39,97 +36,90 @@ export function SearchBar({
|
||||
|
||||
const handleSearch = () => {
|
||||
if (!query.trim()) return;
|
||||
|
||||
const type = detectSearchType(query);
|
||||
|
||||
if (type === "barcode") {
|
||||
// Limpar código de barras (remover espaços/caracteres)
|
||||
const cleanBarcode = query.replace(/\D/g, "");
|
||||
|
||||
if (isValidBarcode(cleanBarcode)) {
|
||||
// Redirecionar para página do produto
|
||||
router.push(`/produto/${cleanBarcode}`);
|
||||
const clean = query.replace(/\D/g, "");
|
||||
if (isValidBarcode(clean)) {
|
||||
router.push(`/produto/${clean}`);
|
||||
} else {
|
||||
// Mostrar erro de código inválido
|
||||
alert("Código de barras inválido. Deve ter entre 8 e 13 dígitos.");
|
||||
router.push(`/resultados?q=${encodeURIComponent(query.trim())}`);
|
||||
}
|
||||
} else {
|
||||
// Busca normal por nome
|
||||
router.push(`/resultados?q=${encodeURIComponent(query.trim())}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearch();
|
||||
}
|
||||
const handleKey = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") handleSearch();
|
||||
};
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div className={`flex items-center gap-2 ${className}`}>
|
||||
<div className={cn("flex items-center gap-2", className)}>
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-[var(--fg-faint)]" strokeWidth={1.5} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
className="pl-8 pr-2 h-9 text-sm"
|
||||
onKeyDown={handleKey}
|
||||
className={cn(
|
||||
"w-full pl-9 pr-3 h-9 text-sm rounded border border-[var(--border-subtle)]",
|
||||
"bg-[var(--bg-elevated)] text-[var(--fg-primary)] placeholder-[var(--fg-faint)]",
|
||||
"focus:outline-none focus:border-[var(--accent)] transition-colors duration-200"
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Indicador visual do tipo de busca - mais compacto */}
|
||||
{searchType && (
|
||||
<div className="absolute right-2 top-1/2 transform -translate-y-1/2">
|
||||
<span className="text-xs text-gray-500 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-[10px]">
|
||||
{searchType === "barcode" ? "Código" : "Nome"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
disabled={!query.trim()}
|
||||
size="sm"
|
||||
className="h-9 px-3"
|
||||
className="btn-accent h-9 px-3 py-0 text-xs disabled:opacity-40"
|
||||
>
|
||||
<Search className="h-4 w-4" />
|
||||
</Button>
|
||||
<Search className="w-3.5 h-3.5" strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-2 ${className}`}>
|
||||
<div className={cn("flex items-center gap-2", className)}>
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-[var(--fg-muted)]">
|
||||
{searchType === "barcode" ? (
|
||||
<ScanLine className="w-4 h-4" strokeWidth={1.5} />
|
||||
) : (
|
||||
<Search className="w-4 h-4" strokeWidth={1.5} />
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
className="pl-10 pr-4 h-10"
|
||||
onKeyDown={handleKey}
|
||||
className={cn(
|
||||
"w-full pl-10 pr-4 h-10 text-sm rounded border border-[var(--border-subtle)]",
|
||||
"bg-[var(--bg-elevated)] text-[var(--fg-primary)] placeholder-[var(--fg-faint)]",
|
||||
"focus:outline-none focus:border-[var(--accent)] transition-colors duration-200"
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Indicador visual do tipo de busca */}
|
||||
{searchType && (
|
||||
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<span className="text-xs text-gray-500 bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">
|
||||
{searchType === "barcode" ? "Código de barras" : "Nome"}
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
||||
<span className="label text-[var(--fg-faint)] bg-[var(--bg-surface)] px-2 py-0.5 rounded border border-[var(--border-subtle)]">
|
||||
{searchType === "barcode" ? "Código" : "Nome"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
disabled={!query.trim()}
|
||||
className="h-10 px-4"
|
||||
className="btn-accent h-10 px-4 text-sm disabled:opacity-40"
|
||||
>
|
||||
<Search className="h-4 w-4 mr-2" />
|
||||
<Search className="w-4 h-4" strokeWidth={1.5} />
|
||||
Buscar
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+77
-93
@@ -3,14 +3,12 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useTheme } from "next-themes";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Sun, Moon, Menu, X } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Image from "next/image"
|
||||
import Image from "next/image";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// 1. Definição de Dados Centralizada (SSOT - Single Source of Truth)
|
||||
const NAV_LINKS = [
|
||||
{ href: "/", label: "Como funciona" },
|
||||
{ href: "/#como-funciona", label: "Como funciona" },
|
||||
{ href: "/#recursos", label: "Recursos" },
|
||||
{ href: "/sobre", label: "Sobre" },
|
||||
];
|
||||
@@ -18,130 +16,116 @@ const NAV_LINKS = [
|
||||
export function Header() {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
// Prevenção de Hydration Mismatch
|
||||
useEffect(() => setMounted(true), []);
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const onScroll = () => setScrolled(window.scrollY > 12);
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => setTheme(theme === "dark" ? "light" : "dark");
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-[100] w-full bg-white/70 dark:bg-gray-900/70 backdrop-blur-md border-b border-gray-200 dark:border-gray-800 transition-colors duration-500">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<header
|
||||
className={cn(
|
||||
"sticky top-0 z-[100] w-full transition-all duration-300",
|
||||
scrolled
|
||||
? "bg-[var(--bg-base)]/90 backdrop-blur-md border-b border-[var(--border-subtle)]"
|
||||
: "bg-[var(--bg-base)] border-b border-transparent"
|
||||
)}
|
||||
>
|
||||
<div className="container-editorial">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
|
||||
{/* Logo Section - Unificada */}
|
||||
|
||||
{/* ── Logo ──────────────────────────────────────────────────── */}
|
||||
<Link href="/" className="flex items-center gap-3 group">
|
||||
{/* Container do Logo com Referencial Relativo */}
|
||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center shadow-lg shadow-blue-500/20 group-hover:rotate-6 transition-transform relative overflow-hidden">
|
||||
<Image
|
||||
src="/apple-touch-icon.png"
|
||||
alt="NutriScan Logo"
|
||||
fill // Ocupa todo o container pai
|
||||
priority // CRÍTICO: Carrega o logo antes de tudo para melhorar o LCP
|
||||
sizes="40px" // Dica para o browser sobre o tamanho real em pixels
|
||||
className="object-contain p-2" // Mantém a proporção e adiciona respiro interno
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-lg font-black tracking-tighter text-gray-900 dark:text-white leading-none">
|
||||
NUTRI<span className="text-blue-500">SCAN</span>
|
||||
<Image
|
||||
src="/apple-touch-icon.png"
|
||||
alt="NutriScan"
|
||||
width={32}
|
||||
height={32}
|
||||
priority
|
||||
className="rounded object-contain"
|
||||
/>
|
||||
<div className="flex flex-col leading-none">
|
||||
<span className="font-sans font-semibold text-[var(--fg-primary)] text-base tracking-tight">
|
||||
NutriScan
|
||||
</span>
|
||||
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest leading-none mt-1">
|
||||
Data Engineering
|
||||
<span className="label text-[var(--fg-faint)] mt-0.5">
|
||||
Dados Nutricionais
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Nav - Com micro-interações */}
|
||||
{/* ── Desktop nav ───────────────────────────────────────────── */}
|
||||
<nav className="hidden md:flex items-center gap-1">
|
||||
{NAV_LINKS.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="px-4 py-2 text-sm font-semibold text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-all"
|
||||
className="px-4 py-2 label text-[var(--fg-muted)] hover:text-[var(--fg-primary)] rounded transition-colors duration-200"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Ações e Mobile Toggle */}
|
||||
{/* ── Actions ───────────────────────────────────────────────── */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Theme toggle */}
|
||||
{mounted && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="rounded-full w-10 h-10 border border-gray-200 dark:border-gray-800"
|
||||
aria-label={theme === "dark" ? "Modo claro" : "Modo escuro"}
|
||||
className="w-9 h-9 flex items-center justify-center rounded border border-[var(--border-subtle)] text-[var(--fg-muted)] hover:text-[var(--fg-primary)] hover:border-[var(--border-strong)] transition-all duration-200"
|
||||
>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ rotate: theme === "dark" ? 0 : 180, scale: 1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
{theme === "dark" ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
|
||||
</motion.div>
|
||||
</Button>
|
||||
{theme === "dark" ? (
|
||||
<Sun className="w-4 h-4" strokeWidth={1.5} />
|
||||
) : (
|
||||
<Moon className="w-4 h-4" strokeWidth={1.5} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="md:hidden rounded-full"
|
||||
{/* Mobile hamburger */}
|
||||
<button
|
||||
className="md:hidden w-9 h-9 flex items-center justify-center rounded border border-[var(--border-subtle)] text-[var(--fg-muted)] hover:text-[var(--fg-primary)] transition-all duration-200"
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
aria-label={isMenuOpen ? "Fechar menu" : "Abrir menu"}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={isMenuOpen ? "close" : "open"}
|
||||
initial={{ opacity: 0, rotate: -90 }}
|
||||
animate={{ opacity: 1, rotate: 0 }}
|
||||
exit={{ opacity: 0, rotate: 90 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{isMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</Button>
|
||||
{isMenuOpen ? (
|
||||
<X className="w-4 h-4" strokeWidth={1.5} />
|
||||
) : (
|
||||
<Menu className="w-4 h-4" strokeWidth={1.5} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu - Animado com AnimatePresence */}
|
||||
<AnimatePresence>
|
||||
{isMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
className="md:hidden border-t border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 overflow-hidden"
|
||||
>
|
||||
<nav className="flex flex-col p-6 gap-4">
|
||||
{NAV_LINKS.map((link, i) => (
|
||||
<motion.div
|
||||
key={link.href}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
>
|
||||
<Link
|
||||
href={link.href}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
className="text-xl font-bold text-gray-900 dark:text-white hover:text-blue-500 transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
<div className="pt-4 border-t border-gray-100 dark:border-gray-800">
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-[0.3em]">
|
||||
NutriScan v2.0
|
||||
</p>
|
||||
</div>
|
||||
</nav>
|
||||
</motion.div>
|
||||
{/* ── Mobile menu ───────────────────────────────────────────────── */}
|
||||
<div
|
||||
className={cn(
|
||||
"md:hidden overflow-hidden transition-all duration-300 border-t border-[var(--border-subtle)]",
|
||||
isMenuOpen ? "max-h-64 opacity-100" : "max-h-0 opacity-0"
|
||||
)}
|
||||
</AnimatePresence>
|
||||
>
|
||||
<nav className="container-editorial py-6 flex flex-col gap-1">
|
||||
{NAV_LINKS.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
className="py-3 font-sans font-medium text-[var(--fg-secondary)] hover:text-[var(--fg-primary)] border-b border-[var(--border-subtle)] last:border-0 transition-colors duration-200"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,97 +1,111 @@
|
||||
"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 { Heart, Server, ShieldCheck, Globe } from "lucide-react";
|
||||
|
||||
export function DonationSection() {
|
||||
return (
|
||||
<section className="py-24 bg-white dark:bg-[#030712] relative overflow-hidden">
|
||||
{/* Background Decor sutil */}
|
||||
<div className="absolute top-0 right-0 w-1/3 h-1/3 bg-blue-500/5 blur-[120px] rounded-full pointer-events-none" />
|
||||
<section className="bg-base section-py">
|
||||
<div className="container-editorial">
|
||||
|
||||
<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.
|
||||
{/* ── Section header ───────────────────────────────────────────── */}
|
||||
<div className="mb-16">
|
||||
<span className="label text-[var(--accent)] block mb-4">Apoie o Projeto</span>
|
||||
<hr className="hairline mb-10" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 items-end">
|
||||
<h2 className="font-display text-display-lg text-[var(--fg-primary)]">
|
||||
Mantenha a informação{" "}
|
||||
<em className="font-display-i text-[var(--accent)]">livre.</em>
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 mb-8 leading-relaxed">
|
||||
O NutriScan é um projeto de código aberto nascido em Garuva, SC. Operamos sem anúncios e sem venda de dados. Nosso único combustível é o apoio da comunidade que acredita na democratização da saúde.
|
||||
<p className="text-[var(--fg-secondary)] text-lg leading-relaxed max-w-md">
|
||||
O NutriScan opera sem anúncios e sem venda de dados. Nosso único
|
||||
combustível é o apoio de quem acredita na democratização da saúde.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* ── Content grid ─────────────────────────────────────────────── */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 lg:gap-20 items-start">
|
||||
|
||||
{/* Left: What your support funds */}
|
||||
<div className="lg:col-span-6">
|
||||
<div className="divide-y divide-[var(--border-subtle)]">
|
||||
{[
|
||||
{ icon: Server, text: "Manutenção de Servidores e API de Alta Latência" },
|
||||
{ icon: ShieldCheck, text: "Segurança de Dados e Auditoria Open Source" },
|
||||
{ icon: Globe, text: "Expansão da Base de Dados Internacional" },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex items-center gap-3 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
|
||||
<item.icon className="w-4 h-4 text-blue-600" />
|
||||
{
|
||||
icon: Server,
|
||||
title: "Infraestrutura",
|
||||
desc: "Servidores, CDN e API de alta disponibilidade para garantir acesso global sem interrupções.",
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: "Segurança & Auditoria",
|
||||
desc: "Revisões de código open source, análise de dependências e manutenção da integridade dos dados.",
|
||||
},
|
||||
{
|
||||
icon: Globe,
|
||||
title: "Expansão da Base",
|
||||
desc: "Integração com novos mercados e suporte a mais países, línguas e categorias de produtos.",
|
||||
},
|
||||
].map(({ icon: Icon, title, desc }, i) => (
|
||||
<div key={i} className="flex gap-6 py-8 group">
|
||||
<div className="shrink-0 mt-0.5">
|
||||
<Icon
|
||||
className="w-5 h-5 text-[var(--fg-muted)] group-hover:text-[var(--accent)] transition-colors duration-300"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-sans font-semibold text-[var(--fg-primary)] mb-1.5">
|
||||
{title}
|
||||
</h4>
|
||||
<p className="text-[var(--fg-secondary)] text-sm leading-relaxed">
|
||||
{desc}
|
||||
</p>
|
||||
</div>
|
||||
{item.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Lado Direito: O Card de Doação (Psychological Anchoring) */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="relative"
|
||||
>
|
||||
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 p-8 rounded-[2.5rem] shadow-2xl backdrop-blur-sm">
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-yellow-400 to-orange-500 rounded-2xl flex items-center justify-center shadow-lg shadow-orange-500/20">
|
||||
<Coffee className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-gray-900 dark:text-white">Apoie com um Café</h4>
|
||||
<p className="text-xs text-gray-500 uppercase tracking-widest font-mono">Status: Sistema Online</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Right: Donation card */}
|
||||
<div className="lg:col-span-6">
|
||||
<div className="card-warm p-10 rounded-xl">
|
||||
|
||||
<blockquote className="italic text-gray-600 dark:text-gray-400 text-sm border-l-2 border-blue-500 pl-4 mb-8">
|
||||
"Como desenvolvedor independente, cada contribuição me permite dedicar mais horas à refatoração e novas features para o NutriScan."
|
||||
{/* Eyebrow */}
|
||||
<span className="label text-[var(--fg-muted)] block mb-8">
|
||||
Desenvolvido em Garuva, SC · Brasil
|
||||
</span>
|
||||
|
||||
{/* Quote */}
|
||||
<blockquote className="border-l-2 border-[var(--accent)] pl-5 mb-10">
|
||||
<p className="font-display text-display-sm text-[var(--fg-primary)] leading-snug mb-3">
|
||||
“Cada contribuição me permite dedicar mais horas ao{" "}
|
||||
<em className="font-display-i">NutriScan</em>.”
|
||||
</p>
|
||||
<cite className="label text-[var(--fg-muted)] not-italic">
|
||||
— Evandro, desenvolvedor
|
||||
</cite>
|
||||
</blockquote>
|
||||
|
||||
{/* CTA */}
|
||||
<a
|
||||
href="https://buymeacoffee.com/evandrotruuta"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block group"
|
||||
className="btn-accent w-full justify-center py-4 text-base"
|
||||
>
|
||||
<Button className="w-full h-16 bg-blue-600 hover:bg-blue-700 text-white rounded-2xl font-bold text-lg transition-all group-hover:scale-[1.02] active:scale-95 shadow-xl shadow-blue-500/25">
|
||||
<Heart className="w-5 h-5 mr-3 fill-white group-hover:animate-pulse" />
|
||||
Contribuir agora
|
||||
</Button>
|
||||
<Heart className="w-4 h-4" strokeWidth={1.5} />
|
||||
Contribuir agora
|
||||
</a>
|
||||
|
||||
<div className="mt-6 flex justify-center gap-4">
|
||||
<div className="flex items-center gap-1.5 text-[10px] font-bold text-gray-400 uppercase tracking-tighter">
|
||||
<Code2 size={12} /> Engenharia Brasileira
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-4 label text-center text-[var(--fg-faint)]">
|
||||
Qualquer valor ajuda a manter o NutriScan gratuito.
|
||||
</p>
|
||||
</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>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,147 +1,125 @@
|
||||
"use client";
|
||||
|
||||
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;
|
||||
number: string;
|
||||
title: string;
|
||||
description: string;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
iconBg: string;
|
||||
benefit: string;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
const features: Feature[] = [
|
||||
{
|
||||
icon: Shield,
|
||||
title: "Dados da Comunidade",
|
||||
description: "Informações fornecidas pela Open Food Facts, a maior base colaborativa global mantida por voluntários.",
|
||||
color: "text-blue-600 dark:text-blue-400",
|
||||
bgColor: "bg-blue-50 dark:bg-blue-900/20",
|
||||
iconBg: "bg-blue-100 dark:bg-blue-900/40",
|
||||
benefit: "Base colaborativa mundial",
|
||||
number: "01",
|
||||
title: "Dados Colaborativos",
|
||||
description:
|
||||
"Informações fornecidas pelo Open Food Facts, a maior base colaborativa global — a Wikipedia dos alimentos, mantida por voluntários em todo o mundo.",
|
||||
detail: "2 M+ produtos",
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
title: "Atualização Contínua",
|
||||
description: "Novos produtos e fórmulas atualizados a cada minuto pela comunidade. Dados sempre frescos no seu dispositivo.",
|
||||
color: "text-green-600 dark:text-green-400",
|
||||
bgColor: "bg-green-50 dark:bg-green-900/20",
|
||||
iconBg: "bg-green-100 dark:bg-green-900/40",
|
||||
benefit: "Atualizado em tempo real",
|
||||
number: "02",
|
||||
title: "Sempre Atualizado",
|
||||
description:
|
||||
"Novos produtos e reformulações são adicionados a cada minuto pela comunidade. Conectamos você direto à fonte, sem intermediários.",
|
||||
detail: "Atualização em tempo real",
|
||||
},
|
||||
{
|
||||
icon: Sparkles,
|
||||
title: "Velocidade de Resposta",
|
||||
description: "Interface otimizada para decisões rápidas. Escaneie e obtenha o diagnóstico nutricional em poucos segundos.",
|
||||
color: "text-purple-600 dark:text-purple-400",
|
||||
bgColor: "bg-purple-50 dark:bg-purple-900/20",
|
||||
iconBg: "bg-purple-100 dark:bg-purple-900/40",
|
||||
benefit: "Resultados em <10s",
|
||||
number: "03",
|
||||
title: "Decisão em Segundos",
|
||||
description:
|
||||
"Interface projetada para clareza imediata. Nutri-Score, NOVA e alérgenos visíveis à primeira vista — sem ruído visual.",
|
||||
detail: "Resultados em < 10 s",
|
||||
},
|
||||
{
|
||||
icon: Heart,
|
||||
title: "Filosofia Open Source",
|
||||
description: "O NutriScan é e sempre será gratuito. Conhecimento sobre saúde deve ser um direito universal.",
|
||||
color: "text-orange-600 dark:text-orange-400",
|
||||
bgColor: "bg-orange-50 dark:bg-orange-900/20",
|
||||
iconBg: "bg-orange-100 dark:bg-orange-900/40",
|
||||
benefit: "Gratuito para sempre",
|
||||
number: "04",
|
||||
title: "Open Source, Sempre",
|
||||
description:
|
||||
"O NutriScan é e sempre será gratuito. Conhecimento sobre saúde deve ser um direito universal, sem anúncios ou venda de dados.",
|
||||
detail: "Licença MIT",
|
||||
},
|
||||
];
|
||||
|
||||
// Variantes de animação para o container pai (Stagger effect)
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: 0.15 },
|
||||
},
|
||||
};
|
||||
|
||||
// Variantes para cada card individual
|
||||
const cardVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: { type: "spring", stiffness: 100 },
|
||||
},
|
||||
};
|
||||
|
||||
export function Features() {
|
||||
return (
|
||||
<section className="py-24 bg-white dark:bg-[#030712] relative overflow-hidden">
|
||||
{/* Background Decor: Sutil gradiente para profundidade */}
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-blue-500/5 via-transparent to-transparent pointer-events-none" />
|
||||
<section className="bg-base section-py">
|
||||
<div className="container-editorial">
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 relative z-10">
|
||||
{/* Header com Motion */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-20"
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter">
|
||||
Por que confiar no <span className="text-blue-600">NutriScan</span>
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
|
||||
Engenharia de dados a serviço da sua saúde, com transparência total e zero custo.
|
||||
</p>
|
||||
</motion.div>
|
||||
{/* ── Section header ───────────────────────────────────────────── */}
|
||||
<div className="mb-16">
|
||||
<span className="label text-[var(--accent)] block mb-4">Recursos</span>
|
||||
<hr className="hairline mb-10" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 items-end">
|
||||
<h2 className="font-display text-display-lg text-[var(--fg-primary)]">
|
||||
Por que confiar no{" "}
|
||||
<em className="font-display-i text-[var(--accent)]">NutriScan</em>
|
||||
</h2>
|
||||
<p className="text-[var(--fg-secondary)] text-lg leading-relaxed max-w-md">
|
||||
Engenharia de dados a serviço da sua saúde, com transparência
|
||||
total e zero custo.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features Grid com Stagger */}
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-8"
|
||||
>
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
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 Container */}
|
||||
<div
|
||||
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={cn("w-7 h-7", feature.color)} />
|
||||
</div>
|
||||
|
||||
{/* Text Content */}
|
||||
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-8 leading-relaxed">
|
||||
{feature.description}
|
||||
</p>
|
||||
|
||||
{/* Benefit Badge */}
|
||||
<div className={cn(
|
||||
"inline-flex items-center px-4 py-2 rounded-full text-xs font-bold uppercase tracking-widest",
|
||||
feature.bgColor,
|
||||
feature.color
|
||||
)}>
|
||||
<CheckCircle className="w-3.5 h-3.5 mr-2" />
|
||||
<span>{feature.benefit}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
{/* ── Feature rows ─────────────────────────────────────────────── */}
|
||||
<div className="divide-y divide-[var(--border-subtle)]">
|
||||
{features.map((f, i) => (
|
||||
<FeatureRow key={f.number} feature={f} index={i} />
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* ── Bottom rule ──────────────────────────────────────────────── */}
|
||||
<hr className="hairline mt-0" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function FeatureRow({
|
||||
feature,
|
||||
index,
|
||||
}: {
|
||||
feature: Feature;
|
||||
index: number;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group grid grid-cols-1 md:grid-cols-12 gap-6 md:gap-12 py-10 md:py-12",
|
||||
"transition-colors duration-300 hover:bg-[var(--bg-surface)]",
|
||||
"cursor-default -mx-4 px-4 md:-mx-6 md:px-6"
|
||||
)}
|
||||
style={{ animationDelay: `${index * 0.1}s` }}
|
||||
>
|
||||
{/* Step number */}
|
||||
<div className="md:col-span-1 flex items-start">
|
||||
<span className="label text-[var(--fg-faint)] group-hover:text-[var(--accent)] transition-colors duration-300">
|
||||
{feature.number}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div className="md:col-span-3 flex items-start">
|
||||
<h3 className="font-display text-display-sm text-[var(--fg-primary)] leading-tight">
|
||||
{feature.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="md:col-span-6 flex items-start">
|
||||
<p className="text-[var(--fg-secondary)] leading-relaxed text-base">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Detail pill */}
|
||||
<div className="md:col-span-2 flex items-start md:justify-end">
|
||||
<span className="inline-flex items-center gap-1.5 text-[var(--accent)] label">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[var(--accent)] shrink-0" />
|
||||
{feature.detail}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+135
-214
@@ -2,48 +2,40 @@
|
||||
|
||||
import React, { useState, useMemo } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Search, ScanLine, Globe, Activity, Database, Zap, Moon, Sun } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Search, ScanLine, Activity, Globe } from "lucide-react";
|
||||
import { useGeolocation } from "@/hooks/use-geolocation";
|
||||
import { useSearchLogic } from "@/hooks/use-search-logic";
|
||||
import { useTheme } from "next-themes";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
export function Hero() {
|
||||
const router = useRouter();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const { countryCode, isMounted } = useGeolocation();
|
||||
const { searchType, isValid, loading, setLoading } = useSearchLogic(searchQuery);
|
||||
const { theme } = useTheme();
|
||||
|
||||
const searchConfig = useMemo(() => ({
|
||||
barcode: {
|
||||
placeholder: "Insira o código EAN/UPC (ex: 789...)",
|
||||
button: "Analisar Produto",
|
||||
icon: <ScanLine className="w-5 h-5" />,
|
||||
subtext: "Protocolo: Detecção de Código de Barras",
|
||||
color: "blue",
|
||||
gradient: "from-blue-600 via-cyan-500 to-blue-400"
|
||||
},
|
||||
name: {
|
||||
placeholder: "Qual alimento deseja investigar?",
|
||||
button: "Consultar Banco",
|
||||
icon: <Search className="w-5 h-5" />,
|
||||
subtext: "Protocolo: Pesquisa em Base Colaborativa",
|
||||
color: "emerald",
|
||||
gradient: "from-emerald-600 via-teal-500 to-green-400"
|
||||
},
|
||||
auto: {
|
||||
placeholder: "Digite o nome ou aponte o código...",
|
||||
button: "Buscar",
|
||||
icon: <Activity className="w-5 h-5" />,
|
||||
subtext: "Aguardando input do operador...",
|
||||
color: "slate",
|
||||
gradient: "from-slate-600 via-blue-500 to-slate-400"
|
||||
}
|
||||
}), []);
|
||||
const searchConfig = useMemo(
|
||||
() => ({
|
||||
barcode: {
|
||||
placeholder: "Cole o código EAN/UPC do produto…",
|
||||
button: "Analisar",
|
||||
icon: <ScanLine className="w-4 h-4" />,
|
||||
label: "Código de barras detectado",
|
||||
},
|
||||
name: {
|
||||
placeholder: "Nome do alimento ou marca…",
|
||||
button: "Buscar",
|
||||
icon: <Search className="w-4 h-4" />,
|
||||
label: "Pesquisa por nome",
|
||||
},
|
||||
auto: {
|
||||
placeholder: "Nome do produto ou código de barras…",
|
||||
button: "Buscar",
|
||||
icon: <Activity className="w-4 h-4" />,
|
||||
label: "Detecção automática",
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const current = searchConfig[searchType];
|
||||
|
||||
@@ -51,211 +43,140 @@ export function Hero() {
|
||||
e.preventDefault();
|
||||
if (!searchQuery.trim()) return;
|
||||
setLoading(true);
|
||||
|
||||
const target = searchType === "barcode" && isValid
|
||||
? `/produto/${searchQuery.trim()}`
|
||||
: `/resultados?q=${encodeURIComponent(searchQuery.trim())}${countryCode ? `&country=${countryCode}` : ""}`;
|
||||
|
||||
const target =
|
||||
searchType === "barcode" && isValid
|
||||
? `/produto/${searchQuery.trim()}`
|
||||
: `/resultados?q=${encodeURIComponent(searchQuery.trim())}${
|
||||
countryCode ? `&country=${countryCode}` : ""
|
||||
}`;
|
||||
router.push(target);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative min-h-[90vh] flex flex-col items-center justify-center px-6 py-20 overflow-hidden bg-white dark:bg-[#030712] transition-colors duration-500">
|
||||
|
||||
{/* 1. INFRAESTRUTURA VISUAL: Background de Engenharia */}
|
||||
<div className="absolute inset-0 z-0">
|
||||
{/* Grid de Coordenadas */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#e2e8f0_1px,transparent_1px),linear-gradient(to_bottom,#e2e8f0_1px,transparent_1px)] dark:bg-[linear-gradient(to_right,#1e293b_1px,transparent_1px),linear-gradient(to_bottom,#1e293b_1px,transparent_1px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_70%,transparent_100%)] opacity-20 dark:opacity-20 transition-opacity duration-500" />
|
||||
|
||||
{/* Glow de Fundo Reativo */}
|
||||
<motion.div
|
||||
animate={{
|
||||
scale: searchQuery ? 1.2 : 1,
|
||||
opacity: searchQuery ? (theme === "dark" ? 0.15 : 0.08) : (theme === "dark" ? 0.05 : 0.02)
|
||||
}}
|
||||
className={cn(
|
||||
"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[500px] rounded-full blur-[120px] transition-colors duration-700",
|
||||
searchType === "barcode" ? "bg-blue-600 dark:bg-blue-600" : searchType === "name" ? "bg-emerald-600 dark:bg-emerald-600" : "bg-blue-400 dark:bg-blue-900"
|
||||
<section className="relative bg-base overflow-hidden">
|
||||
{/* ── Subtle grain texture ──────────────────────────────────────── */}
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-[0.025]"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`,
|
||||
backgroundSize: "200px 200px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="container-editorial relative z-10">
|
||||
|
||||
{/* ── Upper label row ─────────────────────────────────────────── */}
|
||||
<div className="pt-24 pb-6 flex items-center justify-between">
|
||||
<span className="label text-[var(--fg-muted)]">
|
||||
Open Food Facts · Base Colaborativa
|
||||
</span>
|
||||
{isMounted && countryCode && (
|
||||
<span className="label text-[var(--fg-muted)] flex items-center gap-1.5">
|
||||
<Globe className="w-3 h-3" />
|
||||
{countryCode.toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Elemento Decorativo Lua/Sol */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 1, delay: 0.5 }}
|
||||
className="absolute top-16 right-8 md:top-24 md:right-16 lg:top-32 lg:right-24 z-0 pointer-events-none"
|
||||
{/* ── Hairline ────────────────────────────────────────────────── */}
|
||||
<hr className="hairline" />
|
||||
|
||||
{/* ── Hero headline ───────────────────────────────────────────── */}
|
||||
<div className="py-16 md:py-24 max-w-4xl">
|
||||
<h1
|
||||
className="font-display text-display-xl text-[var(--fg-primary)] leading-[0.88] tracking-[-0.03em] mb-8 animate-fade-up"
|
||||
style={{ animationFillMode: "both" }}
|
||||
>
|
||||
A verdade sobre{" "}
|
||||
<em className="not-italic font-display-i text-[var(--accent)]">
|
||||
o que você come.
|
||||
</em>
|
||||
</h1>
|
||||
|
||||
<p
|
||||
className="text-lg md:text-xl text-[var(--fg-secondary)] max-w-xl leading-relaxed animate-fade-up anim-delay-1"
|
||||
style={{ animationFillMode: "both" }}
|
||||
>
|
||||
Dados nutricionais completos de 2 milhões de produtos —
|
||||
Nutri-Score, NOVA, ingredientes e alérgenos. Gratuito, sempre.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ── Search form ─────────────────────────────────────────────── */}
|
||||
<div
|
||||
className="pb-20 animate-fade-up anim-delay-2"
|
||||
style={{ animationFillMode: "both" }}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
{theme === "dark" ? (
|
||||
<motion.div
|
||||
key="moon"
|
||||
initial={{ opacity: 0, rotate: -90 }}
|
||||
animate={{ opacity: 1, rotate: 0 }}
|
||||
exit={{ opacity: 0, rotate: 90 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Moon className="w-24 h-24 md:w-32 md:h-32 lg:w-40 lg:h-40 text-blue-400/20 transition-colors duration-500" />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="sun"
|
||||
initial={{ opacity: 0, rotate: 90 }}
|
||||
animate={{ opacity: 1, rotate: 0 }}
|
||||
exit={{ opacity: 0, rotate: -90 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Sun className="w-24 h-24 md:w-32 md:h-32 lg:w-40 lg:h-40 text-yellow-400/30 transition-colors duration-500" />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 max-w-5xl mx-auto text-center">
|
||||
|
||||
{/* 2. BADGE DE STATUS DO SISTEMA */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="inline-flex items-center gap-3 px-4 py-2 rounded-full bg-gray-100/80 dark:bg-white/5 border border-gray-200 dark:border-white/10 backdrop-blur-md mb-12 transition-colors duration-500"
|
||||
>
|
||||
<div className="flex -space-x-2">
|
||||
<div className="w-6 h-6 rounded-full border-2 border-white dark:border-[#030712] bg-blue-500 flex items-center justify-center">
|
||||
<Database size={10} className="text-white" />
|
||||
</div>
|
||||
<div className="w-6 h-6 rounded-full border-2 border-white dark:border-[#030712] bg-emerald-500 flex items-center justify-center">
|
||||
<Zap size={10} className="text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-gray-600 dark:text-slate-400 transition-colors duration-500">
|
||||
NutriScan Protocol <span className="text-blue-600 dark:text-blue-500">v2.1</span> • {isMounted ? countryCode?.toUpperCase() : "INTL"}
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
{/* 3. HEADLINE DE ALTA DENSIDADE */}
|
||||
<h1 className="text-5xl md:text-8xl font-black text-gray-900 dark:text-white mb-8 tracking-tighter leading-[0.85] perspective-1000 transition-colors duration-500">
|
||||
A verdade sobre <br />
|
||||
<span className={cn(
|
||||
"text-transparent bg-clip-text bg-gradient-to-r transition-all duration-700",
|
||||
current.gradient
|
||||
)}>
|
||||
seus alimentos.
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-gray-600 dark:text-slate-400 text-lg md:text-xl max-w-2xl mx-auto mb-16 leading-relaxed font-medium transition-colors duration-500">
|
||||
Engenharia de dados aplicada à transparência nutricional.
|
||||
Descriptografamos rótulos para que você tome decisões soberanas.
|
||||
</p>
|
||||
|
||||
{/* 4. O TERMINAL DE BUSCA (A PEÇA CENTRAL) */}
|
||||
<form onSubmit={handleSearch} className="relative max-w-3xl mx-auto group">
|
||||
{/* Efeito de Borda Neon Reativa */}
|
||||
<div className={cn(
|
||||
"absolute -inset-[2px] rounded-[2rem] blur-sm opacity-20 group-focus-within:opacity-100 transition-all duration-500 bg-gradient-to-r",
|
||||
current.gradient
|
||||
)} />
|
||||
|
||||
<div className="relative flex items-center bg-white/90 dark:bg-slate-900/90 backdrop-blur-2xl rounded-[1.8rem] border border-gray-200 dark:border-white/10 p-2 shadow-2xl transition-colors duration-500">
|
||||
<div className="flex-1 relative flex items-center">
|
||||
{/* Ícone de Estado Animado */}
|
||||
<div className="absolute left-6">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={searchType}
|
||||
initial={{ rotate: -90, opacity: 0, scale: 0.5 }}
|
||||
animate={{ rotate: 0, opacity: 1, scale: 1 }}
|
||||
exit={{ rotate: 90, opacity: 0, scale: 0.5 }}
|
||||
className={cn(
|
||||
"transition-colors duration-500",
|
||||
searchType === "barcode" ? "text-blue-600 dark:text-blue-400" : searchType === "name" ? "text-emerald-600 dark:text-emerald-400" : "text-gray-400 dark:text-slate-500"
|
||||
)}
|
||||
>
|
||||
{current.icon}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
<form onSubmit={handleSearch} className="max-w-2xl">
|
||||
<div className="search-editorial">
|
||||
{/* Mode icon */}
|
||||
<div className="text-[var(--fg-muted)] shrink-0">
|
||||
{current.icon}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
<input
|
||||
type="text"
|
||||
placeholder={current.placeholder}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-16 pr-4 py-9 text-xl border-0 bg-transparent focus-visible:ring-0 w-full text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-slate-600 font-medium tracking-tight transition-colors duration-500"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
className="flex-1 bg-transparent border-none outline-none py-[1.125rem] text-[var(--fg-primary)] placeholder-[var(--fg-faint)] text-base font-sans"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="btn-accent shrink-0 py-2.5 px-5 text-sm"
|
||||
>
|
||||
{loading ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-3.5 h-3.5 border-2 border-white/40 border-t-white rounded-full animate-spin" />
|
||||
Buscando…
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5">
|
||||
{current.button}
|
||||
<ArrowRight />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className={cn(
|
||||
"h-14 px-10 rounded-[1.2rem] font-black uppercase tracking-widest text-xs transition-all duration-500 active:scale-95 shadow-xl",
|
||||
searchType === "barcode" ? "bg-blue-600 hover:bg-blue-500 shadow-blue-500/20" : "bg-emerald-600 hover:bg-emerald-500 shadow-emerald-500/20"
|
||||
)}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{current.button}</span>
|
||||
<ArrowRight size={14} className="group-hover:translate-x-1 transition-transform" />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{isMounted && countryCode && (
|
||||
<div className="flex items-center gap-2 text-gray-600 dark:text-slate-500 bg-gray-100 dark:bg-white/5 px-3 py-1 rounded-full border border-gray-200 dark:border-white/10 transition-colors duration-500">
|
||||
<Globe size={12} />
|
||||
<span className="text-[10px] font-bold uppercase">Region: {countryCode.toUpperCase()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* 6. INDICADOR DE MÉTRICAS (Bottom Shelf) */}
|
||||
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 w-full max-w-4xl px-6 hidden md:block">
|
||||
<div className="grid grid-cols-3 gap-12 border-t border-gray-200 dark:border-white/5 pt-10 opacity-60 dark:opacity-40 grayscale hover:grayscale-0 transition-all duration-700">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className="text-2xl font-mono font-bold text-gray-900 dark:text-white tracking-tighter transition-colors duration-500">2M+</span>
|
||||
<span className="text-[9px] uppercase tracking-[0.3em] font-black text-gray-500 dark:text-slate-500 transition-colors duration-500">Dataset Entries</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1 border-x border-gray-200 dark:border-white/5">
|
||||
<span className="text-2xl font-mono font-bold text-gray-900 dark:text-white tracking-tighter transition-colors duration-500">100%</span>
|
||||
<span className="text-[9px] uppercase tracking-[0.3em] font-black text-gray-500 dark:text-slate-500 transition-colors duration-500">Open Source</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className="text-2xl font-mono font-bold text-gray-900 dark:text-white tracking-tighter transition-colors duration-500">< 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>
|
||||
{/* Detection status */}
|
||||
<p className="mt-3 label text-[var(--fg-muted)] flex items-center gap-1.5">
|
||||
<span
|
||||
className={cn(
|
||||
"w-1.5 h-1.5 rounded-full inline-block transition-colors duration-300",
|
||||
searchType !== "auto"
|
||||
? "bg-[var(--accent)]"
|
||||
: "bg-[var(--fg-faint)]"
|
||||
)}
|
||||
/>
|
||||
{current.label}
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Sub-componente auxiliar para ícone
|
||||
function ArrowRight({ size, className }: { size: number, className?: string }) {
|
||||
/** Minimal inline arrow icon */
|
||||
function ArrowRight() {
|
||||
return (
|
||||
<svg
|
||||
width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" className={className}
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M5 12h14m-7-7 7 7-7 7"/>
|
||||
<path d="M5 12h14m-7-7 7 7-7 7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
"use client";
|
||||
|
||||
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 { ScanLine, Search, BarChart3, CheckCircle } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Step {
|
||||
@@ -11,8 +9,6 @@ interface Step {
|
||||
icon: React.ElementType;
|
||||
title: string;
|
||||
description: string;
|
||||
color: string;
|
||||
glow: string;
|
||||
}
|
||||
|
||||
const steps: Step[] = [
|
||||
@@ -20,161 +16,145 @@ const steps: Step[] = [
|
||||
number: "01",
|
||||
icon: ScanLine,
|
||||
title: "Digitalize",
|
||||
description: "Reconhecimento instantâneo de códigos em mais de 100 países com tecnologia de baixa latência.",
|
||||
color: "text-blue-500",
|
||||
glow: "shadow-blue-500/20",
|
||||
description:
|
||||
"Insira o código EAN/UPC ou o nome do produto. Detecção automática em mais de 100 países.",
|
||||
},
|
||||
{
|
||||
number: "02",
|
||||
icon: Search,
|
||||
title: "Consulte",
|
||||
description: "Conexão direta ao Open Food Facts, a base de dados colaborativa global (Wikipedia dos alimentos).",
|
||||
color: "text-green-500",
|
||||
glow: "shadow-green-500/20",
|
||||
description:
|
||||
"Conexão direta ao Open Food Facts — a maior base colaborativa de alimentos do planeta.",
|
||||
},
|
||||
{
|
||||
number: "03",
|
||||
icon: BarChart3,
|
||||
title: "Analise",
|
||||
description: "Processamento de Nutri-Score, graus de processamento NOVA e alertas de alérgenos críticos.",
|
||||
color: "text-purple-500",
|
||||
glow: "shadow-purple-500/20",
|
||||
description:
|
||||
"Nutri-Score, classificação NOVA, macronutrientes e alertas de alérgenos em uma tela.",
|
||||
},
|
||||
{
|
||||
number: "04",
|
||||
icon: CheckCircle,
|
||||
title: "Decida",
|
||||
description: "Informação técnica pura e sem vieses comerciais para decisões alinhadas à sua saúde.",
|
||||
color: "text-orange-500",
|
||||
glow: "shadow-orange-500/20",
|
||||
description:
|
||||
"Informação técnica pura, sem vieses comerciais, para escolhas alinhadas à sua saúde.",
|
||||
},
|
||||
];
|
||||
|
||||
export function HowItWorks() {
|
||||
return (
|
||||
<section className="py-24 bg-white dark:bg-[#030712] transition-colors duration-500">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
|
||||
{/* Header - Foco em Valor Técnico */}
|
||||
<div className="text-center mb-24">
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
className="text-4xl md:text-5xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter"
|
||||
>
|
||||
Pipeline de <span className="text-blue-600">Consciência Alimentar</span>
|
||||
</motion.h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
|
||||
Quatro estágios de processamento de dados para transformar um código de barras em inteligência nutricional.
|
||||
</p>
|
||||
<section className="bg-[var(--bg-surface)] section-py">
|
||||
<div className="container-editorial">
|
||||
|
||||
{/* ── Section header ───────────────────────────────────────────── */}
|
||||
<div className="mb-16">
|
||||
<span className="label text-[var(--accent)] block mb-4">
|
||||
Como funciona
|
||||
</span>
|
||||
<hr className="hairline mb-10" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 items-end">
|
||||
<h2 className="font-display text-display-lg text-[var(--fg-primary)]">
|
||||
Quatro etapas,{" "}
|
||||
<em className="font-display-i text-[var(--accent)]">
|
||||
clareza total.
|
||||
</em>
|
||||
</h2>
|
||||
<p className="text-[var(--fg-secondary)] text-lg leading-relaxed max-w-md">
|
||||
De um simples código de barras à inteligência nutricional
|
||||
completa — em poucos segundos.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Steps Grid - O "Motor" da Seção */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 relative">
|
||||
{steps.map((step, index) => (
|
||||
<motion.div
|
||||
key={step.number}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.15 }}
|
||||
className="relative group"
|
||||
>
|
||||
{/* Conector Visual (Desktop) */}
|
||||
{index < steps.length - 1 && (
|
||||
<div className="hidden lg:block absolute top-12 -right-8 w-16 h-[2px] bg-gradient-to-r from-gray-200 to-transparent dark:from-gray-800 z-0" />
|
||||
)}
|
||||
|
||||
<div className={cn(
|
||||
"relative z-10 p-8 rounded-3xl bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 transition-all duration-500 hover:border-blue-500/30",
|
||||
"group-hover:translate-y-[-8px] group-hover:shadow-2xl",
|
||||
step.glow
|
||||
)}>
|
||||
{/* Badge de Número em Estilo Industrial */}
|
||||
<span className="absolute top-4 right-6 text-sm font-black font-mono text-gray-300 dark:text-gray-700">
|
||||
STEP_{step.number}
|
||||
</span>
|
||||
|
||||
<div className={cn(
|
||||
"w-14 h-14 rounded-2xl flex items-center justify-center mb-8 bg-white dark:bg-gray-800 shadow-sm transition-transform duration-500 group-hover:rotate-[10deg]",
|
||||
step.color
|
||||
)}>
|
||||
<step.icon className="w-7 h-7" />
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed font-medium">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
{/* ── Steps grid ───────────────────────────────────────────────── */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-px bg-[var(--border-subtle)]">
|
||||
{steps.map((step, i) => (
|
||||
<StepCard key={step.number} step={step} index={i} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Technical Datasheet (O que você vai encontrar) */}
|
||||
<div className="mt-32 mb-12 flex justify-center">
|
||||
<div className="relative w-full max-w-2xl h-64 rounded-2xl overflow-hidden shadow-2xl">
|
||||
<Image
|
||||
src="/images/nut-facts.webp"
|
||||
alt="Lista de ingredientes e informações nutricionais de produto alimentar"
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-32 p-10 rounded-[2.5rem] bg-slate-900 text-white relative overflow-hidden shadow-3xl">
|
||||
<div className="absolute top-0 right-0 p-10 opacity-10">
|
||||
<BarChart3 size={200} />
|
||||
{/* ── Data spec callout ─────────────────────────────────────────── */}
|
||||
<div className="mt-20 grid grid-cols-1 md:grid-cols-2 gap-px bg-[var(--border-subtle)] rounded-xl overflow-hidden">
|
||||
<div className="bg-[var(--bg-elevated)] p-10">
|
||||
<span className="label text-[var(--accent)] block mb-6">
|
||||
Bio-Markers & Macros
|
||||
</span>
|
||||
<ul className="space-y-4">
|
||||
{[
|
||||
"Calorias (kcal / 100 g)",
|
||||
"Perfil lipídico e glicêmico",
|
||||
"Proteínas e fibras",
|
||||
"Micronutrientes",
|
||||
].map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="flex items-center gap-3 text-[var(--fg-secondary)] text-sm"
|
||||
>
|
||||
<span className="w-1 h-1 rounded-full bg-[var(--accent)] shrink-0" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 grid grid-cols-1 lg:grid-cols-3 gap-12">
|
||||
<div className="lg:col-span-1">
|
||||
<h3 className="text-3xl font-black mb-4 tracking-tighter">Data Specification</h3>
|
||||
<p className="text-slate-400 text-sm leading-relaxed mb-6">
|
||||
Extração de metadados via Open Food Facts API v2. Suporte a protocolos Nutri-Score e classificações NOVA.
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-xs font-mono text-blue-400 uppercase tracking-widest">
|
||||
<Globe size={14} /> Open Source Database
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2 grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Lista de Features Refinada */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-blue-500 font-bold text-xs uppercase tracking-widest">Bio-Markers & Macros</h4>
|
||||
<ul className="space-y-3">
|
||||
{["Calorias (Kcal/100g)", "Perfil Lipídico e Glicêmico", "Micronutrientes & Fibras"].map(item => (
|
||||
<li key={item} className="flex items-center gap-3 text-sm text-slate-300">
|
||||
<Zap size={12} className="text-blue-500" /> {item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="relative w-full h-48 rounded-xl overflow-hidden">
|
||||
<Image
|
||||
src="/images/nutscore.webp"
|
||||
alt="Sistema de classificação Nutri-Score"
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-green-500 font-bold text-xs uppercase tracking-widest">Compliance & Alerta</h4>
|
||||
<ul className="space-y-3">
|
||||
{["Identificação de Alérgenos", "Aditivos e Conservantes", "Classificação de Processamento"].map(item => (
|
||||
<li key={item} className="flex items-center gap-3 text-sm text-slate-300">
|
||||
<CheckCircle size={12} className="text-green-500" /> {item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[var(--bg-elevated)] p-10">
|
||||
<span className="label text-[var(--fg-muted)] block mb-6">
|
||||
Compliance & Alertas
|
||||
</span>
|
||||
<ul className="space-y-4">
|
||||
{[
|
||||
"Identificação de alérgenos",
|
||||
"Aditivos e conservantes",
|
||||
"Classificação NOVA de processamento",
|
||||
"Nutri-Score A–E",
|
||||
].map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="flex items-center gap-3 text-[var(--fg-secondary)] text-sm"
|
||||
>
|
||||
<span className="w-1 h-1 rounded-full bg-[var(--border-strong)] shrink-0" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function StepCard({ step, index }: { step: Step; index: number }) {
|
||||
const Icon = step.icon;
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-[var(--bg-elevated)] p-8 md:p-10 group",
|
||||
"transition-colors duration-300 hover:bg-[var(--bg-base)]"
|
||||
)}
|
||||
style={{ animationDelay: `${index * 0.1}s` }}
|
||||
>
|
||||
{/* Step number — large serif */}
|
||||
<div className="step-number mb-6 group-hover:text-[var(--accent)] transition-colors duration-300">
|
||||
{step.number}
|
||||
</div>
|
||||
|
||||
{/* Monochrome icon */}
|
||||
<div className="mb-6">
|
||||
<Icon
|
||||
className="w-5 h-5 text-[var(--fg-muted)] group-hover:text-[var(--fg-secondary)] transition-colors duration-300"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="font-display text-display-sm text-[var(--fg-primary)] mb-3">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="text-[var(--fg-secondary)] text-sm leading-relaxed">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,130 +1,147 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Search } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Search, ScanLine, Activity } from "lucide-react";
|
||||
import { useSearchLogic } from "@/hooks/use-search-logic";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function PersuasiveCTA() {
|
||||
const [query, setQuery] = useState("");
|
||||
const [searchType, setSearchType] = useState<"barcode" | "name" | null>(null);
|
||||
const router = useRouter();
|
||||
const [query, setQuery] = useState("");
|
||||
const { searchType, isValid, loading, setLoading } = useSearchLogic(query);
|
||||
|
||||
// Detectar tipo de busca em tempo real
|
||||
useEffect(() => {
|
||||
const cleaned = query.replace(/\D/g, ""); // Remove tudo que não é número
|
||||
|
||||
if (cleaned.length >= 8 && cleaned.length <= 13) {
|
||||
setSearchType("barcode");
|
||||
} else if (query.trim()) {
|
||||
setSearchType("name");
|
||||
} else {
|
||||
setSearchType(null);
|
||||
}
|
||||
}, [query]);
|
||||
|
||||
const handleSearch = () => {
|
||||
const handleSearch = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!query.trim()) return;
|
||||
|
||||
const type = detectSearchType(query);
|
||||
|
||||
if (type === "barcode") {
|
||||
// Limpar código de barras (remover espaços/caracteres)
|
||||
const cleanBarcode = query.replace(/\D/g, "");
|
||||
|
||||
if (isValidBarcode(cleanBarcode)) {
|
||||
// Redirecionar para página do produto
|
||||
router.push(`/produto/${cleanBarcode}`);
|
||||
} else {
|
||||
// Mostrar erro de código inválido
|
||||
alert("Código de barras inválido. Deve ter entre 8 e 13 dígitos.");
|
||||
}
|
||||
} else {
|
||||
// Busca normal por nome
|
||||
router.push(`/resultados?q=${encodeURIComponent(query.trim())}`);
|
||||
}
|
||||
setLoading(true);
|
||||
const target =
|
||||
searchType === "barcode" && isValid
|
||||
? `/produto/${query.trim()}`
|
||||
: `/resultados?q=${encodeURIComponent(query.trim())}`;
|
||||
router.push(target);
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearch();
|
||||
}
|
||||
};
|
||||
const icon =
|
||||
searchType === "barcode" ? (
|
||||
<ScanLine className="w-4 h-4" />
|
||||
) : searchType === "name" ? (
|
||||
<Search className="w-4 h-4" />
|
||||
) : (
|
||||
<Activity className="w-4 h-4" />
|
||||
);
|
||||
|
||||
// Funções auxiliares (copiadas do SearchBar)
|
||||
const detectSearchType = (input: string): "barcode" | "name" => {
|
||||
const cleaned = input.replace(/\D/g, "");
|
||||
return cleaned.length >= 8 && cleaned.length <= 13 ? "barcode" : "name";
|
||||
};
|
||||
|
||||
const isValidBarcode = (barcode: string): boolean => {
|
||||
return barcode.length >= 8 && barcode.length <= 13 && /^\d+$/.test(barcode);
|
||||
};
|
||||
const label =
|
||||
searchType === "barcode"
|
||||
? "Código de barras detectado"
|
||||
: searchType === "name"
|
||||
? "Pesquisa por nome"
|
||||
: "Detecção automática";
|
||||
|
||||
return (
|
||||
<section className="py-20 bg-gradient-to-br from-blue-600 to-green-600 text-white">
|
||||
<div className="max-w-4xl mx-auto px-4 text-center">
|
||||
<h2 className="text-3xl md:text-5xl font-bold mb-6">
|
||||
Pronto para descobrir a verdade sobre seus alimentos?
|
||||
</h2>
|
||||
<p className="text-xl mb-8 opacity-90">
|
||||
Junte-se a milhares de pessoas que já tomam decisões alimentares mais
|
||||
conscientes com informações transparentes e confiáveis.
|
||||
</p>
|
||||
<section className="bg-[var(--bg-surface)] section-py border-t border-[var(--border-subtle)]">
|
||||
<div className="container-editorial">
|
||||
|
||||
{/* Contador Animado de Usuários */}
|
||||
<div className="mb-8">
|
||||
<div className="text-2xl font-semibold">
|
||||
<span className="animate-pulse">12.847</span> pessoas já buscaram
|
||||
hoje
|
||||
{/* ── Section header ───────────────────────────────────────────── */}
|
||||
<div className="mb-16">
|
||||
<span className="label text-[var(--accent)] block mb-4">
|
||||
Comece Agora
|
||||
</span>
|
||||
<hr className="hairline mb-10" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 items-end">
|
||||
<h2 className="font-display text-display-lg text-[var(--fg-primary)]">
|
||||
Pronto para descobrir a{" "}
|
||||
<em className="font-display-i text-[var(--accent)]">
|
||||
verdade sobre seus alimentos?
|
||||
</em>
|
||||
</h2>
|
||||
<p className="text-[var(--fg-secondary)] text-lg leading-relaxed max-w-md">
|
||||
Sem cadastro, sem anúncios, sem venda de dados. Resultados
|
||||
nutricionais completos em segundos.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Bar Destacado */}
|
||||
<div className="max-w-2xl mx-auto bg-white rounded-2xl p-6 shadow-2xl">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Digite o código de barras de qualquer produto..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
className="pl-10 pr-4 h-12 text-gray-900"
|
||||
/>
|
||||
{/* ── Search form ──────────────────────────────────────────────── */}
|
||||
<form onSubmit={handleSearch} className="max-w-2xl">
|
||||
<div className="search-editorial">
|
||||
<div className="text-[var(--fg-muted)] shrink-0">{icon}</div>
|
||||
|
||||
{/* Indicador visual do tipo de busca */}
|
||||
{searchType && (
|
||||
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
|
||||
{searchType === "barcode" ? "Código de barras" : "Nome"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleSearch}
|
||||
disabled={!query.trim()}
|
||||
className="h-12 px-6 bg-gradient-to-r from-blue-600 to-green-600 hover:from-blue-700 hover:to-green-700"
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Nome do produto ou código de barras…"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
className="flex-1 bg-transparent border-none outline-none py-[1.125rem] text-[var(--fg-primary)] placeholder-[var(--fg-faint)] text-base font-sans"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="btn-accent shrink-0 py-2.5 px-5 text-sm"
|
||||
>
|
||||
<Search className="h-4 w-4 mr-2" />
|
||||
Buscar
|
||||
</Button>
|
||||
{loading ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-3.5 h-3.5 border-2 border-white/40 border-t-white rounded-full animate-spin" />
|
||||
Buscando…
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5">
|
||||
Buscar
|
||||
<ArrowRight />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-center mt-3 text-sm text-gray-500">
|
||||
💡 Dica: O código de barras está na parte de trás da embalagem
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Trust Indicators */}
|
||||
<div className="mt-8 flex justify-center gap-6 text-sm opacity-75">
|
||||
<span>✓ Sem cadastro necessário</span>
|
||||
<span>✓ Resultados instantâneos</span>
|
||||
<span>✓ 100% gratuito</span>
|
||||
{/* Detection status */}
|
||||
<p className="mt-3 label text-[var(--fg-muted)] flex items-center gap-1.5">
|
||||
<span
|
||||
className={cn(
|
||||
"w-1.5 h-1.5 rounded-full inline-block",
|
||||
searchType !== "auto"
|
||||
? "bg-[var(--accent)]"
|
||||
: "bg-[var(--fg-faint)]"
|
||||
)}
|
||||
/>
|
||||
{label}
|
||||
</p>
|
||||
</form>
|
||||
|
||||
{/* ── Trust indicators ─────────────────────────────────────────── */}
|
||||
<div className="mt-12 flex flex-wrap gap-x-10 gap-y-3">
|
||||
{["Sem cadastro necessário", "Resultados instantâneos", "100% gratuito e open source"].map(
|
||||
(item) => (
|
||||
<span
|
||||
key={item}
|
||||
className="label text-[var(--fg-muted)] flex items-center gap-2"
|
||||
>
|
||||
<span className="w-1 h-1 rounded-full bg-[var(--accent)]" />
|
||||
{item}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ArrowRight() {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M5 12h14m-7-7 7 7-7 7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +1,41 @@
|
||||
import { Database, Globe, Zap, Code } from "lucide-react";
|
||||
|
||||
const stats = [
|
||||
{
|
||||
value: "2 M+",
|
||||
label: "Produtos no Open Food Facts",
|
||||
},
|
||||
{
|
||||
value: "100+",
|
||||
label: "Países cobertos",
|
||||
},
|
||||
{
|
||||
value: "99.9%",
|
||||
label: "Uptime garantido",
|
||||
},
|
||||
{
|
||||
value: "100%",
|
||||
label: "Open Source",
|
||||
},
|
||||
];
|
||||
|
||||
export function TrustBadges() {
|
||||
return (
|
||||
<section className="py-12 bg-gray-50 dark:bg-gray-800 border-t border-b">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 text-center">
|
||||
<div className="animate-fadeIn">
|
||||
<div className="w-16 h-16 mx-auto mb-3 bg-blue-100 dark:bg-blue-900/40 rounded-full flex items-center justify-center">
|
||||
<Database className="w-8 h-8 text-blue-600" />
|
||||
<section className="bg-[var(--bg-surface)] border-y border-[var(--border-subtle)]">
|
||||
<div className="container-editorial">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 md:divide-x divide-[var(--border-subtle)]">
|
||||
{stats.map(({ value, label }, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex flex-col items-center gap-2 px-6 py-8"
|
||||
>
|
||||
<span className="font-display text-2xl text-[var(--fg-primary)] leading-none">
|
||||
{value}
|
||||
</span>
|
||||
<span className="label text-center text-[var(--fg-muted)]">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
2M+
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Produtos no Open Food Facts
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="animate-fadeIn" style={{animationDelay: '0.1s'}}>
|
||||
<div className="w-16 h-16 mx-auto mb-3 bg-green-100 dark:bg-green-900/40 rounded-full flex items-center justify-center">
|
||||
<Globe className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-green-600">100+</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Países cobertos
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="animate-fadeIn" style={{animationDelay: '0.2s'}}>
|
||||
<div className="w-16 h-16 mx-auto mb-3 bg-purple-100 dark:bg-purple-900/40 rounded-full flex items-center justify-center">
|
||||
<Zap className="w-8 h-8 text-purple-600" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-purple-600">99.9%</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Uptime garantido
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="animate-fadeIn" style={{animationDelay: '0.3s'}}>
|
||||
<div className="w-16 h-16 mx-auto mb-3 bg-orange-100 dark:bg-orange-900/40 rounded-full flex items-center justify-center animate-pulse-slow">
|
||||
<Code className="w-8 h-8 text-orange-600" />
|
||||
</div>
|
||||
<div className="text-xl font-bold text-orange-600">100%</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Open Source
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+505
-149
@@ -2,172 +2,528 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Scrollbar utilities */
|
||||
@layer utilities {
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari and Opera */
|
||||
}
|
||||
}
|
||||
|
||||
/* Otimizações de performance para animações */
|
||||
@layer utilities {
|
||||
.animate-in {
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.fade-in-0 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-in-from-bottom-4 {
|
||||
transform: translateY(1rem);
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(1rem);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Animações personalizadas para UI/UX */
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-pulse-slow {
|
||||
animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
.animate-bounce-gentle {
|
||||
animation: bounceGentle 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(2rem);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceGentle {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-0.25rem);
|
||||
}
|
||||
}
|
||||
|
||||
/* Padrão de grid para background */
|
||||
.bg-grid-pattern {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 0, 0, 0.1) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
/* Otimização para listas grandes */
|
||||
.product-grid {
|
||||
contain: layout style paint;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
}
|
||||
/* Fonts are self-hosted through next/font. */
|
||||
|
||||
/* ─── Design Tokens ───────────────────────────────────────────────────── */
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
/* Warm neutral palette */
|
||||
--bg-base: #f9f6f0;
|
||||
--bg-surface: #f4f0e8;
|
||||
--bg-elevated: #ffffff;
|
||||
--fg-primary: #1e1810;
|
||||
--fg-secondary: #5e4e3c;
|
||||
--fg-muted: #9a8870;
|
||||
--fg-faint: #b8a88a;
|
||||
--border-subtle: #e8e0d0;
|
||||
--border-strong: #d4c8b0;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
/* Single accent: editorial green */
|
||||
--accent: oklch(52% 0.11 155);
|
||||
--accent-light: oklch(72% 0.10 155);
|
||||
--accent-dark: oklch(38% 0.10 155);
|
||||
--accent-muted: oklch(92% 0.04 155);
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
/* Nutri-Score grade colors */
|
||||
--score-a: #1a7a4a;
|
||||
--score-b: #5da832;
|
||||
--score-c: #e8c110;
|
||||
--score-d: #e07820;
|
||||
--score-e: #c0201a;
|
||||
|
||||
--primary: 215 90% 50%;
|
||||
/* Azul Royal (#0055FF) */
|
||||
--primary-foreground: 210 40% 98%;
|
||||
/* Spacing primitives */
|
||||
--section-gap: 7rem;
|
||||
--container: 1120px;
|
||||
--radius: 0.375rem;
|
||||
|
||||
--secondary: 185 70% 45%;
|
||||
/* Verde-azulado (#19A7A7) */
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
/* shadcn/ui required tokens → warm neutral mapped */
|
||||
--background: 36 30% 96%;
|
||||
--foreground: 24 20% 10%;
|
||||
--card: 36 30% 98%;
|
||||
--card-foreground: 24 20% 10%;
|
||||
--popover: 36 30% 98%;
|
||||
--popover-foreground: 24 20% 10%;
|
||||
--primary: 155 40% 32%; /* accent green */
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 36 22% 90%;
|
||||
--secondary-foreground: 24 20% 20%;
|
||||
--muted: 36 20% 92%;
|
||||
--muted-foreground: 24 15% 46%;
|
||||
--destructive: 0 72% 44%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--border: 36 20% 86%;
|
||||
--input: 36 20% 86%;
|
||||
--ring: 155 40% 32%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 185 70% 95%;
|
||||
/* Verde-azulado claro */
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 215 90% 50%;
|
||||
--radius: 0.5rem;
|
||||
--chart-1: oklch(52% 0.11 155);
|
||||
--chart-2: oklch(62% 0.14 200);
|
||||
--chart-3: oklch(70% 0.16 60);
|
||||
--chart-4: oklch(56% 0.18 30);
|
||||
--chart-5: oklch(52% 0.15 340);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 217 33% 17%;
|
||||
/* Fundo escuro azulado */
|
||||
--foreground: 210 40% 98%;
|
||||
--bg-base: #110e09;
|
||||
--bg-surface: #1a1610;
|
||||
--bg-elevated: #231e17;
|
||||
--fg-primary: #f4f0e8;
|
||||
--fg-secondary: #c8b898;
|
||||
--fg-muted: #8a7860;
|
||||
--fg-faint: #5e4e3c;
|
||||
--border-subtle: #2e2618;
|
||||
--border-strong: #3e3228;
|
||||
|
||||
--card: 217 33% 17%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--accent: oklch(62% 0.12 155);
|
||||
--accent-light: oklch(75% 0.10 155);
|
||||
--accent-dark: oklch(44% 0.10 155);
|
||||
--accent-muted: oklch(20% 0.06 155);
|
||||
|
||||
--popover: 217 33% 17%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--background: 24 20% 8%;
|
||||
--foreground: 36 30% 94%;
|
||||
--card: 24 20% 10%;
|
||||
--card-foreground: 36 30% 94%;
|
||||
--popover: 24 20% 10%;
|
||||
--popover-foreground: 36 30% 94%;
|
||||
--primary: 155 38% 48%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 24 18% 18%;
|
||||
--secondary-foreground: 36 28% 88%;
|
||||
--muted: 24 16% 16%;
|
||||
--muted-foreground: 36 20% 58%;
|
||||
--destructive: 0 62% 42%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--border: 24 18% 18%;
|
||||
--input: 24 18% 18%;
|
||||
--ring: 155 38% 48%;
|
||||
}
|
||||
|
||||
--primary: 215 90% 60%;
|
||||
/* Azul Royal mais claro */
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
/* ── Base resets & typography ───────────────────────────────────────── */
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
--secondary: 185 70% 40%;
|
||||
/* Verde-azulado ajustado */
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
html {
|
||||
font-feature-settings: "kern" 1, "liga" 1, "calt" 1, "ss01" 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
--muted: 217 33% 25%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
body {
|
||||
background-color: var(--bg-base);
|
||||
color: var(--fg-primary);
|
||||
font-family: var(--font-inter), system-ui, sans-serif;
|
||||
font-size: 1rem;
|
||||
line-height: 1.72;
|
||||
transition: background-color 0.4s ease, color 0.4s ease;
|
||||
}
|
||||
|
||||
--accent: 185 70% 25%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
/* Headings use the active display font. */
|
||||
h1, h2, h3, h4 {
|
||||
font-family: var(--font-display), Georgia, serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217 33% 25%;
|
||||
--input: 217 33% 25%;
|
||||
--ring: 215 90% 60%;
|
||||
/* Hairline horizontal rule */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Utility layer ───────────────────────────────────────────────────── */
|
||||
@layer utilities {
|
||||
|
||||
/* Scrollbar */
|
||||
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
.scrollbar-hide::-webkit-scrollbar { display: none; }
|
||||
|
||||
/* Editorial typography helpers */
|
||||
.font-display { font-family: var(--font-display), Georgia, serif; }
|
||||
.font-display-i { font-family: var(--font-display), Georgia, serif; font-style: italic; }
|
||||
|
||||
/* Container max-width */
|
||||
.container-editorial {
|
||||
width: 100%;
|
||||
max-width: var(--container);
|
||||
margin-inline: auto;
|
||||
padding-inline: clamp(1.25rem, 5vw, 3rem);
|
||||
}
|
||||
|
||||
/* Hairline divider */
|
||||
.hairline {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
.hairline-v {
|
||||
border: none;
|
||||
border-left: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
/* Section vertical rhythm */
|
||||
.section-py { padding-top: var(--section-gap); padding-bottom: var(--section-gap); }
|
||||
.section-pt { padding-top: var(--section-gap); }
|
||||
.section-pb { padding-bottom: var(--section-gap); }
|
||||
|
||||
/* Warm surface tokens */
|
||||
.bg-base { background-color: var(--bg-base); }
|
||||
.bg-surface { background-color: var(--bg-surface); }
|
||||
.bg-elevated { background-color: var(--bg-elevated); }
|
||||
|
||||
.text-primary { color: var(--fg-primary); }
|
||||
.text-secondary{ color: var(--fg-secondary); }
|
||||
.text-muted { color: var(--fg-muted); }
|
||||
.text-faint { color: var(--fg-faint); }
|
||||
|
||||
/* Accent utilities */
|
||||
.text-accent { color: var(--accent); }
|
||||
.bg-accent { background-color: var(--accent); }
|
||||
.bg-accent-muted { background-color: var(--accent-muted); }
|
||||
.border-accent { border-color: var(--accent); }
|
||||
|
||||
/* Label caps */
|
||||
.label {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
font-family: var(--font-inter), system-ui, sans-serif;
|
||||
}
|
||||
.label-lg {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.10em;
|
||||
text-transform: uppercase;
|
||||
font-family: var(--font-inter), system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* Product grid performance */
|
||||
.product-grid { contain: layout style; }
|
||||
.product-card { will-change: transform; backface-visibility: hidden; }
|
||||
|
||||
/* Stagger animation helpers */
|
||||
.anim-delay-1 { animation-delay: 0.1s; }
|
||||
.anim-delay-2 { animation-delay: 0.2s; }
|
||||
.anim-delay-3 { animation-delay: 0.3s; }
|
||||
.anim-delay-4 { animation-delay: 0.4s; }
|
||||
.anim-delay-5 { animation-delay: 0.5s; }
|
||||
|
||||
/* Nutri-Score grade pills */
|
||||
.grade-a { background: var(--score-a); color: #fff; }
|
||||
.grade-b { background: var(--score-b); color: #fff; }
|
||||
.grade-c { background: var(--score-c); color: #1e1810; }
|
||||
.grade-d { background: var(--score-d); color: #fff; }
|
||||
.grade-e { background: var(--score-e); color: #fff; }
|
||||
|
||||
/* Thin top-border accent card */
|
||||
.card-accent-top {
|
||||
border-top: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
/* Hover lift */
|
||||
.hover-lift {
|
||||
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
box-shadow 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 12px 40px -8px rgba(30, 24, 16, 0.12);
|
||||
}
|
||||
|
||||
/* Monochrome icon filter for editorial look */
|
||||
.icon-mono {
|
||||
filter: grayscale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Component layer ─────────────────────────────────────────────────── */
|
||||
@layer components {
|
||||
|
||||
/* ── Editorial Section Header ────────────────────────────────────────── */
|
||||
.section-header {
|
||||
@apply mb-16;
|
||||
}
|
||||
.section-header .eyebrow {
|
||||
@apply label text-accent mb-4 block;
|
||||
}
|
||||
.section-header h2 {
|
||||
@apply font-display text-display-lg text-[var(--fg-primary)] mb-5;
|
||||
}
|
||||
.section-header p {
|
||||
@apply text-[var(--fg-secondary)] text-lg leading-relaxed max-w-[65ch];
|
||||
}
|
||||
|
||||
/* ── Warm Card ───────────────────────────────────────────────────────── */
|
||||
.card-warm {
|
||||
background-color: var(--bg-elevated);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 0.75rem;
|
||||
transition: border-color 0.25s ease, box-shadow 0.25s ease;
|
||||
}
|
||||
.card-warm:hover {
|
||||
border-color: var(--border-strong);
|
||||
box-shadow: 0 8px 32px -8px rgba(30, 24, 16, 0.10);
|
||||
}
|
||||
|
||||
/* ── Accent Button ───────────────────────────────────────────────────── */
|
||||
.btn-accent {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background-color: var(--accent);
|
||||
color: #fff;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
font-family: var(--font-inter), sans-serif;
|
||||
transition: background-color 0.2s ease, transform 0.15s ease;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
.btn-accent:hover {
|
||||
background-color: var(--accent-dark);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.btn-accent:active { transform: translateY(0); }
|
||||
|
||||
.btn-accent:focus-visible,
|
||||
.btn-ghost:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
/* ── Ghost Button ────────────────────────────────────────────────────── */
|
||||
.btn-ghost {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6875rem 1.375rem;
|
||||
background: transparent;
|
||||
color: var(--fg-primary);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-inter), sans-serif;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-ghost:hover {
|
||||
background-color: var(--bg-surface);
|
||||
border-color: var(--fg-muted);
|
||||
}
|
||||
|
||||
/* ── Stat Cell (Hero metrics) ────────────────────────────────────────── */
|
||||
.stat-cell {
|
||||
@apply flex flex-col items-center gap-1;
|
||||
}
|
||||
.stat-cell .stat-value {
|
||||
font-family: var(--font-display), Georgia, serif;
|
||||
font-size: 2rem;
|
||||
line-height: 1;
|
||||
color: var(--fg-primary);
|
||||
}
|
||||
.stat-cell .stat-label {
|
||||
@apply label text-[var(--fg-muted)];
|
||||
}
|
||||
|
||||
/* ── Step number (How It Works) ──────────────────────────────────────── */
|
||||
.step-number {
|
||||
font-family: var(--font-display), Georgia, serif;
|
||||
font-size: 3.5rem;
|
||||
line-height: 1;
|
||||
color: var(--border-strong);
|
||||
}
|
||||
|
||||
/* ── Nutri-Score Hero metric ─────────────────────────────────────────── */
|
||||
.nutriscore-hero {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
border-radius: 50%;
|
||||
font-family: var(--font-display), Georgia, serif;
|
||||
font-size: 2.25rem;
|
||||
animation: score-reveal 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||
}
|
||||
|
||||
/* ── Search input editorial ──────────────────────────────────────────── */
|
||||
.search-editorial {
|
||||
width: 100%;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0 1.25rem;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.search-editorial:focus-within {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px oklch(52% 0.11 155 / 0.12);
|
||||
}
|
||||
.search-editorial input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 1.125rem 0;
|
||||
font-family: var(--font-inter), sans-serif;
|
||||
font-size: 1rem;
|
||||
color: var(--fg-primary);
|
||||
}
|
||||
.search-editorial input::placeholder {
|
||||
color: var(--fg-faint);
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Keyframe animations ─────────────────────────────────────────────── */
|
||||
@keyframes fade-up {
|
||||
from { opacity: 0; transform: translateY(1.5rem); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes stagger-in {
|
||||
from { opacity: 0; transform: translateY(0.75rem); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes line-grow {
|
||||
from { transform: scaleX(0); transform-origin: left; }
|
||||
to { transform: scaleX(1); transform-origin: left; }
|
||||
}
|
||||
@keyframes score-reveal {
|
||||
from { transform: scale(0.6); opacity: 0; }
|
||||
to { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(1rem); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ─── Legacy animation class aliases (keep compatibility) ─────────────── */
|
||||
.animate-in { animation: fadeInUp 0.6s ease-out forwards; }
|
||||
.fade-in-0 { opacity: 0; }
|
||||
.slide-in-from-bottom-4 { transform: translateY(1rem); }
|
||||
.animate-fade-up { animation: fade-up 0.7s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
|
||||
.animate-fade-in { animation: fade-in 0.5s ease forwards; }
|
||||
.animate-stagger-in { animation: stagger-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
|
||||
.animate-pulse-slow { animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
||||
|
||||
/* ─── Redesign: ink palette tokens (mirror the HTML spec) ────────────── */
|
||||
:root {
|
||||
--ink-900: #141312;
|
||||
--ink-800: #26231f;
|
||||
--ink-700: #3d3934;
|
||||
--ink-600: #595149;
|
||||
--ink-500: #7a7269;
|
||||
--ink-400: #a39c93;
|
||||
--ink-300: #c9c3ba;
|
||||
--ink-200: #e4dfd7;
|
||||
--ink-150: #ece7de;
|
||||
--ink-100: #f3efe7;
|
||||
--ink-50: #f9f6f0;
|
||||
--ink-0: #fdfbf5;
|
||||
--nutri-a: #038141;
|
||||
--nutri-b: #85bb2f;
|
||||
--nutri-c: #fecb02;
|
||||
--nutri-d: #ee8100;
|
||||
--nutri-e: #e63e11;
|
||||
--accent-soft: oklch(94% 0.04 155);
|
||||
--accent-ink: oklch(28% 0.06 155);
|
||||
--warn: #b45309;
|
||||
--warn-soft: #fef3c7;
|
||||
--font-display: var(--font-instrument-serif);
|
||||
--font-serif: var(--font-display), 'Iowan Old Style', Georgia, serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, Menlo, monospace;
|
||||
}
|
||||
|
||||
.landing-font {
|
||||
--font-display: var(--font-newsreader);
|
||||
}
|
||||
|
||||
/* ─── Redesign: nutri-letter hero circle ─────────────────────────────── */
|
||||
.nutri-letter {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
font-family: var(--font-display), Georgia, serif;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1;
|
||||
box-shadow: inset 0 -3px 0 rgba(0,0,0,0.15), 0 8px 24px -8px rgba(0,0,0,0.2);
|
||||
}
|
||||
.nutri-letter.grade-a, .nutri-letter.a { background: var(--nutri-a); }
|
||||
.nutri-letter.grade-b, .nutri-letter.b { background: var(--nutri-b); }
|
||||
.nutri-letter.grade-c, .nutri-letter.c { background: var(--nutri-c); color: #3a2a00; }
|
||||
.nutri-letter.grade-d, .nutri-letter.d { background: var(--nutri-d); }
|
||||
.nutri-letter.grade-e, .nutri-letter.e { background: var(--nutri-e); }
|
||||
|
||||
/* ─── Redesign: nutri-scale badge (full A–E row) ─────────────────────── */
|
||||
.nutri-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-family: var(--font-inter), sans-serif;
|
||||
font-weight: 800;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
background: var(--ink-100);
|
||||
padding: 3px;
|
||||
gap: 2px;
|
||||
}
|
||||
.nutri-badge .seg {
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
letter-spacing: -0.02em;
|
||||
transition: transform 0.25s;
|
||||
opacity: 0.35;
|
||||
filter: saturate(0.3);
|
||||
}
|
||||
.nutri-badge .seg.active { opacity: 1; filter: none; transform: scale(1.15); box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
|
||||
.nutri-badge .seg.a { background: var(--nutri-a); }
|
||||
.nutri-badge .seg.b { background: var(--nutri-b); }
|
||||
.nutri-badge .seg.c { background: var(--nutri-c); color: #3a2a00; }
|
||||
.nutri-badge .seg.d { background: var(--nutri-d); }
|
||||
.nutri-badge .seg.e { background: var(--nutri-e); }
|
||||
|
||||
/* ─── Redesign: eyebrow mono label ──────────────────────────────────── */
|
||||
.eyebrow-mono {
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-500);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ─── Redesign: product card chip ─────────────────────────────────── */
|
||||
.rd-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: var(--ink-100, #f3efe7);
|
||||
color: var(--ink-700, #3d3934);
|
||||
border: 1px solid var(--ink-200, #e4dfd7);
|
||||
}
|
||||
.rd-chip.warn {
|
||||
background: var(--warn-soft, #fef3c7);
|
||||
color: var(--warn, #b45309);
|
||||
border-color: #fcd34d;
|
||||
}
|
||||
+25
-18
@@ -1,13 +1,31 @@
|
||||
import "./globals.css";
|
||||
import { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import { Instrument_Serif, Inter, Newsreader } from "next/font/google";
|
||||
import Script from "next/script";
|
||||
import { ThemeProvider } from "@/app/providers";
|
||||
import { ErrorBoundary } from "@/app/components/ErrorBoundary";
|
||||
import { ErrorTracking } from "@/app/components/ErrorTracking";
|
||||
|
||||
// Use Inter como fallback já que é uma fonte similar à Geist
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const instrumentSerif = Instrument_Serif({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-instrument-serif",
|
||||
weight: "400",
|
||||
style: ["normal", "italic"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const newsreader = Newsreader({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-newsreader",
|
||||
style: ["normal", "italic"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://nutriscan.com.br"),
|
||||
@@ -43,14 +61,7 @@ export const metadata: Metadata = {
|
||||
description:
|
||||
"Descubra informações nutricionais completas de milhares de produtos alimentícios.",
|
||||
siteName: "NutriScan",
|
||||
images: [
|
||||
{
|
||||
url: "/icon.png",
|
||||
width: 512,
|
||||
height: 512,
|
||||
alt: "NutriScan Logo",
|
||||
},
|
||||
],
|
||||
images: [{ url: "/icon.png", width: 512, height: 512, alt: "NutriScan Logo" }],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
@@ -74,14 +85,10 @@ export const metadata: Metadata = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="pt-BR" suppressHydrationWarning className={inter.className}>
|
||||
<body className="min-h-screen bg-background antialiased">
|
||||
<html lang="pt-BR" suppressHydrationWarning className={`${inter.variable} ${instrumentSerif.variable} ${newsreader.variable}`}>
|
||||
<body className="min-h-screen bg-base antialiased font-sans">
|
||||
<ErrorBoundary>
|
||||
<ThemeProvider>
|
||||
{children}
|
||||
|
||||
+13
-9
@@ -8,35 +8,39 @@ import { verificarLista } from '@/app/actions';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
interface ListaPageProps {
|
||||
params: {
|
||||
params: Promise<{
|
||||
id: string
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export default async function ListaPage({ params }: ListaPageProps) {
|
||||
const { success } = await verificarLista(params.id);
|
||||
const { id } = await params;
|
||||
const { success } = await verificarLista(id);
|
||||
|
||||
if (!success) {
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors duration-300">
|
||||
<ListHeader listaId={params.id} />
|
||||
<main className="min-h-screen transition-colors duration-300" style={{ background: 'var(--bg-base)' }}>
|
||||
<ListHeader listaId={id} />
|
||||
<ThemeToggle />
|
||||
<div className="container mx-auto px-4 pt-20 pb-8">
|
||||
<div className="mx-auto px-4 pt-20 pb-8" style={{ maxWidth: 1400 }}>
|
||||
<div className="mb-8">
|
||||
<Main listaId={params.id} />
|
||||
<Main listaId={id} />
|
||||
</div>
|
||||
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex justify-center items-center p-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#1118A7] dark:border-[#212FCC]"></div>
|
||||
<div
|
||||
className="animate-spin rounded-full h-8 w-8 border-b-2"
|
||||
style={{ borderColor: 'var(--accent)' }}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CardList listaId={params.id} />
|
||||
<CardList listaId={id} />
|
||||
</Suspense>
|
||||
|
||||
<Footer />
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import { TrustBadges } from "./components/landing/TrustBadges";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen bg-white dark:bg-gray-900">
|
||||
<div className="landing-font min-h-screen bg-base">
|
||||
<Header />
|
||||
<main>
|
||||
<Hero />
|
||||
|
||||
+777
-158
@@ -7,19 +7,173 @@ import {
|
||||
ArrowLeft,
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
Database,
|
||||
Globe,
|
||||
ShieldCheck,
|
||||
Beaker,
|
||||
ClipboardList,
|
||||
Heart,
|
||||
Share2,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Image from "next/image";
|
||||
import { searchByBarcode, Product, formatNutritionData } from "@/lib/openFoodFactsApi";
|
||||
import { NutriScoreBadge, NovaBadge } from "@/app/components/NutriScoreBadge";
|
||||
import { NutritionGrid } from "@/app/components/NutritionBar";
|
||||
import { ProductSkeleton } from "@/app/components/ProductSkeleton";
|
||||
|
||||
/* ── Nutri-Score letter circle ─────────────────────────────────────────── */
|
||||
const NUTRI_BG: Record<string, string> = {
|
||||
a: "#038141",
|
||||
b: "#85bb2f",
|
||||
c: "#fecb02",
|
||||
d: "#ee8100",
|
||||
e: "#e63e11",
|
||||
};
|
||||
const NUTRI_INK: Record<string, string> = {
|
||||
a: "#fff",
|
||||
b: "#fff",
|
||||
c: "#3a2a00",
|
||||
d: "#fff",
|
||||
e: "#fff",
|
||||
};
|
||||
const GRADE_DESC: Record<string, string> = {
|
||||
a: "Excelente qualidade nutricional",
|
||||
b: "Boa qualidade nutricional",
|
||||
c: "Qualidade regular",
|
||||
d: "Qualidade ruim",
|
||||
e: "Qualidade muito ruim",
|
||||
};
|
||||
const NOVA_DESC: Record<number, string> = {
|
||||
1: "Alimento in natura ou minimamente processado.",
|
||||
2: "Ingrediente culinário processado.",
|
||||
3: "Alimento processado com adição de sal, açúcar ou óleo.",
|
||||
4: "Ultraprocessado — formulação industrial com aditivos.",
|
||||
};
|
||||
const NOVA_BG: Record<number, string> = {
|
||||
1: "#038141",
|
||||
2: "#fecb02",
|
||||
3: "#ee8100",
|
||||
4: "#e63e11",
|
||||
};
|
||||
const NOVA_INK: Record<number, string> = {
|
||||
1: "#fff",
|
||||
2: "#3a2a00",
|
||||
3: "#fff",
|
||||
4: "#fff",
|
||||
};
|
||||
|
||||
function NutriLetter({ grade, size = 92 }: { grade: string; size?: number }) {
|
||||
const g = grade?.toLowerCase();
|
||||
if (!NUTRI_BG[g]) return null;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: "50%",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontFamily: "'Instrument Serif', Georgia, serif",
|
||||
fontSize: Math.round(size * 0.63),
|
||||
color: NUTRI_INK[g],
|
||||
background: NUTRI_BG[g],
|
||||
fontWeight: 400,
|
||||
letterSpacing: "-0.03em",
|
||||
lineHeight: 1,
|
||||
flexShrink: 0,
|
||||
boxShadow: "inset 0 -3px 0 rgba(0,0,0,0.15), 0 8px 24px -8px rgba(0,0,0,0.2)",
|
||||
}}
|
||||
>
|
||||
{g.toUpperCase()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Nutri-Scale row (A–E with active highlight) ────────────────────────── */
|
||||
function NutriScale({ active, size = "sm" }: { active: string; size?: "sm" | "md" }) {
|
||||
const px = size === "sm" ? 18 : 26;
|
||||
const fs = size === "sm" ? 9 : 12;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
background: "var(--ink-100, #f3efe7)",
|
||||
padding: 3,
|
||||
borderRadius: 999,
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{["a", "b", "c", "d", "e"].map((l) => (
|
||||
<div
|
||||
key={l}
|
||||
style={{
|
||||
width: px,
|
||||
height: px,
|
||||
borderRadius: 999,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: l === "c" ? "#3a2a00" : "#fff",
|
||||
fontSize: fs,
|
||||
fontWeight: 700,
|
||||
background: NUTRI_BG[l],
|
||||
opacity: active?.toLowerCase() === l ? 1 : 0.3,
|
||||
filter: active?.toLowerCase() === l ? "none" : "saturate(0.3)",
|
||||
transform: active?.toLowerCase() === l ? "scale(1.18)" : "none",
|
||||
boxShadow:
|
||||
active?.toLowerCase() === l
|
||||
? "0 2px 8px rgba(0,0,0,0.15)"
|
||||
: "none",
|
||||
transition: "all 0.25s",
|
||||
}}
|
||||
>
|
||||
{l.toUpperCase()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── NOVA pill ───────────────────────────────────────────────────────────── */
|
||||
function NovaPill({ group }: { group: number }) {
|
||||
const labels: Record<number, string> = {
|
||||
1: "Não processado",
|
||||
2: "Ingrediente culinário",
|
||||
3: "Processado",
|
||||
4: "Ultraprocessado",
|
||||
};
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
padding: "4px 10px 4px 4px",
|
||||
borderRadius: 999,
|
||||
background: "var(--ink-100, #f3efe7)",
|
||||
border: "1px solid var(--ink-200, #e4dfd7)",
|
||||
fontSize: 12,
|
||||
color: "var(--ink-700, #3d3934)",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 999,
|
||||
background: NOVA_BG[group] || "#a39c93",
|
||||
color: NOVA_INK[group] || "#fff",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
fontFamily: "ui-monospace, monospace",
|
||||
fontWeight: 700,
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
{group}
|
||||
</span>
|
||||
NOVA · {labels[group] || "Desconhecido"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProdutoPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
@@ -35,9 +189,9 @@ export default function ProdutoPage() {
|
||||
setError(null);
|
||||
const result = await searchByBarcode(barcode);
|
||||
if (result) setProduct(result);
|
||||
else setError("Produto não catalogado na base ODbL.");
|
||||
else setError("Produto não encontrado na base de dados.");
|
||||
} catch {
|
||||
setError("Falha crítica na sincronização de dados.");
|
||||
setError("Falha ao carregar os dados do produto.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -49,21 +203,94 @@ export default function ProdutoPage() {
|
||||
|
||||
if (loading) return <ProductSkeleton />;
|
||||
|
||||
// Handler de Erro - Estética Industrial
|
||||
/* ── Error state ─────────────────────────────────────────────────────── */
|
||||
if (error || !product) {
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex items-center justify-center p-6 dark:bg-[#030712]">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="max-w-md w-full text-center space-y-6 border border-slate-200 p-10 rounded-[2.5rem] bg-slate-50 dark:bg-red-950/10 dark:border-red-900/20"
|
||||
<div
|
||||
style={{
|
||||
minHeight: "100vh",
|
||||
background: "var(--ink-0, #fdfbf5)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 24,
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
style={{
|
||||
maxWidth: 420,
|
||||
width: "100%",
|
||||
background: "var(--ink-0, #fdfbf5)",
|
||||
border: "1px solid var(--ink-200, #e4dfd7)",
|
||||
borderRadius: 20,
|
||||
padding: 48,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<AlertCircle className="w-16 h-16 text-red-500 mx-auto" />
|
||||
<h1 className="text-2xl font-black text-slate-900 dark:text-white uppercase tracking-tighter">Erro de Protocolo</h1>
|
||||
<p className="text-slate-600 text-sm dark:text-slate-400 font-medium">{error}</p>
|
||||
<div className="flex gap-3">
|
||||
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={() => router.push("/")}>Nova Busca</Button>
|
||||
<Button variant="outline" className="w-full border-slate-300 dark:border-slate-800" onClick={() => router.back()}>Retornar</Button>
|
||||
<AlertCircle
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
color: "#e63e11",
|
||||
margin: "0 auto 20px",
|
||||
}}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<h1
|
||||
style={{
|
||||
fontFamily: "'Instrument Serif', Georgia, serif",
|
||||
fontSize: 28,
|
||||
color: "var(--ink-900, #141312)",
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
Produto não encontrado
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
color: "var(--ink-600, #595149)",
|
||||
fontSize: 14,
|
||||
marginBottom: 28,
|
||||
lineHeight: 1.55,
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
<div style={{ display: "flex", gap: 10 }}>
|
||||
<button
|
||||
onClick={() => router.push("/")}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "12px 0",
|
||||
background: "var(--accent, oklch(52% 0.11 155))",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: 10,
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Nova Busca
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "12px 0",
|
||||
background: "transparent",
|
||||
color: "var(--ink-900, #141312)",
|
||||
border: "1px solid var(--ink-200, #e4dfd7)",
|
||||
borderRadius: 10,
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Voltar
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
@@ -71,161 +298,553 @@ export default function ProdutoPage() {
|
||||
}
|
||||
|
||||
const imageUrl = product.image_front_url || product.image_url || null;
|
||||
const grade = product.nutrition_grades?.toLowerCase();
|
||||
const nova = product.nova_group;
|
||||
const gradeDesc = grade ? GRADE_DESC[grade] : null;
|
||||
const novaDesc = nova ? NOVA_DESC[nova] : null;
|
||||
|
||||
/* ── Nutrition bars — % of daily reference ─────────────────────────── */
|
||||
const dailyRef: Record<string, number> = {
|
||||
energy_100g: 2000,
|
||||
fat_100g: 70,
|
||||
saturated_fat_100g: 20,
|
||||
sugars_100g: 90,
|
||||
salt_100g: 6,
|
||||
fiber_100g: 30,
|
||||
proteins_100g: 50,
|
||||
};
|
||||
const nutriments = product.nutriments || {};
|
||||
const nutritionRows = [
|
||||
{ key: "energy_100g", label: "Energia", unit: "kcal" },
|
||||
{ key: "fat_100g", label: "Gorduras totais", unit: "g" },
|
||||
{ key: "saturated_fat_100g",label: "Gorduras saturadas", unit: "g" },
|
||||
{ key: "sugars_100g", label: "Açúcares", unit: "g" },
|
||||
{ key: "salt_100g", label: "Sódio", unit: "g" },
|
||||
{ key: "fiber_100g", label: "Fibras", unit: "g" },
|
||||
{ key: "proteins_100g", label: "Proteínas", unit: "g" },
|
||||
];
|
||||
|
||||
const allergenTags = product.allergens_tags || [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 text-slate-900 dark:bg-[#030712] dark:text-slate-200 transition-colors duration-500">
|
||||
|
||||
{/* 1. NAVEGAÇÃO E TÍTULO (Sticky Header) */}
|
||||
<header className="sticky top-0 z-50 bg-white/80 backdrop-blur-xl border-b border-slate-200 dark:bg-[#030712]/80 dark:border-white/5">
|
||||
<div className="max-w-7xl mx-auto px-6 h-20 flex items-center gap-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => router.back()}
|
||||
className="rounded-full hover:bg-slate-100 dark:hover:bg-white/5"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="text-xl font-black tracking-tighter text-slate-900 dark:text-white truncate uppercase">
|
||||
{product.product_name || "Unlabeled Product"}
|
||||
</h1>
|
||||
<div className="flex items-center gap-2 text-[10px] font-mono font-bold text-slate-500 uppercase tracking-widest">
|
||||
<Database size={10} className="text-blue-500" /> {product.code}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
minHeight: "100vh",
|
||||
background: "var(--ink-0, #fdfbf5)",
|
||||
color: "var(--ink-900, #141312)",
|
||||
}}
|
||||
>
|
||||
{/* ── Top bar ──────────────────────────────────────────────────────── */}
|
||||
<div
|
||||
style={{
|
||||
padding: "18px clamp(20px, 5vw, 40px)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
borderBottom: "1px solid var(--ink-200, #e4dfd7)",
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
zIndex: 50,
|
||||
background: "rgba(253,251,245,0.92)",
|
||||
backdropFilter: "blur(12px)",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
fontSize: 13,
|
||||
color: "var(--ink-700, #3d3934)",
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<ArrowLeft style={{ width: 16, height: 16 }} strokeWidth={1.75} />
|
||||
Voltar
|
||||
</button>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "ui-monospace, monospace",
|
||||
fontSize: 11,
|
||||
color: "var(--ink-500, #7a7269)",
|
||||
letterSpacing: "0.12em",
|
||||
}}
|
||||
>
|
||||
EAN · {product.code}
|
||||
</div>
|
||||
</header>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<button
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 36,
|
||||
height: 36,
|
||||
border: "1px solid var(--ink-200, #e4dfd7)",
|
||||
borderRadius: 8,
|
||||
background: "transparent",
|
||||
color: "var(--ink-700, #3d3934)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
title="Salvar"
|
||||
>
|
||||
<Heart style={{ width: 14, height: 14 }} strokeWidth={1.75} />
|
||||
</button>
|
||||
<button
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 36,
|
||||
height: 36,
|
||||
border: "1px solid var(--ink-200, #e4dfd7)",
|
||||
borderRadius: 8,
|
||||
background: "transparent",
|
||||
color: "var(--ink-700, #3d3934)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
title="Compartilhar"
|
||||
>
|
||||
<Share2 style={{ width: 14, height: 14 }} strokeWidth={1.75} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-6 py-12">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12">
|
||||
|
||||
{/* COLUNA ESQUERDA: Ativos Visuais (5 Colunas) */}
|
||||
<div className="lg:col-span-5 space-y-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="relative aspect-square bg-white rounded-[3rem] border border-slate-200 overflow-hidden shadow-2xl dark:bg-slate-900 dark:border-white/5 group"
|
||||
{/* ── Hero: image + headline + Nutri-Score ────────────────────────── */}
|
||||
<div
|
||||
style={{
|
||||
padding: "40px clamp(20px, 5vw, 40px) 32px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))",
|
||||
gap: 48,
|
||||
maxWidth: 1100,
|
||||
margin: "0 auto",
|
||||
}}
|
||||
>
|
||||
{/* Product image */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.45 }}
|
||||
style={{
|
||||
background: "var(--ink-50, #f9f6f0)",
|
||||
borderRadius: 18,
|
||||
aspectRatio: "1/1",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
border: "1px solid var(--ink-200, #e4dfd7)",
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{imageUrl ? (
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={product.product_name || "Produto"}
|
||||
fill
|
||||
className="object-contain p-10"
|
||||
priority
|
||||
sizes="(max-width: 768px) 100vw, 500px"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "var(--ink-400, #a39c93)",
|
||||
}}
|
||||
>
|
||||
{imageUrl ? (
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={product.product_name || "Product"}
|
||||
fill
|
||||
className="object-contain p-12 transition-transform duration-700 group-hover:scale-110"
|
||||
priority
|
||||
sizes="(max-width: 768px) 100vw, 500px"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-slate-300 dark:text-slate-800">
|
||||
<Beaker size={80} className="mb-4 opacity-50" />
|
||||
<span className="text-xs font-mono font-black uppercase tracking-[0.4em]">No Visual Metadata</span>
|
||||
<Beaker style={{ width: 64, height: 64 }} strokeWidth={1} />
|
||||
<span
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontFamily: "ui-monospace, monospace",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.1em",
|
||||
marginTop: 12,
|
||||
}}
|
||||
>
|
||||
Sem imagem
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Right column — headline + score cards */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.05 }}
|
||||
>
|
||||
{/* Brand eyebrow */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "var(--ink-500, #7a7269)",
|
||||
fontFamily: "ui-monospace, monospace",
|
||||
letterSpacing: "0.12em",
|
||||
textTransform: "uppercase",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
{product.brands || "Marca desconhecida"}
|
||||
</div>
|
||||
|
||||
{/* Product name */}
|
||||
<h1
|
||||
style={{
|
||||
fontFamily: "'Instrument Serif', Georgia, serif",
|
||||
fontSize: "clamp(28px, 4vw, 44px)",
|
||||
letterSpacing: "-0.01em",
|
||||
lineHeight: 1.08,
|
||||
color: "var(--ink-900, #141312)",
|
||||
marginBottom: 28,
|
||||
}}
|
||||
>
|
||||
{product.product_name || "Produto sem nome"}
|
||||
</h1>
|
||||
|
||||
{/* ── Nutri-Score hero metric ─────────────────────────────────── */}
|
||||
{grade && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.15 }}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 20,
|
||||
padding: 24,
|
||||
background: "var(--ink-50, #f9f6f0)",
|
||||
border: "1px solid var(--ink-200, #e4dfd7)",
|
||||
borderRadius: 16,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<NutriLetter grade={grade} size={92} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "ui-monospace, monospace",
|
||||
fontSize: 10,
|
||||
letterSpacing: "0.14em",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--ink-500, #7a7269)",
|
||||
fontWeight: 500,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
Nutri‑Score
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "'Instrument Serif', Georgia, serif",
|
||||
fontSize: 26,
|
||||
letterSpacing: "-0.01em",
|
||||
color: "var(--ink-900, #141312)",
|
||||
lineHeight: 1.1,
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
{gradeDesc || `Nota ${grade.toUpperCase()}`}
|
||||
</div>
|
||||
<NutriScale active={grade} size="sm" />
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* ── NOVA row ─────────────────────────────────────────────────── */}
|
||||
{nova && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
style={{
|
||||
padding: 20,
|
||||
border: "1px solid var(--ink-200, #e4dfd7)",
|
||||
borderRadius: 14,
|
||||
marginBottom: 16,
|
||||
display: "flex",
|
||||
gap: 18,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<NovaPill group={nova} />
|
||||
{novaDesc && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: "var(--ink-600, #595149)",
|
||||
lineHeight: 1.55,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{novaDesc}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Ficha Técnica (Bento-style) */}
|
||||
<div className="p-8 rounded-[2.5rem] bg-white border border-slate-200 shadow-sm dark:bg-white/[0.02] dark:border-white/5 space-y-6">
|
||||
<h3 className="text-[10px] font-black uppercase tracking-[0.4em] text-blue-600 dark:text-blue-400">SKU Datasheet</h3>
|
||||
<div className="space-y-4 font-medium">
|
||||
{[
|
||||
{ label: "Fabricante", val: product.brands, icon: ShieldCheck },
|
||||
{ label: "Região", val: product.countries, icon: Globe },
|
||||
{ label: "Registro", val: "Auditado", icon: ClipboardList }
|
||||
].map((item, i) => item.val && (
|
||||
<div key={i} className="flex justify-between items-center text-sm">
|
||||
<span className="text-slate-400 flex items-center gap-2 font-bold uppercase text-[11px] tracking-tight">
|
||||
<item.icon size={14} className="text-slate-300" /> {item.label}
|
||||
</span>
|
||||
<span className="text-slate-900 dark:text-white text-right max-w-[60%] truncate">{item.val}</span>
|
||||
{/* ── Allergens ────────────────────────────────────────────────── */}
|
||||
<AnimatePresence>
|
||||
{allergenTags.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -8 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.25 }}
|
||||
style={{
|
||||
padding: 18,
|
||||
borderRadius: 14,
|
||||
background: "#fef3c7",
|
||||
border: "1px solid #fcd34d",
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: 14,
|
||||
}}
|
||||
>
|
||||
<AlertTriangle
|
||||
style={{ width: 20, height: 20, color: "#b45309", flexShrink: 0, marginTop: 1 }}
|
||||
strokeWidth={1.75}
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: "#b45309",
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
Contém alérgenos
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* COLUNA DIREITA: Inteligência de Dados (7 Colunas) */}
|
||||
<div className="lg:col-span-7 space-y-8">
|
||||
|
||||
{/* Veredito Rápido: Scores */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<DataCard
|
||||
title="Nutri-Score"
|
||||
desc="Indicador de Qualidade Nutricional"
|
||||
badge={<NutriScoreBadge grade={product.nutrition_grades || ""} size="lg" />}
|
||||
/>
|
||||
<DataCard
|
||||
title="Protocolo NOVA"
|
||||
desc="Nível de Processamento Industrial"
|
||||
badge={<NovaBadge group={product.nova_group || 0} size="lg" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Análise Macronutricional */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="p-1 rounded-[3rem] bg-gradient-to-br from-slate-200 to-slate-100 dark:from-white/10 dark:to-transparent"
|
||||
>
|
||||
<div className="bg-white rounded-[2.9rem] p-10 dark:bg-[#030712]">
|
||||
<h3 className="text-lg font-black text-slate-900 dark:text-white mb-8 flex items-center gap-3 uppercase tracking-tighter">
|
||||
<div className="w-2 h-6 bg-blue-600 rounded-full" />
|
||||
Composição Nutricional <span className="text-slate-400 font-mono text-xs font-normal">/ 100g</span>
|
||||
</h3>
|
||||
<NutritionGrid nutritionData={formatNutritionData(product) || {}} />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Bloqueadores e Alérgenos */}
|
||||
<AnimatePresence>
|
||||
{product.allergens_tags && product.allergens_tags.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="p-8 rounded-[2.5rem] bg-orange-50 border border-orange-200 dark:bg-orange-500/5 dark:border-orange-500/20"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="p-2 bg-orange-600 rounded-xl text-white shadow-lg shadow-orange-600/20">
|
||||
<AlertTriangle size={20} />
|
||||
</div>
|
||||
<h3 className="text-lg font-black text-orange-950 dark:text-orange-500 uppercase tracking-tighter">Detecção de Alérgenos</h3>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{product.allergens_tags.map(tag => (
|
||||
<span key={tag} className="px-4 py-2 rounded-xl bg-white border border-orange-200 text-orange-700 text-[10px] font-black uppercase tracking-widest dark:bg-orange-500/10 dark:text-orange-400 dark:border-orange-500/20 shadow-sm">
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
|
||||
{allergenTags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
padding: "3px 10px",
|
||||
background: "rgba(255,255,255,0.7)",
|
||||
border: "1px solid #fde68a",
|
||||
borderRadius: 999,
|
||||
color: "#78350f",
|
||||
}}
|
||||
>
|
||||
{tag.replace(/en:|pt:/g, "")}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Lista de Ingredientes (OCR Interpretation) */}
|
||||
<div className="p-10 rounded-[2.5rem] bg-white border border-slate-200 shadow-sm dark:bg-white/[0.02] dark:border-white/5">
|
||||
<h3 className="text-lg font-black text-slate-900 dark:text-white mb-6 uppercase tracking-tighter">Relatório de Ingredientes</h3>
|
||||
<p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400 font-medium">
|
||||
{product.ingredients_text || "O fabricante não forneceu a lista de ingredientes para este SKU."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* ── Nutrition composition section ───────────────────────────────── */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
style={{
|
||||
borderTop: "1px solid var(--ink-200, #e4dfd7)",
|
||||
padding: "32px clamp(20px, 5vw, 40px) 60px",
|
||||
maxWidth: 1100,
|
||||
margin: "0 auto",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 24,
|
||||
gap: 12,
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
style={{
|
||||
fontFamily: "'Instrument Serif', Georgia, serif",
|
||||
fontSize: "clamp(22px, 3vw, 30px)",
|
||||
letterSpacing: "-0.01em",
|
||||
color: "var(--ink-900, #141312)",
|
||||
}}
|
||||
>
|
||||
Composição nutricional
|
||||
</h2>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "ui-monospace, monospace",
|
||||
fontSize: 10,
|
||||
letterSpacing: "0.14em",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--ink-500, #7a7269)",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
por 100g · %VD de referência
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Sub-componente para cards de dados
|
||||
function DataCard({ title, desc, badge }: { title: string, desc: string, badge: React.ReactNode }) {
|
||||
return (
|
||||
<div className="p-8 rounded-[2.5rem] bg-white border border-slate-200 shadow-sm dark:bg-white/[0.02] dark:border-white/5 hover:border-blue-500/30 transition-all duration-300">
|
||||
<h3 className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-400 mb-1">{title}</h3>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300 mb-8 font-bold leading-tight">{desc}</p>
|
||||
<div className="flex justify-start transform hover:scale-105 transition-transform origin-left">
|
||||
{badge}
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))",
|
||||
gap: "0 48px",
|
||||
}}
|
||||
>
|
||||
{nutritionRows.map((row) => {
|
||||
const raw = nutriments[row.key as keyof typeof nutriments];
|
||||
const v = raw != null ? Number(raw) : null;
|
||||
if (v == null) return null;
|
||||
const ref = dailyRef[row.key] || 100;
|
||||
const pct = Math.min(100, Math.round((v / ref) * 100));
|
||||
const isHigh =
|
||||
pct > 40 &&
|
||||
["fat_100g", "saturated_fat_100g", "sugars_100g", "salt_100g"].includes(row.key);
|
||||
const isGood = ["fiber_100g", "proteins_100g"].includes(row.key);
|
||||
const barColor = isHigh
|
||||
? "#e63e11"
|
||||
: isGood
|
||||
? "var(--accent, oklch(52% 0.11 155))"
|
||||
: "var(--ink-500, #7a7269)";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={row.key}
|
||||
style={{
|
||||
padding: "14px 0",
|
||||
borderBottom: "1px solid var(--ink-200, #e4dfd7)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "baseline",
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 14, color: "var(--ink-800, #26231f)" }}>
|
||||
{row.label}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: "'Instrument Serif', Georgia, serif",
|
||||
fontSize: 18,
|
||||
color: "var(--ink-900, #141312)",
|
||||
}}
|
||||
>
|
||||
{v}
|
||||
<span
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: "var(--ink-500, #7a7269)",
|
||||
marginLeft: 3,
|
||||
fontFamily: "ui-monospace, monospace",
|
||||
}}
|
||||
>
|
||||
{row.unit}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: 4,
|
||||
background: "var(--ink-100, #f3efe7)",
|
||||
borderRadius: 999,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${pct}%`,
|
||||
height: "100%",
|
||||
background: barColor,
|
||||
borderRadius: 999,
|
||||
transition: "width 0.6s",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: "var(--ink-500, #7a7269)",
|
||||
marginTop: 4,
|
||||
fontFamily: "ui-monospace, monospace",
|
||||
letterSpacing: "0.04em",
|
||||
}}
|
||||
>
|
||||
{pct}% VD
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Ingredients */}
|
||||
{product.ingredients_text && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 40,
|
||||
padding: 24,
|
||||
background: "var(--ink-50, #f9f6f0)",
|
||||
border: "1px solid var(--ink-200, #e4dfd7)",
|
||||
borderRadius: 14,
|
||||
}}
|
||||
>
|
||||
<h3
|
||||
style={{
|
||||
fontFamily: "'Instrument Serif', Georgia, serif",
|
||||
fontSize: 20,
|
||||
color: "var(--ink-900, #141312)",
|
||||
marginBottom: 14,
|
||||
}}
|
||||
>
|
||||
Lista de Ingredientes
|
||||
</h3>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: "var(--ink-600, #595149)",
|
||||
lineHeight: 1.65,
|
||||
}}
|
||||
>
|
||||
{product.ingredients_text}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* ── Footer attribution ─────────────────────────────────────────── */}
|
||||
<div
|
||||
style={{
|
||||
padding: "24px clamp(20px, 5vw, 40px)",
|
||||
background: "var(--ink-50, #f9f6f0)",
|
||||
borderTop: "1px solid var(--ink-200, #e4dfd7)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 1100,
|
||||
margin: "0 auto",
|
||||
fontSize: 12,
|
||||
color: "var(--ink-500, #7a7269)",
|
||||
fontFamily: "ui-monospace, monospace",
|
||||
letterSpacing: "0.04em",
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
Dados fornecidos pela comunidade Open Food Facts sob licença ODbL.
|
||||
{product.countries && ` · Região: ${product.countries}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+721
-448
File diff suppressed because it is too large.
Load diff
@@ -1,113 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { ArrowRight, Code2, Heart, Globe, Database, ShieldCheck } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
|
||||
export function CTASection() {
|
||||
return (
|
||||
<section className="relative py-32 bg-[#030712] overflow-hidden transition-colors duration-500">
|
||||
|
||||
{/* 1. LAYER DE INFRAESTRUTURA: Background Glows Inteligentes */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[500px] bg-blue-600/10 blur-[160px] rounded-full" />
|
||||
<div className="absolute top-0 right-0 w-[400px] h-[400px] bg-emerald-500/5 blur-[120px] rounded-full" />
|
||||
<div className="absolute inset-0 bg-[url('data:image/svg+xml,...')] opacity-[0.03] invert dark:invert-0" />
|
||||
</div>
|
||||
<section className="bg-[var(--fg-primary)] section-py relative overflow-hidden">
|
||||
{/* Subtle warm texture */}
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-[0.025]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E\")",
|
||||
backgroundSize: "200px 200px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<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>
|
||||
<div className="container-editorial relative z-10">
|
||||
|
||||
{/* 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>
|
||||
</motion.h2>
|
||||
<p className="text-lg md:text-xl text-slate-400 max-w-3xl mx-auto leading-relaxed font-medium">
|
||||
Junte-se à infraestrutura global que está devolvendo o poder de escolha ao cidadão. Dados abertos para uma vida sem processados ocultos.
|
||||
{/* Top hairline — warm on dark */}
|
||||
<div className="border-t border-[var(--border-subtle)]/20 mb-20" />
|
||||
|
||||
{/* Large editorial headline */}
|
||||
<div className="max-w-3xl mb-16">
|
||||
<span className="label text-[var(--accent)] mb-6 block">
|
||||
Comece agora
|
||||
</span>
|
||||
<h2 className="font-display text-display-xl text-[var(--bg-base)] mb-8 leading-[0.9]">
|
||||
Soberania alimentar via{" "}
|
||||
<em className="font-display-i italic">transparência.</em>
|
||||
</h2>
|
||||
<p className="text-lg text-[var(--fg-muted)] leading-relaxed max-w-xl">
|
||||
Junte-se à infraestrutura global que está devolvendo o poder de escolha
|
||||
ao cidadão. Dados abertos para uma vida sem processados ocultos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 4. LAYER DE EXECUÇÃO: CTAs de Alta Tração */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-24"
|
||||
>
|
||||
<Link href="/" className="w-full sm:w-auto">
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full sm:w-auto h-16 px-10 bg-blue-600 hover:bg-blue-700 text-white rounded-2xl font-bold text-lg shadow-2xl shadow-blue-500/20 transition-all hover:scale-[1.02] active:scale-95 group"
|
||||
>
|
||||
<span>Executar Scan</span>
|
||||
<ArrowRight className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Button>
|
||||
{/* CTAs */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-24">
|
||||
<Link href="/" className="btn-accent inline-flex items-center gap-2 group">
|
||||
Executar Scan
|
||||
<ArrowRight
|
||||
size={15}
|
||||
className="transition-transform duration-200 group-hover:translate-x-0.5"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="https://github.com/evandrodevbr/lista-compras"
|
||||
target="_blank"
|
||||
className="w-full sm:w-auto"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-ghost border-[var(--border-subtle)]/30 text-[var(--bg-base)] hover:bg-[var(--bg-base)]/10"
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="w-full sm:w-auto h-16 px-10 border-slate-800 text-white hover:bg-slate-900 rounded-2xl font-bold text-lg transition-all"
|
||||
>
|
||||
<Code2 className="mr-2 w-5 h-5 text-black" />
|
||||
<span className="text-gray-800">Audit Code</span>
|
||||
</Button>
|
||||
Auditar o código
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* 5. LAYER DE VALIDAÇÃO: Bento Grid de Metadados */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* Three-column info grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-px bg-[var(--border-subtle)]/20 border border-[var(--border-subtle)]/20 rounded-lg overflow-hidden mb-20">
|
||||
{[
|
||||
{ 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>
|
||||
{
|
||||
label: "Rede",
|
||||
val: "Global Node",
|
||||
desc: "Integridade em 100+ países",
|
||||
},
|
||||
{
|
||||
label: "Segurança",
|
||||
val: "Privacy First",
|
||||
desc: "Zero tracking de dados",
|
||||
},
|
||||
{
|
||||
label: "Sustentabilidade",
|
||||
val: "Open Source",
|
||||
desc: "Mantido pela comunidade",
|
||||
},
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.label}
|
||||
className="px-8 py-10 group hover:bg-[var(--bg-base)]/5 transition-colors duration-300"
|
||||
>
|
||||
<div className="label text-[var(--fg-faint)] mb-2 block">{item.label}</div>
|
||||
<div className="font-display text-display-sm text-[var(--bg-base)] mb-2">
|
||||
{item.val}
|
||||
</div>
|
||||
<p className="text-xs text-[var(--fg-muted)]">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 6. MENSAGEM FINAL: Minimalismo de Autoridade */}
|
||||
<div className="mt-24 pt-12 border-t border-white/[0.05]">
|
||||
<p className="text-[10px] font-mono font-bold text-slate-600 uppercase tracking-[0.5em] mb-6">
|
||||
NutriScan Governance Protocol v2.0
|
||||
{/* Bottom hairline + governance line */}
|
||||
<div className="border-t border-[var(--border-subtle)]/20 pt-10 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<p className="label text-[var(--fg-faint)]">
|
||||
NutriScan · Transparência Alimentar · 2024
|
||||
</p>
|
||||
<Link
|
||||
href="/sobre"
|
||||
className="inline-flex items-center gap-2 text-sm font-bold text-blue-500 hover:text-blue-400 transition-colors group"
|
||||
<Link
|
||||
href="/sobre"
|
||||
className="inline-flex items-center gap-2 text-sm text-[var(--accent)] hover:text-[var(--accent-light)] transition-colors duration-200 group"
|
||||
>
|
||||
Conheça nossa arquitetura de dados
|
||||
<ArrowRight size={14} className="group-hover:translate-x-1 transition-transform" />
|
||||
Conheça nossa arquitetura de dados
|
||||
<ArrowRight
|
||||
size={13}
|
||||
className="transition-transform duration-200 group-hover:translate-x-0.5"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { ArrowDown } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function HeroSection() {
|
||||
const scrollToNext = () => {
|
||||
const nextSection = document.getElementById("problema");
|
||||
nextSection?.scrollIntoView({ behavior: "smooth" });
|
||||
document.getElementById("problema")?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative min-h-screen 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">
|
||||
{/* Background Pattern */}
|
||||
<div className="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg%20width%3D%2260%22%20height%3D%2260%22%20viewBox%3D%220%200%2060%2060%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cg%20fill%3D%22%230055ff%22%20fill-opacity%3D%220.05%22%3E%3Ccircle%20cx%3D%2230%22%20cy%3D%2230%22%20r%3D%222%22/%3E%3C/g%3E%3C/g%3E%3C/svg%3E')] opacity-40"></div>
|
||||
<section className="relative bg-base overflow-hidden">
|
||||
{/* Subtle warm grain texture overlay */}
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E\")",
|
||||
backgroundSize: "200px 200px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
{/* Logo */}
|
||||
<div className="mb-8">
|
||||
<Image
|
||||
src="/apple-touch-icon.png"
|
||||
alt="NutriScan Logo"
|
||||
width={96}
|
||||
height={96}
|
||||
className="mx-auto rounded-2xl shadow-xl"
|
||||
priority
|
||||
/>
|
||||
{/* Top hairline */}
|
||||
<div className="hairline" />
|
||||
|
||||
<div className="container-editorial">
|
||||
{/* Eyebrow row */}
|
||||
<div className="flex items-center justify-between border-b border-[var(--border-subtle)] py-6">
|
||||
<span className="label text-[var(--fg-muted)]">Sobre o NutriScan</span>
|
||||
<span className="label text-[var(--fg-faint)]">Est. 2024</span>
|
||||
</div>
|
||||
|
||||
{/* Hero Title */}
|
||||
<h1 className="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-bold text-gray-900 dark:text-white mb-6 leading-tight">
|
||||
Saber o que colocamos no{" "}
|
||||
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent">
|
||||
prato
|
||||
</span>
|
||||
<br />é um direito de todos
|
||||
</h1>
|
||||
{/* Main hero content */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[1fr_auto] gap-16 pt-20 pb-24 lg:pt-28 lg:pb-32">
|
||||
<div className="max-w-3xl">
|
||||
{/* Issue label */}
|
||||
<p className="label text-accent mb-8 tracking-widest">
|
||||
Transparência Alimentar
|
||||
</p>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p className="text-xl sm:text-2xl text-gray-600 dark:text-gray-300 mb-12 max-w-3xl mx-auto leading-relaxed">
|
||||
Em um mundo onde rótulos são confusos e ingredientes impossíveis de
|
||||
pronunciar, o NutriScan nasceu para devolver o poder de escolha às
|
||||
suas mãos.
|
||||
</p>
|
||||
{/* Editorial headline — Instrument Serif, weight 400, no gradient */}
|
||||
<h1 className="font-display text-display-xl text-[var(--fg-primary)] mb-8 leading-[0.9]">
|
||||
Saber o que{" "}
|
||||
<em className="font-display-i not-italic italic">colocamos</em>
|
||||
<br />
|
||||
no prato é um direito{" "}
|
||||
<em className="font-display-i not-italic italic">de todos.</em>
|
||||
</h1>
|
||||
|
||||
{/* CTA */}
|
||||
<Button
|
||||
onClick={scrollToNext}
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="group hover:bg-gradient-to-r hover:from-blue-600 hover:to-green-600 hover:text-white hover:border-transparent transition-all duration-300"
|
||||
>
|
||||
<span className="mr-2">Conheça nossa história</span>
|
||||
<ChevronDown className="w-5 h-5 group-hover:animate-bounce" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-lg text-[var(--fg-secondary)] leading-relaxed max-w-xl mb-12">
|
||||
Em um mundo onde rótulos são confusos e ingredientes impossíveis
|
||||
de pronunciar, o NutriScan nasceu para devolver o poder de escolha
|
||||
às suas mãos.
|
||||
</p>
|
||||
|
||||
{/* Scroll Indicator */}
|
||||
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2">
|
||||
<div className="animate-bounce">
|
||||
<div className="w-6 h-10 border-2 border-gray-400 dark:border-gray-500 rounded-full flex justify-center">
|
||||
<div className="w-1 h-3 bg-gray-400 dark:bg-gray-500 rounded-full mt-2 animate-pulse"></div>
|
||||
<button
|
||||
onClick={scrollToNext}
|
||||
className="btn-ghost group inline-flex items-center gap-3"
|
||||
>
|
||||
Conheça nossa história
|
||||
<ArrowDown
|
||||
size={15}
|
||||
className="transition-transform duration-300 group-hover:translate-y-1"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Logo mark — editorial, no shadow */}
|
||||
<div className="hidden lg:flex items-start pt-2">
|
||||
<div className="relative">
|
||||
<div className="w-20 h-20 rounded-xl overflow-hidden border border-[var(--border-subtle)]">
|
||||
<Image
|
||||
src="/apple-touch-icon.png"
|
||||
alt="NutriScan"
|
||||
width={80}
|
||||
height={80}
|
||||
className="w-full h-full object-cover"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats row — hairline separated */}
|
||||
<div className="hairline" />
|
||||
<div className="grid grid-cols-3 divide-x divide-[var(--border-subtle)] py-10">
|
||||
{[
|
||||
{ value: "2M+", label: "Alimentos indexados" },
|
||||
{ value: "100+", label: "Países cobertos" },
|
||||
{ value: "ODBL", label: "Licença aberta" },
|
||||
].map((stat) => (
|
||||
<div key={stat.label} className="stat-cell px-8 first:pl-0 last:pr-0">
|
||||
<span className="stat-value">{stat.value}</span>
|
||||
<span className="stat-label">{stat.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="hairline" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,134 +1,98 @@
|
||||
"use client";
|
||||
|
||||
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[] = [
|
||||
const STEPS = [
|
||||
{
|
||||
id: 1,
|
||||
id: "01",
|
||||
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"],
|
||||
description:
|
||||
"Input instantâneo via scanner óptico ou entrada manual do código de barras do produto.",
|
||||
features: ["OCR de alta precisão", "Busca por código EAN", "Detecção automática"],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
id: "02",
|
||||
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"],
|
||||
description:
|
||||
"Cruzamento com o dataset global Open Food Facts em milissegundos. Dados validados por curadoria colaborativa.",
|
||||
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"],
|
||||
id: "03",
|
||||
title: "Veredito Claro",
|
||||
description:
|
||||
"Saída de dados interpretada para uma tomada de decisão consciente, sem jargões técnicos.",
|
||||
features: ["Sugestões saudáveis", "Histórico local", "Comparativo nutricional"],
|
||||
},
|
||||
];
|
||||
|
||||
export function HowItWorksSection() {
|
||||
return (
|
||||
<section className="relative py-32 bg-white dark:bg-[#030712] overflow-hidden transition-colors duration-500">
|
||||
{/* Background Decor: Engenharia de Luz */}
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-blue-500/5 via-transparent to-transparent pointer-events-none" />
|
||||
<section className="bg-base section-py">
|
||||
<div className="container-editorial">
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 relative z-10">
|
||||
<div className="text-center mb-24">
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
className="text-blue-600 font-mono text-sm font-black uppercase tracking-[0.3em]"
|
||||
>
|
||||
Pipeline de Operação
|
||||
</motion.span>
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
className="text-5xl md:text-7xl font-black text-gray-900 dark:text-white mt-4 mb-8 tracking-tighter"
|
||||
>
|
||||
A inteligência por trás do <span className="bg-gradient-to-r from-blue-600 to-green-500 bg-clip-text text-transparent italic">scan</span>.
|
||||
</motion.h2>
|
||||
{/* Section header */}
|
||||
<div className="section-header">
|
||||
<span className="eyebrow">Como Funciona</span>
|
||||
<h2 className="font-display">
|
||||
A inteligência por trás do{" "}
|
||||
<em className="font-display-i italic">scan.</em>
|
||||
</h2>
|
||||
<p>
|
||||
Três etapas simples entre o código de barras e uma escolha informada.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Main Pipeline Flow */}
|
||||
[Image of a modern user interface flow diagram]
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-32 relative">
|
||||
{STEPS.map((step, index) => (
|
||||
<motion.div
|
||||
{/* Step rows — editorial numbered layout */}
|
||||
<div className="divide-y divide-[var(--border-subtle)] border-y border-[var(--border-subtle)] mb-20">
|
||||
{STEPS.map((step) => (
|
||||
<div
|
||||
key={step.id}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
className="group relative p-8 rounded-[2.5rem] bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 hover:border-blue-500/30 transition-all duration-500"
|
||||
className="group grid grid-cols-1 lg:grid-cols-[5rem_1fr_1fr] gap-8 py-12 hover-lift cursor-default"
|
||||
>
|
||||
{/* 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}
|
||||
{/* Step number */}
|
||||
<div className="step-number">{step.id}</div>
|
||||
|
||||
{/* Title + description */}
|
||||
<div>
|
||||
<h3 className="font-display text-display-sm text-[var(--fg-primary)] mb-3">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="text-[var(--fg-secondary)] leading-relaxed">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={cn(
|
||||
"w-16 h-16 rounded-2xl bg-gradient-to-br flex items-center justify-center mb-10 shadow-xl transition-transform group-hover:scale-110 group-hover:rotate-3",
|
||||
step.color
|
||||
)}>
|
||||
<step.icon className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-8 leading-relaxed font-medium">
|
||||
{step.description}
|
||||
</p>
|
||||
|
||||
<ul className="space-y-3 border-t border-gray-200 dark:border-gray-800 pt-6">
|
||||
{/* Features list */}
|
||||
<ul className="space-y-3 lg:pt-1">
|
||||
{step.features.map((f) => (
|
||||
<li key={f} className="flex items-center text-xs font-bold text-gray-500 dark:text-gray-500 uppercase tracking-widest">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-blue-500 mr-3 animate-pulse" />
|
||||
<li
|
||||
key={f}
|
||||
className="flex items-center gap-3 text-sm text-[var(--fg-muted)]"
|
||||
>
|
||||
<span className="w-1.5 h-px bg-[var(--border-strong)] flex-shrink-0" />
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Global Stats Footer: O 80/20 do Valor */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
className="grid grid-cols-2 lg:grid-cols-4 gap-4"
|
||||
>
|
||||
{/* Bottom stat strip */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-px bg-[var(--border-subtle)] border border-[var(--border-subtle)] rounded-lg overflow-hidden">
|
||||
{[
|
||||
{ icon: Globe, label: "Global", text: "2M+ Alimentos" },
|
||||
{ icon: Shield, label: "Seguro", text: "100% Gratuito" },
|
||||
{ icon: Clock, label: "Veloz", text: "< 10 Segundos" },
|
||||
{ icon: Zap, label: "Offline", text: "Cache Inteligente" },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex flex-col items-center p-6 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100 dark:border-gray-800 shadow-sm">
|
||||
<item.icon className="w-6 h-6 text-blue-500 mb-3" />
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">{item.label}</span>
|
||||
<span className="text-sm font-bold text-gray-900 dark:text-white">{item.text}</span>
|
||||
{ val: "2M+", label: "Alimentos globais" },
|
||||
{ val: "100%", label: "Gratuito" },
|
||||
{ val: "< 10s", label: "Por consulta" },
|
||||
{ val: "Offline", label: "Cache inteligente" },
|
||||
].map((item) => (
|
||||
<div key={item.label} className="bg-elevated px-8 py-8 stat-cell items-start">
|
||||
<span className="stat-value">{item.val}</span>
|
||||
<span className="stat-label">{item.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,158 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Globe, Database, Activity, Zap } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Variantes para animação de entrada (Stagger)
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: 0.1 }
|
||||
}
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: { y: 0, opacity: 1 }
|
||||
};
|
||||
|
||||
export function ImpactSection() {
|
||||
return (
|
||||
<section className="py-32 bg-white dark:bg-[#030712] relative overflow-hidden transition-colors duration-500">
|
||||
|
||||
{/* Background Decor - Grid de Engenharia */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:40px_40px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)]" />
|
||||
<section className="bg-base section-py">
|
||||
<div className="container-editorial">
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 relative z-10">
|
||||
|
||||
{/* Header - Autoridade Técnica */}
|
||||
<div className="text-center mb-24">
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
className="text-4xl md:text-6xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter"
|
||||
>
|
||||
Escalabilidade e <span className="bg-gradient-to-r from-blue-600 to-emerald-500 bg-clip-text text-transparent">Impacto Global</span>
|
||||
</motion.h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
|
||||
Métricas de performance da maior rede colaborativa de transparência alimentar do planeta.
|
||||
{/* Section header */}
|
||||
<div className="section-header">
|
||||
<span className="eyebrow">Impacto</span>
|
||||
<h2 className="font-display">
|
||||
Escala global. Escolha individual.
|
||||
</h2>
|
||||
<p>
|
||||
Métricas da maior rede colaborativa de transparência alimentar do planeta.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid - Estilo Bento Box Sênior */}
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-20"
|
||||
>
|
||||
{/* Stats grid — editorial warm cards */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-px bg-[var(--border-subtle)] border border-[var(--border-subtle)] rounded-lg overflow-hidden 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" },
|
||||
{ val: "2.0M+", label: "Alimentos indexados", sub: "Dataset global" },
|
||||
{ val: "100+", label: "Países cobertos", sub: "Rede distribuída" },
|
||||
{ val: "24/7", label: "Disponibilidade", sub: "Alta redundância" },
|
||||
{ val: "218k", label: "Cache local", sub: "Acesso offline" },
|
||||
].map((stat) => (
|
||||
<motion.div
|
||||
key={stat.label}
|
||||
variants={itemVariants}
|
||||
whileHover={{ y: -5 }}
|
||||
className="bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 p-8 rounded-[2rem] shadow-sm hover:shadow-xl transition-all"
|
||||
>
|
||||
<div className={cn("w-12 h-12 rounded-xl flex items-center justify-center mb-6", stat.bg)}>
|
||||
<stat.icon className={cn("w-6 h-6", stat.color)} />
|
||||
</div>
|
||||
<div className={cn("text-3xl font-mono font-bold tracking-tighter mb-1", stat.color)}>
|
||||
<div key={stat.label} className="bg-elevated px-8 py-10 hover-lift group cursor-default">
|
||||
<div className="font-display text-display-md text-[var(--fg-primary)] mb-1 group-hover:text-accent transition-colors duration-300">
|
||||
{stat.val}
|
||||
</div>
|
||||
<div className="text-sm font-black uppercase tracking-widest text-gray-400">
|
||||
{stat.label}
|
||||
</div>
|
||||
</motion.div>
|
||||
<div className="label text-[var(--fg-primary)] mb-1 block">{stat.label}</div>
|
||||
<div className="text-xs text-[var(--fg-faint)]">{stat.sub}</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* World Map - Visualização de Rede de Dados */}
|
||||
[Image of a world map with data points and connections]
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
className="relative bg-slate-950 rounded-[3rem] p-12 overflow-hidden shadow-3xl mb-20"
|
||||
>
|
||||
<div className="absolute inset-0 opacity-20 bg-[url('https://www.transparenttextures.com/patterns/carbon-fibre.png')]" />
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center">
|
||||
<h3 className="text-2xl font-bold text-white mb-2 tracking-tight">Rede Global de Sincronização</h3>
|
||||
<p className="text-slate-500 text-sm mb-12">Monitoramento de tráfego de metadados em tempo real</p>
|
||||
|
||||
<div className="w-full aspect-[21/9] relative bg-blue-500/5 rounded-2xl border border-blue-500/10 flex items-center justify-center">
|
||||
{/* Pulsing Data Points (Engineering nodes) */}
|
||||
{/* Two-column impact breakdown */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Individual impact */}
|
||||
<div className="card-warm p-10">
|
||||
<span className="label text-accent mb-6 block">Protocolo Individual</span>
|
||||
<div className="space-y-6">
|
||||
{[
|
||||
{ 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)} />
|
||||
{
|
||||
label: "Eficiência Cognitiva",
|
||||
text: "Redução de 80% no tempo de análise de rótulos via consulta direta à base estruturada.",
|
||||
},
|
||||
{
|
||||
label: "Soberania de Dados",
|
||||
text: "Usuários com controle total sobre o histórico de consumo local, sem rastreamento.",
|
||||
},
|
||||
{
|
||||
label: "Camada de Segurança",
|
||||
text: "Detecção imediata de alérgenos via cruzamento com base colaborativa validada.",
|
||||
},
|
||||
].map((item) => (
|
||||
<div key={item.label} className="grid grid-cols-[1px_1fr] gap-5 items-start">
|
||||
<div className="w-px self-stretch bg-[var(--accent)] mt-1" />
|
||||
<p className="text-sm text-[var(--fg-secondary)] leading-relaxed">
|
||||
<span className="font-semibold text-[var(--fg-primary)]">{item.label}:</span>{" "}
|
||||
{item.text}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
<Globe className="w-24 h-24 text-blue-500/20 animate-pulse" />
|
||||
<div className="absolute bottom-4 right-6 flex items-center gap-2 text-[10px] font-mono text-blue-400 uppercase tracking-[0.3em]">
|
||||
<span className="w-2 h-2 bg-blue-400 rounded-full animate-pulse" /> Live Node Updates
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Impact Logs - High Density Info */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 font-medium">
|
||||
<ImpactCard
|
||||
title="Individual Protocol"
|
||||
items={[
|
||||
{ label: "Cognitive Efficiency", text: "Redução de 80% no tempo de análise de rótulos via algoritmos de OCR." },
|
||||
{ label: "Data Sovereignty", text: "Usuários com controle total sobre o histórico de consumo local." },
|
||||
{ label: "Safety Layer", text: "Detecção imediata de alérgenos via cruzamento de base de dados." }
|
||||
]}
|
||||
type="blue"
|
||||
/>
|
||||
<ImpactCard
|
||||
title="Collective Governance"
|
||||
items={[
|
||||
{ label: "Market Transparency", text: "Pressão algorítmica por rótulos honestos e dados auditáveis." },
|
||||
{ label: "Crowdsourced Intelligence", text: "Milhares de contribuições diárias validando a integridade da base." },
|
||||
{ label: "Universal Access", text: "Democratização da informação nutricional de alta fidelidade." }
|
||||
]}
|
||||
type="emerald"
|
||||
/>
|
||||
{/* Collective impact */}
|
||||
<div className="card-warm p-10">
|
||||
<span className="label text-accent mb-6 block">Governança Coletiva</span>
|
||||
<div className="space-y-6">
|
||||
{[
|
||||
{
|
||||
label: "Transparência de Mercado",
|
||||
text: "Pressão por rótulos honestos e dados auditáveis — o escrutínio coletivo como regulador.",
|
||||
},
|
||||
{
|
||||
label: "Inteligência Colaborativa",
|
||||
text: "Milhares de contribuições diárias validando a integridade da base de dados.",
|
||||
},
|
||||
{
|
||||
label: "Acesso Universal",
|
||||
text: "Democratização da informação nutricional de alta fidelidade, sem paywalls.",
|
||||
},
|
||||
].map((item) => (
|
||||
<div key={item.label} className="grid grid-cols-[1px_1fr] gap-5 items-start">
|
||||
<div className="w-px self-stretch bg-[var(--border-strong)] mt-1" />
|
||||
<p className="text-sm text-[var(--fg-secondary)] leading-relaxed">
|
||||
<span className="font-semibold text-[var(--fg-primary)]">{item.label}:</span>{" "}
|
||||
{item.text}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ImpactCard({ title, items, type }: { title: string, items: {label: string, text: string}[], type: 'blue' | 'emerald' }) {
|
||||
return (
|
||||
<div className={cn(
|
||||
"p-10 rounded-[2.5rem] border transition-all duration-500 hover:shadow-2xl",
|
||||
type === 'blue' ? "bg-blue-50/50 dark:bg-blue-900/10 border-blue-100 dark:border-blue-900/20" : "bg-emerald-50/50 dark:bg-emerald-900/10 border-emerald-100 dark:border-emerald-900/20"
|
||||
)}>
|
||||
<h3 className="text-xl font-black mb-8 tracking-tighter uppercase">{title}</h3>
|
||||
<div className="space-y-6">
|
||||
{items.map((item, i) => (
|
||||
<div key={i} className="flex gap-4">
|
||||
<div className={cn("w-1.5 h-1.5 rounded-full mt-2 shrink-0", type === 'blue' ? "bg-blue-500" : "bg-emerald-500")} />
|
||||
<p className="text-sm leading-relaxed text-gray-700 dark:text-gray-300">
|
||||
<span className="font-bold text-gray-900 dark:text-white">{item.label}:</span> {item.text}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,139 +1,110 @@
|
||||
"use client";
|
||||
|
||||
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";
|
||||
import Image from "next/image";
|
||||
|
||||
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",
|
||||
description:
|
||||
"Governança de dados aberta. O que você consome é rastreado sem filtros ou interesses comerciais ocultos.",
|
||||
tag: "Open Data",
|
||||
},
|
||||
{
|
||||
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",
|
||||
description:
|
||||
"Autonomia decisória baseada em evidências. Devolvemos o controle da saúde ao cidadão.",
|
||||
tag: "Citizen First",
|
||||
},
|
||||
{
|
||||
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",
|
||||
description:
|
||||
"Ecossistema colaborativo global. A inteligência coletiva protegendo a saúde de milhões.",
|
||||
tag: "Collaborative",
|
||||
},
|
||||
];
|
||||
|
||||
export function MissionSection() {
|
||||
return (
|
||||
<section className="py-32 bg-white dark:bg-[#030712] relative overflow-hidden transition-colors duration-500">
|
||||
{/* Background Decor: Mesh Gradient sutil */}
|
||||
<div className="absolute top-0 left-1/4 w-1/2 h-1/2 bg-blue-500/5 blur-[120px] rounded-full pointer-events-none" />
|
||||
<section className="bg-surface section-py">
|
||||
<div className="container-editorial">
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 relative z-10">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-24"
|
||||
>
|
||||
<h2 className="text-4xl md:text-6xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter">
|
||||
Governança de <span className="bg-gradient-to-r from-blue-600 to-emerald-500 bg-clip-text text-transparent">Dados Alimentares</span>
|
||||
{/* Section header */}
|
||||
<div className="section-header">
|
||||
<span className="eyebrow">Missão</span>
|
||||
<h2 className="font-display">
|
||||
Engenharia aplicada à transparência nutricional.
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
|
||||
Engenharia de software aplicada à transparência nutricional. Nossa missão é converter caos informacional em escolhas seguras.
|
||||
<p>
|
||||
Nossa missão é converter caos informacional em escolhas seguras — para cada pessoa, em cada supermercado.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Values: Bento-style Cards */}
|
||||
{/* Values — 3-column warm cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-20">
|
||||
{MISSION_VALUES.map((value, i) => (
|
||||
<motion.div
|
||||
key={value.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
className="group p-8 rounded-[2rem] bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 hover:border-blue-500/30 transition-all duration-500"
|
||||
>
|
||||
<div className={cn("w-14 h-14 rounded-2xl flex items-center justify-center mb-8 transition-transform group-hover:scale-110", value.bg)}>
|
||||
<value.icon className={cn("w-7 h-7", value.color)} />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
|
||||
{MISSION_VALUES.map((value) => (
|
||||
<div key={value.title} className="card-warm hover-lift p-8">
|
||||
<span className="label text-accent mb-4 block">{value.tag}</span>
|
||||
<h3 className="font-display text-display-sm text-[var(--fg-primary)] mb-4">
|
||||
{value.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed text-sm font-medium">
|
||||
<p className="text-sm text-[var(--fg-secondary)] leading-relaxed">
|
||||
{value.description}
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Data Source Dashboard Highlight */}
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
className="relative rounded-[3rem] bg-slate-950 p-10 md:p-16 overflow-hidden shadow-3xl"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-full h-full bg-[radial-gradient(circle_at_top_right,_var(--tw-gradient-stops))] from-blue-500/20 via-transparent to-transparent" />
|
||||
|
||||
<div className="relative z-10 flex flex-col lg:flex-row gap-12 items-center">
|
||||
<div className="flex-1 space-y-8">
|
||||
<div className="inline-flex items-center px-4 py-2 rounded-full bg-blue-500/10 border border-blue-500/20 text-blue-400 text-xs font-black uppercase tracking-widest">
|
||||
<Database className="w-3 h-3 mr-2" /> Infraestrutura Global
|
||||
</div>
|
||||
<h3 className="text-3xl md:text-4xl font-black text-white tracking-tighter">
|
||||
Integrado ao Open Food Facts API
|
||||
{/* Open Food Facts callout — accent-bordered panel */}
|
||||
<div className="border border-[var(--border-subtle)] rounded-lg overflow-hidden bg-elevated">
|
||||
<div className="border-l-2 border-[var(--accent)] grid grid-cols-1 lg:grid-cols-[1fr_auto] gap-12 p-10 lg:p-14 items-center">
|
||||
<div>
|
||||
<span className="label text-accent mb-5 block">Infraestrutura de Dados</span>
|
||||
<h3 className="font-display text-display-md text-[var(--fg-primary)] mb-5">
|
||||
Integrado ao Open Food Facts
|
||||
</h3>
|
||||
<p className="text-slate-400 text-lg leading-relaxed">
|
||||
Nossa engine de busca opera sobre o maior dataset colaborativo do planeta, garantindo que cada scan seja validado por uma rede global de curadoria.
|
||||
<p className="text-[var(--fg-secondary)] leading-relaxed max-w-lg mb-8">
|
||||
Nossa engine opera sobre o maior dataset colaborativo de alimentos do planeta,
|
||||
garantindo que cada consulta seja validada por uma rede global de curadoria aberta.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-6">
|
||||
{[
|
||||
{ label: "Produtos", val: "2M+", icon: Activity },
|
||||
{ label: "Países", val: "100+", icon: Map },
|
||||
{ label: "Uptime", val: "24/7", icon: Activity },
|
||||
{ label: "Licença", val: "ODBL", icon: Shield },
|
||||
].map((stat) => (
|
||||
<div key={stat.label} className="space-y-1">
|
||||
<div className="text-2xl font-mono font-bold text-white tracking-tighter">{stat.val}</div>
|
||||
<div className="text-[10px] uppercase font-black text-slate-500 tracking-widest flex items-center gap-1">
|
||||
<stat.icon className="w-3 h-3" /> {stat.label}
|
||||
</div>
|
||||
{ val: "2M+", label: "Produtos" },
|
||||
{ val: "100+", label: "Países" },
|
||||
{ val: "24/7", label: "Uptime" },
|
||||
{ val: "ODBL", label: "Licença" },
|
||||
].map((s) => (
|
||||
<div key={s.label} className="stat-cell items-start">
|
||||
<span className="stat-value">{s.val}</span>
|
||||
<span className="stat-label">{s.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative w-64 h-64 flex items-center justify-center">
|
||||
{/* Efeito Visual de Radar/Sincronização */}
|
||||
<div className="absolute inset-0 border border-blue-500/20 rounded-full animate-ping" />
|
||||
<div className="absolute inset-4 border border-blue-500/10 rounded-full animate-pulse" />
|
||||
<div className="w-32 h-32 bg-blue-600 rounded-3xl flex items-center justify-center shadow-2xl shadow-blue-500/50 rotate-12 group">
|
||||
<Globe className="text-white w-16 h-16" />
|
||||
</div>
|
||||
|
||||
{/* Open Food Facts logo emblem */}
|
||||
<div className="hidden lg:flex items-center justify-center w-40 h-40 rounded-full border border-[var(--border-subtle)] bg-white flex-shrink-0 p-4 shadow-sm">
|
||||
<Image
|
||||
src="/images/CMJN-ICON_WHITE_BG_OFF.svg"
|
||||
alt="Open Food Facts"
|
||||
width={96}
|
||||
height={96}
|
||||
className="h-24 w-auto object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Footer Quote: Minimalista */}
|
||||
<div className="mt-24 text-center">
|
||||
<p className="text-gray-400 dark:text-gray-600 font-mono text-[10px] uppercase tracking-[0.5em] mb-4">
|
||||
NutriScan Open Data Philosophy
|
||||
</p>
|
||||
<p className="text-xl md:text-2xl font-serif italic text-gray-800 dark:text-gray-200">
|
||||
“Dados abertos são a fundação de uma sociedade saudável.”
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Editorial pull-quote */}
|
||||
<div className="mt-20 text-center">
|
||||
<div className="hairline mb-10" />
|
||||
<p className="label text-[var(--fg-faint)] mb-4">NutriScan Open Data Philosophy</p>
|
||||
<blockquote className="font-display-i text-display-md text-[var(--fg-primary)] italic max-w-2xl mx-auto">
|
||||
“Dados abertos são a fundação de uma sociedade saudável.”
|
||||
</blockquote>
|
||||
<div className="hairline mt-10" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,104 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { ShoppingCart, AlertTriangle, Clock, Users } from "lucide-react";
|
||||
|
||||
export function ProblemSection() {
|
||||
const problems = [
|
||||
{
|
||||
num: "01",
|
||||
title: "Rótulos Confusos",
|
||||
body: "Tabelas nutricionais complexas e listas de ingredientes intermináveis projetadas para desorientar.",
|
||||
},
|
||||
{
|
||||
num: "02",
|
||||
title: "Alérgenos Escondidos",
|
||||
body: "Substâncias alergênicas disfarçadas em nomenclaturas técnicas inacessíveis ao consumidor comum.",
|
||||
},
|
||||
{
|
||||
num: "03",
|
||||
title: "Tempo Perdido",
|
||||
body: "Minutos preciosos gastos tentando decifrar o que realmente compõe cada embalagem.",
|
||||
},
|
||||
{
|
||||
num: "04",
|
||||
title: "Desinformação",
|
||||
body: "Marketing enganoso e informações inconsistentes que obscurecem a verdade nutricional.",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section id="problema" className="py-20 bg-gray-50 dark:bg-gray-800">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-6">
|
||||
Você já se sentiu <span className="text-red-500">perdido</span>{" "}
|
||||
entre ingredientes impossíveis de pronunciar?
|
||||
<section id="problema" className="bg-surface section-py">
|
||||
<div className="container-editorial">
|
||||
|
||||
{/* Section header */}
|
||||
<div className="section-header">
|
||||
<span className="eyebrow">O Problema</span>
|
||||
<h2 className="font-display">
|
||||
Você já se sentiu perdido entre ingredientes impossíveis de pronunciar?
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
Não está sozinh{"(a)"}. Milhares de pessoas enfrentam essa mesma
|
||||
dificuldade todos os dias.
|
||||
<p>
|
||||
Não está sozinho. Milhares de pessoas enfrentam essa mesma dificuldade todos os dias.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Problem Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-16">
|
||||
<div className="bg-white dark:bg-gray-900 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-shadow">
|
||||
<div className="w-12 h-12 bg-red-100 dark:bg-red-900/30 rounded-lg flex items-center justify-center mb-4">
|
||||
<ShoppingCart className="w-6 h-6 text-red-600 dark:text-red-400" />
|
||||
{/* Problem list — editorial numbered rows */}
|
||||
<div className="divide-y divide-[var(--border-subtle)] border-y border-[var(--border-subtle)]">
|
||||
{problems.map((p) => (
|
||||
<div
|
||||
key={p.num}
|
||||
className="group grid grid-cols-[3rem_1fr_1fr] gap-8 py-8 hover-lift cursor-default"
|
||||
>
|
||||
<span className="step-number leading-none">{p.num}</span>
|
||||
<h3 className="font-display text-display-sm text-[var(--fg-primary)] self-center">
|
||||
{p.title}
|
||||
</h3>
|
||||
<p className="text-[var(--fg-secondary)] leading-relaxed self-center text-sm">
|
||||
{p.body}
|
||||
</p>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Rótulos Confusos
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Tabelas nutricionais complexas e listas de ingredientes
|
||||
intermináveis
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<AlertTriangle 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-2">
|
||||
Alérgenos Escondidos
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Substâncias alergênicas em nomes técnicos que ninguém entende
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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-yellow-100 dark:bg-yellow-900/30 rounded-lg flex items-center justify-center mb-4">
|
||||
<Clock className="w-6 h-6 text-yellow-600 dark:text-yellow-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Tempo Perdido
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Minutos preciosos tentando decifrar o que realmente está na
|
||||
embalagem
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Users 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-2">
|
||||
Desinformação
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Informações inconsistentes e marketing enganoso nos produtos
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Statistics */}
|
||||
<div className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-lg">
|
||||
<div className="text-center mb-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
O problema é real e afeta milhões
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div className="text-center">
|
||||
<div className="text-4xl font-bold text-red-500 mb-2">73%</div>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
das pessoas têm dificuldade para entender rótulos nutricionais
|
||||
</p>
|
||||
{/* Statistics — hairline grid */}
|
||||
<div className="mt-20 grid grid-cols-1 md:grid-cols-3 divide-y md:divide-y-0 md:divide-x divide-[var(--border-subtle)] border border-[var(--border-subtle)] rounded-lg overflow-hidden bg-elevated">
|
||||
{[
|
||||
{ value: "73%", label: "Dificuldade com rótulos", desc: "das pessoas não conseguem interpretar tabelas nutricionais" },
|
||||
{ value: "85%", label: "Alérgenos invisíveis", desc: "não identificam alérgenos escondidos em nomes técnicos" },
|
||||
{ value: "3 min", label: "Tempo médio perdido", desc: "por produto tentando compreender uma embalagem" },
|
||||
].map((s) => (
|
||||
<div key={s.label} className="px-10 py-10">
|
||||
<div className="font-display text-display-lg text-[var(--fg-primary)] mb-2">
|
||||
{s.value}
|
||||
</div>
|
||||
<div className="label text-accent mb-3 block">{s.label}</div>
|
||||
<p className="text-sm text-[var(--fg-muted)] leading-relaxed">{s.desc}</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-4xl font-bold text-orange-500 mb-2">85%</div>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
não conseguem identificar alérgenos em nomes técnicos
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-4xl font-bold text-blue-500 mb-2">3min</div>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
tempo médio gasto tentando entender um produto
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,164 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import React, { useRef } from "react";
|
||||
import { motion, useScroll, useSpring } from "framer-motion";
|
||||
import { Calendar, Lightbulb, Heart, Target } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// 1. Definição da Interface de Dados
|
||||
interface StoryStep {
|
||||
year: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ElementType;
|
||||
color: string;
|
||||
side: "left" | "right";
|
||||
}
|
||||
|
||||
const STORY_DATA: StoryStep[] = [
|
||||
{
|
||||
year: "2024",
|
||||
title: "A Pergunta Simples",
|
||||
description: "O que realmente tem nesse produto? Decifrar tabelas nutricionais era um desafio individual que percebemos ser global.",
|
||||
icon: Calendar,
|
||||
color: "blue",
|
||||
side: "left",
|
||||
description:
|
||||
"O que realmente tem nesse produto? Decifrar tabelas nutricionais era um desafio individual que percebemos ser global.",
|
||||
},
|
||||
{
|
||||
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",
|
||||
description:
|
||||
"Milhares de pessoas buscam transparência alimentar. A necessidade de dados abertos tornou-se uma urgência coletiva.",
|
||||
},
|
||||
{
|
||||
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",
|
||||
description:
|
||||
"Uma ponte entre o código de barras e a verdade nutricional. Engenharia a serviço de uma escolha consciente.",
|
||||
},
|
||||
{
|
||||
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",
|
||||
description:
|
||||
"Democratizar a informação. Conectar cidadãos à maior base de dados colaborativa de alimentos do planeta.",
|
||||
},
|
||||
];
|
||||
|
||||
export function StorySection() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 2. Lógica de Scroll Progressivo (Engenharia de Movimento)
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: containerRef,
|
||||
offset: ["start end", "end end"],
|
||||
});
|
||||
|
||||
const scaleY = useSpring(scrollYProgress, {
|
||||
stiffness: 100,
|
||||
damping: 30,
|
||||
restDelta: 0.001
|
||||
});
|
||||
|
||||
return (
|
||||
<section ref={containerRef} className="py-32 bg-white dark:bg-[#030712] relative overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
|
||||
{/* Header com Animação de Entrada */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
className="text-center mb-32"
|
||||
>
|
||||
<h2 className="text-4xl md:text-6xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter">
|
||||
Nossa <span className="bg-gradient-to-r from-blue-600 to-green-500 bg-clip-text text-transparent">história</span>
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
|
||||
<section className="bg-base section-py">
|
||||
<div className="container-editorial">
|
||||
|
||||
{/* Section header */}
|
||||
<div className="section-header">
|
||||
<span className="eyebrow">Nossa História</span>
|
||||
<h2 className="font-display">
|
||||
De uma dúvida individual a uma plataforma de transparência global.
|
||||
</p>
|
||||
</motion.div>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Timeline — clean editorial list with hairlines */}
|
||||
<div className="relative">
|
||||
{/* 3. A Linha do Tempo Reativa */}
|
||||
<div className="absolute left-1/2 transform -translate-x-1/2 w-[2px] h-full bg-gray-100 dark:bg-gray-800 hidden md:block">
|
||||
<motion.div
|
||||
style={{ scaleY }}
|
||||
className="absolute top-0 w-full h-full bg-gradient-to-b from-blue-500 via-green-500 to-orange-500 origin-top"
|
||||
/>
|
||||
</div>
|
||||
{/* Vertical accent line */}
|
||||
<div className="absolute left-[5.75rem] top-0 bottom-0 w-px bg-[var(--border-subtle)] hidden md:block" />
|
||||
|
||||
<div className="space-y-24 md:space-y-32">
|
||||
<div className="space-y-0">
|
||||
{STORY_DATA.map((step, index) => (
|
||||
<TimelineItem key={index} step={step} index={index} />
|
||||
<div
|
||||
key={index}
|
||||
className="group relative grid grid-cols-1 md:grid-cols-[6rem_1fr] gap-0 border-b border-[var(--border-subtle)] last:border-b-0 py-10 hover-lift cursor-default"
|
||||
>
|
||||
{/* Year label */}
|
||||
<div className="md:pr-8 mb-4 md:mb-0 flex md:flex-col items-center md:items-start md:pt-1">
|
||||
<span className="label text-accent">{step.year}</span>
|
||||
{/* Timeline dot */}
|
||||
<div className="hidden md:block mt-4 ml-px w-2 h-2 rounded-full bg-[var(--border-strong)] group-hover:bg-[var(--accent)] transition-colors duration-300" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="md:pl-12">
|
||||
<h3 className="font-display text-display-sm text-[var(--fg-primary)] mb-3">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="text-[var(--fg-secondary)] leading-relaxed max-w-xl">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Sub-componente com Encapsulamento de Animação
|
||||
function TimelineItem({ step }: { step: StoryStep; index: number }) {
|
||||
const isLeft = step.side === "left";
|
||||
const colorMap = {
|
||||
blue: "bg-blue-500 text-blue-500 border-blue-200 dark:border-blue-900/30",
|
||||
green: "bg-green-500 text-green-500 border-green-200 dark:border-green-900/30",
|
||||
purple: "bg-purple-500 text-purple-500 border-purple-200 dark:border-purple-900/30",
|
||||
orange: "bg-orange-500 text-orange-500 border-orange-200 dark:border-orange-900/30",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
"relative flex flex-col md:flex-row items-center justify-between w-full",
|
||||
isLeft ? "md:flex-row" : "md:flex-row-reverse"
|
||||
)}>
|
||||
{/* Conteúdo */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: isLeft ? -50 : 50 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
className="w-full md:w-[45%] group"
|
||||
>
|
||||
<div className={cn(
|
||||
"p-8 rounded-[2rem] bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 transition-all duration-500",
|
||||
"hover:shadow-2xl hover:border-transparent group-hover:bg-white dark:group-hover:bg-gray-900"
|
||||
)}>
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className={cn("w-12 h-12 rounded-xl flex items-center justify-center text-white shadow-lg", colorMap[step.color as keyof typeof colorMap].split(' ')[0])}>
|
||||
<step.icon size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<span className={cn("text-xs font-black uppercase tracking-widest", colorMap[step.color as keyof typeof colorMap].split(' ')[1])}>
|
||||
{step.year}
|
||||
</span>
|
||||
<h3 className="text-xl font-bold text-gray-900 dark:text-white leading-tight">
|
||||
{step.title}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Ponto Central com Efeito de Pulso */}
|
||||
<div className="absolute left-1/2 transform -translate-x-1/2 w-4 h-4 z-20 hidden md:block">
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
whileInView={{ scale: 1 }}
|
||||
className={cn("w-full h-full rounded-full ring-4 ring-white dark:ring-[#030712] shadow-sm", colorMap[step.color as keyof typeof colorMap].split(' ')[0])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="hidden md:block w-[45%]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,158 +1,113 @@
|
||||
"use client";
|
||||
|
||||
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",
|
||||
tag: "Segurança Familiar",
|
||||
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"]
|
||||
description:
|
||||
"Decifre rótulos complexos e garanta que o lanche escolar seja livre de ingredientes ultraprocessados ocultos.",
|
||||
features: ["Identificação de alérgenos", "Nutri-Score infantil", "Educação nutricional"],
|
||||
},
|
||||
{
|
||||
id: "athletes",
|
||||
tag: "Performance",
|
||||
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"]
|
||||
description:
|
||||
"Otimize sua composição corporal monitorando densidade calórica e a qualidade dos macronutrientes.",
|
||||
features: ["Monitoramento de macros", "Biodisponibilidade", "Zero açúcares ocultos"],
|
||||
},
|
||||
{
|
||||
id: "allergic",
|
||||
tag: "Risco Zero",
|
||||
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"]
|
||||
description:
|
||||
"Alertas para contaminação cruzada e componentes críticos. Segurança total na palma da mão.",
|
||||
features: ["Glúten & lactose tracker", "Traços de alérgenos", "Scan de segurança"],
|
||||
},
|
||||
{
|
||||
id: "conscious",
|
||||
tag: "Decisão Crítica",
|
||||
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"]
|
||||
}
|
||||
description:
|
||||
"Para quem questiona a indústria. Acesse a lista real de ingredientes e a origem dos dados de forma aberta.",
|
||||
features: ["Análise de aditivos", "Comparação de produtos", "Transparência ODBL"],
|
||||
},
|
||||
];
|
||||
|
||||
export function TargetAudienceSection() {
|
||||
return (
|
||||
<section className="py-32 bg-white dark:bg-[#030712] relative overflow-hidden transition-colors duration-500">
|
||||
<div className="max-w-7xl mx-auto px-6 relative z-10">
|
||||
|
||||
{/* Header com Motion Stagger */}
|
||||
<div className="text-center mb-24">
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-4xl md:text-6xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter"
|
||||
>
|
||||
Projetado para <span className="bg-gradient-to-r from-blue-600 to-emerald-500 bg-clip-text text-transparent">Cidadãos Conscientes</span>
|
||||
</motion.h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
|
||||
Se a transparência é um requisito para sua mesa, o NutriScan é sua ferramenta de governança alimentar.
|
||||
<section className="bg-surface section-py">
|
||||
<div className="container-editorial">
|
||||
|
||||
{/* Section header */}
|
||||
<div className="section-header">
|
||||
<span className="eyebrow">Para Quem</span>
|
||||
<h2 className="font-display">
|
||||
Projetado para cidadãos que exigem transparência.
|
||||
</h2>
|
||||
<p>
|
||||
Se a clareza é um requisito para sua mesa, o NutriScan é a 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
|
||||
{/* Persona grid — clean, warm cards with numbered list features */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-px bg-[var(--border-subtle)] border border-[var(--border-subtle)] rounded-lg overflow-hidden mb-20">
|
||||
{PERSONAS.map((persona) => (
|
||||
<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"
|
||||
)}
|
||||
className="bg-elevated p-8 hover-lift group cursor-default flex flex-col"
|
||||
>
|
||||
<div className={cn("w-14 h-14 rounded-2xl flex items-center justify-center mb-8 transition-transform group-hover:scale-110", persona.bg)}>
|
||||
<persona.icon className={cn("w-7 h-7", persona.color)} />
|
||||
</div>
|
||||
|
||||
<span className={cn("text-[10px] font-black uppercase tracking-[0.2em] mb-2 block", persona.color)}>
|
||||
{persona.tagline}
|
||||
</span>
|
||||
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
|
||||
<span className="label text-accent mb-4 block">{persona.tag}</span>
|
||||
<h3 className="font-display text-display-sm text-[var(--fg-primary)] mb-4">
|
||||
{persona.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed mb-8">
|
||||
<p className="text-sm text-[var(--fg-secondary)] leading-relaxed mb-8 flex-1">
|
||||
{persona.description}
|
||||
</p>
|
||||
|
||||
<ul className="space-y-3">
|
||||
{persona.features.map(f => (
|
||||
<li key={f} className="flex items-center gap-2 text-xs font-bold text-gray-500 dark:text-gray-500">
|
||||
<div className={cn("w-1 h-1 rounded-full", persona.bg.replace('/10', ''))} />
|
||||
<ul className="space-y-2 border-t border-[var(--border-subtle)] pt-6">
|
||||
{persona.features.map((f) => (
|
||||
<li
|
||||
key={f}
|
||||
className="flex items-center gap-3 text-xs text-[var(--fg-muted)]"
|
||||
>
|
||||
<span className="w-1 h-1 rounded-full bg-[var(--accent)] flex-shrink-0" />
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* High-Impact Callout: Glassmorphism Card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
className="relative rounded-[3rem] bg-gradient-to-br from-blue-600 to-emerald-600 p-1 md:p-px shadow-3xl"
|
||||
>
|
||||
<div className="bg-white/5 dark:bg-black/20 backdrop-blur-3xl rounded-[2.9rem] p-10 md:p-16 text-center text-white">
|
||||
<h3 className="text-3xl md:text-5xl font-black mb-6 tracking-tighter">
|
||||
A informação alimentar é um <span className="text-yellow-400">Direito Universal</span>
|
||||
</h3>
|
||||
<p className="text-blue-50 text-lg md:text-xl mb-12 max-w-4xl mx-auto leading-relaxed opacity-90">
|
||||
Se você busca soberania sobre suas escolhas, o NutriScan é o seu protocolo de acesso à verdade. Sem interesses corporativos, apenas dados puros e auditáveis.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-6">
|
||||
{[
|
||||
{ icon: Zap, label: "Baixa Latência", text: "Dados em < 2s" },
|
||||
{ icon: Heart, label: "Auditável", text: "Open Source" },
|
||||
{ icon: ShieldCheck, label: "Privacidade", text: "No Tracking" },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex flex-col items-center gap-2 bg-white/10 px-8 py-6 rounded-2xl border border-white/10 backdrop-blur-md">
|
||||
<item.icon className="w-6 h-6 text-yellow-400" />
|
||||
<span className="text-xs font-black uppercase tracking-widest">{item.label}</span>
|
||||
<span className="text-[10px] text-blue-100 opacity-70 uppercase">{item.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="mt-16 inline-flex items-center gap-3 px-10 py-5 bg-white text-blue-600 rounded-2xl font-black uppercase tracking-widest text-sm shadow-xl shadow-black/10 transition-all hover:bg-yellow-400 hover:text-black"
|
||||
>
|
||||
Iniciar Scan Agora <ArrowRight size={18} />
|
||||
</motion.button>
|
||||
{/* Callout banner — warm, accent-bordered, no color blobs */}
|
||||
<div className="card-accent-top card-warm p-10 lg:p-16 text-center">
|
||||
<span className="label text-accent mb-5 block">Princípio Fundamental</span>
|
||||
<h3 className="font-display text-display-md text-[var(--fg-primary)] mb-6 max-w-2xl mx-auto">
|
||||
A informação alimentar é um direito universal.
|
||||
</h3>
|
||||
<p className="text-[var(--fg-secondary)] leading-relaxed max-w-2xl mx-auto mb-10">
|
||||
Sem interesses corporativos, apenas dados puros e auditáveis. O NutriScan é
|
||||
o protocolo de acesso à verdade sobre o que você consome.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-6">
|
||||
{[
|
||||
{ label: "Baixa Latência", sub: "Dados em < 2s" },
|
||||
{ label: "Auditável", sub: "Open Source" },
|
||||
{ label: "Privacidade", sub: "Zero tracking" },
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.label}
|
||||
className="flex flex-col items-center gap-1 px-8 py-5 border border-[var(--border-subtle)] rounded bg-[var(--bg-surface)]"
|
||||
>
|
||||
<span className="label text-[var(--fg-primary)]">{item.label}</span>
|
||||
<span className="text-xs text-[var(--fg-muted)]">{item.sub}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,173 +1,129 @@
|
||||
"use client";
|
||||
|
||||
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 = [
|
||||
{
|
||||
tag: "Open Data",
|
||||
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"
|
||||
desc: "Acesso irrestrito a metadados alimentares sob licença ODbL. Nenhum dado fica preso atrás de paywall.",
|
||||
},
|
||||
{
|
||||
tag: "Non-Profit",
|
||||
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"
|
||||
desc: "Arquitetura sustentada pela comunidade. Livre de assinaturas, anúncios ou interesses comerciais.",
|
||||
},
|
||||
{
|
||||
tag: "Privacy First",
|
||||
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"
|
||||
desc: "Zero rastreamento. Coleta mínima de dados conforme LGPD/GDPR. Sua navegação é sua.",
|
||||
},
|
||||
{
|
||||
tag: "Accuracy",
|
||||
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"
|
||||
}
|
||||
desc: "Algoritmos de validação cruzada para garantir a integridade de cada registro consultado.",
|
||||
},
|
||||
];
|
||||
|
||||
export function ValuesSection() {
|
||||
return (
|
||||
<section className="py-32 bg-white dark:bg-[#030712] relative overflow-hidden transition-colors duration-500">
|
||||
|
||||
{/* Background Decor - Grid de Engenharia sutil */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808008_1px,transparent_1px),linear-gradient(to_bottom,#80808008_1px,transparent_1px)] bg-[size:32px_32px]" />
|
||||
<section className="bg-surface section-py">
|
||||
<div className="container-editorial">
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 relative z-10">
|
||||
|
||||
{/* Header - Tom de Autoridade */}
|
||||
<div className="text-center mb-24">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-blue-500/5 border border-blue-500/20 text-blue-600 dark:text-blue-400 text-[10px] font-black uppercase tracking-[0.3em] mb-6"
|
||||
>
|
||||
<Scale size={12} /> Compliance & Ethics
|
||||
</motion.div>
|
||||
<h2 className="text-4xl md:text-6xl font-black text-gray-900 dark:text-white mb-6 tracking-tighter">
|
||||
Protocolos de <span className="bg-gradient-to-r from-blue-600 to-emerald-500 bg-clip-text text-transparent">Integridade</span>
|
||||
{/* Section header */}
|
||||
<div className="section-header">
|
||||
<span className="eyebrow">Valores</span>
|
||||
<h2 className="font-display">
|
||||
Protocolos de integridade.
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto font-medium">
|
||||
Nossa governança é pautada pela transparência radical. Não maquiamos dados; entregamos a verdade técnica.
|
||||
<p>
|
||||
Nossa governança é pautada pela transparência radical. Não maquiamos dados — entregamos a verdade técnica.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Values Grid - Bento Box Style */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-20">
|
||||
{PROTOCOLS.map((item, i) => (
|
||||
<motion.div
|
||||
{/* Values grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-px bg-[var(--border-subtle)] border border-[var(--border-subtle)] rounded-lg overflow-hidden mb-20">
|
||||
{PROTOCOLS.map((item) => (
|
||||
<div
|
||||
key={item.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
className="group p-8 rounded-[2.5rem] bg-gray-50 dark:bg-gray-900/40 border border-gray-100 dark:border-gray-800 hover:border-blue-500/30 transition-all duration-500"
|
||||
className="bg-elevated p-8 hover-lift group cursor-default flex flex-col"
|
||||
>
|
||||
<div className={cn("w-14 h-14 rounded-2xl flex items-center justify-center mb-8 transition-transform group-hover:scale-110", item.bg)}>
|
||||
<item.icon className={cn("w-7 h-7", item.color)} />
|
||||
</div>
|
||||
<span className={cn("text-[10px] font-mono font-bold uppercase tracking-widest mb-2 block", item.color)}>
|
||||
{item.tag}
|
||||
</span>
|
||||
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4 tracking-tight">
|
||||
<span className="label text-accent mb-4 block">{item.tag}</span>
|
||||
<h3 className="font-display text-display-sm text-[var(--fg-primary)] mb-4">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 leading-relaxed font-medium mb-6">
|
||||
<p className="text-sm text-[var(--fg-secondary)] leading-relaxed flex-1">
|
||||
{item.desc}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 text-[10px] font-black text-gray-400 uppercase tracking-tighter group-hover:text-blue-500 transition-colors">
|
||||
<CheckCircle2 size={12} className="text-emerald-500" /> System Verified
|
||||
<div className="mt-8 pt-6 border-t border-[var(--border-subtle)] flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[var(--accent)]" />
|
||||
<span className="text-xs text-[var(--fg-faint)] uppercase tracking-widest font-medium">
|
||||
Verified
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* OSS Deep Dive - O Diferencial Sênior */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
className="relative rounded-[3rem] bg-slate-950 border border-slate-800 p-10 md:p-16 overflow-hidden shadow-3xl"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-full h-full bg-[radial-gradient(circle_at_top_right,#1e293b,transparent)] opacity-50" />
|
||||
|
||||
<div className="relative z-10 flex flex-col lg:flex-row gap-12 items-center">
|
||||
<div className="flex-1 space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 bg-white/10 rounded-2xl backdrop-blur-md">
|
||||
<Github className="text-white w-8 h-8" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-3xl font-black text-white tracking-tighter">Open Source Manifesto</h3>
|
||||
<p className="text-blue-400 font-mono text-xs uppercase tracking-widest mt-1">Status: Fully Auditability</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-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.
|
||||
{/* Open Source manifest — dark panel with warm contrast */}
|
||||
<div className="bg-[var(--fg-primary)] rounded-lg overflow-hidden">
|
||||
<div className="border-l-2 border-[var(--accent)] grid grid-cols-1 lg:grid-cols-[1fr_auto] gap-12 p-10 lg:p-14 items-center">
|
||||
<div>
|
||||
<span className="label text-[var(--accent)] mb-5 block">Open Source Manifesto</span>
|
||||
<h3 className="font-display text-display-md text-[var(--bg-base)] mb-5">
|
||||
Algoritmos que impactam a saúde pública pertencem ao domínio público.
|
||||
</h3>
|
||||
<p className="text-[var(--fg-muted)] leading-relaxed max-w-lg mb-8 text-sm">
|
||||
Nosso código é aberto para inspeção, garantindo que não existam backdoors
|
||||
ou manipulação de dados comerciais. A auditabilidade é o nosso nível mais alto de confiança.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{[
|
||||
{ label: "Core Logic", val: "MIT Licensed", icon: Terminal },
|
||||
{ label: "Dataset", val: "ODbL Support", icon: Database },
|
||||
{ label: "Auth", val: "Privacy-First", icon: Fingerprint },
|
||||
{ label: "Core Logic", val: "MIT Licensed" },
|
||||
{ label: "Dataset", val: "ODbL Support" },
|
||||
{ label: "Auth", val: "Privacy-First" },
|
||||
].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
|
||||
key={spec.label}
|
||||
className="p-4 border border-[var(--border-subtle)]/20 rounded"
|
||||
>
|
||||
<div className="label text-[var(--fg-faint)] mb-1 block">{spec.label}</div>
|
||||
<div className="text-sm font-medium text-[var(--bg-base)]">{spec.val}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GitHub Stats Mockup / Placeholder visual */}
|
||||
<div className="w-full lg:w-72 h-48 bg-slate-900/50 rounded-3xl border border-slate-800 p-6 font-mono text-[10px] space-y-3 relative">
|
||||
<div className="flex justify-between text-slate-500 border-b border-slate-800 pb-2">
|
||||
<span>nutriscan-core / main</span>
|
||||
<span className="text-emerald-500">online</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-blue-400">$ npm run audit</p>
|
||||
<p className="text-slate-400"> Scanning dependencies...</p>
|
||||
<p className="text-emerald-500"> 0 vulnerabilities found</p>
|
||||
<p className="text-slate-400"> Data Integrity: 100% Verified</p>
|
||||
</div>
|
||||
<div className="absolute bottom-4 right-6 animate-pulse">
|
||||
<div className="w-3 h-3 bg-emerald-500 rounded-full shadow-[0_0_10px_rgba(16,185,129,0.5)]" />
|
||||
</div>
|
||||
{/* Terminal mockup */}
|
||||
<div className="hidden lg:block w-72 shrink-0 border border-[var(--border-subtle)]/20 rounded-lg overflow-hidden">
|
||||
<div className="flex items-center gap-1.5 px-4 py-3 border-b border-[var(--border-subtle)]/20 bg-[var(--bg-surface)]/5">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-[var(--border-strong)]/40" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-[var(--border-strong)]/40" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-[var(--accent)]/60" />
|
||||
<span className="ml-2 text-[10px] text-[var(--fg-muted)] font-mono">nutriscan-core</span>
|
||||
</div>
|
||||
<div className="p-5 font-mono text-[11px] space-y-2">
|
||||
<p className="text-[var(--accent)]">$ npm run audit</p>
|
||||
<p className="text-[var(--fg-muted)]"> Scanning dependencies...</p>
|
||||
<p className="text-[var(--accent-light)]"> 0 vulnerabilities found</p>
|
||||
<p className="text-[var(--fg-muted)]"> Data Integrity: 100%</p>
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<span className="w-2 h-2 rounded-full bg-[var(--accent)] animate-pulse" />
|
||||
<span className="text-[var(--accent)]">All checks passed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Final Trust Verification */}
|
||||
<div className="mt-24 grid grid-cols-2 md:grid-cols-4 gap-12 opacity-50 grayscale hover:grayscale-0 transition-all duration-700">
|
||||
{/* Aqui você colocaria logotipos de bases de dados ou menções técnicas (ex: Open Food Facts, GitHub, Next.js, Radix UI) */}
|
||||
<div className="text-center font-mono text-[10px] font-black uppercase tracking-[0.4em] text-gray-500">Open Food Facts</div>
|
||||
<div className="text-center font-mono text-[10px] font-black uppercase tracking-[0.4em] text-gray-500">MIT License</div>
|
||||
<div className="text-center font-mono text-[10px] font-black uppercase tracking-[0.4em] text-gray-500">Privacy First</div>
|
||||
<div className="text-center font-mono text-[10px] font-black uppercase tracking-[0.4em] text-gray-500">ODbL Data</div>
|
||||
</div>
|
||||
|
||||
{/* Trust footer — editorial badge row */}
|
||||
<div className="mt-16 pt-8 hairline flex flex-wrap items-center justify-center gap-12 opacity-50 hover:opacity-100 transition-opacity duration-500">
|
||||
{["Open Food Facts", "MIT License", "Privacy First", "ODbL Data"].map((name) => (
|
||||
<span key={name} className="label text-[var(--fg-muted)]">
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -37,10 +37,10 @@ export const metadata: Metadata = {
|
||||
|
||||
export default function SobrePage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-white dark:bg-gray-900">
|
||||
<div className="bg-base min-h-screen">
|
||||
<HeroSection />
|
||||
<ProblemSection />
|
||||
<StorySection />
|
||||
<ProblemSection />
|
||||
<MissionSection />
|
||||
<ImpactSection />
|
||||
<TargetAudienceSection />
|
||||
|
||||
Reference in new issue
Block a user