165 lines
8.8 KiB
Markdown
165 lines
8.8 KiB
Markdown
<div align="center">
|
||
|
||
<img src="public/apple-touch-icon.png" width="110" alt="Í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.**
|
||
|
||

|
||

|
||

|
||
|
||
[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)
|
||
|
||
</div>
|
||
|
||
> [!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<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"]
|
||
```
|
||
|
||
<details>
|
||
<summary>Detalhes técnicos</summary>
|
||
|
||
| 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
|
||
```
|
||
|
||
</details>
|
||
|
||
## 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 `[email protected]`).
|
||
|
||
```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`, `[email protected]`, `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/).
|