feat: complete NutriScan redesign
This commit is contained in:
1 parent
c1fe4af997
commit
f71cd167b3
163 files changed
+30802
-14169
No files matched your search
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: ["next/core-web-vitals", "next/typescript"],
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
+5
-3
@@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
@@ -40,7 +39,6 @@ next-env.d.ts
|
||||
#database
|
||||
/prisma/db
|
||||
/prisma/migrations
|
||||
=======
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
@@ -171,4 +169,8 @@ dist
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
>>>>>>> ad0ba4c28c763a4748bd81d9039ccce4feab6b9e
|
||||
|
||||
.env*
|
||||
.flaskenv*
|
||||
!.env.project
|
||||
!.env.vault
|
||||
@@ -0,0 +1,21 @@
|
||||
node_modules/
|
||||
.next/
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
coverage/
|
||||
.git/
|
||||
.pnpm-store/
|
||||
.cache/
|
||||
*.log
|
||||
.env
|
||||
.env.*
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
venv/
|
||||
target/
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
@@ -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 />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ImageOff } from "lucide-react";
|
||||
|
||||
export interface OptimizedImageProps {
|
||||
src: string;
|
||||
@@ -18,6 +19,7 @@ export interface OptimizedImageProps {
|
||||
blurDataURL?: string;
|
||||
quality?: number;
|
||||
loading?: "lazy" | "eager";
|
||||
unoptimized?: boolean;
|
||||
onLoad?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
@@ -43,6 +45,7 @@ export function OptimizedImage({
|
||||
blurDataURL,
|
||||
quality,
|
||||
loading = "lazy",
|
||||
unoptimized,
|
||||
onLoad,
|
||||
onError,
|
||||
...props
|
||||
@@ -50,6 +53,11 @@ export function OptimizedImage({
|
||||
const [shouldLoad, setShouldLoad] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
// Usa unoptimized=true para imagens do Open Food Facts, pois o servidor deles
|
||||
// costuma ser lento e causa erros de timeout no otimizador de imagens do Next.js.
|
||||
const isOffImage = src?.includes("openfoodfacts.org") ?? false;
|
||||
const isUnoptimized = unoptimized !== undefined ? unoptimized : isOffImage;
|
||||
|
||||
// Usar priority para debug/logging se necessário
|
||||
console.debug(`OptimizedImage: ${src} (priority: ${priority})`);
|
||||
const imgRef = useRef<HTMLDivElement>(null);
|
||||
@@ -113,14 +121,14 @@ export function OptimizedImage({
|
||||
const renderError = () => (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-gray-100 dark:bg-gray-800 flex items-center justify-center",
|
||||
"bg-gray-50 dark:bg-gray-800/50 flex flex-col items-center justify-center gap-2",
|
||||
"text-gray-400 dark:text-gray-500",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl mb-2">📷</div>
|
||||
<div className="text-xs">Erro ao carregar</div>
|
||||
<ImageOff className="w-8 h-8 text-gray-300 dark:text-gray-600" />
|
||||
<div className="text-[10px] font-bold uppercase tracking-widest text-gray-400 dark:text-gray-500">
|
||||
Imagem Indisponível
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -141,6 +149,7 @@ export function OptimizedImage({
|
||||
blurDataURL={blurDataURL}
|
||||
quality={quality}
|
||||
loading={loading}
|
||||
unoptimized={isUnoptimized}
|
||||
className={cn("transition-opacity duration-300", className)}
|
||||
onLoad={() => {
|
||||
onLoad?.();
|
||||
|
||||
+6795
-284
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
||||
not found
|
||||
@@ -0,0 +1,142 @@
|
||||
# NutriScan — Prompts para Geração de Logo via IA
|
||||
|
||||
> **Guia de uso:** Copie qualquer prompt abaixo diretamente no Midjourney (use `/imagine`) ou no DALL-E 3.
|
||||
> Os prompts estão em inglês para maximizar a qualidade de saída das IAs de imagem.
|
||||
> Cada prompt já inclui parâmetros técnicos otimizados para resultado profissional SaaS-level.
|
||||
|
||||
---
|
||||
|
||||
## Contexto da Marca
|
||||
|
||||
| Atributo | Valor |
|
||||
|---|---|
|
||||
| **Nome** | NutriScan |
|
||||
| **Categoria** | App de escaneamento alimentar / Food Tech SaaS |
|
||||
| **Personalidade** | Transparente, clean, editorial, confiável, moderno |
|
||||
| **Fundo primário** | `#f9f6f0` (bege quente neutro) |
|
||||
| **Acento** | `oklch(52% 0.11 155)` — verde editorial profundo |
|
||||
| **Inspirações** | Apple, Anthropic, OpenAI, iFood, Notion, Linear |
|
||||
|
||||
---
|
||||
|
||||
## Prompts por Categoria
|
||||
|
||||
---
|
||||
|
||||
### 01 · Geométrico Abstrato
|
||||
> *Formas puras e reduzidas que comunicam "escanear" e "nutrição" sem literalidade.*
|
||||
|
||||
```
|
||||
Minimalist geometric logo for a food tech SaaS app called "NutriScan". A single abstract mark composed of two overlapping circles forming a lens or scan aperture, with a subtle leaf shape emerging from the negative space at the center. Deep editorial green (#3a6b4a) on warm off-white (#f9f6f0) background. Swiss International Typographic Style, Paul Rand inspired, flat vector, no gradients, no shadows, razor-sharp edges, generous negative space, optical balance. The wordmark "NutriScan" set in a refined geometric sans-serif (similar to Söhne or GT America) placed underneath with precise tracking. Professional SaaS brand identity. --ar 3:2 --stylize 750 --v 6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 02 · Marca de App Icon (Ícone de Loja)
|
||||
> *Formato quadrado arredondado para App Store e Google Play, com presença forte em thumbnail.*
|
||||
|
||||
```
|
||||
App icon logo for "NutriScan", a food scanning and nutrition transparency app. Rounded square canvas. Bold minimal mark: a stylized barcode morphing into a leaf silhouette, rendered in deep forest green on a warm cream (#f9f6f0) background. Flat vector illustration, zero gradients, single color accent. Inspired by Linear, Notion, and Anthropic brand aesthetics. Clean, iconic, instantly recognizable at 64px. No text, symbol only. Ultra-sharp vector, professional SaaS iconography. --ar 1:1 --stylize 800 --v 6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 03 · Espaço Negativo / Wordmark
|
||||
> *A forma do logo vive no espaço negativo — elegância máxima com mínimo de elementos.*
|
||||
|
||||
```
|
||||
Negative space wordmark logo for "NutriScan". The letter "N" constructed with masterful negative space revealing a hidden leaf or a scanning beam slicing through produce. Deep editorial green typography on warm off-white. Helvetica Neue or Futura proportions, optically corrected. Swiss design precision, Paul Rand philosophy, no decoration, no gradients, no effects. The hidden symbol should feel like a discovery — an "aha moment". White label-ready, vector, scalable. --ar 2:1 --stylize 900 --v 6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 04 · Orgânico Minimalista
|
||||
> *Curvas suaves e referências à natureza sem cair no clichê do "brócolis app".*
|
||||
|
||||
```
|
||||
Organic minimalist logo for "NutriScan", a premium food transparency platform. Abstract mark: a single continuous line forming both a leaf and a lens/viewfinder in one fluid gesture. Monochromatic deep green (#3a6b4a) on warm parchment white. Inspired by the restraint of Apple's early identity work and the warmth of iFood's visual language. Single weight line, no fills, no gradients, no drop shadows. Elegant, modern, editorial. The wordmark in a refined humanist sans-serif below. --ar 3:2 --stylize 700 --v 6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 05 · Símbolo Estilo Bauhaus / Construtivista
|
||||
> *Geometria radical de influência europeia — Bauhaus, De Stijl, Constructivism.*
|
||||
|
||||
```
|
||||
Bauhaus-inspired constructivist logo mark for "NutriScan". Strict geometric composition: one circle (representing a scanner lens or magnifying element) intersected by a vertical rectangle in perfect golden ratio proportions. A thin horizontal line bisects the circle, suggesting a Nutri-Score bar. Deep forest green and warm off-white only. Zero ornamentation. Absolute flatness, mathematical precision, no humanist curves. Inspired by Paul Rand, Otl Aicher, and Dieter Rams. Wordmark in geometric monospace. --ar 1:1 --stylize 850 --v 6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 06 · Editorial Moderno / Revista de Luxo
|
||||
> *Estética de revista de culinária de luxo encontra identidade de startup de tecnologia.*
|
||||
|
||||
```
|
||||
Editorial luxury logo for "NutriScan", a high-end food tech brand. Large typographic wordmark: "NUTRI" and "SCAN" stacked, set in a refined condensed serif (similar to Canela or Cardinal) with extreme tracking on "SCAN". A single thin green horizontal rule separates the two words. Warm off-white background, deep editorial green accent. Inspired by Kinfolk magazine, Aesop branding, and early Anthropic visual identity. No icon, pure typographic confidence. Elegant, premium, editorial. --ar 3:1 --stylize 600 --v 6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 07 · Ícone de Escaneamento / Tech Minimalista
|
||||
> *Linguagem visual diretamente ligada à tecnologia de escaneamento — sem perder sofisticação.*
|
||||
|
||||
```
|
||||
Tech minimalist logo for "NutriScan" food scanning app. Icon: a clean scan line (single horizontal line) moving across a stylized apple or leaf silhouette, rendered as a flat vector mark. The scan line is the accent color (deep green), the produce silhouette is a warm neutral outline. Inspired by Microsoft Fluent Design simplicity and OpenAI's restrained logo language. No gradients, no 3D effects, no textures. Symbol sits left of wordmark "NutriScan" in a geometric sans-serif with tight tracking. --ar 2:1 --stylize 750 --v 6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 08 · Abstrato — Forma de N + Folha
|
||||
> *Monograma sofisticado: a identidade da marca comprimida em uma única letra.*
|
||||
|
||||
```
|
||||
Monogram logo for "NutriScan". The letter "N" redesigned with one of its diagonal strokes replaced by a stylized leaf or plant sprig, executed with absolute geometric precision. Deep editorial green (#3a6b4a) on off-white. The modification is subtle — the letter N is still immediately readable, but the leaf is undeniable upon closer inspection. No bevels, no effects, no gradients. Inspired by the conceptual rigor of FedEx's hidden arrow, applied to organic tech. Monochrome vector, scalable to favicon. --ar 1:1 --stylize 900 --v 6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 09 · Símbolo de Verificação / Nutri-Score Integrado
|
||||
> *O logo comunica visualmente a proposta central do produto: verificação e pontuação nutricional.*
|
||||
|
||||
```
|
||||
Logo concept for "NutriScan" that integrates the concept of a Nutri-Score rating bar into the mark itself. A horizontal bar divided into 5 segments (like a Nutri-Score scale), with the leftmost segment elevated or highlighted in deep forest green, suggesting an "A" rating — the ideal. Below, a minimal leaf or checkmark icon. Flat vector, strictly two colors: deep green and warm off-white. No gradients, no 3D. Data visualization meets brand identity. Swiss design clarity, Information design sensibility, inspired by Edward Tufte's minimalism. Wordmark "NutriScan" in neutral geometric sans-serif. --ar 3:2 --stylize 700 --v 6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10 · Símbolo Cósmico / Premium Futuro
|
||||
> *Elevação máxima da marca — para quando a NutriScan virar referência global de food tech.*
|
||||
|
||||
```
|
||||
Premium futuristic logo for "NutriScan". Abstract symbol: a perfectly drawn circle (representing wholeness, a scan aperture, a plate) with a single minimal leaf shape cut through it using negative space. The cut creates a subtle crescent of warm white within the green circle. Deeply inspired by the restraint of the Anthropic and OpenAI logos — simple geometry with philosophical depth. Deep forest green circle on warm off-white parchment. Zero noise, zero effects, absolute flatness. The wordmark "NutriScan" below in a wide-tracked geometric sans-serif, lowercase. Timeless. --ar 3:2 --stylize 950 --v 6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dicas de Refinamento
|
||||
|
||||
### Parâmetros úteis para iteração no Midjourney
|
||||
|
||||
| Parâmetro | Uso |
|
||||
|---|---|
|
||||
| `--stylize 600–950` | Quanto maior, mais "opiniosa" e artística a saída. Para logos, 700–900 costuma funcionar bem. |
|
||||
| `--ar 1:1` | Ideal para app icons e símbolos isolados |
|
||||
| `--ar 2:1` | Ideal para wordmarks horizontais |
|
||||
| `--ar 3:2` | Ideal para apresentação de marca completa |
|
||||
| `--v 6` | Versão mais recente — mantém melhor a intenção do prompt |
|
||||
| `--no shadows, gradients, 3D` | Adicione ao final para reforçar o estilo flat vector |
|
||||
|
||||
### Cores para referenciar nos prompts
|
||||
- **Verde profundo:** `deep forest green`, `editorial green`, `#3a6b4a`
|
||||
- **Fundo neutro:** `warm off-white`, `parchment white`, `cream`, `#f9f6f0`
|
||||
- **Evitar:** `lime green`, `bright green`, `neon` — fogem da personalidade da marca
|
||||
|
||||
### Próximos passos
|
||||
1. Gere 4 variações de cada prompt favorito (`--v 6` + `--seed` diferente)
|
||||
2. Refine os eleitos com `Vary (Subtle)` dentro do Midjourney
|
||||
3. Exporte o símbolo vencedor como SVG via Adobe Illustrator (trace) ou Vectorizer.ai
|
||||
4. Aplique nas variáveis CSS já existentes no projeto (`--color-accent`, `--color-bg`)
|
||||
|
||||
---
|
||||
|
||||
*Documento gerado em 19/04/2026 — NutriScan Brand Studio*
|
||||
@@ -0,0 +1,179 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>NutriScan · Redesign</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #f9f6f0; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
||||
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
|
||||
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f9f6f0; z-index: 9999; }
|
||||
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
|
||||
#__bundler_placeholder { color: #999; font-size: 14px; }
|
||||
</style>
|
||||
<noscript>
|
||||
<style>#__bundler_loading { display: none; }</style>
|
||||
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
|
||||
This page requires JavaScript to display.
|
||||
</div>
|
||||
</noscript>
|
||||
</head>
|
||||
<body>
|
||||
<div id="__bundler_thumbnail">
|
||||
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="1200" height="800" fill="#f9f6f0"></rect>
|
||||
<circle cx="600" cy="400" r="180" fill="oklch(52% 0.11 155)"></circle>
|
||||
<text x="600" y="470" font-family="Georgia, serif" font-size="220" text-anchor="middle" fill="#fff">N</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="__bundler_loading">Unpacking...</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
const loading = document.getElementById('__bundler_loading');
|
||||
function setStatus(msg) { if (loading) loading.textContent = msg; }
|
||||
|
||||
// Error sink persists across replaceWith since it's on window, not the DOM.
|
||||
window.addEventListener('error', function(e) {
|
||||
var p = document.body || document.documentElement;
|
||||
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
|
||||
d.id = '__bundler_err';
|
||||
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
|
||||
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
|
||||
'[bundle] ' + (e.message || e.type) +
|
||||
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
|
||||
}, true);
|
||||
|
||||
try {
|
||||
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
|
||||
const templateEl = document.querySelector('script[type="__bundler/template"]');
|
||||
if (!manifestEl || !templateEl) {
|
||||
setStatus('Error: missing bundle data');
|
||||
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
|
||||
return;
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(manifestEl.textContent);
|
||||
let template = JSON.parse(templateEl.textContent);
|
||||
|
||||
const uuids = Object.keys(manifest);
|
||||
setStatus('Unpacking ' + uuids.length + ' assets...');
|
||||
|
||||
const blobUrls = {};
|
||||
await Promise.all(uuids.map(async (uuid) => {
|
||||
const entry = manifest[uuid];
|
||||
try {
|
||||
const binaryStr = atob(entry.data);
|
||||
const bytes = new Uint8Array(binaryStr.length);
|
||||
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
|
||||
|
||||
let finalBytes = bytes;
|
||||
if (entry.compressed) {
|
||||
if (typeof DecompressionStream !== 'undefined') {
|
||||
const ds = new DecompressionStream('gzip');
|
||||
const writer = ds.writable.getWriter();
|
||||
const reader = ds.readable.getReader();
|
||||
writer.write(bytes);
|
||||
writer.close();
|
||||
const chunks = [];
|
||||
let totalLen = 0;
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(value);
|
||||
totalLen += value.length;
|
||||
}
|
||||
finalBytes = new Uint8Array(totalLen);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
|
||||
} else {
|
||||
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
|
||||
}
|
||||
}
|
||||
|
||||
blobUrls[uuid] = URL.createObjectURL(new Blob([finalBytes], { type: entry.mime }));
|
||||
} catch (err) {
|
||||
console.error('Failed to decode asset ' + uuid + ':', err);
|
||||
blobUrls[uuid] = URL.createObjectURL(new Blob([], { type: entry.mime }));
|
||||
}
|
||||
}));
|
||||
|
||||
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
|
||||
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
|
||||
const resourceMap = {};
|
||||
for (const entry of extResources) {
|
||||
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
|
||||
}
|
||||
|
||||
setStatus('Rendering...');
|
||||
for (const uuid of uuids) template = template.split(uuid).join(blobUrls[uuid]);
|
||||
|
||||
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
|
||||
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
|
||||
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
|
||||
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
|
||||
|
||||
const resourceScript = '<script>window.__resources = ' +
|
||||
JSON.stringify(resourceMap).split('</' + 'script>').join('<\\/' + 'script>') +
|
||||
';</' + 'script>';
|
||||
// Inject after <head> so the DOCTYPE stays first; prepending the script
|
||||
// would push the parser into quirks mode. DOMParser always emits a <head>
|
||||
// (synthesizing one if the source HTML omitted it) but may carry
|
||||
// attributes through, so match the full opening tag. slice() rather than
|
||||
// replace() keeps us clear of $-pattern substitution in resourceScript.
|
||||
const headOpen = template.match(/<head[^>]*>/i);
|
||||
if (headOpen) {
|
||||
const i = headOpen.index + headOpen[0].length;
|
||||
template = template.slice(0, i) + resourceScript + template.slice(i);
|
||||
}
|
||||
|
||||
// Parse the template and swap the root element. Scripts inserted via
|
||||
// DOMParser/replaceWith are inert per spec — re-create each with
|
||||
// createElement so they execute, awaiting onload for src scripts to
|
||||
// preserve ordering (React before ReactDOM before Babel before text/babel).
|
||||
const doc = new DOMParser().parseFromString(template, 'text/html');
|
||||
document.documentElement.replaceWith(doc.documentElement);
|
||||
const dead = Array.from(document.scripts);
|
||||
for (const old of dead) {
|
||||
const s = document.createElement('script');
|
||||
for (const a of old.attributes) s.setAttribute(a.name, a.value);
|
||||
s.textContent = old.textContent;
|
||||
// text/babel scripts with a src: fetch and inline. transformScriptTags
|
||||
// does XHR against the src, but blob:null/ from a file:// origin is
|
||||
// silently dropped. Inlining makes it a plain inline babel script,
|
||||
// which transformScriptTags handles unconditionally.
|
||||
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
|
||||
const r = await fetch(s.src);
|
||||
s.textContent = await r.text();
|
||||
s.removeAttribute('src');
|
||||
}
|
||||
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
|
||||
old.replaceWith(s);
|
||||
if (p) await p;
|
||||
}
|
||||
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
|
||||
// which fired before we swapped the document. Trigger manually if present.
|
||||
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
|
||||
window.Babel.transformScriptTags();
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus('Error unpacking: ' + err.message);
|
||||
console.error('Bundle unpack error:', err);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<script type="__bundler/manifest">
|
||||
{"e785f210-fa4d-4569-be2d-fe96636d2fb3":{"mime":"text/javascript","compressed":true,"data":"H4sIAAAAAAAACuy9eXvbRpYv/L8/RcW3Y5I2BcXO9CxSKxlFlrs18XYlOZm+bl+pSBRJRCDARoGS2Ym/+33OUisKlOw4PT3vO36e7ohAodZTp06d5Xd2Hz68Jx6Kfy+Lqaq0EqdKTlt40sAfWa6uVVmvlqpqs5/0PYGFj+rVpinmi1YMpyPxTE7VpK6vxuKkmmZCVrkoWi3kbFaUhWyVzviz80Whha7XzVSJaZ0rUWjBzeZiXeWqEe1CiRcn5+axmNVrqK6CF1DF85Oj45dnx2JWlIofi6auW5EXjZq2dbMR9Uy0XkNtoxR0YPfecLaupm1RV2I4L+uJLMdiJvGbkfj5nhDtZqXqmVDvV3XTanFwcCAG9eQnNW0H4sED83pZ5+tSiS/gLfR5VlQqH4hvTV1D/n4k9lydVIyqNL3ASulFJpe5+JZ/DN8OuIrBO6+HUBv3WxwI/uOXX4RW5cwW4wIZLqI4ED9/GI32730YwoSMhTcBtpM/i8FaK6Hbppi2g/1794S4lg1RwQ+q0VD6QAwe/2v2dfaY3u/uisPz8+OX5yevXtLPHxeqEjLPi2ouKnUj9GY5qUst2pqWAlZrTEVfl0pqWP5KF7DgstS1+bStxQCpbidX121dl3pHL2Sj8l3dTHcncnqlqnwXu3ZGLQyo0vOFEvRErIGWoF05R+LA0selAgLeKYsrhUsCJEnjPD48Or84fn784vjl+cX5n18fiwOuKpvVzZD6kymqYDDaD757/er0/PB5/2cwx7KMv3p2evjH7c3NGjlPtXd2fnpydH7x4tXT4/6PaSkvlnWuOv09ffXs5Pnx6ZYeNzUsVpP48oeTp7d8eQ0rGn959Orl+fF/bhnstK5a9b4z1mevTn88PH16cXr8bMtE1c2NbPKLRs06c/Xm7DWwii0TtdYrYDK9Xz4/OdvSb/P5RVnoTu9fHL941f/pUi3r+Ivnh//nz/1flPJvm/iLV8+enR2dHh+/7P+sns30tFGqct++OPzzd8cXJ+fHp4fnr04vzv784rtXz93HRasa2daNKf7s8M1/JkoP/v3fTckBFLWcZa7aE37xrBou5Wai8PekVMRlhShmInyBfLFal8jPDJsNCnzh8WJTjRCNatdNhV/u46MP9/A/0G/3fVs34qBn2A8ehA29TRZ7B/0Ky6Wm5d3+PTu+ziiwFwH774wjKBwMKBooPt2Fc1uIh+J7pVZatI2cXtHZp8R03TSqgkNRr2Q7Xagmw7K7Pnc/okJPbRk4L7A5rhoq//eialVTydI+gJGJn6mGermqK1W1H/jtLv6XW9/D/kJ39+/a3wl0ZKDhdJgV83UjkaT0eroQUotFfSPKupoLWYn1Kpetogr1ol6XuaD9mItZ3cASFK2olMrhEOof/HfQ4BG2ZkffNrLSBbQcjSD++HDa/u+1Wiv7ZTDwMU3lrnjDJ1KjVk2dr6dKTNRCXhd1A8O/xDGr/A0OSF+CUFOquZxuQNJQGVGUxo4W1fw5vtoTM1lqRU3kRX4GNaxLRW+pKi7zUfNf31SGVKjsefwSRDb8wiy9uFnUZgXqm0rIauNeQlnZCtnwSnFV5UZMFBz4IAW0zXraqrx/jV5hu78pbdomn6rJes7tPmvkEtf2g+GF/PHx+7aRZ62cXpkiZmNaLqhVXGqo4U+z583O760RSxsewDuePkr3M+u2KA5cf8LWXft36wH1ASYL6BlLiWK5IpmI9mhRAW9WuZhsAopqFIj1QFT3tvV+rsxqU+1mSm/5Bgsf5rmq8vUyGK8bKiyc5koHAxzBYZ4DD1EwZtHWKzHDMd8s4FIBL2hkQOtEp9eyLGBP5fe4UmDx6ZkbeXNLzT466JlkO7nQp6cKdn2rSGZWbkLNDO7olZoWs2IaTf29e95A4ZU4uOMs7/ujgS+TfYcXwxGcgIOBI4fw5PKp1R1Qu2Lnc/4zG1VVcAKfTeuVOnx9AqsOfA4X9vj9SjUFTk0pjhoF83l8DUv5J1nlpRKHr0+yoJojOV0ovh7YqoIS5/Y4OG/kFKjBb/FlLa4q4HuT9VyPxWRtTp2VamZ1s5QVXEGVbotqHg6AePWfCqDecBD4Wot1pVv460Je10UOd+dnsizhFiRmSrbrBi/Az4qJami2dbuezTJxUrVAMnjmUFNED2JaN0os1XKiGryaLeG3krooNyIHYhGazhHoK9ZYaL1WGpp5evyDmKyLMtdZOAykstTMnOMBc7Mopgvq5FCPTAOKqVrc1M1VFnDgM7zwnTBn15bvp0WWvZ7n48433km/1/ei+xWePXvdR/bk8DoX9TzrPVDSL/ZvqakrdKQe+7tvshFTWZpruNKw+O2izjW8keI7OVGlWJXreVFl+AV9dlKJ16evnoohyFGVWMnplZwrLW6KdlGvWyGnU6WxUiIrcwrrEV/x24XawJkvSjVrQXCzD4pKt0rmuOKWV9/IphriXmnj49GxIxDqhkAkFyVuF9nM17BndVaqat4uxvAEqAXUD4dNIzdDLPmNeCy+pY92xGOxJ74ajcXFldqIA/F4n/76A76nH48e+UxQYK1vsdSOePzObxifvtu3ZS1PFGLVFFX7o2yqopoPBzC+wVjQCKmfo+hcvUf/s1OimqZuPm5Ontx9Up54s/IkmpYnbl6e8MQ84Z+9U/MkPTdP7jI5ONK7zI4/PUEVJSgoowqMrMiaqelCVnNUMIEyqqznxXQsNvVaLFGHeSOrFsgZVFHmO7pfoIBal+rHol3gkflUXWc3NzfZTxro+kaVZRYszlZB8iO4xL5XnxFe7igGDUfBuU5f4/15MAqJZylbOOAHX+qBWymmGPgPaGamsh2+xTrejQJ5RemyqNqdvNBwCuxU6j2o1ypFauMdwxR2tZypHVBJVfOdaa2aaSyyQEswv8+oQ9z0Uq48bWXRqqXfeRY7zrBaems7N8LT50g2arYu98TpS+/WkSu4IWpRV0QKq0bNivemN2FPsnWlF8WsHQ6Y0vbEQDzieaNGflTI/CropCyh/roatAL1qatGyRx5aCZXq3IzYg013n2mEsoULTcMvLpR8kqLk+N/2xOLtl3pvd3dedEu1pNsWi93Z6xi38XZ3aVzeffx1//8+CtXx12XpKp36CYK/d4p6/mchRP494wnHfR5bQ0XKup/BkfJkPfDW9x078Zmf4yjuRtFNxdY57zIYSLPWtkquqE+q5s31bJeV63K7XWNL1zxAfGyrlfD1XpSFtMTkIuqqRrj6aaal57U7e/DC3u7RAVQ+HHmvfQ3m726vqQ9G1Ty4IEY+g8y0K+UcoNlf/klKJxV2C0Qm+mcLqV2+wyauiGy+h7PorDdR+J+dl888gYY7Ok7TuVb18K77u7pyPJ06Nw/kkDD0LL4EneK9C77eJ0vtKjqVmxUK7jJTEBvB2jfKbSQoqp36hWJw0XLbLao8mIKTFWCsAyixaZeNwKoC54DzeEOG5yQlDAWUutiXgFrvoTtmmkY8KXbSHVjDCtSXOJLop5LqmcKUy5WTb1STbtB+QWvVrnSRaNyQV+wEelL7YaZDXzaGoeL49jrJyyDOBBts1b+9vA0M2b+oD9yokFF08KVBU96q/QSfwVBz+hLAuEZdgn1JFRMeUqTo4WaXmlxs1AtqPzqBtcSuSEOUxetr90ptF1jq2VZyUYujZoF5vhDtLlQbVSYHzfKnrBwEXL1MBf/eVLXpZLVB3HerBUQOLc4ptuEqKGnN4VWXheausXrsX0yK5wiaJeVZi+onj1PLxB2tKOCtfc/IT6M78Vz96xupkq7lcjYosnarwp5uyiq6/pK5TDFFVB/ofmKCARoqpqqppVF1W5oS90olI8fPqzq9uFDoEkpnr56QbpISQyZPzU1/BnkF7mxc4s7FtcRGwbxBpql+nW9BLpXK7C4gSKhZdWf7Y9Z8YHmfbGQmgQnUOqsW3GpUdCADXgjiQPgHsnjjuGc3BRliWVoNsQlTZLdFESll5ZFYHkqa+q5tH36sShL/gCty+7N04IVp5dxLz6WSmmaaCUbRdfTDsl/awjpA44d7+JHOAlCzlrVhBuHiKS7cb4lceiDx2MEnBZWGytLuII7qmUWRd03OoykCpQoX1XIIpBcrTK4ZwuM7UhS56noP3wHM1f9YNS7Z07VqpS4a8rSDBAJLBOH5Y3caBSXkG7rxicyUFCskRKpuE/3vE4taHjCs0FqUSzhu0mpuoSpGkWnl5ivZSOrVvHCB3UgLcJGXi5VDg4M5cas5VjwFUE85HswLFTYAyIEXkMcGF27cbcyn4FZqMscFIvrTj9/E9Il49kHpNBStQpnWbxU79tofv970jqTGY5qK7H7w/902m+85rYQ/5kCPxih1xOt2oj6kU3imaHeF7rV9lpwAXeUoprTChXaVGaGz5+yvR3qX6oGr7cgMbRqvgnENHktixK1gG2NR4CpjwWjAlQ7qKUryPC2BprOxPmrp6/2QKNaayWCLoHYZS865hLzUOTrhiheYYf+vmS9kk1byNKjan7Cp1lbw47GfuUkCf5dqP7lb0DqZ8wit5G5Px2fTuWGGQ/8+5w1mLF0fiBe4Rpk9Nu+VstVu6FX7k5HbfIHs0apv6mhV3C0H4vE36GzEAryC1WuQHMNwjDMG04+kT1S5cy/qfjisZ0mK30MgfY1Xl/B9WTMC9mYSUF+jmXg4gj/3XfP+SO8tOFfqAs4CZpH6YmIQDRqpscoBcO5wrMmRV7MZooMqzRJJfQgu+cagg/FgT+RTutQwPIWf1N8oZnJddmaUaBMhY5xSpairhQ4ZejYRGc0XZ6FzrZsKjqwVf7yS/KGYdXNdmo9vUGhQ0ustaXa1d3GH2PpwMoD9CmX+khpwJm1P5ck8LnlgNt7aHX6vkQOHW3WldCbarpo6qpe63Iz5g6yBh7aVWANW6OyaqIE+x6Itp7Line truncated
|
||||
</script>
|
||||
|
||||
<script type="__bundler/ext_resources">
|
||||
[]
|
||||
</script>
|
||||
|
||||
<script type="__bundler/template">
|
||||
"<!DOCTYPE html>\n<html lang=\"pt-BR\"><head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>NutriScan · Redesign<\u002Ftitle>\n\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin=\"\">\n <style>/* latin-ext */\n@font-face {\n font-family: 'Instrument Serif';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"60fa588b-1760-4d48-b4cc-04aa12eb709f\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Instrument Serif';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"aaf27971-679d-4cf1-8dc2-75768121d40c\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Instrument Serif';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"129d834f-f5f8-4f19-af81-0920e9288457\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Instrument Serif';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"72b67702-a356-405e-8772-6f0a2a9b136f\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Inter';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"66e75bcd-2938-4e43-b87c-4720c7ceb8df\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Inter';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"64671880-b4e5-4ebc-8150-30f9a3eae5fa\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Inter';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"cb114583-31ec-4a43-8c57-9946423fdd16\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Inter';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"125ff4dd-646d-4f13-816d-177869568bce\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Inter';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"28bdc14b-6ceb-424a-be18-8bebc3e41446\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Inter';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"2fa653f7-680d-43f8-9aea-adfaac230e8f\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Inter';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"17662fa0-bb4d-46cc-b4f5-e2727b1192a7\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Inter';\n font-style: normal;\n font-weight: 500;\n font-display: swap;\n src: url(\"66e75bcd-2938-4e43-b87c-4720c7ceb8df\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Inter';\n font-style: normal;\n font-weight: 500;\n font-display: swap;\n src: url(\"64671880-b4e5-4ebc-8150-30f9a3eae5fa\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Inter';\n font-style: normal;\n font-weight: 500;\n font-display: swap;\n src: url(\"cb114583-31ec-4a43-8c57-9946423fdd16\") format('woff2');\n unicode-range: U+1F0Line truncated
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 844 KiB |
@@ -0,0 +1,16 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const html = fs.readFileSync(path.join(__dirname, 'NutriScan Redesign.html'), 'utf8');
|
||||
|
||||
// Find the template script tag
|
||||
const start = html.indexOf('type="__bundler/template">');
|
||||
const end = html.indexOf('</script>', start);
|
||||
const raw = html.slice(start + 'type="__bundler/template">'.length, end).trim();
|
||||
|
||||
try {
|
||||
const decoded = JSON.parse(raw);
|
||||
fs.writeFileSync(path.join(__dirname, 'extracted-template.html'), decoded);
|
||||
console.log('Written, length:', decoded.length);
|
||||
} catch(e) {
|
||||
console.log('Error:', e.message);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,529 @@
|
||||
{
|
||||
"communities": {
|
||||
"0": [
|
||||
"app_components_cacheonlyswitch_tsx",
|
||||
"app_components_donatecard_tsx",
|
||||
"app_components_header_tsx",
|
||||
"app_components_landing_features_tsx",
|
||||
"app_components_nutriscorebadge_tsx",
|
||||
"app_components_nutritionbar_tsx",
|
||||
"app_components_productcard_tsx",
|
||||
"app_components_productskeleton_tsx",
|
||||
"barcode_page_nutriletter",
|
||||
"components_donatecard_donatecardprops",
|
||||
"components_header_header",
|
||||
"components_header_nav_links",
|
||||
"components_nutriscorebadge_grade_class",
|
||||
"components_nutriscorebadge_grade_info",
|
||||
"components_nutriscorebadge_nova_info",
|
||||
"components_nutriscorebadge_novabadge",
|
||||
"components_nutriscorebadge_novabadgeprops",
|
||||
"components_nutriscorebadge_nutriscorebadgeprops",
|
||||
"components_nutriscorebadge_size_class",
|
||||
"components_nutritionbar_extractnumber",
|
||||
"components_nutritionbar_nutritionbar",
|
||||
"components_nutritionbar_nutritiongrid",
|
||||
"components_nutritionbar_nutritiongridprops",
|
||||
"components_nutritionbar_pct",
|
||||
"components_nutritionbar_reference",
|
||||
"components_optimizedimage_tsx",
|
||||
"components_productcard_defaultcard",
|
||||
"components_productcard_getimageurl",
|
||||
"components_productcard_getverdict",
|
||||
"components_productcard_mobilecard",
|
||||
"components_productcard_nutri_grade_class",
|
||||
"components_productcard_productcardprops",
|
||||
"components_productcard_verdictdot",
|
||||
"components_productskeleton_badgeskeleton",
|
||||
"components_productskeleton_nutritionbarskeleton",
|
||||
"components_productskeleton_skeletonprops",
|
||||
"components_ui_label_tsx",
|
||||
"components_ui_switch_tsx",
|
||||
"landing_features_feature",
|
||||
"landing_features_featurerow",
|
||||
"landing_hero_arrowright",
|
||||
"landing_howitworks_howitworks",
|
||||
"landing_howitworks_step",
|
||||
"landing_howitworks_stepcard",
|
||||
"landing_persuasivecta_persuasivecta",
|
||||
"lib_openfoodfactsapi_formatnutritiondata",
|
||||
"lib_utils_cn",
|
||||
"lib_utils_measureperformance",
|
||||
"lib_utils_memoize",
|
||||
"lib_utils_perfcache",
|
||||
"lib_utils_ts",
|
||||
"ui_checkbox_checkbox",
|
||||
"ui_label_label",
|
||||
"ui_label_labelvariants",
|
||||
"ui_switch_switch"
|
||||
],
|
||||
"1": [
|
||||
"app_components_footer_tsx",
|
||||
"app_page_home",
|
||||
"app_page_tsx",
|
||||
"app_sobre_components_herosection_tsx",
|
||||
"app_sobre_components_howitworkssection_tsx",
|
||||
"app_sobre_components_impactsection_tsx",
|
||||
"app_sobre_components_missionsection_tsx",
|
||||
"app_sobre_components_problemsection_tsx",
|
||||
"app_sobre_components_storysection_tsx",
|
||||
"app_sobre_components_targetaudiencesection_tsx",
|
||||
"app_sobre_components_valuessection_tsx",
|
||||
"barcode_page_grade_desc",
|
||||
"barcode_page_nova_bg",
|
||||
"barcode_page_nova_desc",
|
||||
"barcode_page_nova_ink",
|
||||
"barcode_page_novapill",
|
||||
"barcode_page_nutri_bg",
|
||||
"barcode_page_nutri_ink",
|
||||
"barcode_page_nutriscale",
|
||||
"barcode_page_produtopage",
|
||||
"components_ctasection_ctasection",
|
||||
"components_footer_footer",
|
||||
"components_footer_footer_links",
|
||||
"components_herosection_herosection",
|
||||
"components_main_main",
|
||||
"components_missionsection_mission_values",
|
||||
"components_problemsection_problemsection",
|
||||
"components_storysection_story_data",
|
||||
"components_storysection_storysection",
|
||||
"components_storysection_storystep",
|
||||
"components_targetaudiencesection_personas",
|
||||
"components_ui_sheet_tsx",
|
||||
"components_valuessection_protocols",
|
||||
"id_page_listapageprops",
|
||||
"landing_donationsection_donationsection",
|
||||
"landing_howitworks_steps",
|
||||
"landing_trustbadges_stats",
|
||||
"landing_trustbadges_trustbadges",
|
||||
"resultados_page_convertfilterstorecord",
|
||||
"resultados_page_grade_colors",
|
||||
"resultados_page_resultadospage",
|
||||
"sobre_page_sobrepage",
|
||||
"ui_sheet_sheetcontent",
|
||||
"ui_sheet_sheetcontentprops",
|
||||
"ui_sheet_sheetdescription",
|
||||
"ui_sheet_sheetfooter",
|
||||
"ui_sheet_sheetheader",
|
||||
"ui_sheet_sheetoverlay",
|
||||
"ui_sheet_sheettitle",
|
||||
"ui_sheet_sheetvariants"
|
||||
],
|
||||
"2": [
|
||||
"app_actions_criaritens",
|
||||
"app_actions_deletarproduto",
|
||||
"app_actions_togglecomprado",
|
||||
"app_actions_ts",
|
||||
"app_actions_verificarlista",
|
||||
"app_components_deletebutton_tsx",
|
||||
"app_components_listheader_tsx",
|
||||
"app_components_main_tsx",
|
||||
"app_components_pagination_tsx",
|
||||
"components_idinput_idinput",
|
||||
"components_listheader_listheader",
|
||||
"components_listheader_listheaderprops",
|
||||
"components_main_mainprops",
|
||||
"components_ui_alert_dialog_tsx",
|
||||
"components_ui_button_tsx",
|
||||
"components_ui_dialog_tsx",
|
||||
"components_ui_input_tsx",
|
||||
"components_ui_select_tsx",
|
||||
"id_page_listapage",
|
||||
"ui_alert_dialog_alertdialogaction",
|
||||
"ui_alert_dialog_alertdialogcancel",
|
||||
"ui_alert_dialog_alertdialogcontent",
|
||||
"ui_alert_dialog_alertdialogdescription",
|
||||
"ui_alert_dialog_alertdialogfooter",
|
||||
"ui_alert_dialog_alertdialogheader",
|
||||
"ui_alert_dialog_alertdialogoverlay",
|
||||
"ui_alert_dialog_alertdialogtitle",
|
||||
"ui_button_button",
|
||||
"ui_button_buttonprops",
|
||||
"ui_button_buttonvariants",
|
||||
"ui_dialog_dialogcontent",
|
||||
"ui_dialog_dialogdescription",
|
||||
"ui_dialog_dialogfooter",
|
||||
"ui_dialog_dialogheader",
|
||||
"ui_dialog_dialogoverlay",
|
||||
"ui_dialog_dialogtitle",
|
||||
"ui_input_input",
|
||||
"ui_select_selectcontent",
|
||||
"ui_select_selectitem",
|
||||
"ui_select_selectlabel",
|
||||
"ui_select_selectscrolldownbutton",
|
||||
"ui_select_selectscrollupbutton",
|
||||
"ui_select_selectseparator",
|
||||
"ui_select_selecttrigger"
|
||||
],
|
||||
"3": [
|
||||
"app_components_filtersidebar_tsx",
|
||||
"components_filtersidebar_filtersidebar",
|
||||
"components_ui_accordion_tsx",
|
||||
"components_ui_radio_group_tsx",
|
||||
"components_ui_separator_tsx",
|
||||
"components_ui_slider_tsx",
|
||||
"hooks_usecacheonlymode_ts",
|
||||
"hooks_useproductfilters_loadfiltersfromstorage",
|
||||
"hooks_useproductfilters_parsefiltersfromurl",
|
||||
"hooks_useproductfilters_savefilterstostorage",
|
||||
"hooks_useproductfilters_ts",
|
||||
"hooks_useproductfilters_useproductfiltersoptions",
|
||||
"hooks_usestoragemonitor_ts",
|
||||
"hooks_usestoragemonitor_usestoragemonitorreturn",
|
||||
"lib_types_cachefile",
|
||||
"lib_types_cacheprogress",
|
||||
"lib_types_cachesearchresult",
|
||||
"lib_types_common_additives",
|
||||
"lib_types_common_allergens",
|
||||
"lib_types_filteroption",
|
||||
"lib_types_filtersidebarprops",
|
||||
"lib_types_filterstate",
|
||||
"lib_types_imageloadingstate",
|
||||
"lib_types_imageloadtask",
|
||||
"lib_types_novagroup",
|
||||
"lib_types_nutritiongrade",
|
||||
"lib_types_paginationconfig",
|
||||
"lib_types_paginationprops",
|
||||
"lib_types_paginationstate",
|
||||
"lib_types_popular_categories",
|
||||
"lib_types_popular_categories_with_icons",
|
||||
"lib_types_productfilters",
|
||||
"lib_types_searchcacheconfig",
|
||||
"lib_types_storagestats",
|
||||
"lib_types_ts",
|
||||
"lib_types_useoptimizedimageloadingoptions",
|
||||
"lib_utils_debounce",
|
||||
"resultados_page_resultadoscontent",
|
||||
"ui_accordion_accordioncontent",
|
||||
"ui_accordion_accordionitem",
|
||||
"ui_accordion_accordiontrigger",
|
||||
"ui_radio_group_radiogroup",
|
||||
"ui_separator_separator",
|
||||
"ui_slider_slider"
|
||||
],
|
||||
"4": [
|
||||
"app_actions_listarprodutos",
|
||||
"app_components_cardlist_tsx",
|
||||
"app_components_filterbadge_tsx",
|
||||
"app_components_listaproductcard_tsx",
|
||||
"app_components_nutritionchart_tsx",
|
||||
"app_components_productcardskeleton_tsx",
|
||||
"components_card_cardprops",
|
||||
"components_cardlist_cardlist",
|
||||
"components_cardlist_cardlistprops",
|
||||
"components_cardlist_produto",
|
||||
"components_filterbadge_filterbadgeprops",
|
||||
"components_filterbadge_filterbadgesprops",
|
||||
"components_filterbadge_getfiltercolor",
|
||||
"components_filterbadge_getfilterlabel",
|
||||
"components_listaproductcard_listaproductcardprops",
|
||||
"components_nutritionchart_nutritiondata",
|
||||
"components_nutritionchart_nutritionsummary",
|
||||
"components_ui_badge_tsx",
|
||||
"components_ui_card_tsx",
|
||||
"ui_badge_badge",
|
||||
"ui_badge_badgeprops",
|
||||
"ui_badge_badgevariants",
|
||||
"ui_card_card",
|
||||
"ui_card_cardcontent",
|
||||
"ui_card_carddescription",
|
||||
"ui_card_cardfooter",
|
||||
"ui_card_cardheader",
|
||||
"ui_card_cardtitle",
|
||||
"ui_skeleton_skeleton"
|
||||
],
|
||||
"5": [
|
||||
"hooks_usesearchcache_searchcachestate",
|
||||
"hooks_usesearchcache_ts",
|
||||
"hooks_usesearchcache_usesearchcachereturn",
|
||||
"lib_cachemanager_cacheconfig",
|
||||
"lib_cachemanager_cachedsearch",
|
||||
"lib_cachemanager_cachemanager_cleanupoldcaches",
|
||||
"lib_cachemanager_cachemanager_clearallcaches",
|
||||
"lib_cachemanager_cachemanager_generatecachekey",
|
||||
"lib_cachemanager_cachemanager_generatesearchid",
|
||||
"lib_cachemanager_cachemanager_getcacheindex",
|
||||
"lib_cachemanager_cachemanager_getcachestats",
|
||||
"lib_cachemanager_cachemanager_getobjectsize",
|
||||
"lib_cachemanager_cachemanager_getsearch",
|
||||
"lib_cachemanager_cachemanager_hasspacefordata",
|
||||
"lib_cachemanager_cachemanager_removesearch",
|
||||
"lib_cachemanager_cachemanager_savesearch",
|
||||
"lib_cachemanager_cachemanager_updatecacheindex",
|
||||
"lib_cachemanager_default_config",
|
||||
"lib_cachemanager_filterrecord",
|
||||
"lib_cachemanager_filtervalue",
|
||||
"lib_cachemanager_ts",
|
||||
"lib_cachemanager_usecachemanager"
|
||||
],
|
||||
"6": [
|
||||
"app_api_search_route_ts",
|
||||
"lib_jsoncachemanager_jsoncachedata",
|
||||
"lib_jsoncachemanager_jsoncachemanager_clearcache",
|
||||
"lib_jsoncachemanager_jsoncachemanager_converttoapiproduct",
|
||||
"lib_jsoncachemanager_jsoncachemanager_converttocachedproduct",
|
||||
"lib_jsoncachemanager_jsoncachemanager_getinstance",
|
||||
"lib_jsoncachemanager_jsoncachemanager_getproductbycode",
|
||||
"lib_jsoncachemanager_jsoncachemanager_getstoragestats",
|
||||
"lib_jsoncachemanager_jsoncachemanager_isinitialized",
|
||||
"lib_jsoncachemanager_jsoncachemanager_loadcache",
|
||||
"lib_jsoncachemanager_jsoncachemanager_mergeproductdata",
|
||||
"lib_jsoncachemanager_jsoncachemanager_savecache",
|
||||
"lib_jsoncachemanager_jsoncachemanager_saveproduct",
|
||||
"lib_jsoncachemanager_jsoncachemanager_searchproducts",
|
||||
"lib_jsoncachemanager_ts",
|
||||
"lib_openfoodfactsapi_product",
|
||||
"lib_searchutils_calculaterelevancescore",
|
||||
"lib_searchutils_sortbyrelevance",
|
||||
"lib_searchutils_ts",
|
||||
"lib_types_cachedproduct",
|
||||
"search_route_get",
|
||||
"sync_route_post"
|
||||
],
|
||||
"7": [
|
||||
"app_components_searchbar_tsx",
|
||||
"hooks_use_search_logic_searchtype",
|
||||
"hooks_use_search_logic_ts",
|
||||
"landing_searchbar_searchbar",
|
||||
"lib_openfoodfactsapi_calculatecompletenessscore",
|
||||
"lib_openfoodfactsapi_config",
|
||||
"lib_openfoodfactsapi_detectsearchtype",
|
||||
"lib_openfoodfactsapi_fetchallresults",
|
||||
"lib_openfoodfactsapi_getgradevalue",
|
||||
"lib_openfoodfactsapi_isvalidbarcode",
|
||||
"lib_openfoodfactsapi_makeapirequest",
|
||||
"lib_openfoodfactsapi_searchbybarcode",
|
||||
"lib_openfoodfactsapi_searchbyname",
|
||||
"lib_openfoodfactsapi_searchresult",
|
||||
"lib_openfoodfactsapi_sortproducts",
|
||||
"lib_openfoodfactsapi_ts",
|
||||
"postcss_config_mjs",
|
||||
"tailwind_config_ts"
|
||||
],
|
||||
"8": [
|
||||
"app_components_errorboundary_tsx",
|
||||
"app_components_errortracking_tsx",
|
||||
"app_layout_inter",
|
||||
"app_layout_metadata",
|
||||
"app_layout_rootlayout",
|
||||
"app_layout_tsx",
|
||||
"app_providers_providersprops",
|
||||
"app_providers_themeprovider",
|
||||
"app_providers_tsx",
|
||||
"components_errorboundary_errorboundary",
|
||||
"components_errorboundary_errorboundary_componentdidcatch",
|
||||
"components_errorboundary_errorboundary_getderivedstatefromerror",
|
||||
"components_errorboundary_errorboundary_render",
|
||||
"components_errorboundary_props",
|
||||
"components_errorboundary_state",
|
||||
"lib_cachemanager_cachemanager_constructor",
|
||||
"lib_openfoodfactsapi_nutriscanerror"
|
||||
],
|
||||
"9": [
|
||||
"app_components_landing_hero_tsx",
|
||||
"hooks_use_geolocation_setscannedcountry",
|
||||
"hooks_use_geolocation_ts",
|
||||
"landing_hero_hero",
|
||||
"lib_geolocation_clearcountrycache",
|
||||
"lib_geolocation_countryinfo",
|
||||
"lib_geolocation_detectusercountry",
|
||||
"lib_geolocation_getcachedcountry",
|
||||
"lib_geolocation_getcountrycodeforapi",
|
||||
"lib_geolocation_getcurrentcountry",
|
||||
"lib_geolocation_isbrazil",
|
||||
"lib_geolocation_iscachevalid",
|
||||
"lib_geolocation_savetocache",
|
||||
"lib_geolocation_ts"
|
||||
],
|
||||
"10": [
|
||||
"app_components_sortdropdown_tsx",
|
||||
"components_sortdropdown_sort_icons",
|
||||
"components_sortdropdown_sorttoggle",
|
||||
"components_theme_toggle_tsx",
|
||||
"components_ui_dropdown_menu_tsx",
|
||||
"lib_types_sortoption",
|
||||
"ui_dropdown_menu_dropdownmenucontent",
|
||||
"ui_dropdown_menu_dropdownmenuitem",
|
||||
"ui_dropdown_menu_dropdownmenulabel",
|
||||
"ui_dropdown_menu_dropdownmenuseparator",
|
||||
"ui_dropdown_menu_dropdownmenusubcontent",
|
||||
"ui_dropdown_menu_dropdownmenusubtrigger"
|
||||
],
|
||||
"11": [
|
||||
"docs_resedesign_extract_js",
|
||||
"resedesign_extract_decoded",
|
||||
"resedesign_extract_end",
|
||||
"resedesign_extract_fs",
|
||||
"resedesign_extract_html",
|
||||
"resedesign_extract_path",
|
||||
"resedesign_extract_raw",
|
||||
"resedesign_extract_start"
|
||||
],
|
||||
"12": [
|
||||
"app_components_items_tsx",
|
||||
"components_items_items",
|
||||
"lib_prisma_prisma",
|
||||
"lib_prisma_ts"
|
||||
],
|
||||
"13": [
|
||||
"lib_types_rybbit_d_ts",
|
||||
"types_rybbit_d_rybbit",
|
||||
"types_rybbit_d_rybbittrackevent",
|
||||
"types_rybbit_d_window"
|
||||
],
|
||||
"14": [
|
||||
"app_robots_robots",
|
||||
"app_robots_ts"
|
||||
],
|
||||
"15": [
|
||||
"next_env_d_ts"
|
||||
],
|
||||
"16": [
|
||||
"next_config_mjs"
|
||||
],
|
||||
"17": [
|
||||
"sw_js"
|
||||
],
|
||||
"18": [
|
||||
"app_sitemap_ts"
|
||||
],
|
||||
"19": [
|
||||
"app_components_pagetitle_tsx"
|
||||
]
|
||||
},
|
||||
"cohesion": {
|
||||
"0": 0.06,
|
||||
"1": 0.05,
|
||||
"2": 0.09,
|
||||
"3": 0.06,
|
||||
"4": 0.11,
|
||||
"5": 0.17,
|
||||
"6": 0.16,
|
||||
"7": 0.15,
|
||||
"8": 0.13,
|
||||
"9": 0.24,
|
||||
"10": 0.23,
|
||||
"11": 0.25,
|
||||
"12": 0.5,
|
||||
"13": 0.5,
|
||||
"14": 1.0,
|
||||
"15": 1.0,
|
||||
"16": 1.0,
|
||||
"17": 1.0,
|
||||
"18": 1.0,
|
||||
"19": 1.0
|
||||
},
|
||||
"gods": [
|
||||
{
|
||||
"id": "lib_utils_cn",
|
||||
"label": "cn()",
|
||||
"degree": 51
|
||||
},
|
||||
{
|
||||
"id": "ui_button_button",
|
||||
"label": "Button",
|
||||
"degree": 13
|
||||
},
|
||||
{
|
||||
"id": "lib_openfoodfactsapi_product",
|
||||
"label": "Product",
|
||||
"degree": 8
|
||||
},
|
||||
{
|
||||
"id": "ui_badge_badge",
|
||||
"label": "Badge()",
|
||||
"degree": 7
|
||||
},
|
||||
{
|
||||
"id": "components_errorboundary_errorboundary",
|
||||
"label": "ErrorBoundary",
|
||||
"degree": 6
|
||||
},
|
||||
{
|
||||
"id": "landing_howitworks_howitworks",
|
||||
"label": "HowItWorks()",
|
||||
"degree": 6
|
||||
},
|
||||
{
|
||||
"id": "lib_geolocation_iscachevalid",
|
||||
"label": "isCacheValid()",
|
||||
"degree": 6
|
||||
},
|
||||
{
|
||||
"id": "lib_geolocation_detectusercountry",
|
||||
"label": "detectUserCountry()",
|
||||
"degree": 6
|
||||
},
|
||||
{
|
||||
"id": "app_actions_verificarlista",
|
||||
"label": "verificarLista()",
|
||||
"degree": 5
|
||||
},
|
||||
{
|
||||
"id": "landing_hero_hero",
|
||||
"label": "Hero()",
|
||||
"degree": 5
|
||||
}
|
||||
],
|
||||
"surprises": [
|
||||
{
|
||||
"source": "SheetFooter()",
|
||||
"target": "cn()",
|
||||
"source_files": [
|
||||
"components/ui/sheet.tsx",
|
||||
"lib/utils.ts"
|
||||
],
|
||||
"confidence": "EXTRACTED",
|
||||
"relation": "calls",
|
||||
"why": "connects across different repos/directories; bridges separate communities; peripheral node `SheetFooter()` unexpectedly reaches hub `cn()`"
|
||||
},
|
||||
{
|
||||
"source": "NovaBadge()",
|
||||
"target": "cn()",
|
||||
"source_files": [
|
||||
"app/components/NutriScoreBadge.tsx",
|
||||
"lib/utils.ts"
|
||||
],
|
||||
"confidence": "EXTRACTED",
|
||||
"relation": "calls",
|
||||
"why": "connects across different repos/directories; peripheral node `NovaBadge()` unexpectedly reaches hub `cn()`"
|
||||
},
|
||||
{
|
||||
"source": "NutritionBar()",
|
||||
"target": "cn()",
|
||||
"source_files": [
|
||||
"app/components/NutritionBar.tsx",
|
||||
"lib/utils.ts"
|
||||
],
|
||||
"confidence": "EXTRACTED",
|
||||
"relation": "calls",
|
||||
"why": "connects across different repos/directories; peripheral node `NutritionBar()` unexpectedly reaches hub `cn()`"
|
||||
},
|
||||
{
|
||||
"source": "NutritionGrid()",
|
||||
"target": "cn()",
|
||||
"source_files": [
|
||||
"app/components/NutritionBar.tsx",
|
||||
"lib/utils.ts"
|
||||
],
|
||||
"confidence": "EXTRACTED",
|
||||
"relation": "calls",
|
||||
"why": "connects across different repos/directories; peripheral node `NutritionGrid()` unexpectedly reaches hub `cn()`"
|
||||
},
|
||||
{
|
||||
"source": "NutritionBarSkeleton()",
|
||||
"target": "cn()",
|
||||
"source_files": [
|
||||
"app/components/ProductSkeleton.tsx",
|
||||
"lib/utils.ts"
|
||||
],
|
||||
"confidence": "EXTRACTED",
|
||||
"relation": "calls",
|
||||
"why": "connects across different repos/directories; peripheral node `NutritionBarSkeleton()` unexpectedly reaches hub `cn()`"
|
||||
}
|
||||
],
|
||||
"tokens": {
|
||||
"input": 0,
|
||||
"output": 0
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_errortracking_tsx", "label": "ErrorTracking.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ErrorTracking.tsx", "source_location": "L1"}, {"id": "components_errortracking_errortracking", "label": "ErrorTracking()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ErrorTracking.tsx", "source_location": "L9"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_errortracking_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ErrorTracking.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_errortracking_tsx", "target": "components_errortracking_errortracking", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ErrorTracking.tsx", "source_location": "L9", "weight": 1.0}], "raw_calls": [{"caller_nid": "components_errortracking_errortracking", "callee": "useEffect", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ErrorTracking.tsx", "source_location": "L10"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_header_tsx", "label": "header.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L1"}, {"id": "components_header_nav_links", "label": "NAV_LINKS", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L10"}, {"id": "components_header_header", "label": "Header()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L16"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_header_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_header_tsx", "target": "link", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_header_tsx", "target": "next_themes", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_header_tsx", "target": "lucide_react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_header_tsx", "target": "image", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_header_tsx", "target": "d_documents_github_nutriscan_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L8", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_header_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L8", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_header_tsx", "target": "components_header_nav_links", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L10", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_header_tsx", "target": "components_header_header", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L16", "weight": 1.0}], "raw_calls": [{"caller_nid": "components_header_header", "callee": "useState", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L17"}, {"caller_nid": "components_header_header", "callee": "useState", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L18"}, {"caller_nid": "components_header_header", "callee": "useState", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L19"}, {"caller_nid": "components_header_header", "callee": "useTheme", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L20"}, {"caller_nid": "components_header_header", "callee": "useEffect", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L22"}, {"caller_nid": "components_header_header", "callee": "cn", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L33"}, {"caller_nid": "components_header_header", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L65"}, {"caller_nid": "components_header_header", "callee": "cn", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L111"}, {"caller_nid": "components_header_header", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\header.tsx", "source_location": "L117"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_landing_howitworks_tsx", "label": "HowItWorks.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\HowItWorks.tsx", "source_location": "L1"}, {"id": "landing_howitworks_step", "label": "Step", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\HowItWorks.tsx", "source_location": "L7"}, {"id": "landing_howitworks_steps", "label": "steps", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\HowItWorks.tsx", "source_location": "L14"}, {"id": "landing_howitworks_howitworks", "label": "HowItWorks()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\HowItWorks.tsx", "source_location": "L45"}, {"id": "landing_howitworks_stepcard", "label": "StepCard()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\HowItWorks.tsx", "source_location": "L129"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_landing_howitworks_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\HowItWorks.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_howitworks_tsx", "target": "lucide_react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\HowItWorks.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_howitworks_tsx", "target": "d_documents_github_nutriscan_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\HowItWorks.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_howitworks_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\HowItWorks.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_howitworks_tsx", "target": "landing_howitworks_step", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\HowItWorks.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_howitworks_tsx", "target": "landing_howitworks_steps", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\HowItWorks.tsx", "source_location": "L14", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_howitworks_tsx", "target": "landing_howitworks_howitworks", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\HowItWorks.tsx", "source_location": "L45", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_howitworks_tsx", "target": "landing_howitworks_stepcard", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\HowItWorks.tsx", "source_location": "L129", "weight": 1.0}], "raw_calls": [{"caller_nid": "landing_howitworks_howitworks", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\HowItWorks.tsx", "source_location": "L72"}, {"caller_nid": "landing_howitworks_howitworks", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\HowItWorks.tsx", "source_location": "L84"}, {"caller_nid": "landing_howitworks_howitworks", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\HowItWorks.tsx", "source_location": "L106"}, {"caller_nid": "landing_howitworks_stepcard", "callee": "cn", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\HowItWorks.tsx", "source_location": "L133"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_lista_id_page_tsx", "label": "page.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\lista\\[id]\\page.tsx", "source_location": "L1"}, {"id": "id_page_listapageprops", "label": "ListaPageProps", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\lista\\[id]\\page.tsx", "source_location": "L10"}, {"id": "id_page_listapage", "label": "ListaPage()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\lista\\[id]\\page.tsx", "source_location": "L16"}], "edges": [{"source": "d_documents_github_nutriscan_app_lista_id_page_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\lista\\[id]\\page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_lista_id_page_tsx", "target": "d_documents_github_nutriscan_app_components_main_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\lista\\[id]\\page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_lista_id_page_tsx", "target": "components_main_main", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\lista\\[id]\\page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_lista_id_page_tsx", "target": "d_documents_github_nutriscan_app_components_cardlist_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\lista\\[id]\\page.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_lista_id_page_tsx", "target": "components_cardlist_cardlist", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\lista\\[id]\\page.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_lista_id_page_tsx", "target": "d_documents_github_nutriscan_components_theme_toggle_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\lista\\[id]\\page.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_lista_id_page_tsx", "target": "components_theme_toggle_themetoggle", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\lista\\[id]\\page.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_lista_id_page_tsx", "target": "d_documents_github_nutriscan_app_components_listheader_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\lista\\[id]\\page.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_lista_id_page_tsx", "target": "components_listheader_listheader", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\lista\\[id]\\page.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_lista_id_page_tsx", "target": "d_documents_github_nutriscan_app_components_footer_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\lista\\[id]\\page.tsx", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_lista_id_page_tsx", "target": "components_footer_footer", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\lista\\[id]\\page.tsx", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_lista_id_page_tsx", "target": "d_documents_github_nutriscan_app_actions_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\lista\\[id]\\page.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_lista_id_page_tsx", "target": "app_actions_verificarlista", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\lista\\[id]\\page.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_lista_id_page_tsx", "target": "navigation", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\lista\\[id]\\page.tsx", "source_location": "L8", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_lista_id_page_tsx", "target": "id_page_Line truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_components_optimizedimage_tsx", "label": "OptimizedImage.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L1"}, {"id": "components_optimizedimage_optimizedimageprops", "label": "OptimizedImageProps", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L8"}, {"id": "components_optimizedimage_optimizedimage", "label": "OptimizedImage()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L34"}], "edges": [{"source": "d_documents_github_nutriscan_components_optimizedimage_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_optimizedimage_tsx", "target": "image", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_optimizedimage_tsx", "target": "d_documents_github_nutriscan_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_optimizedimage_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_optimizedimage_tsx", "target": "lucide_react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_optimizedimage_tsx", "target": "components_optimizedimage_optimizedimageprops", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L8", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_optimizedimage_tsx", "target": "components_optimizedimage_optimizedimage", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L34", "weight": 1.0}], "raw_calls": [{"caller_nid": "components_optimizedimage_optimizedimage", "callee": "useState", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L53"}, {"caller_nid": "components_optimizedimage_optimizedimage", "callee": "useState", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L54"}, {"caller_nid": "components_optimizedimage_optimizedimage", "callee": "includes", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L58"}, {"caller_nid": "components_optimizedimage_optimizedimage", "callee": "debug", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L62"}, {"caller_nid": "components_optimizedimage_optimizedimage", "callee": "useRef", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L63"}, {"caller_nid": "components_optimizedimage_optimizedimage", "callee": "useRef", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L64"}, {"caller_nid": "components_optimizedimage_optimizedimage", "callee": "useRef", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L65"}, {"caller_nid": "components_optimizedimage_optimizedimage", "callee": "useEffect", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L68"}, {"caller_nid": "components_optimizedimage_optimizedimage", "callee": "useEffect", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L73"}, {"caller_nid": "components_optimizedimage_optimizedimage", "callee": "useEffect", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\components\\OptimizedImage.tsx", "source_location": "L81"}, {"caller_nid": "components_optimizedLine truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_components_ui_select_tsx", "label": "select.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L1"}, {"id": "ui_select_selecttrigger", "label": "SelectTrigger", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L15"}, {"id": "ui_select_selectscrollupbutton", "label": "SelectScrollUpButton", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L35"}, {"id": "ui_select_selectscrolldownbutton", "label": "SelectScrollDownButton", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L52"}, {"id": "ui_select_selectcontent", "label": "SelectContent", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L70"}, {"id": "ui_select_selectlabel", "label": "SelectLabel", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L102"}, {"id": "ui_select_selectitem", "label": "SelectItem", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L114"}, {"id": "ui_select_selectseparator", "label": "SelectSeparator", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L136"}], "edges": [{"source": "d_documents_github_nutriscan_components_ui_select_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_select_tsx", "target": "react_select", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_select_tsx", "target": "lucide_react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_select_tsx", "target": "d_documents_github_nutriscan_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_select_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_select_tsx", "target": "ui_select_selecttrigger", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L15", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_select_tsx", "target": "ui_select_selectscrollupbutton", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L35", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_select_tsx", "target": "ui_select_selectscrolldownbutton", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L52", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_select_tsx", "target": "ui_select_selectcontent", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L70", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_select_tsx", "target": "ui_select_selectlabel", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L102", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_select_tsx", "target": "ui_select_selectitem", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L114", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_select_tsx", "target": "ui_select_selectseparator", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\select.tsx", "source_location": "L136", "weight": 1.0}], "raLine truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_components_ui_dropdown_menu_tsx", "label": "dropdown-menu.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L1"}, {"id": "ui_dropdown_menu_dropdownmenusubtrigger", "label": "DropdownMenuSubTrigger", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L21"}, {"id": "ui_dropdown_menu_dropdownmenusubcontent", "label": "DropdownMenuSubContent", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L43"}, {"id": "ui_dropdown_menu_dropdownmenucontent", "label": "DropdownMenuContent", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L59"}, {"id": "ui_dropdown_menu_dropdownmenuitem", "label": "DropdownMenuItem", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L77"}, {"id": "ui_dropdown_menu_dropdownmenulabel", "label": "DropdownMenuLabel", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L95"}, {"id": "ui_dropdown_menu_dropdownmenuseparator", "label": "DropdownMenuSeparator", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L113"}], "edges": [{"source": "d_documents_github_nutriscan_components_ui_dropdown_menu_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dropdown_menu_tsx", "target": "react_dropdown_menu", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dropdown_menu_tsx", "target": "lucide_react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dropdown_menu_tsx", "target": "d_documents_github_nutriscan_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dropdown_menu_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dropdown_menu_tsx", "target": "ui_dropdown_menu_dropdownmenusubtrigger", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L21", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dropdown_menu_tsx", "target": "ui_dropdown_menu_dropdownmenusubcontent", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L43", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dropdown_menu_tsx", "target": "ui_dropdown_menu_dropdownmenucontent", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L59", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dropdown_menu_tsx", "target": "ui_dropdown_menu_dropdownmenuitem", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L77", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dropdown_menu_tsx", "target": "ui_dropdown_menu_dropdownmenulabel", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L95", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dropdown_menu_tsx", "target": "ui_dropdown_menu_dropdownmenuseparator", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dropdown-menu.tsx", "source_location": "L113", "weight": 1.0}], "raw_calls": []}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_listaproductcard_tsx", "label": "ListaProductCard.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ListaProductCard.tsx", "source_location": "L1"}, {"id": "components_listaproductcard_produto", "label": "Produto", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ListaProductCard.tsx", "source_location": "L9"}, {"id": "components_listaproductcard_listaproductcardprops", "label": "ListaProductCardProps", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ListaProductCard.tsx", "source_location": "L20"}, {"id": "components_listaproductcard_listaproductcard", "label": "ListaProductCard()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ListaProductCard.tsx", "source_location": "L25"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_listaproductcard_tsx", "target": "d_documents_github_nutriscan_components_ui_badge_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ListaProductCard.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_listaproductcard_tsx", "target": "ui_badge_badge", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ListaProductCard.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_listaproductcard_tsx", "target": "d_documents_github_nutriscan_components_ui_button_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ListaProductCard.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_listaproductcard_tsx", "target": "ui_button_button", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ListaProductCard.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_listaproductcard_tsx", "target": "d_documents_github_nutriscan_components_ui_card_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ListaProductCard.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_listaproductcard_tsx", "target": "ui_card_card", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ListaProductCard.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_listaproductcard_tsx", "target": "ui_card_cardcontent", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ListaProductCard.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_listaproductcard_tsx", "target": "lucide_react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ListaProductCard.tsx", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_listaproductcard_tsx", "target": "d_documents_github_nutriscan_app_actions_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ListaProductCard.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_listaproductcard_tsx", "target": "app_actions_togglecomprado", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ListaProductCard.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_listaproductcard_tsx", "target": "components_listaproductcard_produto", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ListaProductCard.tsx", "source_location": "L9", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_listaproductcard_tsx", "target": "components_listaproductcard_listaproductcardprops", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ListaProductCard.tsx", "source_location": "L20", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_listaproductcard_tsx", "target": "components_listaproductcard_listaproductcard", "relation": "contains", "conLine truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_landing_donationsection_tsx", "label": "DonationSection.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\DonationSection.tsx", "source_location": "L1"}, {"id": "landing_donationsection_donationsection", "label": "DonationSection()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\DonationSection.tsx", "source_location": "L6"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_landing_donationsection_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\DonationSection.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_donationsection_tsx", "target": "lucide_react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\DonationSection.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_donationsection_tsx", "target": "landing_donationsection_donationsection", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\DonationSection.tsx", "source_location": "L6", "weight": 1.0}], "raw_calls": [{"caller_nid": "landing_donationsection_donationsection", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\DonationSection.tsx", "source_location": "L33"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_landing_features_tsx", "label": "Features.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\Features.tsx", "source_location": "L1"}, {"id": "landing_features_feature", "label": "Feature", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\Features.tsx", "source_location": "L6"}, {"id": "landing_features_features", "label": "features", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\Features.tsx", "source_location": "L13"}, {"id": "landing_features_featurerow", "label": "FeatureRow()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\Features.tsx", "source_location": "L79"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_landing_features_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\Features.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_features_tsx", "target": "d_documents_github_nutriscan_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\Features.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_features_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\Features.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_features_tsx", "target": "landing_features_feature", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\Features.tsx", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_features_tsx", "target": "landing_features_features", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\Features.tsx", "source_location": "L13", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_features_tsx", "target": "landing_features_features", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\Features.tsx", "source_location": "L44", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_features_tsx", "target": "landing_features_featurerow", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\Features.tsx", "source_location": "L79", "weight": 1.0}], "raw_calls": [{"caller_nid": "landing_features_features", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\Features.tsx", "source_location": "L67"}, {"caller_nid": "landing_features_featurerow", "callee": "cn", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\Features.tsx", "source_location": "L88"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_components_ui_radio_group_tsx", "label": "radio-group.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\radio-group.tsx", "source_location": "L1"}, {"id": "ui_radio_group_radiogroup", "label": "RadioGroup", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\radio-group.tsx", "source_location": "L9"}, {"id": "ui_radio_group_radiogroupitem", "label": "RadioGroupItem", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\radio-group.tsx", "source_location": "L23"}], "edges": [{"source": "d_documents_github_nutriscan_components_ui_radio_group_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\radio-group.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_radio_group_tsx", "target": "react_radio_group", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\radio-group.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_radio_group_tsx", "target": "lucide_react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\radio-group.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_radio_group_tsx", "target": "d_documents_github_nutriscan_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\radio-group.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_radio_group_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\radio-group.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_radio_group_tsx", "target": "ui_radio_group_radiogroup", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\radio-group.tsx", "source_location": "L9", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_radio_group_tsx", "target": "ui_radio_group_radiogroupitem", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\radio-group.tsx", "source_location": "L23", "weight": 1.0}], "raw_calls": []}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_api_products_barcode_route_ts", "label": "route.ts", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L1"}, {"id": "barcode_route_get", "label": "GET()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L6"}], "edges": [{"source": "d_documents_github_nutriscan_app_api_products_barcode_route_ts", "target": "server", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L1", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_api_products_barcode_route_ts", "target": "d_documents_github_nutriscan_lib_jsoncachemanager_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_api_products_barcode_route_ts", "target": "lib_jsoncachemanager_jsoncachemanager", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_api_products_barcode_route_ts", "target": "barcode_route_get", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L6", "weight": 1.0}], "raw_calls": [{"caller_nid": "barcode_route_get", "callee": "replace", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L15"}, {"caller_nid": "barcode_route_get", "callee": "json", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L17"}, {"caller_nid": "barcode_route_get", "callee": "log", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L24"}, {"caller_nid": "barcode_route_get", "callee": "log", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L27"}, {"caller_nid": "barcode_route_get", "callee": "getProductByCode", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L28"}, {"caller_nid": "barcode_route_get", "callee": "log", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L31"}, {"caller_nid": "barcode_route_get", "callee": "json", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L32"}, {"caller_nid": "barcode_route_get", "callee": "log", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L41"}, {"caller_nid": "barcode_route_get", "callee": "json", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L44"}, {"caller_nid": "barcode_route_get", "callee": "log", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L51"}, {"caller_nid": "barcode_route_get", "callee": "fetch", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L54"}, {"caller_nid": "barcode_route_get", "callee": "json", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L65"}, {"caller_nid": "barcode_route_get", "callee": "json", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L71"}, {"caller_nid": "barcode_route_get", "callee": "json", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L74"}, {"caller_nid": "barcode_route_get", "callee": "log", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L82"}, {"caller_nid": "barcode_route_get", "callee": "saveProduct", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\products\\[barcode]\\route.ts", "source_location": "L83"}, {"caller_nid": "barcode_route_get", "callee": "log", "is_member_call": true, "source_file": Line truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_layout_tsx", "label": "layout.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\layout.tsx", "source_location": "L1"}, {"id": "app_layout_inter", "label": "inter", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\layout.tsx", "source_location": "L9"}, {"id": "app_layout_metadata", "label": "metadata", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\layout.tsx", "source_location": "L15"}, {"id": "app_layout_rootlayout", "label": "RootLayout()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\layout.tsx", "source_location": "L73"}], "edges": [{"source": "d_documents_github_nutriscan_app_layout_tsx", "target": "d_documents_github_nutriscan_app_globals_css", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\layout.tsx", "source_location": "L1", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_layout_tsx", "target": "next", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\layout.tsx", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_layout_tsx", "target": "google", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\layout.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_layout_tsx", "target": "script", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\layout.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_layout_tsx", "target": "d_documents_github_nutriscan_app_providers_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\layout.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_layout_tsx", "target": "app_providers_themeprovider", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\layout.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_layout_tsx", "target": "d_documents_github_nutriscan_app_components_errorboundary_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\layout.tsx", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_layout_tsx", "target": "components_errorboundary_errorboundary", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\layout.tsx", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_layout_tsx", "target": "d_documents_github_nutriscan_app_components_errortracking_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\layout.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_layout_tsx", "target": "components_errortracking_errortracking", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\layout.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_layout_tsx", "target": "app_layout_inter", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\layout.tsx", "source_location": "L9", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_layout_tsx", "target": "app_layout_metadata", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\layout.tsx", "source_location": "L15", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_layout_tsx", "target": "app_layout_rootlayout", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\layout.tsx", "source_location": "L73", "weight": 1.0}], "raw_calls": []}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_postcss_config_mjs", "label": "postcss.config.mjs", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\postcss.config.mjs", "source_location": "L1"}, {"id": "nutriscan_postcss_config_config", "label": "config", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\postcss.config.mjs", "source_location": "L2"}], "edges": [{"source": "d_documents_github_nutriscan_postcss_config_mjs", "target": "nutriscan_postcss_config_config", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\postcss.config.mjs", "source_location": "L2", "weight": 1.0}], "raw_calls": []}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_providers_tsx", "label": "providers.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\providers.tsx", "source_location": "L1"}, {"id": "app_providers_providersprops", "label": "ProvidersProps", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\providers.tsx", "source_location": "L5"}, {"id": "app_providers_themeprovider", "label": "ThemeProvider()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\providers.tsx", "source_location": "L9"}], "edges": [{"source": "d_documents_github_nutriscan_app_providers_tsx", "target": "next_themes", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\providers.tsx", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_providers_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\providers.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_providers_tsx", "target": "app_providers_providersprops", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\providers.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_providers_tsx", "target": "app_providers_themeprovider", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\providers.tsx", "source_location": "L9", "weight": 1.0}], "raw_calls": []}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_lib_utils_ts", "label": "utils.ts", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L1"}, {"id": "lib_utils_cn", "label": "cn()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L4"}, {"id": "lib_utils_perfcache", "label": "perfCache", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L9"}, {"id": "lib_utils_debounce", "label": "debounce()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L12"}, {"id": "lib_utils_memoize", "label": "memoize()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L25"}, {"id": "lib_utils_measureperformance", "label": "measurePerformance()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L46"}], "edges": [{"source": "d_documents_github_nutriscan_lib_utils_ts", "target": "clsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L1", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_utils_ts", "target": "tailwind_merge", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_utils_ts", "target": "lib_utils_cn", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_utils_ts", "target": "lib_utils_perfcache", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L9", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_utils_ts", "target": "lib_utils_debounce", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L12", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_utils_ts", "target": "lib_utils_memoize", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L25", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_utils_ts", "target": "lib_utils_measureperformance", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L46", "weight": 1.0}], "raw_calls": [{"caller_nid": "lib_utils_cn", "callee": "twMerge", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L5"}, {"caller_nid": "lib_utils_cn", "callee": "clsx", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L5"}, {"caller_nid": "lib_utils_measureperformance", "callee": "now", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L47"}, {"caller_nid": "lib_utils_measureperformance", "callee": "callback", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L48"}, {"caller_nid": "lib_utils_measureperformance", "callee": "now", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L49"}, {"caller_nid": "lib_utils_measureperformance", "callee": "set", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\utils.ts", "source_location": "L50"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_landing_trustbadges_tsx", "label": "TrustBadges.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\TrustBadges.tsx", "source_location": "L1"}, {"id": "landing_trustbadges_stats", "label": "stats", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\TrustBadges.tsx", "source_location": "L3"}, {"id": "landing_trustbadges_trustbadges", "label": "TrustBadges()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\TrustBadges.tsx", "source_location": "L26"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_landing_trustbadges_tsx", "target": "lucide_react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\TrustBadges.tsx", "source_location": "L1", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_trustbadges_tsx", "target": "landing_trustbadges_stats", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\TrustBadges.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_trustbadges_tsx", "target": "landing_trustbadges_trustbadges", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\TrustBadges.tsx", "source_location": "L26", "weight": 1.0}], "raw_calls": [{"caller_nid": "landing_trustbadges_trustbadges", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\TrustBadges.tsx", "source_location": "L31"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_nutritionbar_tsx", "label": "NutritionBar.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L1"}, {"id": "components_nutritionbar_nutritionbarprops", "label": "NutritionBarProps", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L4"}, {"id": "components_nutritionbar_nutritionbar", "label": "NutritionBar()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L15"}, {"id": "components_nutritionbar_nutritiongridprops", "label": "NutritionGridProps", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L61"}, {"id": "components_nutritionbar_reference", "label": "REFERENCE", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L74"}, {"id": "components_nutritionbar_extractnumber", "label": "extractNumber()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L79"}, {"id": "components_nutritionbar_pct", "label": "pct()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L85"}, {"id": "components_nutritionbar_nutritiongrid", "label": "NutritionGrid()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L89"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_nutritionbar_tsx", "target": "d_documents_github_nutriscan_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionbar_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionbar_tsx", "target": "components_nutritionbar_nutritionbarprops", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionbar_tsx", "target": "components_nutritionbar_nutritionbar", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L15", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionbar_tsx", "target": "components_nutritionbar_nutritiongridprops", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L61", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionbar_tsx", "target": "components_nutritionbar_reference", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L74", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionbar_tsx", "target": "components_nutritionbar_extractnumber", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L79", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionbar_tsx", "target": "components_nutritionbar_pct", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L85", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionbar_tsx", "target": "components_nutritionbar_nutritiongrid", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L89", "weight": 1.0}, {"source": "components_nutritionbar_pct", "target": "components_nutritionbar_extractnumber", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L86", "weight": 1.0}], "raw_calls": [{"caller_nid": "components_nutritionbar_nutritionbar", "callee": "min", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionBar.tsx", "source_location": "L24"}, {"caller_nid": "compoLine truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_api_location_route_ts", "label": "route.ts", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L1"}, {"id": "location_route_get", "label": "GET()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L6"}], "edges": [{"source": "d_documents_github_nutriscan_app_api_location_route_ts", "target": "server", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L1", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_api_location_route_ts", "target": "location_route_get", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L6", "weight": 1.0}], "raw_calls": [{"caller_nid": "location_route_get", "callee": "trim", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L15"}, {"caller_nid": "location_route_get", "callee": "split", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L15"}, {"caller_nid": "location_route_get", "callee": "startsWith", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L25"}, {"caller_nid": "location_route_get", "callee": "startsWith", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L26"}, {"caller_nid": "location_route_get", "callee": "log", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L28"}, {"caller_nid": "location_route_get", "callee": "fetch", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L33"}, {"caller_nid": "location_route_get", "callee": "json", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L40"}, {"caller_nid": "location_route_get", "callee": "log", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L42"}, {"caller_nid": "location_route_get", "callee": "warn", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L45"}, {"caller_nid": "location_route_get", "callee": "warn", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L51"}, {"caller_nid": "location_route_get", "callee": "json", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L52"}, {"caller_nid": "location_route_get", "callee": "log", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L58"}, {"caller_nid": "location_route_get", "callee": "fetch", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L61"}, {"caller_nid": "location_route_get", "callee": "json", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L74"}, {"caller_nid": "location_route_get", "callee": "warn", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L78"}, {"caller_nid": "location_route_get", "callee": "json", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L79"}, {"caller_nid": "location_route_get", "callee": "log", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L85"}, {"caller_nid": "location_route_get", "callee": "json", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L87"}, {"caller_nid": "location_route_get", "callee": "toLowerCase", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L89"}, {"caller_nid": "location_route_get", "callee": "error", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L92"}, {"caller_nid": "location_route_get", "callee": "json", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\location\\route.ts", "source_location": "L95"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_pagination_tsx", "label": "Pagination.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Pagination.tsx", "source_location": "L1"}, {"id": "components_pagination_default_config", "label": "DEFAULT_CONFIG", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Pagination.tsx", "source_location": "L20"}, {"id": "components_pagination_pagination", "label": "Pagination()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Pagination.tsx", "source_location": "L27"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_pagination_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Pagination.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_pagination_tsx", "target": "lucide_react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Pagination.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_pagination_tsx", "target": "d_documents_github_nutriscan_components_ui_button_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Pagination.tsx", "source_location": "L10", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_pagination_tsx", "target": "ui_button_button", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Pagination.tsx", "source_location": "L10", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_pagination_tsx", "target": "d_documents_github_nutriscan_components_ui_select_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Pagination.tsx", "source_location": "L11", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_pagination_tsx", "target": "ui_select_select", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Pagination.tsx", "source_location": "L11", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_pagination_tsx", "target": "ui_select_selectcontent", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Pagination.tsx", "source_location": "L11", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_pagination_tsx", "target": "ui_select_selectitem", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Pagination.tsx", "source_location": "L11", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_pagination_tsx", "target": "ui_select_selecttrigger", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Pagination.tsx", "source_location": "L11", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_pagination_tsx", "target": "ui_select_selectvalue", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Pagination.tsx", "source_location": "L11", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_pagination_tsx", "target": "d_documents_github_nutriscan_lib_types_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Pagination.tsx", "source_location": "L18", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_pagination_tsx", "target": "lib_types_paginationprops", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Pagination.tsx", "source_location": "L18", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_pagination_tsx", "target": "lib_types_paginationconfig", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Pagination.tsx", "source_location": "L18", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_pagination_tsx", "target": "components_pagination_default_config", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Pagination.tsx", "source_location": "L20", "weight": 1Line truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_main_tsx", "label": "Main.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Main.tsx", "source_location": "L1"}, {"id": "components_main_mainprops", "label": "MainProps", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Main.tsx", "source_location": "L17"}, {"id": "components_main_main", "label": "Main()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Main.tsx", "source_location": "L21"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_main_tsx", "target": "d_documents_github_nutriscan_components_ui_input_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Main.tsx", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_main_tsx", "target": "ui_input_input", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Main.tsx", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_main_tsx", "target": "d_documents_github_nutriscan_components_ui_select_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Main.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_main_tsx", "target": "ui_select_select", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Main.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_main_tsx", "target": "ui_select_selectcontent", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Main.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_main_tsx", "target": "ui_select_selectgroup", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Main.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_main_tsx", "target": "ui_select_selectitem", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Main.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_main_tsx", "target": "ui_select_selectlabel", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Main.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_main_tsx", "target": "ui_select_selecttrigger", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Main.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_main_tsx", "target": "ui_select_selectvalue", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Main.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_main_tsx", "target": "d_documents_github_nutriscan_components_ui_button_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Main.tsx", "source_location": "L12", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_main_tsx", "target": "ui_button_button", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Main.tsx", "source_location": "L12", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_main_tsx", "target": "d_documents_github_nutriscan_app_actions_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Main.tsx", "source_location": "L13", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_main_tsx", "target": "app_actions_criaritens", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\Main.tsx", "source_location": "L13", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_main_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\Line truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_api_storage_stats_route_ts", "label": "route.ts", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L1"}, {"id": "storage_stats_route_get", "label": "GET()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L4"}], "edges": [{"source": "d_documents_github_nutriscan_app_api_storage_stats_route_ts", "target": "server", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L1", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_api_storage_stats_route_ts", "target": "d_documents_github_nutriscan_lib_jsoncachemanager_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_api_storage_stats_route_ts", "target": "lib_jsoncachemanager_jsoncachemanager", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_api_storage_stats_route_ts", "target": "storage_stats_route_get", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L4", "weight": 1.0}], "raw_calls": [{"caller_nid": "storage_stats_route_get", "callee": "log", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L6"}, {"caller_nid": "storage_stats_route_get", "callee": "isInitialized", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L9"}, {"caller_nid": "storage_stats_route_get", "callee": "log", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L11"}, {"caller_nid": "storage_stats_route_get", "callee": "json", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L12"}, {"caller_nid": "storage_stats_route_get", "callee": "now", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L20"}, {"caller_nid": "storage_stats_route_get", "callee": "getStorageStats", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L25"}, {"caller_nid": "storage_stats_route_get", "callee": "log", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L26"}, {"caller_nid": "storage_stats_route_get", "callee": "json", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L28"}, {"caller_nid": "storage_stats_route_get", "callee": "now", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L31"}, {"caller_nid": "storage_stats_route_get", "callee": "error", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L34"}, {"caller_nid": "storage_stats_route_get", "callee": "json", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L37"}, {"caller_nid": "storage_stats_route_get", "callee": "now", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\api\\storage-stats\\route.ts", "source_location": "L45"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_sobre_components_targetaudiencesection_tsx", "label": "TargetAudienceSection.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\TargetAudienceSection.tsx", "source_location": "L1"}, {"id": "components_targetaudiencesection_personas", "label": "PERSONAS", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\TargetAudienceSection.tsx", "source_location": "L3"}, {"id": "components_targetaudiencesection_targetaudiencesection", "label": "TargetAudienceSection()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\TargetAudienceSection.tsx", "source_location": "L38"}], "edges": [{"source": "d_documents_github_nutriscan_app_sobre_components_targetaudiencesection_tsx", "target": "components_targetaudiencesection_personas", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\TargetAudienceSection.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_sobre_components_targetaudiencesection_tsx", "target": "components_targetaudiencesection_targetaudiencesection", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\TargetAudienceSection.tsx", "source_location": "L38", "weight": 1.0}], "raw_calls": [{"caller_nid": "components_targetaudiencesection_targetaudiencesection", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\TargetAudienceSection.tsx", "source_location": "L56"}, {"caller_nid": "components_targetaudiencesection_targetaudiencesection", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\TargetAudienceSection.tsx", "source_location": "L94"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_hooks_usestoragemonitor_ts", "label": "useStorageMonitor.ts", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useStorageMonitor.ts", "source_location": "L1"}, {"id": "hooks_usestoragemonitor_usestoragemonitorreturn", "label": "UseStorageMonitorReturn", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useStorageMonitor.ts", "source_location": "L9"}, {"id": "hooks_usestoragemonitor_usestoragemonitor", "label": "useStorageMonitor()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useStorageMonitor.ts", "source_location": "L19"}], "edges": [{"source": "d_documents_github_nutriscan_hooks_usestoragemonitor_ts", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useStorageMonitor.ts", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_usestoragemonitor_ts", "target": "d_documents_github_nutriscan_lib_types_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useStorageMonitor.ts", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_usestoragemonitor_ts", "target": "lib_types_storagestats", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useStorageMonitor.ts", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_usestoragemonitor_ts", "target": "hooks_usestoragemonitor_usestoragemonitorreturn", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useStorageMonitor.ts", "source_location": "L9", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_usestoragemonitor_ts", "target": "hooks_usestoragemonitor_usestoragemonitor", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useStorageMonitor.ts", "source_location": "L19", "weight": 1.0}], "raw_calls": [{"caller_nid": "hooks_usestoragemonitor_usestoragemonitor", "callee": "useState", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useStorageMonitor.ts", "source_location": "L20"}, {"caller_nid": "hooks_usestoragemonitor_usestoragemonitor", "callee": "useState", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useStorageMonitor.ts", "source_location": "L21"}, {"caller_nid": "hooks_usestoragemonitor_usestoragemonitor", "callee": "useState", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useStorageMonitor.ts", "source_location": "L22"}, {"caller_nid": "hooks_usestoragemonitor_usestoragemonitor", "callee": "useCallback", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useStorageMonitor.ts", "source_location": "L27"}, {"caller_nid": "hooks_usestoragemonitor_usestoragemonitor", "callee": "useCallback", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useStorageMonitor.ts", "source_location": "L42"}, {"caller_nid": "hooks_usestoragemonitor_usestoragemonitor", "callee": "useCallback", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useStorageMonitor.ts", "source_location": "L80"}, {"caller_nid": "hooks_usestoragemonitor_usestoragemonitor", "callee": "useEffect", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useStorageMonitor.ts", "source_location": "L85"}, {"caller_nid": "hooks_usestoragemonitor_usestoragemonitor", "callee": "useEffect", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useStorageMonitor.ts", "source_location": "L90"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_productcardskeleton_tsx", "label": "ProductCardSkeleton.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCardSkeleton.tsx", "source_location": "L1"}, {"id": "components_productcardskeleton_productcardskeleton", "label": "ProductCardSkeleton()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCardSkeleton.tsx", "source_location": "L4"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_productcardskeleton_tsx", "target": "d_documents_github_nutriscan_components_ui_card_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCardSkeleton.tsx", "source_location": "L1", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_productcardskeleton_tsx", "target": "ui_card_card", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCardSkeleton.tsx", "source_location": "L1", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_productcardskeleton_tsx", "target": "ui_card_cardcontent", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCardSkeleton.tsx", "source_location": "L1", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_productcardskeleton_tsx", "target": "d_documents_github_nutriscan_components_ui_skeleton_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCardSkeleton.tsx", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_productcardskeleton_tsx", "target": "ui_skeleton_skeleton", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCardSkeleton.tsx", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_productcardskeleton_tsx", "target": "components_productcardskeleton_productcardskeleton", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCardSkeleton.tsx", "source_location": "L4", "weight": 1.0}], "raw_calls": []}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_components_ui_dialog_tsx", "label": "dialog.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L1"}, {"id": "ui_dialog_dialogoverlay", "label": "DialogOverlay", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L17"}, {"id": "ui_dialog_dialogcontent", "label": "DialogContent", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L32"}, {"id": "ui_dialog_dialogheader", "label": "DialogHeader()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L56"}, {"id": "ui_dialog_dialogfooter", "label": "DialogFooter()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L70"}, {"id": "ui_dialog_dialogtitle", "label": "DialogTitle", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L84"}, {"id": "ui_dialog_dialogdescription", "label": "DialogDescription", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L99"}], "edges": [{"source": "d_documents_github_nutriscan_components_ui_dialog_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dialog_tsx", "target": "react_dialog", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dialog_tsx", "target": "lucide_react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dialog_tsx", "target": "d_documents_github_nutriscan_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dialog_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dialog_tsx", "target": "ui_dialog_dialogoverlay", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L17", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dialog_tsx", "target": "ui_dialog_dialogcontent", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L32", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dialog_tsx", "target": "ui_dialog_dialogheader", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L56", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dialog_tsx", "target": "ui_dialog_dialogfooter", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L70", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dialog_tsx", "target": "ui_dialog_dialogtitle", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L84", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_dialog_tsx", "target": "ui_dialog_dialogdescription", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L99", "weight": 1.0}], "raw_calls": [{"caller_nid": "ui_dialog_dialogheader", "callee": "cn", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L61"}, {"caller_nid": "ui_dialog_dialogfooter", "callee": "cn", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\dialog.tsx", "source_location": "L75"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_tailwind_config_ts", "label": "tailwind.config.ts", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\tailwind.config.ts", "source_location": "L1"}, {"id": "nutriscan_tailwind_config_config", "label": "config", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\tailwind.config.ts", "source_location": "L3"}], "edges": [{"source": "d_documents_github_nutriscan_tailwind_config_ts", "target": "tailwindcss", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\tailwind.config.ts", "source_location": "L1", "weight": 1.0}, {"source": "d_documents_github_nutriscan_tailwind_config_ts", "target": "nutriscan_tailwind_config_config", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\tailwind.config.ts", "source_location": "L3", "weight": 1.0}], "raw_calls": []}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_hooks_useproductfilters_ts", "label": "useProductFilters.ts", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L1"}, {"id": "hooks_useproductfilters_useproductfiltersoptions", "label": "UseProductFiltersOptions", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L7"}, {"id": "hooks_useproductfilters_useproductfilters", "label": "useProductFilters()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L13"}, {"id": "hooks_useproductfilters_parsefiltersfromurl", "label": "parseFiltersFromURL()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L167"}, {"id": "hooks_useproductfilters_loadfiltersfromstorage", "label": "loadFiltersFromStorage()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L236"}, {"id": "hooks_useproductfilters_savefilterstostorage", "label": "saveFiltersToStorage()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L245"}, {"id": "hooks_useproductfilters_debounce", "label": "debounce()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L253"}], "edges": [{"source": "d_documents_github_nutriscan_hooks_useproductfilters_ts", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_useproductfilters_ts", "target": "navigation", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_useproductfilters_ts", "target": "d_documents_github_nutriscan_lib_types_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_useproductfilters_ts", "target": "lib_types_productfilters", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_useproductfilters_ts", "target": "lib_types_filterstate", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_useproductfilters_ts", "target": "hooks_useproductfilters_useproductfiltersoptions", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_useproductfilters_ts", "target": "hooks_useproductfilters_useproductfilters", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L13", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_useproductfilters_ts", "target": "hooks_useproductfilters_parsefiltersfromurl", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L167", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_useproductfilters_ts", "target": "hooks_useproductfilters_loadfiltersfromstorage", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L236", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_useproductfilters_ts", "target": "hooks_useproductfilters_savefilterstostorage", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L245", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_useproductfilters_ts", "target": "hooks_useproductfilters_debounce", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useProductFilters.ts", "source_location": "L253", "weight": 1.0}, {"source": "hooks_useproductfilters_useproductfilters", "target": "hooks_useproductfilters_debounce", "relation": "calls", "context": "call", "confidence": "EXTRACTED"Line truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_components_ui_button_tsx", "label": "button.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\button.tsx", "source_location": "L1"}, {"id": "ui_button_buttonvariants", "label": "buttonVariants", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\button.tsx", "source_location": "L7"}, {"id": "ui_button_buttonprops", "label": "ButtonProps", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\button.tsx", "source_location": "L37"}, {"id": "ui_button_button", "label": "Button", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\button.tsx", "source_location": "L43"}], "edges": [{"source": "d_documents_github_nutriscan_components_ui_button_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\button.tsx", "source_location": "L1", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_button_tsx", "target": "react_slot", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\button.tsx", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_button_tsx", "target": "class_variance_authority", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\button.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_button_tsx", "target": "d_documents_github_nutriscan_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\button.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_button_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\button.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_button_tsx", "target": "ui_button_buttonvariants", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\button.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_button_tsx", "target": "ui_button_buttonprops", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\button.tsx", "source_location": "L37", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_button_tsx", "target": "ui_button_button", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\button.tsx", "source_location": "L43", "weight": 1.0}], "raw_calls": []}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_next_config_mjs", "label": "next.config.mjs", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\next.config.mjs", "source_location": "L1"}, {"id": "nutriscan_next_config_nextconfig", "label": "nextConfig", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\next.config.mjs", "source_location": "L2"}], "edges": [{"source": "d_documents_github_nutriscan_next_config_mjs", "target": "nutriscan_next_config_nextconfig", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\next.config.mjs", "source_location": "L2", "weight": 1.0}], "raw_calls": []}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_hooks_usesearchcache_ts", "label": "useSearchCache.ts", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L1"}, {"id": "hooks_usesearchcache_searchcachestate", "label": "SearchCacheState", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L7"}, {"id": "hooks_usesearchcache_usesearchcachereturn", "label": "UseSearchCacheReturn", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L20"}, {"id": "hooks_usesearchcache_usesearchcache", "label": "useSearchCache()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L51"}], "edges": [{"source": "d_documents_github_nutriscan_hooks_usesearchcache_ts", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_usesearchcache_ts", "target": "d_documents_github_nutriscan_lib_openfoodfactsapi_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_usesearchcache_ts", "target": "lib_openfoodfactsapi_product", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_usesearchcache_ts", "target": "d_documents_github_nutriscan_lib_cachemanager_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_usesearchcache_ts", "target": "lib_cachemanager_cachemanager", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_usesearchcache_ts", "target": "lib_cachemanager_cachedsearch", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_usesearchcache_ts", "target": "lib_cachemanager_filterrecord", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_usesearchcache_ts", "target": "hooks_usesearchcache_searchcachestate", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_usesearchcache_ts", "target": "hooks_usesearchcache_usesearchcachereturn", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L20", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_usesearchcache_ts", "target": "hooks_usesearchcache_usesearchcache", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L51", "weight": 1.0}], "raw_calls": [{"caller_nid": "hooks_usesearchcache_usesearchcache", "callee": "useState", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L52"}, {"caller_nid": "hooks_usesearchcache_usesearchcache", "callee": "useCallback", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L60"}, {"caller_nid": "hooks_usesearchcache_usesearchcache", "callee": "useCallback", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L87"}, {"caller_nid": "hooks_usesearchcache_usesearchcache", "callee": "useCallback", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L122"}, {"caller_nid": "hooks_usesearchcache_usesearchcache", "callee": "useCallback", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\useSearchCache.ts", "source_location": "L151"}, {"caller_nid": "hooks_usesearchcache_usesearchcache", "callee": "useCallback", "is_member_call": false, "Line truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_sobre_components_storysection_tsx", "label": "StorySection.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\StorySection.tsx", "source_location": "L1"}, {"id": "components_storysection_storystep", "label": "StoryStep", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\StorySection.tsx", "source_location": "L3"}, {"id": "components_storysection_story_data", "label": "STORY_DATA", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\StorySection.tsx", "source_location": "L9"}, {"id": "components_storysection_storysection", "label": "StorySection()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\StorySection.tsx", "source_location": "L36"}], "edges": [{"source": "d_documents_github_nutriscan_app_sobre_components_storysection_tsx", "target": "components_storysection_storystep", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\StorySection.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_sobre_components_storysection_tsx", "target": "components_storysection_story_data", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\StorySection.tsx", "source_location": "L9", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_sobre_components_storysection_tsx", "target": "components_storysection_storysection", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\StorySection.tsx", "source_location": "L36", "weight": 1.0}], "raw_calls": [{"caller_nid": "components_storysection_storysection", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\StorySection.tsx", "source_location": "L55"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_card_tsx", "label": "card.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\card.tsx", "source_location": "L1"}, {"id": "components_card_cardprops", "label": "CardProps", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\card.tsx", "source_location": "L1"}, {"id": "components_card_card", "label": "Card()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\card.tsx", "source_location": "L14"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_card_tsx", "target": "components_card_cardprops", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\card.tsx", "source_location": "L1", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_card_tsx", "target": "components_card_card", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\card.tsx", "source_location": "L14", "weight": 1.0}], "raw_calls": []}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_sobre_components_impactsection_tsx", "label": "ImpactSection.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\ImpactSection.tsx", "source_location": "L1"}, {"id": "components_impactsection_impactsection", "label": "ImpactSection()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\ImpactSection.tsx", "source_location": "L3"}], "edges": [{"source": "d_documents_github_nutriscan_app_sobre_components_impactsection_tsx", "target": "components_impactsection_impactsection", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\ImpactSection.tsx", "source_location": "L3", "weight": 1.0}], "raw_calls": [{"caller_nid": "components_impactsection_impactsection", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\ImpactSection.tsx", "source_location": "L21"}, {"caller_nid": "components_impactsection_impactsection", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\ImpactSection.tsx", "source_location": "L43"}, {"caller_nid": "components_impactsection_impactsection", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\ImpactSection.tsx", "source_location": "L72"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_deletebutton_tsx", "label": "DeleteButton.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\DeleteButton.tsx", "source_location": "L1"}, {"id": "components_deletebutton_deletebuttonprops", "label": "DeleteButtonProps", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\DeleteButton.tsx", "source_location": "L19"}, {"id": "components_deletebutton_deletebutton", "label": "DeleteButton()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\DeleteButton.tsx", "source_location": "L25"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_deletebutton_tsx", "target": "d_documents_github_nutriscan_components_ui_button_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\DeleteButton.tsx", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_deletebutton_tsx", "target": "ui_button_button", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\DeleteButton.tsx", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_deletebutton_tsx", "target": "d_documents_github_nutriscan_components_ui_alert_dialog_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\DeleteButton.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_deletebutton_tsx", "target": "ui_alert_dialog_alertdialog", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\DeleteButton.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_deletebutton_tsx", "target": "ui_alert_dialog_alertdialogaction", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\DeleteButton.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_deletebutton_tsx", "target": "ui_alert_dialog_alertdialogcancel", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\DeleteButton.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_deletebutton_tsx", "target": "ui_alert_dialog_alertdialogcontent", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\DeleteButton.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_deletebutton_tsx", "target": "ui_alert_dialog_alertdialogdescription", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\DeleteButton.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_deletebutton_tsx", "target": "ui_alert_dialog_alertdialogfooter", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\DeleteButton.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_deletebutton_tsx", "target": "ui_alert_dialog_alertdialogheader", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\DeleteButton.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_deletebutton_tsx", "target": "ui_alert_dialog_alertdialogtitle", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\DeleteButton.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_deletebutton_tsx", "target": "ui_alert_dialog_alertdialogtrigger", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\DeleteButton.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_deletebutton_tsx", "target": "d_documents_github_nutriscan_app_actions_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\DeleteButton.tsx", "source_location": "L14", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_deletebutton_tsx", "target": "app_actions_deletarLine truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_pagetitle_tsx", "label": "PageTitle.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\PageTitle.tsx", "source_location": "L1"}, {"id": "components_pagetitle_pagetitle", "label": "PageTitle()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\PageTitle.tsx", "source_location": "L4"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_pagetitle_tsx", "target": "framer_motion", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\PageTitle.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_pagetitle_tsx", "target": "components_pagetitle_pagetitle", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\PageTitle.tsx", "source_location": "L4", "weight": 1.0}], "raw_calls": []}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_components_ui_skeleton_tsx", "label": "skeleton.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\skeleton.tsx", "source_location": "L1"}, {"id": "ui_skeleton_skeleton", "label": "Skeleton()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\skeleton.tsx", "source_location": "L3"}], "edges": [{"source": "d_documents_github_nutriscan_components_ui_skeleton_tsx", "target": "d_documents_github_nutriscan_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\skeleton.tsx", "source_location": "L1", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_skeleton_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\skeleton.tsx", "source_location": "L1", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_skeleton_tsx", "target": "ui_skeleton_skeleton", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\skeleton.tsx", "source_location": "L3", "weight": 1.0}], "raw_calls": [{"caller_nid": "ui_skeleton_skeleton", "callee": "cn", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\skeleton.tsx", "source_location": "L9"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_lib_openfoodfactsapi_ts", "label": "openFoodFactsApi.ts", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L1"}, {"id": "lib_openfoodfactsapi_product", "label": "Product", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L6"}, {"id": "lib_openfoodfactsapi_searchresult", "label": "SearchResult", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L34"}, {"id": "lib_openfoodfactsapi_config", "label": "CONFIG", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L43"}, {"id": "lib_openfoodfactsapi_nutriscanerror", "label": "NutriScanError", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L54"}, {"id": "lib_openfoodfactsapi_nutriscanerror_constructor", "label": ".constructor()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L55"}, {"id": "lib_openfoodfactsapi_makeapirequest", "label": "makeApiRequest()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L64"}, {"id": "lib_openfoodfactsapi_searchbybarcode", "label": "searchByBarcode()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L99"}, {"id": "lib_openfoodfactsapi_searchbyname", "label": "searchByName()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L118"}, {"id": "lib_openfoodfactsapi_fetchallresults", "label": "fetchAllResults()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L138"}, {"id": "lib_openfoodfactsapi_sortproducts", "label": "sortProducts()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L175"}, {"id": "lib_openfoodfactsapi_calculatecompletenessscore", "label": "calculateCompletenessScore()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L218"}, {"id": "lib_openfoodfactsapi_getgradevalue", "label": "getGradeValue()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L231"}, {"id": "lib_openfoodfactsapi_formatnutritiondata", "label": "formatNutritionData()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L239"}, {"id": "lib_openfoodfactsapi_detectsearchtype", "label": "detectSearchType()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L264"}, {"id": "lib_openfoodfactsapi_isvalidbarcode", "label": "isValidBarcode()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L269"}], "edges": [{"source": "d_documents_github_nutriscan_lib_openfoodfactsapi_ts", "target": "lib_openfoodfactsapi_product", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_openfoodfactsapi_ts", "target": "lib_openfoodfactsapi_searchresult", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L34", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_openfoodfactsapi_ts", "target": "lib_openfoodfactsapi_config", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L43", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_openfoodfactsapi_ts", "target": "lib_openfoodfactsapi_nutriscanerror", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L54", "weight": 1.0}, {"source": "lib_openfoodfactsapi_nutriscanerror", "target": "lib_openfoodfactsapi_nutriscanerror_constructor", "relation": "method", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L55", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_openfoodfactsapi_ts", "target": "lib_openfoodfactsapi_makeapirequest", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\openFoodFactsApi.ts", "source_location": "L64", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_openfoodfactsapLine truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_lib_cachemanager_ts", "label": "cacheManager.ts", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L1"}, {"id": "lib_cachemanager_filtervalue", "label": "FilterValue", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L9"}, {"id": "lib_cachemanager_filterrecord", "label": "FilterRecord", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L17"}, {"id": "lib_cachemanager_cachedsearch", "label": "CachedSearch", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L19"}, {"id": "lib_cachemanager_cacheconfig", "label": "CacheConfig", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L29"}, {"id": "lib_cachemanager_default_config", "label": "DEFAULT_CONFIG", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L35"}, {"id": "lib_cachemanager_cachemanager", "label": "CacheManager", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L41"}, {"id": "lib_cachemanager_cachemanager_constructor", "label": ".constructor()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L46"}, {"id": "lib_cachemanager_cachemanager_generatecachekey", "label": ".generateCacheKey()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L53"}, {"id": "lib_cachemanager_cachemanager_generatesearchid", "label": ".generateSearchId()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L66"}, {"id": "lib_cachemanager_cachemanager_iscachevalid", "label": ".isCacheValid()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L73"}, {"id": "lib_cachemanager_cachemanager_getcacheindex", "label": ".getCacheIndex()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L80"}, {"id": "lib_cachemanager_cachemanager_updatecacheindex", "label": ".updateCacheIndex()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L92"}, {"id": "lib_cachemanager_cachemanager_getobjectsize", "label": ".getObjectSize()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L103"}, {"id": "lib_cachemanager_cachemanager_cleanupoldcaches", "label": ".cleanupOldCaches()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L110"}, {"id": "lib_cachemanager_cachemanager_savesearch", "label": ".saveSearch()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L158"}, {"id": "lib_cachemanager_cachemanager_getsearch", "label": ".getSearch()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L210"}, {"id": "lib_cachemanager_cachemanager_removesearch", "label": ".removeSearch()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L238"}, {"id": "lib_cachemanager_cachemanager_clearallcaches", "label": ".clearAllCaches()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L261"}, {"id": "lib_cachemanager_cachemanager_getcachestats", "label": ".getCacheStats()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L274"}, {"id": "lib_cachemanager_cachemanager_hasspacefordata", "label": ".hasSpaceForData()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L324"}, {"id": "lib_cachemanager_usecachemanager", "label": "useCacheManager()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L338"}], "edges": [{"source": "d_documents_github_nutriscan_lib_cachemanager_ts", "target": "d_documents_github_nutriscan_lib_openfoodfactsapi_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_cachemanager_ts", "target": "lib_openfoodfactsapi_product", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\cacheManager.ts", "source_location": "Line truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_filterbadge_tsx", "label": "FilterBadge.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L1"}, {"id": "components_filterbadge_filterbadgeprops", "label": "FilterBadgeProps", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L7"}, {"id": "components_filterbadge_filterbadge", "label": "FilterBadge()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L14"}, {"id": "components_filterbadge_filterbadgesprops", "label": "FilterBadgesProps", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L40"}, {"id": "components_filterbadge_filterbadges", "label": "FilterBadges()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L46"}, {"id": "components_filterbadge_getfilterlabel", "label": "getFilterLabel()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L127"}, {"id": "components_filterbadge_getfiltercolor", "label": "getFilterColor()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L146"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_filterbadge_tsx", "target": "lucide_react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_filterbadge_tsx", "target": "d_documents_github_nutriscan_components_ui_badge_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_filterbadge_tsx", "target": "ui_badge_badge", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_filterbadge_tsx", "target": "d_documents_github_nutriscan_components_ui_button_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_filterbadge_tsx", "target": "ui_button_button", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_filterbadge_tsx", "target": "components_filterbadge_filterbadgeprops", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_filterbadge_tsx", "target": "components_filterbadge_filterbadge", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L14", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_filterbadge_tsx", "target": "components_filterbadge_filterbadgesprops", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L40", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_filterbadge_tsx", "target": "components_filterbadge_filterbadges", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L46", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_filterbadge_tsx", "target": "components_filterbadge_getfilterlabel", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L127", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_filterbadge_tsx", "target": "components_filterbadge_getfiltercolor", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\FilterBadge.tsx", "source_location": "L146", "weight": 1.0}], "raw_calls": [{"caller_nid": "components_filterbadge_filterbadges", "callee": "filtLine truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_sobre_components_ctasection_tsx", "label": "CTASection.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\CTASection.tsx", "source_location": "L1"}, {"id": "components_ctasection_ctasection", "label": "CTASection()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\CTASection.tsx", "source_location": "L6"}], "edges": [{"source": "d_documents_github_nutriscan_app_sobre_components_ctasection_tsx", "target": "link", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\CTASection.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_sobre_components_ctasection_tsx", "target": "lucide_react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\CTASection.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_sobre_components_ctasection_tsx", "target": "components_ctasection_ctasection", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\CTASection.tsx", "source_location": "L6", "weight": 1.0}], "raw_calls": [{"caller_nid": "components_ctasection_ctasection", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\sobre\\components\\CTASection.tsx", "source_location": "L60"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_landing_persuasivecta_tsx", "label": "PersuasiveCTA.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\PersuasiveCTA.tsx", "source_location": "L1"}, {"id": "landing_persuasivecta_persuasivecta", "label": "PersuasiveCTA()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\PersuasiveCTA.tsx", "source_location": "L9"}, {"id": "landing_persuasivecta_arrowright", "label": "ArrowRight()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\PersuasiveCTA.tsx", "source_location": "L132"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_landing_persuasivecta_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\PersuasiveCTA.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_persuasivecta_tsx", "target": "navigation", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\PersuasiveCTA.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_persuasivecta_tsx", "target": "lucide_react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\PersuasiveCTA.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_persuasivecta_tsx", "target": "d_documents_github_nutriscan_hooks_use_search_logic_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\PersuasiveCTA.tsx", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_persuasivecta_tsx", "target": "hooks_use_search_logic_usesearchlogic", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\PersuasiveCTA.tsx", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_persuasivecta_tsx", "target": "d_documents_github_nutriscan_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\PersuasiveCTA.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_persuasivecta_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\PersuasiveCTA.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_persuasivecta_tsx", "target": "landing_persuasivecta_persuasivecta", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\PersuasiveCTA.tsx", "source_location": "L9", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_landing_persuasivecta_tsx", "target": "landing_persuasivecta_arrowright", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\PersuasiveCTA.tsx", "source_location": "L132", "weight": 1.0}], "raw_calls": [{"caller_nid": "landing_persuasivecta_persuasivecta", "callee": "useRouter", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\PersuasiveCTA.tsx", "source_location": "L10"}, {"caller_nid": "landing_persuasivecta_persuasivecta", "callee": "useState", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\PersuasiveCTA.tsx", "source_location": "L11"}, {"caller_nid": "landing_persuasivecta_persuasivecta", "callee": "useSearchLogic", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\PersuasiveCTA.tsx", "source_location": "L12"}, {"caller_nid": "landing_persuasivecta_persuasivecta", "callee": "cn", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\PersuasiveCTA.tsx", "source_location": "L102"}, {"caller_nid": "landing_persuasivecta_persuasivecta", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\landing\\PersuasiveCTA.tsx", "source_location": "L115"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_cardlist_tsx", "label": "CardList.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\CardList.tsx", "source_location": "L1"}, {"id": "components_cardlist_produto", "label": "Produto", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\CardList.tsx", "source_location": "L4"}, {"id": "components_cardlist_cardlistprops", "label": "CardListProps", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\CardList.tsx", "source_location": "L15"}, {"id": "components_cardlist_cardlist", "label": "CardList()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\CardList.tsx", "source_location": "L19"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_cardlist_tsx", "target": "d_documents_github_nutriscan_app_actions_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\CardList.tsx", "source_location": "L1", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_cardlist_tsx", "target": "app_actions_listarprodutos", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\CardList.tsx", "source_location": "L1", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_cardlist_tsx", "target": "d_documents_github_nutriscan_app_components_listaproductcard_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\CardList.tsx", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_cardlist_tsx", "target": "components_listaproductcard_listaproductcard", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\CardList.tsx", "source_location": "L2", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_cardlist_tsx", "target": "components_cardlist_produto", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\CardList.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_cardlist_tsx", "target": "components_cardlist_cardlistprops", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\CardList.tsx", "source_location": "L15", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_cardlist_tsx", "target": "components_cardlist_cardlist", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\CardList.tsx", "source_location": "L19", "weight": 1.0}], "raw_calls": [{"caller_nid": "components_cardlist_cardlist", "callee": "listarProdutos", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\CardList.tsx", "source_location": "L20"}, {"caller_nid": "components_cardlist_cardlist", "callee": "filter", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\CardList.tsx", "source_location": "L30"}, {"caller_nid": "components_cardlist_cardlist", "callee": "filter", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\CardList.tsx", "source_location": "L31"}, {"caller_nid": "components_cardlist_cardlist", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\CardList.tsx", "source_location": "L38"}, {"caller_nid": "components_cardlist_cardlist", "callee": "map", "is_member_call": true, "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\CardList.tsx", "source_location": "L54"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_lib_geolocation_ts", "label": "geolocation.ts", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L1"}, {"id": "lib_geolocation_countryinfo", "label": "CountryInfo", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L6"}, {"id": "lib_geolocation_iscachevalid", "label": "isCacheValid()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L18"}, {"id": "lib_geolocation_getcachedcountry", "label": "getCachedCountry()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L35"}, {"id": "lib_geolocation_savetocache", "label": "saveToCache()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L52"}, {"id": "lib_geolocation_detectusercountry", "label": "detectUserCountry()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L74"}, {"id": "lib_geolocation_clearcountrycache", "label": "clearCountryCache()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L115"}, {"id": "lib_geolocation_getcurrentcountry", "label": "getCurrentCountry()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L124"}, {"id": "lib_geolocation_isbrazil", "label": "isBrazil()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L134"}, {"id": "lib_geolocation_getcountrycodeforapi", "label": "getCountryCodeForAPI()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L142"}], "edges": [{"source": "d_documents_github_nutriscan_lib_geolocation_ts", "target": "lib_geolocation_countryinfo", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_geolocation_ts", "target": "lib_geolocation_iscachevalid", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L18", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_geolocation_ts", "target": "lib_geolocation_getcachedcountry", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L35", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_geolocation_ts", "target": "lib_geolocation_savetocache", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L52", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_geolocation_ts", "target": "lib_geolocation_detectusercountry", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L74", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_geolocation_ts", "target": "lib_geolocation_clearcountrycache", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L115", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_geolocation_ts", "target": "lib_geolocation_getcurrentcountry", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L124", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_geolocation_ts", "target": "lib_geolocation_isbrazil", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L134", "weight": 1.0}, {"source": "d_documents_github_nutriscan_lib_geolocation_ts", "target": "lib_geolocation_getcountrycodeforapi", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L142", "weight": 1.0}, {"source": "lib_geolocation_detectusercountry", "target": "lib_geolocation_iscachevalid", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L78", "weight": 1.0}, {"source": "lib_geolocation_detectusercountry", "target": "lib_geolocation_getcachedcountry", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\lib\\geolocation.ts", "source_location": "L79", "weight": 1.0}, {"source": "lib_geolocation_detectusercountry", "target": "lib_geLine truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_productcard_tsx", "label": "ProductCard.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L1"}, {"id": "components_productcard_productcardprops", "label": "ProductCardProps", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L10"}, {"id": "components_productcard_nutri_grade_class", "label": "NUTRI_GRADE_CLASS", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L18"}, {"id": "components_productcard_getverdict", "label": "getVerdict()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L22"}, {"id": "components_productcard_getverdictdotbg", "label": "getVerdictDotBg()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L28"}, {"id": "components_productcard_getimageurl", "label": "getImageUrl()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L34"}, {"id": "components_productcard_nutriletter", "label": "NutriLetter()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L45"}, {"id": "components_productcard_verdictdot", "label": "VerdictDot()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L60"}, {"id": "components_productcard_mobilecard", "label": "MobileCard()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L75"}, {"id": "components_productcard_defaultcard", "label": "DefaultCard()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L152"}, {"id": "components_productcard_productcard", "label": "ProductCard()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L289"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_productcard_tsx", "target": "link", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_productcard_tsx", "target": "d_documents_github_nutriscan_components_optimizedimage_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_productcard_tsx", "target": "components_optimizedimage_optimizedimage", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_productcard_tsx", "target": "d_documents_github_nutriscan_lib_openfoodfactsapi_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_productcard_tsx", "target": "lib_openfoodfactsapi_product", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L5", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_productcard_tsx", "target": "lucide_react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_productcard_tsx", "target": "d_documents_github_nutriscan_lib_openfoodfactsapi_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_productcard_tsx", "target": "lib_openfoodfactsapi_formatnutritiondata", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\ProductCard.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_productcard_tsx", "target": "d_documenLine truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_hooks_use_search_logic_ts", "label": "use-search-logic.ts", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\use-search-logic.ts", "source_location": "L1"}, {"id": "hooks_use_search_logic_searchtype", "label": "SearchType", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\use-search-logic.ts", "source_location": "L7"}, {"id": "hooks_use_search_logic_usesearchlogic", "label": "useSearchLogic()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\use-search-logic.ts", "source_location": "L9"}], "edges": [{"source": "d_documents_github_nutriscan_hooks_use_search_logic_ts", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\use-search-logic.ts", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_use_search_logic_ts", "target": "d_documents_github_nutriscan_lib_openfoodfactsapi_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\use-search-logic.ts", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_use_search_logic_ts", "target": "lib_openfoodfactsapi_detectsearchtype", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\use-search-logic.ts", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_use_search_logic_ts", "target": "lib_openfoodfactsapi_isvalidbarcode", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\use-search-logic.ts", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_use_search_logic_ts", "target": "hooks_use_search_logic_searchtype", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\use-search-logic.ts", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_hooks_use_search_logic_ts", "target": "hooks_use_search_logic_usesearchlogic", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\use-search-logic.ts", "source_location": "L9", "weight": 1.0}], "raw_calls": [{"caller_nid": "hooks_use_search_logic_usesearchlogic", "callee": "useState", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\use-search-logic.ts", "source_location": "L10"}, {"caller_nid": "hooks_use_search_logic_usesearchlogic", "callee": "useState", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\use-search-logic.ts", "source_location": "L11"}, {"caller_nid": "hooks_use_search_logic_usesearchlogic", "callee": "useEffect", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\use-search-logic.ts", "source_location": "L13"}, {"caller_nid": "hooks_use_search_logic_usesearchlogic", "callee": "isValidBarcode", "is_member_call": false, "source_file": "D:\\Documents\\Github\\nutriscan\\hooks\\use-search-logic.ts", "source_location": "L22"}]}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_components_ui_alert_dialog_tsx", "label": "alert-dialog.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L1"}, {"id": "ui_alert_dialog_alertdialogoverlay", "label": "AlertDialogOverlay", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L15"}, {"id": "ui_alert_dialog_alertdialogcontent", "label": "AlertDialogContent", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L30"}, {"id": "ui_alert_dialog_alertdialogheader", "label": "AlertDialogHeader()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L48"}, {"id": "ui_alert_dialog_alertdialogfooter", "label": "AlertDialogFooter()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L62"}, {"id": "ui_alert_dialog_alertdialogtitle", "label": "AlertDialogTitle", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L76"}, {"id": "ui_alert_dialog_alertdialogdescription", "label": "AlertDialogDescription", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L88"}, {"id": "ui_alert_dialog_alertdialogaction", "label": "AlertDialogAction", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L101"}, {"id": "ui_alert_dialog_alertdialogcancel", "label": "AlertDialogCancel", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L113"}], "edges": [{"source": "d_documents_github_nutriscan_components_ui_alert_dialog_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_alert_dialog_tsx", "target": "react_alert_dialog", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_alert_dialog_tsx", "target": "d_documents_github_nutriscan_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_alert_dialog_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_alert_dialog_tsx", "target": "d_documents_github_nutriscan_components_ui_button_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_alert_dialog_tsx", "target": "ui_button_buttonvariants", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L7", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_alert_dialog_tsx", "target": "ui_alert_dialog_alertdialogoverlay", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L15", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_alert_dialog_tsx", "target": "ui_alert_dialog_alertdialogcontent", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L30", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_alert_dialog_tsx", "target": "ui_alert_dialog_alertdialogheader", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L48", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_alert_dialog_tsx", "target": "ui_alert_dialog_alertdialogfooter", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\alert-dialog.tsx", "source_location": "L62", "weightLine truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_app_components_nutritionchart_tsx", "label": "NutritionChart.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionChart.tsx", "source_location": "L1"}, {"id": "components_nutritionchart_nutritiondata", "label": "NutritionData", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionChart.tsx", "source_location": "L6"}, {"id": "components_nutritionchart_nutritionchartprops", "label": "NutritionChartProps", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionChart.tsx", "source_location": "L16"}, {"id": "components_nutritionchart_nutritionchart", "label": "NutritionChart()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionChart.tsx", "source_location": "L21"}, {"id": "components_nutritionchart_nutritionsummaryprops", "label": "NutritionSummaryProps", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionChart.tsx", "source_location": "L165"}, {"id": "components_nutritionchart_nutritionsummary", "label": "NutritionSummary()", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionChart.tsx", "source_location": "L170"}], "edges": [{"source": "d_documents_github_nutriscan_app_components_nutritionchart_tsx", "target": "d_documents_github_nutriscan_components_ui_card_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionChart.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionchart_tsx", "target": "ui_card_card", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionChart.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionchart_tsx", "target": "ui_card_cardcontent", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionChart.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionchart_tsx", "target": "ui_card_cardheader", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionChart.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionchart_tsx", "target": "ui_card_cardtitle", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionChart.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionchart_tsx", "target": "d_documents_github_nutriscan_components_ui_badge_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionChart.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionchart_tsx", "target": "ui_badge_badge", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionChart.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionchart_tsx", "target": "components_nutritionchart_nutritiondata", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionChart.tsx", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionchart_tsx", "target": "components_nutritionchart_nutritionchartprops", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionChart.tsx", "source_location": "L16", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionchart_tsx", "target": "components_nutritionchart_nutritionchart", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionChart.tsx", "source_location": "L21", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionchart_tsx", "target": "components_nutritionchart_nutritionsummaryprops", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\app\\components\\NutritionChart.tsx", "source_location": "L165", "weight": 1.0}, {"source": "d_documents_github_nutriscan_app_components_nutritionchart_tsx", "target": "components_nutritionchart_nutritionsummary", "relation": "coLine truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "d_documents_github_nutriscan_components_ui_slider_tsx", "label": "slider.tsx", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\slider.tsx", "source_location": "L1"}, {"id": "ui_slider_slider", "label": "Slider", "file_type": "code", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\slider.tsx", "source_location": "L8"}], "edges": [{"source": "d_documents_github_nutriscan_components_ui_slider_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\slider.tsx", "source_location": "L3", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_slider_tsx", "target": "react_slider", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\slider.tsx", "source_location": "L4", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_slider_tsx", "target": "d_documents_github_nutriscan_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\slider.tsx", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_slider_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\slider.tsx", "source_location": "L6", "weight": 1.0}, {"source": "d_documents_github_nutriscan_components_ui_slider_tsx", "target": "ui_slider_slider", "relation": "contains", "confidence": "EXTRACTED", "source_file": "D:\\Documents\\Github\\nutriscan\\components\\ui\\slider.tsx", "source_location": "L8", "weight": 1.0}], "raw_calls": []}
|
||||
Loaded 100 of 163 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user