From a89251d7e06a057d7e2d3b0566bafc899e3b2818 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sat, 24 Oct 2026 04:46:46 -0300 Subject: [PATCH] docs(readme): redesenha o README com hero em SVG [skip ci] --- README.md | 171 ++++++++++++++--------------------------- assets/readme/hero.svg | 58 ++++++++++++++ 2 files changed, 116 insertions(+), 113 deletions(-) create mode 100644 assets/readme/hero.svg diff --git a/README.md b/README.md index b2977ac..2c13605 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,54 @@
-Ícone do NutriScan +NutriScan: busca de alimentos no Open Food Facts por código de barras ou nome, mostrando um produto com Nutri-Score D e NOVA grupo 4 -# NutriScan +![Next.js 15.5](https://img.shields.io/badge/Next.js-15.5-317257?style=flat-square) +![React 19](https://img.shields.io/badge/React-19-317257?style=flat-square) +![Prisma 6 + SQLite](https://img.shields.io/badge/Prisma_6-SQLite-317257?style=flat-square) +![Licença: uso pessoal](https://img.shields.io/badge/licen%C3%A7a-uso_pessoal-317257?style=flat-square) -**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) +[Notas da API Open Food Facts](docs/openFoodFacts.md) · [Issues](https://git.evandro.dev.br/evandro/nutriscan/issues)
-> [!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`. +O cache JSON do servidor vem no repositório (`data/products-cache.json`) com produtos reais do Open Food Facts. Contado por Nutri-Score, num clone novo e sem instalar nada: -> [!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. +```console +$ node -e "const p = Object.values(require('./data/products-cache.json').products); const g = {}; for (const x of p) g[x.nutrition_grades] = (g[x.nutrition_grades] || 0) + 1; console.log(p.length, g)" +232 { e: 26, unknown: 167, d: 24, c: 9, a: 2, b: 4 } +``` -## Recursos +## O que é + +NutriScan é um app web em Next.js para buscar produtos alimentícios no [Open Food Facts](https://world.openfoodfacts.org/) e montar listas de compras. A entrada é detectada sozinha: 8 a 13 dígitos viram busca por código de barras; o resto, busca por nome ou marca. + +Páginas: `/` (landing), `/resultados`, `/produto/[barcode]`, `/lista/[id]` e `/sobre`. | 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`. +## Por que usar + +- **Menos chamadas ao Open Food Facts.** A busca passa por dois caches antes da API: `localStorage` no navegador (TTL padrão de 1 hora) e o cache JSON no servidor. Tudo que vem da API é gravado no cache JSON. +- **Funciona sem a API.** No modo cache-only o servidor responde só com o cache JSON, sem consultar o Open Food Facts. +- **Filtros combinados**: Nutri-Score, NOVA, alérgenos, aditivos e faixas de nutrientes ao mesmo tempo, com 12 ordenações. + +Quando não usar: se você precisa publicar o app ou usá-lo comercialmente (a licença só permite uso pessoal em localhost), ler código de barras pela câmera (a entrada é digitada ou colada) ou esperar Nutri-Score em cada produto (no cache acima, 167 de 232 produtos vieram sem nota). ## 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"] -``` +1. O navegador procura a busca no `localStorage` (`lib/cacheManager.ts`, `hooks/useSearchCache.ts`). +2. Se não achar, chama as rotas Next.js `/api/search` e `/api/products/[barcode]`. +3. As rotas consultam o cache JSON do servidor (`lib/jsonCacheManager.ts`, `data/products-cache.json`). +4. Fora do modo cache-only, o que falta vem do Open Food Facts (`lib/openFoodFactsApi.ts`: timeout de 8 s, 200 ms entre páginas, até 1000 produtos por página) e é gravado no cache JSON. `POST /api/cache/sync` também grava produtos enviados pelo cliente. +5. As listas de compras usam Server Actions (`app/actions.ts`) sobre Prisma + SQLite: modelos `Lista` e `Produtos` em `prisma/schema.prisma`. -
-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). +## Início rápido Requisitos: Node.js 18+ (o deploy usa `nodejs_18`) e pnpm (o deploy fixa `pnpm@10.34.5`). @@ -123,42 +64,46 @@ pnpm db:push # cria o banco SQLite a partir do schema pnpm dev # http://localhost:3000 ``` -Variáveis opcionais lidas pelo código: +> [!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). + +Outros scripts: `pnpm build` (`prisma generate && next build`), `pnpm start`, `pnpm lint` (`next lint`), `pnpm db:generate`. + +## Configuração | Variável | Efeito | |---|---| -| `NEXT_PUBLIC_CACHE_ONLY_MODE=true` | A rota de produto responde só com o cache JSON | +| `DATABASE_URL` | Banco SQLite das listas de compras (obrigatória) | +| `NEXT_PUBLIC_CACHE_ONLY_MODE=true` | As rotas de busca e de produto respondem 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) | +O switch de cache-only na interface só funciona em produção (`NODE_ENV=production`); as rotas `/api/search` e `/api/products/[barcode]` aceitam `?cache_only=true` em qualquer ambiente. -## Deploy +O que sai do seu servidor: -O repositório traz duas configurações, sem workflow de CI: +- **Open Food Facts**: dados e imagens de produtos (`next.config.mjs` libera `static.openfoodfacts.org` e `images.openfoodfacts.org`). +- **ip-api.com**: o IP do visitante, enviado pelo servidor por HTTP, para descobrir o país. +- **Rybbit Analytics** (self-hosted, via proxy `/analytics/`): com session replay e rastreamento de erros. +- O app também exibe um card de doação via [Buy Me a Coffee](https://buymeacoffee.com/evandrotruuta). -- [`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`. +## Estado do projeto + +Projeto pessoal, sem suíte de testes; o único check é `pnpm lint`. O último trabalho de código foi o redesign completo e os ajustes no deploy com Prisma/SQLite, em 2026-07-16. O repositório traz [`dokploy.yaml`](dokploy.yaml) e [`nixpacks.toml`](nixpacks.toml) para o deploy do autor, sem workflow de CI. A instância pública do autor em `nutriscan.evandro.dev.br` não respondia com o app em 2026-10-24. + +Limites conhecidos: + +- Toda busca nova depende do Open Food Facts; além do cache, não há fila nem fallback. +- A localização é por IP, não pela API de geolocalização do navegador. +- O `sitemap.xml` lista só a página inicial. ## 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. +Licença de Uso Pessoal, em [`LICENSE`](LICENSE) (Copyright (c) 2024 Evandro): uso pessoal, não comercial e só em localhost. É proibido fazer deploy público, redistribuir, usar comercialmente ou criar obras derivadas para distribuição. 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). +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/). -## 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/). +Feito por Evandro Fonseca Junior · git.evandro.dev.br + +
diff --git a/assets/readme/hero.svg b/assets/readme/hero.svg new file mode 100644 index 0000000..fda65ec --- /dev/null +++ b/assets/readme/hero.svg @@ -0,0 +1,58 @@ + + NutriScan + NutriScan busca produtos alimentícios no Open Food Facts por código de barras ou nome. À direita, a busca pelo código 7891962032450 devolve o Biscoito Maizena Bauducco com Nutri-Score D e NOVA grupo 4, dados reais do cache do repositório. + + + + + Open Food Facts · busca e listas + NutriScan + Busque alimentos por código de barras + ou nome e compare Nutri-Score e NOVA. + Next.js 15 · Prisma + SQLite + + + + + + + + + + + + + + + + + + + 7891962032450 + EAN-13 + + + Biscoito Maizena + Bauducco · pacote 170 g + + + NUTRI-SCORE + + A + + B + + C + + D + + E + + + + NOVA + + grupo 4 + + +