-
-
+
+
+
+ {stats.map(({ value, label }, i) => (
+
+
+ {value}
+
+
+ {label}
+
-
- 2M+
-
-
- Produtos no Open Food Facts
-
-
-
-
-
-
-
-
100+
-
- Países cobertos
-
-
-
-
-
-
-
-
99.9%
-
- Uptime garantido
-
-
-
-
-
-
-
-
100%
-
- Open Source
-
-
+ ))}
diff --git a/app/globals.css b/app/globals.css
index f87cb94..7436513 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -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;
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index bc0acca..b67627b 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -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 (
-
-
+
+
{children}
diff --git a/app/lista/[id]/page.tsx b/app/lista/[id]/page.tsx
index 9f221ec..b642357 100644
--- a/app/lista/[id]/page.tsx
+++ b/app/lista/[id]/page.tsx
@@ -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 (
-
-
+
+
-
+
}
>
-
+
diff --git a/app/page.tsx b/app/page.tsx
index 9d7defd..a3a23cf 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -8,7 +8,7 @@ import { TrustBadges } from "./components/landing/TrustBadges";
export default function Home() {
return (
-
+
diff --git a/app/produto/[barcode]/page.tsx b/app/produto/[barcode]/page.tsx
index 0ceec1e..836ea94 100644
--- a/app/produto/[barcode]/page.tsx
+++ b/app/produto/[barcode]/page.tsx
@@ -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 = {
+ a: "#038141",
+ b: "#85bb2f",
+ c: "#fecb02",
+ d: "#ee8100",
+ e: "#e63e11",
+};
+const NUTRI_INK: Record = {
+ a: "#fff",
+ b: "#fff",
+ c: "#3a2a00",
+ d: "#fff",
+ e: "#fff",
+};
+const GRADE_DESC: Record = {
+ a: "Excelente qualidade nutricional",
+ b: "Boa qualidade nutricional",
+ c: "Qualidade regular",
+ d: "Qualidade ruim",
+ e: "Qualidade muito ruim",
+};
+const NOVA_DESC: Record = {
+ 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 = {
+ 1: "#038141",
+ 2: "#fecb02",
+ 3: "#ee8100",
+ 4: "#e63e11",
+};
+const NOVA_INK: Record = {
+ 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 (
+
+ {g.toUpperCase()}
+
+ );
+}
+
+/* ── 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 (
+
+ {["a", "b", "c", "d", "e"].map((l) => (
+
+ {l.toUpperCase()}
+
+ ))}
+
+ );
+}
+
+/* ── NOVA pill ───────────────────────────────────────────────────────────── */
+function NovaPill({ group }: { group: number }) {
+ const labels: Record = {
+ 1: "Não processado",
+ 2: "Ingrediente culinário",
+ 3: "Processado",
+ 4: "Ultraprocessado",
+ };
+ return (
+
+
+ {group}
+
+ NOVA · {labels[group] || "Desconhecido"}
+
+ );
+}
+
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 ;
- // Handler de Erro - Estética Industrial
+ /* ── Error state ─────────────────────────────────────────────────────── */
if (error || !product) {
return (
-
-
+
-
- Erro de Protocolo
- {error}
-
-
-
+
+
+ Produto não encontrado
+
+
+ {error}
+
+
+
+
@@ -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 = {
+ 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 (
-
-
- {/* 1. NAVEGAÇÃO E TÍTULO (Sticky Header) */}
-
-
-
-
-
- {product.product_name || "Unlabeled Product"}
-
-
- {product.code}
-
-
+
+ {/* ── Top bar ──────────────────────────────────────────────────────── */}
+
+
+
+ EAN · {product.code}
-
+
+
+
+
+
-
-
-
- {/* COLUNA ESQUERDA: Ativos Visuais (5 Colunas) */}
-
-
+ {/* Product image */}
+
+ {imageUrl ? (
+
+ ) : (
+
- {imageUrl ? (
-
- ) : (
-
-
- No Visual Metadata
+
+
+ Sem imagem
+
+
+ )}
+
+
+ {/* Right column — headline + score cards */}
+
+ {/* Brand eyebrow */}
+
+ {product.brands || "Marca desconhecida"}
+
+
+ {/* Product name */}
+
+ {product.product_name || "Produto sem nome"}
+
+
+ {/* ── Nutri-Score hero metric ─────────────────────────────────── */}
+ {grade && (
+
+
+
+
+ Nutri‑Score
+
+
+ {gradeDesc || `Nota ${grade.toUpperCase()}`}
+
+
+
+
+ )}
+
+ {/* ── NOVA row ─────────────────────────────────────────────────── */}
+ {nova && (
+
+
+ {novaDesc && (
+
+ {novaDesc}
)}
+ )}
- {/* Ficha Técnica (Bento-style) */}
-
-
SKU Datasheet
-
- {[
- { 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 && (
-
-
- {item.label}
-
-
{item.val}
+ {/* ── Allergens ────────────────────────────────────────────────── */}
+
+ {allergenTags.length > 0 && (
+
+
+
+
+ Contém alérgenos
- ))}
-
-
-
-
- {/* COLUNA DIREITA: Inteligência de Dados (7 Colunas) */}
-
-
- {/* Veredito Rápido: Scores */}
-
- }
- />
- }
- />
-
-
- {/* Análise Macronutricional */}
-
-
-
-
- Composição Nutricional / 100g
-
-
-
-
-
- {/* Bloqueadores e Alérgenos */}
-
- {product.allergens_tags && product.allergens_tags.length > 0 && (
-
-
-
-
Detecção de Alérgenos
-
-
- {product.allergens_tags.map(tag => (
-
+
+ {allergenTags.map((tag) => (
+
{tag.replace(/en:|pt:/g, "")}
))}
-
- )}
-
-
- {/* Lista de Ingredientes (OCR Interpretation) */}
-
-
Relatório de Ingredientes
-
- {product.ingredients_text || "O fabricante não forneceu a lista de ingredientes para este SKU."}
-
-
+
+
+ )}
+
+
+
+ {/* ── Nutrition composition section ───────────────────────────────── */}
+
+
+
+ Composição nutricional
+
+
+ por 100g · %VD de referência
-
-
- );
-}
-// Sub-componente para cards de dados
-function DataCard({ title, desc, badge }: { title: string, desc: string, badge: React.ReactNode }) {
- return (
-
-
{title}
-
{desc}
-
- {badge}
+
+ {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 (
+
+
+
+ {row.label}
+
+
+ {v}
+
+ {row.unit}
+
+
+
+
+
+ {pct}% VD
+
+
+ );
+ })}
+
+
+ {/* Ingredients */}
+ {product.ingredients_text && (
+
+
+ Lista de Ingredientes
+
+
+ {product.ingredients_text}
+
+
+ )}
+
+
+ {/* ── Footer attribution ─────────────────────────────────────────── */}
+
+
+ Dados fornecidos pela comunidade Open Food Facts sob licença ODbL.
+ {product.countries && ` · Região: ${product.countries}`}
+
);
-}
\ No newline at end of file
+}
diff --git a/app/resultados/page.tsx b/app/resultados/page.tsx
index 19b2110..9bb177b 100644
--- a/app/resultados/page.tsx
+++ b/app/resultados/page.tsx
@@ -5,23 +5,24 @@ import {
Search,
ArrowLeft,
Loader2,
- SlidersHorizontal,
Database,
- ArrowUpDown,
+ Leaf,
+ SlidersHorizontal,
+ LayoutGrid,
+ List,
} from "lucide-react";
+import { ThemeToggle } from "@/components/theme-toggle";
import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
import {
Sheet,
SheetContent,
- SheetTrigger,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
+import { FilterSidebar } from "@/app/components/FilterSidebar";
import { ProductCard } from "@/app/components/ProductCard";
import { ProductCardSkeleton } from "@/app/components/ProductCardSkeleton";
import { DonateCard } from "@/app/components/DonateCard";
-import { FilterSidebar } from "@/app/components/FilterSidebar";
import { SearchBar } from "@/app/components/SearchBar";
import { Pagination } from "@/app/components/Pagination";
import { useProductFilters } from "@/hooks/useProductFilters";
@@ -35,43 +36,48 @@ import { ProductFilters } from "@/lib/types";
import Link from "next/link";
import { Product, sortProducts } from "@/lib/openFoodFactsApi";
-// Função para converter ProductFilters para FilterRecord
function convertFiltersToRecord(filters: ProductFilters): FilterRecord {
const record: FilterRecord = {};
-
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
record[key] = value as FilterRecord[string];
}
});
-
return record;
}
+/* ── Grade filter badge ──────────────────────────────────────────────────── */
+const GRADE_COLORS: Record
= {
+ a: { bg: "#038141", ink: "#fff" },
+ b: { bg: "#85bb2f", ink: "#fff" },
+ c: { bg: "#fecb02", ink: "#3a2a00" },
+ d: { bg: "#ee8100", ink: "#fff" },
+ e: { bg: "#e63e11", ink: "#fff" },
+};
+
function ResultadosContent() {
const searchParams = useSearchParams();
const router = useRouter();
const query = searchParams.get("q") || "";
const countryParam = searchParams.get("country") || "";
- // Estados principais
const [allProducts, setAllProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [detectedCountry, setDetectedCountry] = useState("");
- const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
const [forceRefresh, setForceRefresh] = useState(false);
+ const [gradeFilter, setGradeFilter] = useState(null);
+ const [allergenFree, setAllergenFree] = useState(false);
- // Estados de paginação
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(20);
- // Estados para modais mobile
- const [showFilters, setShowFilters] = useState(false);
+ const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
+
const [showSort, setShowSort] = useState(false);
const [showCacheInfo, setShowCacheInfo] = useState(false);
+ const [showFilterSheet, setShowFilterSheet] = useState(false);
- // Hook de cache
const {
state: cacheState,
getCachedSearch,
@@ -81,7 +87,6 @@ function ResultadosContent() {
setLoading: setCacheLoading,
} = useSearchCache();
- // Hook de filtros
const {
filters,
updateFilters,
@@ -91,14 +96,11 @@ function ResultadosContent() {
setIsLoading,
} = useProductFilters();
- // Hook de monitoramento de storage
const { stats: storageStats, refreshStats: refreshStorageStats } =
useStorageMonitor();
- // Hook de modo cache-only
const { cacheOnly } = useCacheOnlyMode();
- // Detectar país do usuário
useEffect(() => {
const detectCountry = async () => {
try {
@@ -108,7 +110,6 @@ function ResultadosContent() {
setDetectedCountry("br");
}
};
-
detectCountry();
}, []);
@@ -122,7 +123,6 @@ function ResultadosContent() {
const countryToUse = detectedCountry || countryParam || "br";
- // Verificar cache primeiro (se não for refresh forçado)
if (!forceRefresh) {
const cached = getCachedSearch(
query,
@@ -130,7 +130,6 @@ function ResultadosContent() {
convertFiltersToRecord(filters)
);
if (cached) {
- console.log("Usando dados do cache");
setAllProducts(cached.products);
setTotalResults(cached.totalCount);
setLoading(false);
@@ -140,21 +139,15 @@ function ResultadosContent() {
}
}
- console.log("Fazendo busca completa na API");
-
- // Construir parâmetros da API
const params = new URLSearchParams({
q: query,
country: countryToUse,
page: "1",
- page_size: "1000", // Buscar até 1000 produtos por página
+ page_size: "1000",
});
- if (cacheOnly) {
- params.append("cache_only", "true");
- }
+ if (cacheOnly) params.append("cache_only", "true");
- // Fazer busca na API
const response = await fetch(`/api/search?${params.toString()}`);
const data = await response.json();
@@ -168,19 +161,17 @@ function ResultadosContent() {
actualPages: 1,
};
- // Salvar no cache
- const searchId = saveSearch(
+ saveSearch(
query,
countryToUse,
result.products,
result.totalCount,
convertFiltersToRecord(filters)
);
- console.log(`Busca salva no cache com ID: ${searchId}`);
setAllProducts(result.products);
setTotalResults(result.totalCount);
- setCurrentPage(1); // Voltar para primeira página
+ setCurrentPage(1);
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : "Erro ao buscar produtos";
@@ -214,10 +205,8 @@ function ResultadosContent() {
}
}, [query, countryParam, detectedCountry, loadProducts, forceRefresh]);
- // Sincronizar produtos com cache do servidor
useEffect(() => {
if (allProducts.length > 0) {
- // Enviar produtos para cache em background
fetch("/api/cache/sync", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -226,38 +215,36 @@ function ResultadosContent() {
}
}, [allProducts]);
- // Produtos filtrados e ordenados para exibição
const filteredAndSortedProducts = useMemo(() => {
let filtered = [...allProducts];
- // Aplicar filtros aqui (implementar conforme necessário)
- // Por enquanto, apenas ordenação
- if (filters.sortBy) {
- filtered = sortProducts(
- filtered,
- filters.sortBy,
- filters.sortOrder || "asc"
+ if (gradeFilter) {
+ filtered = filtered.filter(
+ (p) => p.nutrition_grades?.toLowerCase() === gradeFilter
+ );
+ }
+ if (allergenFree) {
+ filtered = filtered.filter(
+ (p) => !p.allergens_tags || p.allergens_tags.length === 0
);
}
+ const activeSortBy = filters.sortBy || "relevance";
+ const activeSortOrder = filters.sortOrder || "asc";
+ filtered = sortProducts(filtered, activeSortBy, activeSortOrder);
+
return filtered;
- }, [allProducts, filters.sortBy, filters.sortOrder]);
+ }, [allProducts, filters.sortBy, filters.sortOrder, gradeFilter, allergenFree]);
- // Produtos da página atual
const currentPageProducts = useMemo(() => {
const startIndex = (currentPage - 1) * itemsPerPage;
- const endIndex = startIndex + itemsPerPage;
- return filteredAndSortedProducts.slice(startIndex, endIndex);
+ return filteredAndSortedProducts.slice(startIndex, startIndex + itemsPerPage);
}, [filteredAndSortedProducts, currentPage, itemsPerPage]);
- // Cálculos de paginação
const totalPages = Math.ceil(filteredAndSortedProducts.length / itemsPerPage);
const totalItems = filteredAndSortedProducts.length;
- const handleRefresh = useCallback(() => {
- setForceRefresh(true);
- }, []);
-
+ const handleRefresh = useCallback(() => setForceRefresh(true), []);
const handleClearCache = useCallback(() => {
clearCache(
query,
@@ -267,10 +254,6 @@ function ResultadosContent() {
setForceRefresh(true);
}, [clearCache, query, detectedCountry, countryParam, filters]);
- const handleRefreshStorageStats = useCallback(() => {
- refreshStorageStats();
- }, [refreshStorageStats]);
-
const handleSortChange = useCallback(
(sortBy: string) => {
updateFilters({
@@ -294,318 +277,606 @@ function ResultadosContent() {
if (!query) {
return (
-
-
-
-
+
+
+
+
Nenhuma busca realizada
-
- Digite um termo de busca para encontrar produtos
+
+ Digite um termo para encontrar produtos.
-
+
);
}
return (
-
- {/* Header Mobile Otimizado */}
-
-
-
-
+
+ {/* ── Mobile top bar ──────────────────────────────────────────────── */}
+
+
+
+
-
-
-
- {/* Botões de ação - apenas filtros e ordenar */}
-
-
-
-
+
- {/* Header Desktop (mantido) */}
-
-
-
-
-
-
-
-
- Resultados para "{query}"
-
- {!loading && totalItems > 0 && (
-
- {totalItems} produto{totalItems !== 1 ? "s" : ""} encontrado
- {totalItems !== 1 ? "s" : ""}
- {detectedCountry && ` em ${detectedCountry}`}
- {cacheState.isCached && (
-
-
- Cache Local
-
- )}
- {storageStats && (
-
- • Storage: {storageStats?.storageSizeMB || 0} MB (
- {storageStats?.totalProducts || 0} produtos)
-
- )}
-
- )}
-
-
- {/* Switch Cache-Only em produção */}
-
-
- {/* Barra de Pesquisa - Desktop */}
-
-
-
-
- {/* Mobile Controls */}
-
-
-
-
-
-
-
- {
- if (value) {
- const currentArray =
- (filters[key as keyof typeof filters] as string[]) ||
- [];
- updateFilters({
- [key]: currentArray.filter((item) => item !== value),
- });
- } else {
- updateFilters({ [key]: undefined });
- }
- }}
- />
-
-
-
-
-
-
- {/* Layout principal */}
-
-
- {/* Sidebar de filtros - Desktop */}
-
-
-
{
- if (value) {
- const currentArray =
- (filters[key as keyof typeof filters] as string[]) || [];
- updateFilters({
- [key]: currentArray.filter((item) => item !== value),
- });
- } else {
- updateFilters({ [key]: undefined });
- }
+ {/* ── Desktop header ───────────────────────────────────────────────── */}
+
+
+
+ Voltar
+
+
+
+ {!loading && totalItems > 0 ? `${totalItems} resultado${totalItems !== 1 ? "s" : ""}` : "Buscando…"}
+ {!loading && detectedCountry && ` · ${detectedCountry.toUpperCase()}`}
+ {!loading && cacheState.isCached && (
+
-
+ >
+
+ Cache
+
+ )}
+
+ {query ? (
+ <>
+ Para{" "}
+
+ “{query}”
+
+ >
+ ) : (
+ "Todos os produtos"
+ )}
+
+
- {/* Conteúdo principal */}
-
- {/* Loading Inicial */}
- {loading && (
-
- {/* Indicador de progresso do cache */}
- {cacheState.progress && (
-
-
-
-
-
- Carregando todos os resultados...
-
-
- Página {cacheState.progress?.currentPage || 0} de{" "}
- {cacheState.progress?.totalPages || 0} •
- {cacheState.progress?.loadedProducts || 0} produtos
- carregados
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ {/* ── Full-width content container ─────────────────────────────────── */}
+
+
+
+ {/* ── Inline filter strip ────────────────────────────────────────── */}
+ {!loading && totalItems > 0 && (
+
+
+ Filtrar
+
+
+ {/* Grade pills A–E */}
+
+ {["a", "b", "c", "d", "e"].map((g) => {
+ const active = gradeFilter === g;
+ const col = GRADE_COLORS[g];
+ return (
+
+ );
+ })}
+
+
+ {/* Allergen-free chip */}
+
+
+ {/* ── Mais Filtros button ─────────────────────────────────── */}
+
- {/* Skeleton dos produtos */}
-
+
- {[...Array(8)].map((_, i) => (
-
+ Ordenar
+
+
+
+ {/* ── View mode toggle (inline, all breakpoints) ─────────── */}
+
+ {(
+ [
+ { id: "grid" as const, Icon: LayoutGrid, label: "Cards" },
+ { id: "list" as const, Icon: List, label: "Lista" },
+ ] as const
+ ).map(({ id, Icon, label }) => (
+
))}
- )}
+
+ )}
- {/* Erro */}
- {error && !loading && (
-
-
-
- Erro ao buscar produtos
-
-
{error}
-
-
+ {/* ── Loading skeletons ──────────────────────────────────────────── */}
+ {loading && (
+ viewMode === "grid" ? (
+
+ {[...Array(8)].map((_, i) => (
+
+ ))}
- )}
-
- {/* Sem Resultados */}
- {!loading && !error && totalItems === 0 && (
-
-
-
- Nenhum produto encontrado
-
-
- Tente ajustar os filtros ou buscar com outros termos
-
-
-
- {hasActiveFilters && (
-
- )}
-
+ ) : (
+
+ {[...Array(6)].map((_, i) => (
+
+ ))}
- )}
+ )
+ )}
- {/* Grid de Produtos */}
- {!loading && totalItems > 0 && (
- <>
-
+
+ Erro ao buscar produtos
+
+
+ {error}
+
+
+
+ )}
+
+ {/* ── Empty state ────────────────────────────────────────────────── */}
+ {!loading && !error && totalItems === 0 && (
+
+
+ Nada encontrado
+
+
+ Tente limpar os filtros ou usar outros termos.
+
+
+
+ )}
+
+ {/* ── Product results ─────────────────────────────────────────────── */}
+ {!loading && totalItems > 0 && (
+ <>
+ {viewMode === "grid" ? (
+ /* ── Grid view ──────────────────────────────────────────────── */
+
+
-
-
+
-
{currentPageProducts.map((product, index) => (
+ ))}
+
+ ) : (
+ /* ── List view ──────────────────────────────────────────────── */
+
+
+
+
+ {currentPageProducts.map((product, index) => (
+
@@ -614,100 +885,113 @@ function ResultadosContent() {
priority={index}
currentPage={currentPage}
variant="mobile"
- className="sm:hidden"
- />
-
))}
+ )}
- {/* Paginação */}
- {totalPages > 1 && (
-
- )}
-
- {/* Informações adicionais */}
-
-
- {cacheState.isCached ? (
- <>
- Dados carregados do cache local •
-
- Atualizar busca
-
- >
- ) : (
- <>
- Dados carregados da API externa •
-
- Atualizar busca
-
- >
- )}
- {storageStats && (
- <>
- {" • "}
-
- Storage: {storageStats?.storageSizeMB || 0} MB
-
- >
- )}
-
+ {/* Pagination */}
+ {totalPages > 1 && (
+
- >
- )}
-
+ )}
+
+ {/* Footer note */}
+
+ {cacheState.isCached ? "Dados do cache local" : "Dados da API Open Food Facts"} ·{" "}
+
+ Atualizar
+
+
+ >
+ )}
- {/* Modais Mobile */}
- {/* Modal de Filtros */}
-
-
-
-
Filtros
+ {/* ── Advanced filter sheet (right drawer) ──────────────────────────── */}
+
+
+
+
+ Filtros avançados
+
+
+
+
{
+ updateFilters(newFilters);
+ setCurrentPage(1);
+ }}
+ onClearFilters={() => {
+ clearFilters();
+ setCurrentPage(1);
+ }}
isLoading={loading}
+ totalResults={totalItems}
+ sortBy={filters.sortBy || "relevance"}
+ onSortChange={handleSortChange}
+ onRefresh={handleRefresh}
+ onClearCache={handleClearCache}
+ isCached={cacheState.isCached}
/>
- {/* Modal de Ordenação */}
+ {/* ── Mobile sort sheet ──────────────────────────────────────────────── */}
@@ -715,19 +999,14 @@ function ResultadosContent() {
{[
{ value: "relevance", label: "Relevância" },
- {
- value: "nutrition_grade",
- label: "Classificação Nutricional",
- },
- { value: "name", label: "Nome" },
+ { value: "nutrition_grade", label: "Nutri‑Score" },
+ { value: "name", label: "Nome (A–Z)" },
{ value: "energy", label: "Energia" },
{ value: "with_images", label: "Com Imagens" },
].map((option) => (
{
handleSortChange(option.value);
@@ -742,7 +1021,7 @@ function ResultadosContent() {
- {/* Modal de Informações de Cache */}
+ {/* ── Cache info sheet ───────────────────────────────────────────────── */}
-
-
-
+
+
Produtos em cache:
-
+
{storageStats?.totalProducts ?? 0}
-
-
-
- Tamanho do storage:
-
-
- {storageStats?.storageSizeMB
- ? `${storageStats?.storageSizeMB} MB`
- : "0 KB"}
-
-
-
-
-
- Última atualização:
-
-
- {storageStats?.lastUpdated
- ? new Date(storageStats?.lastUpdated).toLocaleDateString(
- "pt-BR"
- )
- : "Nunca"}
-
-
-
- {/* Botão de limpar cache */}
{
await clearCache();
@@ -811,8 +1069,23 @@ export default function ResultadosPage() {
return (
-
+
+
}
>
diff --git a/app/sobre/components/CTASection.tsx b/app/sobre/components/CTASection.tsx
index ad7bb73..4e5dd72 100644
--- a/app/sobre/components/CTASection.tsx
+++ b/app/sobre/components/CTASection.tsx
@@ -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 (
-
-
- {/* 1. LAYER DE INFRAESTRUTURA: Background Glows Inteligentes */}
-
+
+ {/* Subtle warm texture */}
+
-
-
- {/* 2. LAYER DE CONTEXTO: Tag de Sistema */}
-
- Open Access Protocol
-
+
- {/* 3. LAYER DE MENSAGEM: Headlines de Alta Densidade */}
-
-
- Soberania alimentar via
-
- transparência algorítmica.
-
-
-
- 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 */}
+
+
+ {/* Large editorial headline */}
+
+
+ Comece agora
+
+
+ Soberania alimentar via{" "}
+ transparência.
+
+
+ Junte-se à infraestrutura global que está devolvendo o poder de escolha
+ ao cidadão. Dados abertos para uma vida sem processados ocultos.
- {/* 4. LAYER DE EXECUÇÃO: CTAs de Alta Tração */}
-
-
-
- Executar Scan
-
-
+ {/* CTAs */}
+
+
+ Executar Scan
+
-
-
-
- Audit Code
-
+ Auditar o código
-
+
- {/* 5. LAYER DE VALIDAÇÃO: Bento Grid de Metadados */}
-
+ {/* Three-column info grid */}
+
{[
- { 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) => (
-
-
-
{item.label}
-
{item.val}
-
{item.desc}
+ {
+ 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) => (
+
+