feat: complete NutriScan redesign

This commit is contained in:
evandrodevbr committed 2026-07-16 16:31:50 -03:00
1 parent c1fe4af997
commit f71cd167b3
163 files changed
+30802 -14169

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: ["next/core-web-vitals", "next/typescript"],
};
-3
View File
@@ -1,3 +0,0 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}
+5 -3
View File
@@ -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
+21
View File
@@ -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
+2 -2
View File
@@ -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";
+2 -2
View File
@@ -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">
+1 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
);
}
}
+26 -9
View File
@@ -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>
+25 -19
View File
@@ -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
View File
@@ -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>
);
+61 -187
View File
@@ -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
View File
@@ -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
View File
@@ -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} />;
}
+57 -129
View File
@@ -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>
+46 -56
View File
@@ -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
View File
@@ -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>
);
}
}
+79 -65
View File
@@ -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">
&quot;Como desenvolvedor independente, cada contribuição me permite dedicar mais horas à refatoração e novas features para o NutriScan.&quot;
{/* 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">
&ldquo;Cada contribuição me permite dedicar mais horas ao{" "}
<em className="font-display-i">NutriScan</em>.&rdquo;
</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>
);
}
}
+96 -118
View File
@@ -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
View File
@@ -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">&lt; 10s</span>
<span className="text-[9px] uppercase tracking-[0.3em] font-black text-gray-500 dark:text-slate-500 transition-colors duration-500">Analysis Speed</span>
</div>
{/* 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>
);
}
}
+111 -131
View File
@@ -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 &amp; 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 &amp; 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>
);
}
+121 -104
View File
@@ -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>
);
}
+34 -44
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+80 -83
View File
@@ -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>
);
}
}
+82 -48
View File
@@ -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>
);
+63 -99
View File
@@ -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>
);
}
}
+76 -133
View File
@@ -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>
);
}
+68 -97
View File
@@ -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">
&ldquo;Dados abertos são a fundação de uma sociedade saudável.&rdquo;
</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">
&ldquo;Dados abertos são a fundação de uma sociedade saudável.&rdquo;
</blockquote>
<div className="hairline mt-10" />
</div>
</div>
</section>
);
}
}
+63 -88
View File
@@ -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>
);
+43 -123
View File
@@ -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>
);
}
+70 -115
View File
@@ -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>
);
}
}
+82 -126
View File
@@ -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
View File
@@ -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 />
+2505
View File
File diff suppressed because it is too large. Load diff
+13 -4
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
not found
+142
View File
@@ -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*
+179
View File
@@ -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

+16
View File
@@ -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
+529
View File
@@ -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
}
}
@@ -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"}]}
@@ -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"}]}
@@ -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"}]}
@@ -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
@@ -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
@@ -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
@@ -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": []}
@@ -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
@@ -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"}]}
@@ -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"}]}
@@ -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": []}
@@ -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
@@ -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": []}
@@ -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": []}
@@ -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": []}
@@ -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"}]}
@@ -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"}]}
@@ -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
@@ -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"}]}
@@ -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
@@ -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
@@ -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"}]}
@@ -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"}]}
@@ -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"}]}
@@ -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": []}
@@ -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"}]}
@@ -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": []}
@@ -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
@@ -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": []}
@@ -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": []}
@@ -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
@@ -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"}]}
@@ -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": []}
@@ -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"}]}
@@ -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
@@ -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": []}
@@ -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"}]}
@@ -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
@@ -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
@@ -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
@@ -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"}]}
@@ -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"}]}
@@ -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"}]}
@@ -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
@@ -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
@@ -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"}]}
@@ -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
@@ -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
@@ -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