8.8 KiB
NutriScan
Busca de produtos alimentícios sobre o Open Food Facts — por código de barras ou nome, com filtros de Nutri-Score e NOVA, cache em camadas e listas de compras por ID.
Demo · Recursos · Como funciona · Execução local · Licença
Note
Estado do projeto: redesign completo concluído em 2026-07-16 (
feat: complete NutriScan redesign), seguido de correções de deploy com Prisma/SQLite e de integração do Rybbit Analytics. A instância pública do autor roda em nutriscan.evandro.dev.br. Não há suíte de testes automatizados no repositório; o único check disponível épnpm lint.
Important
Licença de Uso Pessoal: o código só pode ser usado para fins pessoais e não comerciais, apenas em localhost. É proibido fazer deploy público, redistribuir, usar comercialmente ou criar obras derivadas para distribuição (texto completo em
LICENSE). A instância pública acima é a do próprio autor.
Recursos
| Recurso | O que faz |
|---|---|
| Busca por código de barras ou nome | O tipo é detectado sozinho: entrada com 8 a 13 dígitos é tratada como código de barras, o resto como nome/marca |
| Filtros | Nutri-Score A–E, grupos NOVA 1–4, categorias, marcas, alérgenos e aditivos (incluir/excluir, ou "sem aditivos"), faixas por 100 g de energia, gordura, carboidratos, proteínas, açúcares, fibras e sódio, e países |
| 12 critérios de ordenação | Relevância, Nutri-Score, nome, marca, energia, gordura, açúcares, completude, com imagem, com dados nutricionais, Eco-Score e grupo NOVA, em ordem crescente ou decrescente |
| Cache em camadas | localStorage no navegador (TTL padrão de 1 hora) e um cache JSON no servidor (data/products-cache.json, 232 produtos no repositório) que é consultado antes do Open Food Facts |
| Modo cache-only | Em produção, o usuário pode ligar um switch para o servidor responder só com o cache JSON, sem consultar o Open Food Facts |
| País por IP | A rota /api/location descobre o país do visitante e a busca prioriza produtos dele; o resultado fica 7 dias no localStorage, com Brasil como fallback |
| Listas de compras | Várias listas acessíveis por ID em /lista/[id]; cada item tem nome, quantidade, unidade e status de comprado, persistidos com Prisma + SQLite |
| Interface | Tema claro/escuro, componentes shadcn/ui sobre Radix UI, animações com Framer Motion, skeletons, paginação e manifest de PWA |
Páginas: / (landing), /resultados, /produto/[barcode], /lista/[id] e /sobre.
Como funciona
flowchart LR
U["Navegador<br/>SearchBar · FilterSidebar"] --> L{"localStorage<br/>TTL 1 h"}
L -->|hit| R["Resultados"]
L -->|miss| A["Rotas Next.js<br/>/api/search · /api/products/[barcode]"]
A --> J{"data/products-cache.json<br/>cache JSON no servidor"}
J -->|hit| R
J -->|"miss (fora do cache-only)"| O["Open Food Facts<br/>world.openfoodfacts.org"]
O -->|salva no cache JSON| R
P["/lista/[id]"] --> S["Server Actions<br/>app/actions.ts"] --> D["Prisma + SQLite"]
Detalhes técnicos
| Peça | Onde | O que faz |
|---|---|---|
| Wrapper da API | lib/openFoodFactsApi.ts |
Timeout de 8 s por requisição, intervalo de 200 ms entre páginas, páginas de até 1000 produtos, ordenação com score de completude |
| Cache do navegador | lib/cacheManager.ts, hooks/useSearchCache.ts |
Buscas com filtros em localStorage, índice de chaves e estatísticas |
| Cache do servidor | lib/jsonCacheManager.ts |
Singleton que lê e grava data/products-cache.json com merge dos produtos |
| Sincronização | POST /api/cache/sync |
Recebe produtos do cliente e os grava no cache JSON |
| Estatísticas | GET /api/storage-stats |
Estatísticas de armazenamento do cache |
| Localização | GET /api/location |
IP via x-forwarded-for/x-real-ip → país via ip-api.com |
| Listas | app/actions.ts |
verificarLista, criarItens, listarProdutos, deletarProduto, toggleComprado |
| Imagens | next.config.mjs |
Libera static.openfoodfacts.org e images.openfoodfacts.org, formatos WebP/AVIF |
Modelos Prisma (prisma/schema.prisma, SQLite): Lista (id, timestamps) e
Produtos (nome, qtd, tipoUN, comprado, listaId).
Estrutura:
app/ App Router: landing, /resultados, /produto, /lista, /sobre
app/api/ search, products/[barcode], location, cache/sync, storage-stats
app/actions.ts Server Actions das listas de compras
lib/ wrapper da API, caches (navegador e JSON), geolocalização, Prisma
hooks/ busca, filtros, cache-only, geolocalização, monitor de storage
components/ui/ componentes shadcn/ui
data/ products-cache.json
prisma/ schema.prisma
docs/ notas da API Open Food Facts e material do redesign
Limitações conhecidas
- O switch de cache-only só existe em produção (
NODE_ENV=production); em desenvolvimento ele fica desligado. - Toda busca nova depende do Open Food Facts: o wrapper aplica timeout de 8 s e intervalo de 200 ms entre páginas, mas não há fila ou fallback além do cache.
- A localização é por IP (o servidor consulta o ip-api.com), não pela API de geolocalização do navegador.
- O código de barras é digitado ou colado; não há leitura por câmera.
- O
sitemap.xmllista só a página inicial.
Execução local
Warning
Não existe
.env.exampleepnpm db:migratenão funciona num clone novo: ele rodaprisma migrate deploy, mas/prisma/migrationsestá no.gitignore. Crie o banco compnpm db:push. ODATABASE_URLé resolvido a partir da pastaprisma/, entãofile:./db/database.sqliteviraprisma/db/database.sqlite(pasta também ignorada pelo git).
Requisitos: Node.js 18+ (o deploy usa nodejs_18) e pnpm (o deploy fixa [email protected]).
git clone https://git.evandro.dev.br/evandro/nutriscan.git
cd nutriscan
pnpm install # o postinstall já roda `prisma generate`
# .env na raiz (o Prisma lê .env, não .env.local)
echo 'DATABASE_URL="file:./db/database.sqlite"' > .env
pnpm db:push # cria o banco SQLite a partir do schema
pnpm dev # http://localhost:3000
Variáveis opcionais lidas pelo código:
| Variável | Efeito |
|---|---|
NEXT_PUBLIC_CACHE_ONLY_MODE=true |
A rota de produto responde só com o cache JSON |
NEXT_PUBLIC_RYBBIT_HOST |
Destino do proxy /analytics/* (padrão https://analytics.evandro.dev.br/api) |
| Script | Comando |
|---|---|
pnpm dev |
next dev |
pnpm build |
prisma generate && next build |
pnpm start |
next start |
pnpm lint |
next lint |
pnpm db:push |
prisma db push |
pnpm db:generate |
prisma generate |
pnpm db:migrate |
prisma migrate deploy (exige migrações locais, veja o aviso acima) |
Deploy
O repositório traz duas configurações, sem workflow de CI:
dokploy.yaml: instala o pnpm, rodapnpm install --no-frozen-lockfile,pnpx prisma generateepnpm run build.nixpacks.toml:nodejs_18+openssl,[email protected],DATABASE_URL=file:./db/database.sqlite,prisma db push --accept-data-loss, build epnpm start.
Licença
Licença de Uso Pessoal, em LICENSE (Copyright (c) 2024 Evandro): uso
pessoal, não comercial e local; sem deploy público, redistribuição ou obras
derivadas. Dúvidas sobre a licença: [email protected].
Bugs e sugestões: issues do repositório. Autor: evandro.dev.br.
Avisos
- Analytics: o app carrega o Rybbit Analytics (self-hosted, via proxy
/analytics/) com session replay e rastreamento de erros. - Dados externos: a localização envia o IP do visitante, pelo servidor, ao ip-api.com; os dados de produtos vêm do Open Food Facts.
- Doação: o app exibe um card de doação via Buy Me a Coffee.
- Créditos: Open Food Facts, Next.js, Prisma, shadcn/ui e Radix UI.