Files
nutriscan/README.md
T

165 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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.**
![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)
</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/).