Ícone do NutriScan # 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.** ![Next.js](https://img.shields.io/badge/Next.js-%5E15.5.10-informational) ![TypeScript](https://img.shields.io/badge/TypeScript-%5E5.9.3-informational) ![Licença](https://img.shields.io/badge/licen%C3%A7a-uso_pessoal-informational) [Demo](https://nutriscan.evandro.dev.br) · [Recursos](#recursos) · [Como funciona](#como-funciona) · [Execução local](#execu%C3%A7%C3%A3o-local) · [Licença](#licen%C3%A7a)
> [!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](https://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`](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 ```mermaid flowchart LR U["Navegador
SearchBar · FilterSidebar"] --> L{"localStorage
TTL 1 h"} L -->|hit| R["Resultados"] L -->|miss| A["Rotas Next.js
/api/search · /api/products/[barcode]"] A --> J{"data/products-cache.json
cache JSON no servidor"} J -->|hit| R J -->|"miss (fora do cache-only)"| O["Open Food Facts
world.openfoodfacts.org"] O -->|salva no cache JSON| R P["/lista/[id]"] --> S["Server Actions
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: ```text 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.xml` lista só a página inicial. ## Execução local > [!WARNING] > **Não existe `.env.example`** e **`pnpm db:migrate` não funciona num clone novo**: > ele roda `prisma migrate deploy`, mas `/prisma/migrations` está no `.gitignore`. > Crie o banco com `pnpm db:push`. O `DATABASE_URL` é resolvido a partir da pasta > `prisma/`, então `file:./db/database.sqlite` vira `prisma/db/database.sqlite` > (pasta também ignorada pelo git). Requisitos: Node.js 18+ (o deploy usa `nodejs_18`) e pnpm (o deploy fixa `pnpm@10.34.5`). ```bash 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`](dokploy.yaml): instala o pnpm, roda `pnpm install --no-frozen-lockfile`, `pnpx prisma generate` e `pnpm run build`. - [`nixpacks.toml`](nixpacks.toml): `nodejs_18` + `openssl`, `pnpm@10.34.5`, `DATABASE_URL=file:./db/database.sqlite`, `prisma db push --accept-data-loss`, build e `pnpm start`. ## Licença Licença de Uso Pessoal, em [`LICENSE`](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: evandro@evandro.dev.br. Bugs e sugestões: [issues do repositório](https://git.evandro.dev.br/evandro/nutriscan/issues). Autor: [evandro.dev.br](https://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](https://ip-api.com); os dados de produtos vêm do [Open Food Facts](https://world.openfoodfacts.org/). - **Doação:** o app exibe um card de doação via [Buy Me a Coffee](https://buymeacoffee.com/evandrotruuta). - **Créditos:** [Open Food Facts](https://world.openfoodfacts.org/), [Next.js](https://nextjs.org/), [Prisma](https://www.prisma.io/), [shadcn/ui](https://ui.shadcn.com/) e [Radix UI](https://www.radix-ui.com/).