Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc7243e5f8 | ||
|
|
fa07ba8910 | ||
|
|
f037001339 | ||
|
|
7d4efce025 | ||
|
|
15ddbfbde9 |
No files matched your search
@@ -0,0 +1,12 @@
|
|||||||
|
# Banco de dados SQLite usado pelo Prisma.
|
||||||
|
# O caminho é relativo à pasta prisma/ (onde fica o schema), então este valor
|
||||||
|
# aponta para prisma/prisma/db/database.sqlite.
|
||||||
|
DATABASE_URL="file:./prisma/db/database.sqlite"
|
||||||
|
|
||||||
|
# Opcional. Quando "true", a aplicação responde apenas com o cache local
|
||||||
|
# (data/products-cache.json) e não chama a API do Open Food Facts.
|
||||||
|
# NEXT_PUBLIC_CACHE_ONLY_MODE="false"
|
||||||
|
|
||||||
|
# Opcional. Origem do proxy de analytics (Rybbit) configurado em next.config.mjs.
|
||||||
|
# Padrão quando vazio: https://analytics.evandro.dev.br/api
|
||||||
|
# NEXT_PUBLIC_RYBBIT_HOST="https://analytics.evandro.dev.br/api"
|
||||||
@@ -1,539 +1,261 @@
|
|||||||
# NutriScan
|
# NutriScan
|
||||||
|
|
||||||
<div align="center">
|
**Consulta nutricional de produtos de supermercado: busca por nome ou código de barras na base colaborativa do Open Food Facts, mostra Nutri-Score e grau de processamento (NOVA) e organiza listas de compras, com cache local para continuar funcionando sem internet.**
|
||||||
|
|
||||||

|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
**Plataforma completa de informações nutricionais alimentares**
|
Demonstração publicada pelo autor: <https://nutriscan.evandro.dev.br>
|
||||||
|
|
||||||
_Descubra o que a comunidade alimentar sabe sobre seus alimentos_
|
## Sobre
|
||||||
|
|
||||||
[](https://nextjs.org/)
|
Saber o que um produto industrializado tem de verdade exige consultar bases públicas e cruzar informação espalhada: nota Nutri-Score, grau de processamento, alérgenos, aditivos e tabela nutricional. O NutriScan junta isso em uma interface só.
|
||||||
[](https://www.typescriptlang.org/)
|
|
||||||
[](https://www.prisma.io/)
|
|
||||||
[](https://tailwindcss.com/)
|
|
||||||
[](https://world.openfoodfacts.org/)
|
|
||||||
|
|
||||||
[](https://nutriscan.evandro.dev.br)
|
A aplicação consulta a base do Open Food Facts (mais de 2 milhões de produtos), aceita busca por texto ou por código de barras (8 a 13 dígitos, com detecção automática do tipo) e exibe Nutri-Score e grau de processamento NOVA de cada produto. O painel de filtros avançados oferece classificação nutricional, categorias, marcas, alérgenos, aditivos e faixas de valores nutricionais, com 12 opções de ordenação. Além da consulta, mantém listas de compras persistidas em SQLite.
|
||||||
[](https://github.com/evandrodevbr/lista-compras/issues)
|
|
||||||
[](https://github.com/evandrodevbr/lista-compras/issues)
|
|
||||||
|
|
||||||
</div>
|
O ponto de arquitetura que diferencia o projeto é o cache em duas camadas: um arquivo JSON no servidor, versionado no repositório, com os produtos já vistos, e um cache no `localStorage` do navegador. Com o modo cache-only ligado, a aplicação responde apenas com o cache local, sem chamar a API externa.
|
||||||
|
|
||||||
## 📋 Índice
|
## Como funciona
|
||||||
|
|
||||||
- [Sobre o Projeto](#-sobre-o-projeto)
|
```
|
||||||
- [Demonstração Visual](#-demonstração-visual)
|
navegador
|
||||||
- [Funcionalidades](#-funcionalidades)
|
│
|
||||||
- [Stack Tecnológica](#-stack-tecnológica)
|
├─ páginas Next.js (App Router, Server Components + Client Components)
|
||||||
- [Arquitetura](#-arquitetura)
|
│
|
||||||
- [Instalação e Setup](#-instalação-e-setup)
|
├─ /api/search ──────────────▶ data/products-cache.json (cache do servidor, 232 produtos)
|
||||||
- [API e Integrações](#-api-e-integrações)
|
│ └─ se faltam resultados ─▶ Open Food Facts (world.openfoodfacts.org/cgi/search.pl)
|
||||||
- [Database Schema](#-database-schema)
|
├─ /api/products/[barcode] ──▶ cache JSON ─▶ Open Food Facts (api/v2/product/{code}.json)
|
||||||
- [Performance e Otimizações](#-performance-e-otimizações)
|
│ └─ produto novo é gravado no cache JSON
|
||||||
- [Deployment](#-deployment)
|
├─ /api/location ────────────▶ headers do proxy ou api.ipify.org (país do usuário)
|
||||||
- [Scripts Disponíveis](#-scripts-disponíveis)
|
└─ /api/storage-stats ───────▶ estatísticas do cache JSON
|
||||||
- [Contribuindo](#-contribuindo)
|
|
||||||
- [Licença](#-licença)
|
|
||||||
- [Contato](#-contato)
|
|
||||||
- [Agradecimentos](#-agradecimentos)
|
|
||||||
|
|
||||||
## 🚀 Sobre o Projeto
|
listas de compras: componentes ─▶ Server Actions (app/actions.ts) ─▶ Prisma ─▶ SQLite
|
||||||
|
cache do cliente: resultados de busca e filtros no localStorage (TTL configurável)
|
||||||
|
```
|
||||||
|
|
||||||
**NutriScan** é uma plataforma web moderna que democratiza o acesso a informações nutricionais transparentes e confiáveis. Desenvolvida com foco na experiência do usuário e performance, a aplicação conecta consumidores conscientes à maior base de dados colaborativa de alimentos do mundo.
|
- Produto não encontrado no cache JSON é buscado na API e salvo no cache, o que aumenta o acervo offline a cada uso.
|
||||||
|
- A rota `/api/search` pagina em blocos de 1000 resultados na API externa, com pausa de 200 ms entre requisições.
|
||||||
|
- Não há autenticação nem multiusuário: cada lista de compras é identificada por um id na URL.
|
||||||
|
|
||||||
### 🎯 Missão
|
## Stack
|
||||||
|
|
||||||
Empoderar usuários com dados nutricionais precisos e abrangentes para tomada de decisões alimentares mais conscientes através de uma plataforma intuitiva, rápida e confiável.
|
| Camada | Escolha |
|
||||||
|
|---|---|
|
||||||
|
| Framework | Next.js 15 (App Router, Server Actions, Route Handlers) |
|
||||||
|
| UI | React 19, shadcn/ui sobre Radix UI, Tailwind CSS 3.4, Framer Motion 11, lucide-react 0.460 |
|
||||||
|
| Tema | next-themes 0.4 (claro e escuro, persistido) |
|
||||||
|
| Linguagem | TypeScript 5.9 em modo `strict` |
|
||||||
|
| Listas de compras | SQLite com Prisma 6 (`prisma/schema.prisma`) |
|
||||||
|
| Dados de produtos | API pública do Open Food Facts + cache JSON em `data/` |
|
||||||
|
| Cache do cliente | `localStorage`, com modo cache-only |
|
||||||
|
| Pacotes | pnpm (lockfile versionado) |
|
||||||
|
| Deploy | nixpacks (`nixpacks.toml`) e Dokploy (`dokploy.yaml`) |
|
||||||
|
|
||||||
### ⭐ Diferenciais
|
## Requisitos
|
||||||
|
|
||||||
- **Dados Colaborativos**: Acesso a milhões de produtos da maior base de dados open source do mundo
|
- Node.js `>=18.18` (o deploy fixa Node 18; o build foi verificado em Node 24.20.0)
|
||||||
- **Filtros Avançados**: Sistema sofisticado de busca e filtros para descoberta precisa de produtos
|
- pnpm 10 ou superior (o deploy fixa 10.34.5; a instalação foi verificada com pnpm 12.4.1 e a resolução de dependências foi verificada com pnpm 10.34.5)
|
||||||
- **Classificações de Saúde**: Nutri-Score e NOVA group para avaliação rápida da qualidade
|
- Nenhum serviço externo é obrigatório para subir a aplicação: sem internet ela responde pelo cache local
|
||||||
- **Atualizações em Tempo Real**: Sincronização de dados ao vivo e resultados instantâneos
|
|
||||||
- **Arquitetura Moderna**: Desenvolvida com tecnologias de ponta e foco em performance
|
|
||||||
|
|
||||||
### 📊 Estatísticas
|
## Início rápido
|
||||||
|
|
||||||
| Métrica | Valor |
|
```bash
|
||||||
| ------------------- | ----- |
|
git clone https://github.com/evandrodevbr/nutriscan.git
|
||||||
| Produtos no banco | 2M+ |
|
cd nutriscan
|
||||||
| Países cobertos | 100+ |
|
|
||||||
| Disponibilidade | 24/7 |
|
|
||||||
| Dados colaborativos | Sim |
|
|
||||||
|
|
||||||
## 📱 Demonstração Visual
|
pnpm install # o postinstall roda `prisma generate`
|
||||||
|
cp .env.example .env
|
||||||
|
pnpm db:push # cria as tabelas Lista e Produtos no SQLite
|
||||||
|
pnpm dev # http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
> **Nota**: Screenshots serão adicionados em breve mostrando as principais funcionalidades da aplicação.
|
Duas pegadinhas que quebram a primeira execução e estão resolvidas acima:
|
||||||
|
|
||||||
### Principais Telas
|
- O arquivo de ambiente precisa se chamar `.env`, não `.env.local`: a CLI do Prisma (`prisma generate`, `prisma db push`) só lê `.env`. O Next.js lê os dois.
|
||||||
|
- `pnpm db:migrate` roda `prisma migrate deploy` e não cria nada aqui, porque o repositório não tem a pasta `prisma/migrations`. Quem cria o schema é `pnpm db:push`.
|
||||||
|
|
||||||
- **Página Inicial**: Interface de busca com hero section e barra de pesquisa
|
Com o servidor no ar:
|
||||||
- **Resultados**: Lista de produtos com filtros avançados e ordenação
|
|
||||||
- **Detalhes do Produto**: Informações nutricionais completas e classificações
|
|
||||||
- **Lista de Compras**: Gerenciamento de produtos com quantidades e status
|
|
||||||
- **Modos de Tema**: Visualização em tema claro e escuro
|
|
||||||
|
|
||||||
## ✨ Funcionalidades
|
```bash
|
||||||
|
curl -s "http://localhost:3000/api/search?q=bis&cache_only=true&page_size=5"
|
||||||
|
curl -s http://localhost:3000/api/storage-stats
|
||||||
|
```
|
||||||
|
|
||||||
### 🔍 Sistema de Busca Inteligente
|
## Uso
|
||||||
|
|
||||||
O NutriScan oferece múltiplas formas de encontrar produtos alimentícios:
|
| Rota | O que é |
|
||||||
|
|---|---|
|
||||||
|
| `/` | Página inicial com busca por nome ou código de barras |
|
||||||
|
| `/resultados?q=<termo>` | Resultados com filtros, ordenação e paginação |
|
||||||
|
| `/produto/<barcode>` | Detalhe do produto: Nutri-Score, grupo NOVA, composição por 100 g e lista de ingredientes |
|
||||||
|
| `/lista/<id>` | Lista de compras: adicionar item com quantidade e unidade, marcar como comprado, remover |
|
||||||
|
| `/sobre` | Página institucional do projeto |
|
||||||
|
| `/robots.txt`, `/sitemap.xml` | Gerados pelo Next.js |
|
||||||
|
|
||||||
- **Busca por Código de Barras**: Reconhecimento automático de códigos de 8-13 dígitos
|
## API
|
||||||
- **Busca por Nome/Marca**: Pesquisa textual inteligente com sugestões
|
|
||||||
- **Detecção Automática**: Sistema que identifica o tipo de busca automaticamente
|
|
||||||
- **Geolocalização**: Priorização de produtos locais baseada na localização do usuário
|
|
||||||
- **API Proxy**: Chamadas seguras através de rotas Next.js para evitar problemas de CORS
|
|
||||||
|
|
||||||
### 💾 Sistema de Cache Avançado
|
Todas as rotas ficam em `app/api` e são Route Handlers do Next.js.
|
||||||
|
|
||||||
Sistema multi-camada de cache para performance otimizada:
|
| Rota | Método | O que faz |
|
||||||
|
|---|---|---|
|
||||||
|
| `/api/search` | GET | Busca produtos no cache JSON e, se faltar, no Open Food Facts |
|
||||||
|
| `/api/products/[barcode]` | GET | Busca um produto pelo código de barras (cache primeiro, depois API) |
|
||||||
|
| `/api/location` | GET | País estimado a partir do IP (`x-forwarded-for`, `x-real-ip` ou api.ipify.org) |
|
||||||
|
| `/api/cache/sync` | POST | Grava um lote de produtos no cache JSON. Corpo: `{ "products": [...] }` |
|
||||||
|
| `/api/storage-stats` | GET | Total de produtos, tamanho do arquivo e data da última atualização do cache |
|
||||||
|
|
||||||
- **Cache localStorage**: Armazenamento local com TTL configurável
|
Parâmetros de `/api/search`:
|
||||||
- **Cache de Buscas**: Armazenamento completo de buscas com filtros aplicados
|
|
||||||
- **Cache JSON**: 218 produtos pré-carregados para uso offline
|
|
||||||
- **Modo Cache-Only**: Funcionamento completo sem conexão com internet
|
|
||||||
- **Sincronização Inteligente**: Atualização automática de dados quando online
|
|
||||||
- **Estatísticas de Storage**: Monitoramento de uso de armazenamento
|
|
||||||
|
|
||||||
### 🎛️ Filtros e Ordenação
|
| Parâmetro | Valores | Observação |
|
||||||
|
|---|---|---|
|
||||||
|
| `q` | texto | Obrigatório; sem ele a rota responde 400 |
|
||||||
|
| `page`, `page_size` | inteiros `>= 1` | Padrão 1 e 20; valores inválidos respondem 400 |
|
||||||
|
| `cache_only` | `true` | Responde só com o cache local, sem chamar a API externa |
|
||||||
|
| `country` | código do país | Enviado como `countries_tags_en` |
|
||||||
|
| `countries`, `categories`, `brands` | tags | Filtros repassados como `countries_tags`, `categories_tags`, `brands_tags` |
|
||||||
|
| `nutrition_grades`, `nova_groups` | `a` a `e`, `1` a `4` | Filtros de classificação |
|
||||||
|
| `allergens`, `exclude_allergens`, `additives`, `exclude_additives`, `no_additives` | tags, `true` | Filtros de composição |
|
||||||
|
| `energy_min`, `energy_max`, `fat_min`, `fat_max`, `carbohydrates_min`, `carbohydrates_max`, `proteins_min`, `proteins_max`, `sugars_min`, `sugars_max`, `fiber_min`, `fiber_max`, `sodium_min`, `sodium_max` | número (por 100 g) | Faixas nutricionais |
|
||||||
|
| `sort_by` | `nutrition_grade`, `name`, `brand`, `energy`, `fat`, `sugars` | Ordenação; o padrão é `popularity` |
|
||||||
|
| `sort_order` | `asc`, `desc` | Padrão `asc` |
|
||||||
|
|
||||||
Sistema robusto de filtros para descoberta precisa:
|
No modo `cache_only` a busca considera apenas texto (nome, marca, categoria) e país; os demais filtros só são repassados para o Open Food Facts nas buscas online.
|
||||||
|
|
||||||
#### Classificações de Saúde
|
Exemplo de resposta de `/api/search`:
|
||||||
|
|
||||||
- **Nutri-Score**: Filtro por classificação A-E (muito bom a muito ruim)
|
```json
|
||||||
- **NOVA Group**: Filtro por nível de processamento 1-4 (não processado a ultraprocessado)
|
{
|
||||||
|
"products": [{ "code": "7622210575975", "product_name": "Bis", "brands": "Lacta" }],
|
||||||
|
"count": 5,
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 5,
|
||||||
|
"fromCache": true,
|
||||||
|
"fromAPI": false,
|
||||||
|
"cacheStats": { "localProducts": 5, "apiProducts": 0, "newProducts": 0, "totalReturned": 5 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### Filtros de Produto
|
## Produção
|
||||||
|
|
||||||
- **Categorias**: Filtro por tipo de alimento (bebidas, laticínios, snacks, etc.)
|
```bash
|
||||||
- **Marcas**: Filtro por fabricante
|
pnpm build # prisma generate + next build
|
||||||
- **Alérgenos**: Identificação e filtro por substâncias alergênicas
|
pnpm start # next start, porta 3000 (use -p <porta> para trocar)
|
||||||
- **Aditivos**: Filtro por aditivos alimentares e conservantes
|
```
|
||||||
|
|
||||||
#### Filtros Nutricionais
|
O `build` precisa de `DATABASE_URL` no ambiente para gerar o Prisma Client, e o `start` precisa dela para as rotas de lista de compras.
|
||||||
|
|
||||||
- **Ranges Personalizados**: Filtros por valores nutricionais (calorias, gorduras, açúcares, etc.)
|
## Deploy
|
||||||
- **12 Opções de Ordenação**: Por relevância, nutrição, nome, marca, valores nutricionais, etc.
|
|
||||||
|
|
||||||
### 🛒 Listas de Compras Colaborativas
|
O repositório traz duas configurações de build, ambas para plataformas que usam nixpacks:
|
||||||
|
|
||||||
Sistema completo de gerenciamento de compras:
|
- `nixpacks.toml`: Node 18 e pnpm 10.34.5, instala com `--no-frozen-lockfile`, roda `prisma generate`, `prisma db push --accept-data-loss` e o build, com `DATABASE_URL=file:./db/database.sqlite`. O start é `pnpm start` com a mesma variável.
|
||||||
|
- `dokploy.yaml`: repete os passos de instalação (`pnpm install --no-frozen-lockfile`), geração do Prisma Client e build.
|
||||||
|
|
||||||
- **Múltiplas Listas**: Criação e gerenciamento de várias listas de compras
|
Como o `DATABASE_URL` do deploy aponta para `file:./db/database.sqlite` e o caminho relativo do SQLite é resolvido a partir da pasta `prisma/`, o banco de produção fica em `prisma/db/database.sqlite` no contêiner, separado do arquivo versionado no repositório.
|
||||||
- **Produtos Detalhados**: Adição com quantidade, unidade e observações
|
|
||||||
- **Status de Compra**: Marcação de itens como comprados
|
|
||||||
- **Sincronização**: Persistência via Prisma + SQLite
|
|
||||||
- **Compartilhamento**: Compartilhamento de listas via ID único
|
|
||||||
|
|
||||||
### 🎨 Interface e Experiência do Usuário
|
## Estrutura do projeto
|
||||||
|
|
||||||
Design moderno e acessível:
|
|
||||||
|
|
||||||
- **Design Responsivo**: Mobile-first com adaptação para todos os dispositivos
|
|
||||||
- **Temas**: Modo claro e escuro com persistência de preferência
|
|
||||||
- **Animações**: Transições suaves com Framer Motion
|
|
||||||
- **Acessibilidade**: Componentes Radix UI com suporte completo a leitores de tela
|
|
||||||
- **Estados de Loading**: Skeletons e indicadores de carregamento
|
|
||||||
- **Paginação Otimizada**: Carregamento eficiente de grandes listas
|
|
||||||
- **Imagens Otimizadas**: Lazy loading e otimização automática
|
|
||||||
|
|
||||||
## 🛠 Stack Tecnológica
|
|
||||||
|
|
||||||
### Framework Principal
|
|
||||||
|
|
||||||
- **[Next.js 14.2.18](https://nextjs.org/)** - React framework com App Router e Server Actions
|
|
||||||
- **[TypeScript 5.7.2](https://www.typescriptlang.org/)** - JavaScript tipado para maior confiabilidade
|
|
||||||
- **[React 18.3.1](https://react.dev/)** - Biblioteca de interface moderna com recursos concorrentes
|
|
||||||
|
|
||||||
### Banco de Dados e ORM
|
|
||||||
|
|
||||||
- **[Prisma 5.22.0](https://www.prisma.io/)** - ORM type-safe para gerenciamento de dados
|
|
||||||
- **[SQLite](https://www.sqlite.org/)** - Banco de dados leve e serverless
|
|
||||||
|
|
||||||
### Estilização e UI
|
|
||||||
|
|
||||||
- **[Tailwind CSS 3.4.15](https://tailwindcss.com/)** - Framework CSS utility-first
|
|
||||||
- **[shadcn/ui](https://ui.shadcn.com/)** - Biblioteca de componentes reutilizáveis
|
|
||||||
- **[Radix UI](https://www.radix-ui.com/)** - Primitivos acessíveis não estilizados
|
|
||||||
- **[Lucide Icons 0.460.0](https://lucide.dev/)** - Ícones bonitos e customizáveis
|
|
||||||
- **[Framer Motion 11.12.0](https://www.framer.com/motion/)** - Biblioteca de animações
|
|
||||||
|
|
||||||
### APIs Externas
|
|
||||||
|
|
||||||
- **[Open Food Facts](https://world.openfoodfacts.org/)** - Base de dados global de produtos alimentícios
|
|
||||||
- **Geolocation API** - Recomendações baseadas em localização
|
|
||||||
|
|
||||||
### Ferramentas de Desenvolvimento
|
|
||||||
|
|
||||||
- **[ESLint 8.57.1](https://eslint.org/)** - Linting e formatação de código
|
|
||||||
- **[PostCSS 8.4.49](https://postcss.org/)** - Processamento de CSS
|
|
||||||
- **[pnpm](https://pnpm.io/)** - Gerenciador de pacotes eficiente
|
|
||||||
|
|
||||||
## 🏗 Arquitetura
|
|
||||||
|
|
||||||
### Estrutura do App Router
|
|
||||||
|
|
||||||
```
|
```
|
||||||
app/
|
app/
|
||||||
├── actions.ts # Server Actions para mutações de dados
|
├── page.tsx página inicial (hero, recursos, como funciona, doação)
|
||||||
├── api/ # Rotas de API para proxies de serviços externos
|
├── sobre/ página institucional
|
||||||
│ ├── cache/sync/ # Sincronização de cache
|
├── resultados/ busca com filtros, ordenação e paginação
|
||||||
│ ├── location/ # Serviços de geolocalização
|
├── produto/[barcode]/ detalhe do produto
|
||||||
│ ├── products/[barcode]/ # Proxy de busca de produto
|
├── lista/[id]/ lista de compras
|
||||||
│ ├── search/ # Proxy de busca Open Food Facts
|
├── api/ search, products/[barcode], location, cache/sync, storage-stats
|
||||||
│ └── storage-stats/ # Estatísticas de armazenamento
|
├── actions.ts Server Actions das listas (criar, listar, marcar, remover)
|
||||||
├── components/ # Componentes específicos de funcionalidades
|
├── components/ componentes de tela (landing/, cards, filtros, paginação)
|
||||||
│ ├── landing/ # Seções da página inicial
|
├── layout.tsx layout raiz: fontes, metadados, analytics
|
||||||
│ └── [componentes diversos] # Componentes reutilizáveis
|
├── robots.ts, sitemap.ts metadados de indexação
|
||||||
├── lista/[id]/ # Páginas de lista de compras
|
├── globals.css estilos globais e tokens de tema
|
||||||
├── produto/[barcode]/ # Páginas de detalhes do produto
|
├── fonts/ fontes Geist
|
||||||
├── resultados/ # Página de resultados de busca
|
hooks/ busca, filtros, cache, geolocalização e monitor de storage
|
||||||
└── layout.tsx # Layout raiz
|
lib/
|
||||||
|
├── openFoodFactsApi.ts cliente da API do Open Food Facts e ordenação
|
||||||
|
├── jsonCacheManager.ts cache JSON do servidor (data/products-cache.json)
|
||||||
|
├── cacheManager.ts cache de buscas no localStorage
|
||||||
|
├── geolocation.ts detecção de país do usuário
|
||||||
|
├── searchUtils.ts ordenação por relevância
|
||||||
|
├── types.ts tipos compartilhados
|
||||||
|
└── prisma.ts singleton do Prisma Client
|
||||||
|
components/ui/ componentes shadcn/ui (button, dialog, select, sheet, ...)
|
||||||
|
prisma/schema.prisma modelos Lista e Produtos
|
||||||
|
data/products-cache.json cache inicial com 232 produtos
|
||||||
|
docs/ documentação interna
|
||||||
|
nixpacks.toml, dokploy.yaml configuração de deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fluxo de Dados
|
## Verificação
|
||||||
|
|
||||||
1. **Requisições de Busca**: Entrada do usuário → Rota de API → API Open Food Facts → Resposta
|
Há testes offline com o runner nativo do Node.js (sem framework adicional) e não há CI configurada. O que existe hoje:
|
||||||
2. **Gerenciamento de Listas**: Ações do usuário → Server Actions → Prisma → Banco de dados
|
|
||||||
3. **Atualizações em Tempo Real**: Server Actions → Banco de dados → Revalidação → Atualização da UI
|
- `pnpm lint` roda o ESLint (via `next lint`);
|
||||||
|
- `pnpm build` roda `prisma generate` e o `next build` com checagem de tipos;
|
||||||
### Sistema de Cache
|
- verificação manual de rotas com `curl`, descrita abaixo.
|
||||||
|
|
||||||
```mermaid
|
Medições feitas nesta auditoria, em 14/09/2026, com Node 24.20.0 e pnpm 12.4.1:
|
||||||
graph TD
|
|
||||||
A[Usuário] --> B[Interface]
|
| Comando / requisição | Resultado |
|
||||||
B --> C{Cache Local?}
|
|---|---|
|
||||||
C -->|Sim| D[Retornar Dados]
|
| `pnpm install` | exit 0, sem precisar de flags |
|
||||||
C -->|Não| E[API Route]
|
| `pnpm lint` | exit 0, 6 avisos de variável não usada |
|
||||||
E --> F[Open Food Facts API]
|
| `pnpm build` | exit 0, 13 páginas geradas, First Load JS compartilhado de 102 kB |
|
||||||
F --> G[Salvar no Cache]
|
| `pnpm start -p 3111` | servidor no ar, respondeu 200 em 1 s |
|
||||||
G --> D
|
| `GET /` | 200, 46 kB |
|
||||||
```
|
| `GET /sobre`, `/resultados`, `/robots.txt`, `/sitemap.xml`, `/manifest.json` | 200 |
|
||||||
|
| `GET /produto/3017620422003` | 200, produto Nutella obtido na hora na API |
|
||||||
## 🚦 Instalação e Setup
|
| `GET /lista/audit-teste` | 200, página de lista renderizada |
|
||||||
|
| `GET /api/search?q=bis&cache_only=true` | 200, 20 produtos vindos do cache |
|
||||||
### Pré-requisitos
|
| `GET /api/search?q=bis&cache_only=true&page_size=5` | 200, 5 produtos |
|
||||||
|
| `GET /api/products/7622210575975?cache_only=true` | 200, `fromCache: true` |
|
||||||
- **Node.js** 18.0 ou superior
|
| `GET /api/products/3017620422003` | 200, `fromAPI: true` (produto buscado e gravado no cache) |
|
||||||
- **pnpm** 8.0 ou superior (gerenciador de pacotes recomendado)
|
| `GET /api/storage-stats` | 200, `totalProducts: 232`, 0,55 MB |
|
||||||
|
| `GET /api/location` | 200, `{"country":"Brazil","countryCode":"br"}` |
|
||||||
### Instalação Passo a Passo
|
| `POST /api/cache/sync` com `{"products":[]}` | 200, `{"success":true,"saved":0,"failed":0}` |
|
||||||
|
| `POST /api/cache/sync` com corpo inválido | 400, `{"error":"Produtos inválidos"}` |
|
||||||
1. **Clone o repositório**
|
| `GET /api/search` sem `q` | 400, `{"error":"Query parameter is required"}` |
|
||||||
|
| `GET /api/products/123` | 400, `{"error":"Código de barras inválido"}` |
|
||||||
```bash
|
| `GET /api/search?q=x&page=0` | 400, `{"error":"Invalid page parameter"}` |
|
||||||
git clone https://github.com/evandrodevbr/lista-compras.git
|
|
||||||
cd lista-compras
|
## Estado atual e limitações
|
||||||
```
|
|
||||||
|
- A busca textual depende do endpoint legado `cgi/search.pl` do Open Food Facts, que respondeu HTTP 503 durante a verificação. Na prática, buscar por nome retorna lista vazia, com a rota respondendo 200 e `count: 0`, sem derrubar a aplicação. A consulta por código de barras continua funcionando pelo endpoint `api/v2/product`.
|
||||||
2. **Instale as dependências**
|
- Testes offline de normalização e ordenação nutricional; ainda sem CI.
|
||||||
|
- O painel de filtros avançados da página de resultados guarda as escolhas na chave do cache, mas a busca enviada para `/api/search` leva apenas texto, país e paginação. Hoje o que realmente filtra a lista são os atalhos de classificação Nutri-Score e o botão de produtos sem alérgenos.
|
||||||
```bash
|
- A ordenação implementada em `lib/openFoodFactsApi.ts` cobre quatro critérios (relevância, Nutri-Score, energia e nome). As demais opções do seletor caem no caso padrão da função e não reordenam o resultado.
|
||||||
pnpm install
|
- O servidor grava em `data/products-cache.json`, que é versionado. Usar a aplicação altera o conteúdo do arquivo no diretório de trabalho.
|
||||||
```
|
- Duas lockfiles versionadas: `pnpm-lock.yaml` (usada pelo deploy) e `bun.lock`.
|
||||||
|
- Metadados, Open Graph e User-Agent apontam para `nutriscan.com.br`, domínio que não responde; a instância publicada está em `nutriscan.evandro.dev.br`, que também é a URL do `sitemap.xml` e do `robots.txt`.
|
||||||
3. **Configure as variáveis de ambiente**
|
- O banco SQLite versionado em `prisma/prisma/db/` vem só com a tabela `_prisma_migrations`, sem `Lista` e `Produtos`; é necessário rodar `pnpm db:push` antes de usar as listas.
|
||||||
|
- `pnpm db:migrate` existe no `package.json` mas não tem migrations para aplicar no repositório.
|
||||||
```bash
|
- O analytics é servido via proxy em `/analytics/*` para o Rybbit configurado em `NEXT_PUBLIC_RYBBIT_HOST`; sem esse serviço no ar, o script falha silenciosamente no cliente.
|
||||||
cp .env.example .env.local
|
- A página de lista de compras renderiza para qualquer id, mesmo sem registro no banco, porque `verificarLista` retorna sucesso quando a lista não existe.
|
||||||
```
|
|
||||||
|
## Documentação
|
||||||
Configure as seguintes variáveis:
|
|
||||||
|
| Documento | Conteúdo |
|
||||||
```env
|
|---|---|
|
||||||
DATABASE_URL="file:./prisma/db/database.sqlite"
|
| [`docs/openFoodFacts.md`](docs/openFoodFacts.md) | Anotações sobre a API do Open Food Facts |
|
||||||
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
|
| [`docs/logo-prompts.md`](docs/logo-prompts.md) | Prompts usados na criação das logos |
|
||||||
```
|
| [`docs/resedesign/`](docs/resedesign) | Referência visual do redesign |
|
||||||
|
| [`prisma/schema.prisma`](prisma/schema.prisma) | Modelo de dados das listas |
|
||||||
4. **Inicialize o banco de dados**
|
|
||||||
|
## Licença
|
||||||
```bash
|
|
||||||
pnpm db:generate
|
Licença de uso pessoal, definida em [`LICENSE`](LICENSE): é permitido usar em máquina local, estudar o código e rodar para desenvolvimento. Não é permitido deploy público, uso comercial, redistribuição nem obras derivadas para distribuição. A instância em `nutriscan.evandro.dev.br` é do próprio autor.
|
||||||
pnpm db:migrate
|
|
||||||
```
|
Autor: Evandro, <https://evandro.dev.br> e [@evandrodevbr](https://github.com/evandrodevbr).
|
||||||
|
|
||||||
5. **Inicie o servidor de desenvolvimento**
|
## Validação de dados nutricionais (2026-09-30)
|
||||||
|
|
||||||
```bash
|
A exibição e a ordenação de energia priorizam `energy-kcal_100g`; na ausência dele, convertem o campo em kJ para kcal usando `1 kcal = 4,184 kJ`. A página do produto usa a mesma normalização. Zero é preservado como dado válido, e valores ausentes, negativos ou não finitos não entram no cálculo. Produtos sem energia ficam no fim da ordenação nas duas direções. A linha baseada em `salt_100g` é identificada como sal.
|
||||||
pnpm dev
|
|
||||||
```
|
Os exemplos de campos vêm da [documentação oficial do Open Food Facts](https://openfoodfacts.github.io/openfoodfacts-server/api/tutorial-off-api/). Valores por 100 g e referências existentes da interface são mantidos; o Nutri-Score continua vindo da API.
|
||||||
|
|
||||||
6. **Abra seu navegador**
|
Com Node.js 22.7+ (verificado em 24.19.0), `pnpm test` executa 6 regressões offline usando o runner nativo, sem adicionar um framework. `pnpm lint`, `pnpm exec tsc --noEmit` e `pnpm build` completam a validação. O build foi verificado com SQLite temporário, sem conexão com um banco de produção. Next.js e eslint-config-next foram atualizados para 15.5.24.
|
||||||
Navegue para [http://localhost:3000](http://localhost:3000)
|
|
||||||
|
|
||||||
### Troubleshooting
|
|
||||||
|
|
||||||
**Problema**: Erro de conexão com banco de dados
|
|
||||||
|
|
||||||
- **Solução**: Verifique se o arquivo `database.sqlite` existe em `prisma/db/`
|
|
||||||
|
|
||||||
**Problema**: Erro de build do Prisma
|
|
||||||
|
|
||||||
- **Solução**: Execute `pnpm db:generate` antes do build
|
|
||||||
|
|
||||||
**Problema**: Dependências não instaladas
|
|
||||||
|
|
||||||
- **Solução**: Use `pnpm install --no-frozen-lockfile`
|
|
||||||
|
|
||||||
## 🔌 API e Integrações
|
|
||||||
|
|
||||||
### Integração Open Food Facts
|
|
||||||
|
|
||||||
O NutriScan integra com a API Open Food Facts através de um wrapper customizado (`lib/openFoodFactsApi.ts`) que fornece:
|
|
||||||
|
|
||||||
- **Busca de Produtos**: Pesquisa por nome, marca ou categoria
|
|
||||||
- **Lookup por Código de Barras**: Busca direta usando códigos de barras
|
|
||||||
- **Filtros Avançados**: Filtros por notas nutricionais, alérgenos, aditivos e mais
|
|
||||||
- **Cache**: Estratégia de cache inteligente para melhor performance
|
|
||||||
- **Tratamento de Erros**: Tratamento abrangente de erros e mecanismos de fallback
|
|
||||||
|
|
||||||
### Endpoints Disponíveis
|
|
||||||
|
|
||||||
| Endpoint | Descrição | Método |
|
|
||||||
| ------------------------- | ----------------------------- | ------ |
|
|
||||||
| `/api/search` | Busca de produtos | GET |
|
|
||||||
| `/api/products/[barcode]` | Busca de produto específico | GET |
|
|
||||||
| `/api/location` | Serviços de geolocalização | GET |
|
|
||||||
| `/api/cache/sync` | Sincronização de cache | POST |
|
|
||||||
| `/api/storage-stats` | Estatísticas de armazenamento | GET |
|
|
||||||
|
|
||||||
### Rate Limits e Cache
|
|
||||||
|
|
||||||
- **Resultados de Busca**: Cache de 1 hora para reduzir chamadas à API
|
|
||||||
- **Detalhes de Produto**: Cache de 24 horas para produtos frequentemente acessados
|
|
||||||
- **Opções de Filtro**: Cache de 1 semana para dados estáticos como categorias e marcas
|
|
||||||
|
|
||||||
## 🗄 Database Schema
|
|
||||||
|
|
||||||
### Schema Prisma
|
|
||||||
|
|
||||||
```prisma
|
|
||||||
model Lista {
|
|
||||||
id String @id
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
|
||||||
produtos Produtos[]
|
|
||||||
}
|
|
||||||
|
|
||||||
model Produtos {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
nome String
|
|
||||||
qtd Float
|
|
||||||
tipoUN String
|
|
||||||
comprado Boolean @default(false)
|
|
||||||
listaId String @default("default")
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
|
||||||
lista Lista @relation(fields: [listaId], references: [id])
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Características Principais
|
|
||||||
|
|
||||||
- **Estrutura Simples**: Fácil de entender e manter
|
|
||||||
- **Listas Flexíveis**: Suporte para múltiplas listas de compras
|
|
||||||
- **Rastreamento de Produtos**: Gerenciamento de quantidade e unidade
|
|
||||||
- **Rastreamento de Status**: Status de compra para cada item
|
|
||||||
- **Timestamps**: Rastreamento automático de criação e atualização
|
|
||||||
|
|
||||||
## ⚡ Performance e Otimizações
|
|
||||||
|
|
||||||
### Estratégia de Cache Multi-Camada
|
|
||||||
|
|
||||||
1. **Cache de Busca**: Armazenamento local de resultados de busca
|
|
||||||
2. **Cache de Produtos**: Cache JSON de produtos populares
|
|
||||||
3. **Cache de Imagens**: Lazy loading e otimização automática
|
|
||||||
4. **Cache de API**: Cache de respostas da API Open Food Facts
|
|
||||||
|
|
||||||
### Otimizações de Performance
|
|
||||||
|
|
||||||
- **Lazy Loading**: Carregamento sob demanda de imagens e componentes
|
|
||||||
- **Code Splitting**: Divisão automática de código por rota
|
|
||||||
- **Otimização de Bundle**: Minificação e tree-shaking automáticos
|
|
||||||
- **Server Actions**: Atualizações de dados sem recarregamento de página
|
|
||||||
- **Image Optimization**: Otimização automática de imagens com Next.js
|
|
||||||
|
|
||||||
### Métricas de Performance
|
|
||||||
|
|
||||||
- **First Load JS**: ~87.3 kB compartilhado
|
|
||||||
- **Build Time**: Otimizado com Prisma e Next.js
|
|
||||||
- **Cache Hit Rate**: Monitorado via storage stats API
|
|
||||||
|
|
||||||
## 🚀 Deployment
|
|
||||||
|
|
||||||
### Configuração Dokploy
|
|
||||||
|
|
||||||
O projeto inclui configuração para deployment via Dokploy (`dokploy.yaml`):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
build:
|
|
||||||
pre_build:
|
|
||||||
- npm install -g pnpm@latest
|
|
||||||
- export PNPM_HOME="/root/.local/share/pnpm"
|
|
||||||
- export PATH="$PNPM_HOME:$PATH"
|
|
||||||
steps:
|
|
||||||
- name: Verify pnpm
|
|
||||||
run: which pnpm && pnpm --version
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --no-frozen-lockfile
|
|
||||||
- name: Generate Prisma Client
|
|
||||||
run: pnpx prisma generate
|
|
||||||
- name: Build application
|
|
||||||
run: pnpm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuração Nixpacks
|
|
||||||
|
|
||||||
Configuração alternativa para deployment (`nixpacks.toml`):
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[phases.setup]
|
|
||||||
nixPkgs = ['nodejs_18', 'openssl']
|
|
||||||
cmds = [
|
|
||||||
'npm install -g pnpm@latest',
|
|
||||||
'npm install -g prisma'
|
|
||||||
]
|
|
||||||
|
|
||||||
[phases.install]
|
|
||||||
cmds = [
|
|
||||||
'npx pnpm install --no-frozen-lockfile',
|
|
||||||
'mkdir -p prisma',
|
|
||||||
'touch prisma/dev.db',
|
|
||||||
'chmod -R 777 prisma'
|
|
||||||
]
|
|
||||||
|
|
||||||
[phases.build]
|
|
||||||
cmds = [
|
|
||||||
'mkdir -p node_modules/.prisma',
|
|
||||||
'mkdir -p .next',
|
|
||||||
'chmod -R 777 node_modules/.prisma node_modules/.bin .next prisma',
|
|
||||||
'npx prisma generate',
|
|
||||||
'npx prisma db push --accept-data-loss',
|
|
||||||
'npx pnpm run build'
|
|
||||||
]
|
|
||||||
|
|
||||||
[start]
|
|
||||||
cmd = 'npx pnpm start'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Checklist de Deployment
|
|
||||||
|
|
||||||
- [ ] Configurar variáveis de ambiente
|
|
||||||
- [ ] Executar migrações do banco de dados
|
|
||||||
- [ ] Gerar Prisma Client
|
|
||||||
- [ ] Build da aplicação
|
|
||||||
- [ ] Configurar domínio e SSL
|
|
||||||
- [ ] Testar funcionalidades principais
|
|
||||||
|
|
||||||
## 📜 Scripts Disponíveis
|
|
||||||
|
|
||||||
### Desenvolvimento
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm dev # Inicia servidor de desenvolvimento (http://localhost:3000)
|
|
||||||
pnpm build # Build para produção
|
|
||||||
pnpm start # Inicia servidor de produção
|
|
||||||
pnpm lint # Executa ESLint
|
|
||||||
```
|
|
||||||
|
|
||||||
### Banco de Dados
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm db:push # Push do schema Prisma para o banco
|
|
||||||
pnpm db:generate # Gera Prisma Client
|
|
||||||
pnpm db:migrate # Executa migrações do banco
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pós-instalação
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm postinstall # Gera Prisma Client após instalação
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🤝 Contribuindo
|
|
||||||
|
|
||||||
Agradecemos contribuições da comunidade! Aqui está como você pode ajudar:
|
|
||||||
|
|
||||||
### Para Desenvolvedores
|
|
||||||
|
|
||||||
1. **Fork o repositório**
|
|
||||||
2. **Crie uma branch de feature**
|
|
||||||
```bash
|
|
||||||
git checkout -b feature/amazing-feature
|
|
||||||
```
|
|
||||||
3. **Faça suas alterações**
|
|
||||||
4. **Commit suas alterações**
|
|
||||||
```bash
|
|
||||||
git commit -m 'Add: amazing feature'
|
|
||||||
```
|
|
||||||
5. **Push para a branch**
|
|
||||||
```bash
|
|
||||||
git push origin feature/amazing-feature
|
|
||||||
```
|
|
||||||
6. **Abra um Pull Request**
|
|
||||||
|
|
||||||
### Para Usuários Finais
|
|
||||||
|
|
||||||
- **Reportar Bugs**: Use [GitHub Issues](https://github.com/evandrodevbr/lista-compras/issues)
|
|
||||||
- **Solicitar Features**: Sugira novas funcionalidades via [GitHub Issues](https://github.com/evandrodevbr/lista-compras/issues)
|
|
||||||
- **Fornecer Feedback**: Compartilhe sua experiência e sugestões
|
|
||||||
|
|
||||||
### Guidelines de Desenvolvimento
|
|
||||||
|
|
||||||
- Siga o estilo de código existente
|
|
||||||
- Escreva mensagens de commit claras
|
|
||||||
- Adicione testes para novas funcionalidades
|
|
||||||
- Atualize documentação quando necessário
|
|
||||||
- Certifique-se de que todos os checks passem antes de submeter PR
|
|
||||||
|
|
||||||
## 📄 Licença
|
|
||||||
|
|
||||||
Este projeto está licenciado sob uma **Licença de Uso Pessoal**.
|
|
||||||
|
|
||||||
### Permissões
|
|
||||||
|
|
||||||
- ✅ Uso pessoal em localhost
|
|
||||||
- ✅ Inspeção de código e aprendizado
|
|
||||||
- ✅ Desenvolvimento e teste local
|
|
||||||
|
|
||||||
### Restrições
|
|
||||||
|
|
||||||
- ❌ Deploy público na web
|
|
||||||
- ❌ Uso comercial
|
|
||||||
- ❌ Redistribuição para terceiros
|
|
||||||
- ❌ Criação de obras derivadas para distribuição pública
|
|
||||||
|
|
||||||
Para os termos completos da licença, consulte o arquivo [LICENSE](LICENSE).
|
|
||||||
|
|
||||||
## 📫 Contato
|
|
||||||
|
|
||||||
**Autor:** Evandro
|
|
||||||
**Website:** [evandro.dev.br](https://evandro.dev.br)
|
|
||||||
**GitHub:** [@evandrodevbr](https://github.com/evandrodevbr)
|
|
||||||
**Email:** evandro@evandro.dev.br
|
|
||||||
|
|
||||||
### Links do Projeto
|
|
||||||
|
|
||||||
- **Live Demo:** [nutriscan.evandro.dev.br](https://nutriscan.evandro.dev.br)
|
|
||||||
- **Repositório:** [github.com/evandrodevbr/lista-compras](https://github.com/evandrodevbr/lista-compras)
|
|
||||||
- **Issues:** [GitHub Issues](https://github.com/evandrodevbr/lista-compras/issues)
|
|
||||||
|
|
||||||
## 🙏 Agradecimentos
|
|
||||||
|
|
||||||
- **[Open Food Facts](https://world.openfoodfacts.org/)** - Pela base de dados abrangente de alimentos
|
|
||||||
- **[shadcn/ui](https://ui.shadcn.com/)** - Pela biblioteca de componentes acessíveis
|
|
||||||
- **[Next.js Team](https://nextjs.org/)** - Pelo framework React incrível
|
|
||||||
- **[Vercel](https://vercel.com/)** - Por soluções de hosting e deployment
|
|
||||||
- **[Prisma](https://www.prisma.io/)** - Pelo excelente ORM
|
|
||||||
- **Contribuidores** - Obrigado a todos que ajudam a melhorar este projeto
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<p>Desenvolvido com ❤️ por <a href="https://evandro.dev.br">evandro.dev.br</a></p>
|
|
||||||
<p>Feito para usuários conscientes com a saúde e desenvolvedores ao redor do mundo</p>
|
|
||||||
</div>
|
|
||||||
@@ -12,7 +12,8 @@ import {
|
|||||||
Share2,
|
Share2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { searchByBarcode, Product, formatNutritionData } from "@/lib/openFoodFactsApi";
|
import { searchByBarcode, Product } from "@/lib/openFoodFactsApi";
|
||||||
|
import { getEnergyKcal, getNutrientValue } from "@/lib/nutrition";
|
||||||
import { ProductSkeleton } from "@/app/components/ProductSkeleton";
|
import { ProductSkeleton } from "@/app/components/ProductSkeleton";
|
||||||
|
|
||||||
/* ── Nutri-Score letter circle ─────────────────────────────────────────── */
|
/* ── Nutri-Score letter circle ─────────────────────────────────────────── */
|
||||||
@@ -305,7 +306,7 @@ export default function ProdutoPage() {
|
|||||||
|
|
||||||
/* ── Nutrition bars — % of daily reference ─────────────────────────── */
|
/* ── Nutrition bars — % of daily reference ─────────────────────────── */
|
||||||
const dailyRef: Record<string, number> = {
|
const dailyRef: Record<string, number> = {
|
||||||
energy_100g: 2000,
|
"energy-kcal_100g": 2000,
|
||||||
fat_100g: 70,
|
fat_100g: 70,
|
||||||
saturated_fat_100g: 20,
|
saturated_fat_100g: 20,
|
||||||
sugars_100g: 90,
|
sugars_100g: 90,
|
||||||
@@ -315,11 +316,11 @@ export default function ProdutoPage() {
|
|||||||
};
|
};
|
||||||
const nutriments = product.nutriments || {};
|
const nutriments = product.nutriments || {};
|
||||||
const nutritionRows = [
|
const nutritionRows = [
|
||||||
{ key: "energy_100g", label: "Energia", unit: "kcal" },
|
{ key: "energy-kcal_100g", label: "Energia", unit: "kcal" },
|
||||||
{ key: "fat_100g", label: "Gorduras totais", unit: "g" },
|
{ key: "fat_100g", label: "Gorduras totais", unit: "g" },
|
||||||
{ key: "saturated_fat_100g",label: "Gorduras saturadas", unit: "g" },
|
{ key: "saturated_fat_100g",label: "Gorduras saturadas", unit: "g" },
|
||||||
{ key: "sugars_100g", label: "Açúcares", unit: "g" },
|
{ key: "sugars_100g", label: "Açúcares", unit: "g" },
|
||||||
{ key: "salt_100g", label: "Sódio", unit: "g" },
|
{ key: "salt_100g", label: "Sal", unit: "g" },
|
||||||
{ key: "fiber_100g", label: "Fibras", unit: "g" },
|
{ key: "fiber_100g", label: "Fibras", unit: "g" },
|
||||||
{ key: "proteins_100g", label: "Proteínas", unit: "g" },
|
{ key: "proteins_100g", label: "Proteínas", unit: "g" },
|
||||||
];
|
];
|
||||||
@@ -701,7 +702,7 @@ export default function ProdutoPage() {
|
|||||||
>
|
>
|
||||||
{nutritionRows.map((row) => {
|
{nutritionRows.map((row) => {
|
||||||
const raw = nutriments[row.key as keyof typeof nutriments];
|
const raw = nutriments[row.key as keyof typeof nutriments];
|
||||||
const v = raw != null ? Number(raw) : null;
|
const v = row.key === "energy-kcal_100g" ? getEnergyKcal(nutriments) : getNutrientValue(raw);
|
||||||
if (v == null) return null;
|
if (v == null) return null;
|
||||||
const ref = dailyRef[row.key] || 100;
|
const ref = dailyRef[row.key] || 100;
|
||||||
const pct = Math.min(100, Math.round((v / ref) * 100));
|
const pct = Math.min(100, Math.round((v / ref) * 100));
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
not found
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/** Normalização dos valores nutricionais recebidos do Open Food Facts. */
|
||||||
|
export function getNutrientValue(value: unknown): number | null {
|
||||||
|
if (typeof value !== "number" && typeof value !== "string") return null;
|
||||||
|
if (typeof value === "string" && value.trim() === "") return null;
|
||||||
|
const number = Number(value);
|
||||||
|
return Number.isFinite(number) && number >= 0 ? number : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefere o campo explícito em kcal. Os campos energy/energy-kj usam kJ;
|
||||||
|
* 1 kcal = 4.184 kJ. Zero é um valor válido, distinto de dado ausente.
|
||||||
|
* https://openfoodfacts.github.io/openfoodfacts-server/api/tutorial-off-api/
|
||||||
|
*/
|
||||||
|
export function getEnergyKcal(nutriments?: {
|
||||||
|
"energy-kcal_100g"?: unknown;
|
||||||
|
"energy-kj_100g"?: unknown;
|
||||||
|
energy_100g?: unknown;
|
||||||
|
}): number | null {
|
||||||
|
const kcal = getNutrientValue(nutriments?.["energy-kcal_100g"]);
|
||||||
|
if (kcal !== null) return kcal;
|
||||||
|
const kj = getNutrientValue(nutriments?.["energy-kj_100g"])
|
||||||
|
?? getNutrientValue(nutriments?.energy_100g);
|
||||||
|
return kj === null ? null : kj / 4.184;
|
||||||
|
}
|
||||||
+21
-14
@@ -3,6 +3,8 @@
|
|||||||
* Melhorias: Memoização de Scores, Rate Limiting Robusto e Tipagem Estrita.
|
* Melhorias: Memoização de Scores, Rate Limiting Robusto e Tipagem Estrita.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { getEnergyKcal, getNutrientValue } from "./nutrition.ts";
|
||||||
|
|
||||||
export interface Product {
|
export interface Product {
|
||||||
code: string;
|
code: string;
|
||||||
product_name: string;
|
product_name: string;
|
||||||
@@ -11,6 +13,8 @@ export interface Product {
|
|||||||
ingredients_text?: string;
|
ingredients_text?: string;
|
||||||
nutriments?: {
|
nutriments?: {
|
||||||
energy_100g?: number;
|
energy_100g?: number;
|
||||||
|
"energy-kcal_100g"?: number;
|
||||||
|
"energy-kj_100g"?: number;
|
||||||
fat_100g?: number;
|
fat_100g?: number;
|
||||||
carbohydrates_100g?: number;
|
carbohydrates_100g?: number;
|
||||||
proteins_100g?: number;
|
proteins_100g?: number;
|
||||||
@@ -185,6 +189,7 @@ export function sortProducts(
|
|||||||
value: p,
|
value: p,
|
||||||
score: calculateCompletenessScore(p),
|
score: calculateCompletenessScore(p),
|
||||||
nutritionValue: getGradeValue(p.nutrition_grades),
|
nutritionValue: getGradeValue(p.nutrition_grades),
|
||||||
|
energyKcal: getEnergyKcal(p.nutriments),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 2. Sort: Ordenação sobre valores em memória (O(n log n))
|
// 2. Sort: Ordenação sobre valores em memória (O(n log n))
|
||||||
@@ -199,7 +204,9 @@ export function sortProducts(
|
|||||||
diff = a.nutritionValue - b.nutritionValue;
|
diff = a.nutritionValue - b.nutritionValue;
|
||||||
break;
|
break;
|
||||||
case "energy":
|
case "energy":
|
||||||
diff = (a.value.nutriments?.energy_100g || 9999) - (b.value.nutriments?.energy_100g || 9999);
|
if (a.energyKcal === null) return b.energyKcal === null ? 0 : 1;
|
||||||
|
if (b.energyKcal === null) return -1;
|
||||||
|
diff = a.energyKcal - b.energyKcal;
|
||||||
break;
|
break;
|
||||||
case "name":
|
case "name":
|
||||||
diff = (a.value.product_name || "").localeCompare(b.value.product_name || "");
|
diff = (a.value.product_name || "").localeCompare(b.value.product_name || "");
|
||||||
@@ -240,21 +247,21 @@ export function formatNutritionData(p: Product) {
|
|||||||
const n = p.nutriments;
|
const n = p.nutriments;
|
||||||
if (!n) return null;
|
if (!n) return null;
|
||||||
|
|
||||||
const fmt = (v: unknown, d: number = 1) => {
|
const fmt = (v: unknown, unit: string, d: number = 1) => {
|
||||||
const num =
|
const num = getNutrientValue(v);
|
||||||
typeof v === "number"
|
return num === null ? "N/A" : `${num.toFixed(d)}${unit}`;
|
||||||
? v
|
|
||||||
: parseFloat(typeof v === "string" ? v : String(v));
|
|
||||||
return isNaN(num) ? "N/A" : num.toFixed(d);
|
|
||||||
};
|
};
|
||||||
|
const energy = getEnergyKcal(n);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
energy: n.energy_100g ? `${Math.round(n.energy_100g)} kcal` : "N/A",
|
energy: energy === null ? "N/A" : `${Math.round(energy)} kcal`,
|
||||||
fat: n.fat_100g ? `${fmt(n.fat_100g)}g` : "N/A",
|
fat: fmt(n.fat_100g, "g"),
|
||||||
carbs: n.carbohydrates_100g ? `${fmt(n.carbohydrates_100g)}g` : "N/A",
|
carbs: fmt(n.carbohydrates_100g, "g"),
|
||||||
proteins: n.proteins_100g ? `${fmt(n.proteins_100g)}g` : "N/A",
|
carbohydrates: fmt(n.carbohydrates_100g, "g"),
|
||||||
sugars: n.sugars_100g ? `${fmt(n.sugars_100g)}g` : "N/A",
|
proteins: fmt(n.proteins_100g, "g"),
|
||||||
sodium: n.sodium_100g ? `${fmt(n.sodium_100g, 2)}g` : "N/A",
|
sugars: fmt(n.sugars_100g, "g"),
|
||||||
|
fiber: fmt(n.fiber_100g, "g"),
|
||||||
|
sodium: fmt(n.sodium_100g, "g", 2),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,4 +276,4 @@ export function detectSearchType(input: string): "barcode" | "name" {
|
|||||||
export function isValidBarcode(barcode: string): boolean {
|
export function isValidBarcode(barcode: string): boolean {
|
||||||
const cleaned = barcode.replace(/\D/g, "");
|
const cleaned = barcode.replace(/\D/g, "");
|
||||||
return cleaned.length >= 8 && cleaned.length <= 13 && /^\d+$/.test(cleaned);
|
return cleaned.length >= 8 && cleaned.length <= 13 && /^\d+$/.test(cleaned);
|
||||||
}
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
Get-Content: Cannot find path 'C:\Users\Evandro\.config\opencode\agents.json' because it does not exist.
|
|
||||||
+4
-3
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "resenhaitapoa2",
|
"name": "nutriscan",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"test": "node --experimental-transform-types --test tests/*.test.ts",
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"postinstall": "prisma generate",
|
"postinstall": "prisma generate",
|
||||||
@@ -30,7 +31,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^11.18.2",
|
"framer-motion": "^11.18.2",
|
||||||
"lucide-react": "^0.460.0",
|
"lucide-react": "^0.460.0",
|
||||||
"next": "^15.5.10",
|
"next": "^15.5.24",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
@@ -43,7 +44,7 @@
|
|||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"depcheck": "^1.4.7",
|
"depcheck": "^1.4.7",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"eslint-config-next": "^15.5.10",
|
"eslint-config-next": "^15.5.24",
|
||||||
"postcss": "^8.5.10",
|
"postcss": "^8.5.10",
|
||||||
"prisma": "^6.19.3",
|
"prisma": "^6.19.3",
|
||||||
"tailwindcss": "^3.4.19",
|
"tailwindcss": "^3.4.19",
|
||||||
|
|||||||
Generated
+130
-126
@@ -60,8 +60,8 @@ importers:
|
|||||||
specifier: ^0.460.0
|
specifier: ^0.460.0
|
||||||
version: 0.460.0([email protected])
|
version: 0.460.0([email protected])
|
||||||
next:
|
next:
|
||||||
specifier: ^15.5.10
|
specifier: ^15.5.24
|
||||||
version: 15.5.20([email protected]([email protected]))([email protected])
|
version: 15.5.24([email protected]([email protected]))([email protected])
|
||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.4.6
|
specifier: ^0.4.6
|
||||||
version: 0.4.6([email protected]([email protected]))([email protected])
|
version: 0.4.6([email protected]([email protected]))([email protected])
|
||||||
@@ -89,13 +89,13 @@ importers:
|
|||||||
version: 19.2.3(@types/[email protected])
|
version: 19.2.3(@types/[email protected])
|
||||||
depcheck:
|
depcheck:
|
||||||
specifier: ^1.4.7
|
specifier: ^1.4.7
|
||||||
version: 1.4.7
|
version: 1.4.7([email protected])
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^8.57.1
|
specifier: ^8.57.1
|
||||||
version: 8.57.1
|
version: 8.57.1([email protected])
|
||||||
eslint-config-next:
|
eslint-config-next:
|
||||||
specifier: ^15.5.10
|
specifier: ^15.5.24
|
||||||
version: 15.5.20([email protected])([email protected])
|
version: 15.5.24([email protected]([email protected]))([email protected])([email protected])
|
||||||
postcss:
|
postcss:
|
||||||
specifier: ^8.5.10
|
specifier: ^8.5.10
|
||||||
version: 8.5.19
|
version: 8.5.19
|
||||||
@@ -382,60 +382,60 @@ packages:
|
|||||||
'@emnapi/core': ^1.7.1
|
'@emnapi/core': ^1.7.1
|
||||||
'@emnapi/runtime': ^1.7.1
|
'@emnapi/runtime': ^1.7.1
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
resolution: {integrity: sha512-dXh51Wvddf8daEyBXryZZEe1FdVxEWx9lgaTseLZUtC1XP/W8Wri+Z+VPOElHlByk23CyqHdc2oVByX7wsTWsw==}
|
resolution: {integrity: sha512-mBDF7T0XKZjs9SpUAl0buizVO+O02ULjOvWX8o/AZo/5AGw/UAS1Zzcylmd4pqbftzmKQi+L/nB4jgBYKEAl5Q==}
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
resolution: {integrity: sha512-MZUgFpVd9rGSZpb8bNceUWvkAZe6aQw/6h2SSqHQuYzKfaiEUEVPIO6mXqaBmiBEBSN1f1sOc1uV8GCM90oegg==}
|
resolution: {integrity: sha512-K6jcYhwMY73qpeJ5SP+rrXs7Fuvv94HEvOCEen/zjwG7aq83utZY8n+8YdKHZR1lv0n5cB4z9C6QqwpE9acN+A==}
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
resolution: {integrity: sha512-in0yXG7/pRBVjWeEl7f7ZZETpletSMFKXVS4GJgHENTPVrJFNJKPrYewa9rpZcvdjwFece5fZP0CK34G4PxowA==}
|
resolution: {integrity: sha512-AGdNLvxZNY6eR2iSnV+6wUa8CiHTMr4F7g3uHH7fT4ICIJBE00R9u4tzN/Vuwsw0cOi8MTD2HJcTCb6siMH88Q==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
resolution: {integrity: sha512-0hsFshdPnTzGJdDTHeHJ+XPUShOpnyp9pUFDwDhqctsA0Cd8NcIVGRPtptYhgYY9DjkKgCDRkXxmgRc+CgT5Wg==}
|
resolution: {integrity: sha512-9HrQajBMmGcrrrvDfRimiCrbAPh3E6uHJmwBovYr6Yrmi9p9PZqI876BrXX280wICh3o2XwUlp4blkB0NNBqFg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
resolution: {integrity: sha512-DMvkoBtAABOzE6pMZRW/xNm7sKqql3wzzzZJ1R/d/rp4BCxv6LykouD3tHjGY8WdQqGpZs11t+R9AtjPxvvljw==}
|
resolution: {integrity: sha512-rl9LSfE75si0WT3cDgdUC1XYCKS+TgxC+/IjitmeycrAG18X/plIP1/vy8dd/HPycYcIvE688PD7FuvEAiEAew==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
resolution: {integrity: sha512-RQmDfeYBtXV2FSId7dfA1hE6M/T6+g7wdbYnFQ47tw/gUBwV+CccLVejNmCGa9yLDitk83foeg8hl/3DjfYQ5g==}
|
resolution: {integrity: sha512-TlNAnpsjxSF3aAUtqnfmtXXf8m9sIDBlmF3c7bTAlnshUYu2U0OxN2uf5d0gcFwqHVEdivJNBcCaqNOwPGNimw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
libc: [musl]
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
resolution: {integrity: sha512-DkWLEdKajJwdGt27M3i1VEO2kelTvZrK6Pcb7JvW2BY+nofWm7FBsBNDj7g7Pr1NuQ5PLJvqEqYa20GTsBDnKQ==}
|
resolution: {integrity: sha512-7dwtlhr0SLndqTG1z9ncRkbJswDZiKWlxzFyXDvJ2RDZRDRHp8zyMJ4D9UH/FgnQeXxxB6gZy2pMcIUoNKQ4pA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
resolution: {integrity: sha512-rAO5b7pKHvX+ExdmJskusDXTNbiNZfptifIPZItbUx+AOXxxTydVBsPt7Oz84DRd5mY8e0DcE8kvLj3AIfjE6w==}
|
resolution: {integrity: sha512-kGZxM+WhkYs0276lFrMkj7PRtXT3Btp6cwvfSO/cCVLxJJttB5Ccnl2niaCgUja8HgSbEVnMHpg3FJWoOJ9e/g==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
libc: [musl]
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
resolution: {integrity: sha512-Hp3zFsN8N8Kj9+vY6L4vnZ9EtA9eXyATu0q4EfGbZTiocgPUNSfz8NWhym6xvaOmHpJ8EuoypuU1WejCPsTFtg==}
|
resolution: {integrity: sha512-jBDDkZ/qKAqkWivWDMkJSXUzbzV0QKRBKJjEHUAvSB97Hzw7NLzJ6yV56Lts/wjir7s4P31GYgpbS6ZL+hasAA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
resolution: {integrity: sha512-T/L7CXpR1M0wij/xbF3rT1+7KvSkfOLr7C+ToHHWZTG2eKmb52C5WvsyGCBNtkVvDEUESWkRUbbqSH4rSbOCYQ==}
|
resolution: {integrity: sha512-JqtwjvvorjacQ0spgjmUJoxySoYgPwdT1sFdQ0/zmW4iMlP2hjYlCoJIyS7o6Epb4Fug8eco3HXFoBDUCDeH7Q==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
@@ -1549,8 +1549,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
[email protected]0:
|
[email protected]4:
|
||||||
resolution: {integrity: sha512-Pl/I5544gmkcVVWcnaOMfhJSBK8lZ1NCJ+mGBkd2qvbGt2Gi7sEpgHF06OR13a2p6THODlncpvGsZzY2vUqwxw==}
|
resolution: {integrity: sha512-RXne8azyj9DKV9N15elyRsnh98BtKINYHCG7bmVhpIhaOtaYvrNJ37XwWpjY9Z09f5RHYOf9vn5uU0MgV1iLrw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
|
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
|
||||||
typescript: '>=3.3.1'
|
typescript: '>=3.3.1'
|
||||||
@@ -2184,8 +2184,8 @@ packages:
|
|||||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
|
|
||||||
[email protected]0:
|
[email protected]4:
|
||||||
resolution: {integrity: sha512-cvyS3/geydan1xLtE3FA8VCgdoQ/Gg/dlOldFkFCbB5VcVYJV7090hQLBnvTW2PwT76Z/dHdzDZCsVhZpoOlUA==}
|
resolution: {integrity: sha512-Y+xn8EQCoC3ZbsFPyzE+tE8XOdrWeUdUF7NeXbmg9DsgAxl5UYxlsrvgVESHTyTGigoTa1bCUrxn70F5bqt0Gw==}
|
||||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2882,7 +2882,7 @@ snapshots:
|
|||||||
'@babel/parser': 7.29.7
|
'@babel/parser': 7.29.7
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
|
|
||||||
'@babel/[email protected]':
|
'@babel/[email protected]([email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.7
|
'@babel/code-frame': 7.29.7
|
||||||
'@babel/generator': 7.29.7
|
'@babel/generator': 7.29.7
|
||||||
@@ -2890,7 +2890,7 @@ snapshots:
|
|||||||
'@babel/parser': 7.29.7
|
'@babel/parser': 7.29.7
|
||||||
'@babel/template': 7.29.7
|
'@babel/template': 7.29.7
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
debug: 4.4.3
|
debug: 4.4.3([email protected])
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -2920,17 +2920,17 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@eslint-community/[email protected]([email protected])':
|
'@eslint-community/[email protected]([email protected]([email protected]))':
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1([email protected])
|
||||||
eslint-visitor-keys: 3.4.3
|
eslint-visitor-keys: 3.4.3
|
||||||
|
|
||||||
'@eslint-community/[email protected]': {}
|
'@eslint-community/[email protected]': {}
|
||||||
|
|
||||||
'@eslint/[email protected]':
|
'@eslint/[email protected]([email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.15.0
|
ajv: 6.15.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3([email protected])
|
||||||
espree: 9.6.1
|
espree: 9.6.1
|
||||||
globals: 13.24.0
|
globals: 13.24.0
|
||||||
ignore: 5.3.2
|
ignore: 5.3.2
|
||||||
@@ -2960,10 +2960,10 @@ snapshots:
|
|||||||
|
|
||||||
'@floating-ui/[email protected]': {}
|
'@floating-ui/[email protected]': {}
|
||||||
|
|
||||||
'@humanwhocodes/[email protected]':
|
'@humanwhocodes/[email protected]([email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanwhocodes/object-schema': 2.0.3
|
'@humanwhocodes/object-schema': 2.0.3
|
||||||
debug: 4.4.3
|
debug: 4.4.3([email protected])
|
||||||
minimatch: 3.1.5
|
minimatch: 3.1.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -3090,34 +3090,34 @@ snapshots:
|
|||||||
'@tybys/wasm-util': 0.10.3
|
'@tybys/wasm-util': 0.10.3
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/[email protected]0': {}
|
'@next/[email protected]4': {}
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-glob: 3.3.1
|
fast-glob: 3.3.1
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/[email protected]0':
|
'@next/[email protected]4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@nodelib/[email protected]':
|
'@nodelib/[email protected]':
|
||||||
@@ -3643,15 +3643,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.2.3
|
csstype: 3.2.3
|
||||||
|
|
||||||
'@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected])':
|
'@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
'@typescript-eslint/parser': 8.64.0([email protected])([email protected])
|
'@typescript-eslint/parser': 8.64.0([email protected]([email protected]))([email protected])([email protected])
|
||||||
'@typescript-eslint/scope-manager': 8.64.0
|
'@typescript-eslint/scope-manager': 8.64.0
|
||||||
'@typescript-eslint/type-utils': 8.64.0([email protected])([email protected])
|
'@typescript-eslint/type-utils': 8.64.0([email protected]([email protected]))([email protected])([email protected])
|
||||||
'@typescript-eslint/utils': 8.64.0([email protected])([email protected])
|
'@typescript-eslint/utils': 8.64.0([email protected]([email protected]))([email protected])([email protected])
|
||||||
'@typescript-eslint/visitor-keys': 8.64.0
|
'@typescript-eslint/visitor-keys': 8.64.0
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1([email protected])
|
||||||
ignore: 7.0.6
|
ignore: 7.0.6
|
||||||
natural-compare: 1.4.0
|
natural-compare: 1.4.0
|
||||||
ts-api-utils: 2.5.0([email protected])
|
ts-api-utils: 2.5.0([email protected])
|
||||||
@@ -3659,23 +3659,23 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/[email protected]([email protected])([email protected])':
|
'@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/scope-manager': 8.64.0
|
'@typescript-eslint/scope-manager': 8.64.0
|
||||||
'@typescript-eslint/types': 8.64.0
|
'@typescript-eslint/types': 8.64.0
|
||||||
'@typescript-eslint/typescript-estree': 8.64.0([email protected])
|
'@typescript-eslint/typescript-estree': 8.64.0([email protected])([email protected])
|
||||||
'@typescript-eslint/visitor-keys': 8.64.0
|
'@typescript-eslint/visitor-keys': 8.64.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3([email protected])
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1([email protected])
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/[email protected]([email protected])':
|
'@typescript-eslint/[email protected]([email protected])([email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.64.0([email protected])
|
'@typescript-eslint/tsconfig-utils': 8.64.0([email protected])
|
||||||
'@typescript-eslint/types': 8.64.0
|
'@typescript-eslint/types': 8.64.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3([email protected])
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -3689,13 +3689,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
'@typescript-eslint/[email protected]([email protected])([email protected])':
|
'@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.64.0
|
'@typescript-eslint/types': 8.64.0
|
||||||
'@typescript-eslint/typescript-estree': 8.64.0([email protected])
|
'@typescript-eslint/typescript-estree': 8.64.0([email protected])([email protected])
|
||||||
'@typescript-eslint/utils': 8.64.0([email protected])([email protected])
|
'@typescript-eslint/utils': 8.64.0([email protected]([email protected]))([email protected])([email protected])
|
||||||
debug: 4.4.3
|
debug: 4.4.3([email protected])
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1([email protected])
|
||||||
ts-api-utils: 2.5.0([email protected])
|
ts-api-utils: 2.5.0([email protected])
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -3703,13 +3703,13 @@ snapshots:
|
|||||||
|
|
||||||
'@typescript-eslint/[email protected]': {}
|
'@typescript-eslint/[email protected]': {}
|
||||||
|
|
||||||
'@typescript-eslint/[email protected]([email protected])':
|
'@typescript-eslint/[email protected]([email protected])([email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/project-service': 8.64.0([email protected])
|
'@typescript-eslint/project-service': 8.64.0([email protected])([email protected])
|
||||||
'@typescript-eslint/tsconfig-utils': 8.64.0([email protected])
|
'@typescript-eslint/tsconfig-utils': 8.64.0([email protected])
|
||||||
'@typescript-eslint/types': 8.64.0
|
'@typescript-eslint/types': 8.64.0
|
||||||
'@typescript-eslint/visitor-keys': 8.64.0
|
'@typescript-eslint/visitor-keys': 8.64.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3([email protected])
|
||||||
minimatch: 10.2.5
|
minimatch: 10.2.5
|
||||||
semver: 7.8.5
|
semver: 7.8.5
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
@@ -3718,13 +3718,13 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/[email protected]([email protected])([email protected])':
|
'@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1([email protected])
|
'@eslint-community/eslint-utils': 4.9.1([email protected]([email protected]))
|
||||||
'@typescript-eslint/scope-manager': 8.64.0
|
'@typescript-eslint/scope-manager': 8.64.0
|
||||||
'@typescript-eslint/types': 8.64.0
|
'@typescript-eslint/types': 8.64.0
|
||||||
'@typescript-eslint/typescript-estree': 8.64.0([email protected])
|
'@typescript-eslint/typescript-estree': 8.64.0([email protected])([email protected])
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1([email protected])
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -4121,13 +4121,17 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
is-data-view: 1.0.2
|
is-data-view: 1.0.2
|
||||||
|
|
||||||
[email protected]:
|
[email protected]([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
optionalDependencies:
|
||||||
|
supports-color: 7.2.0
|
||||||
|
|
||||||
[email protected]:
|
[email protected]([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
optionalDependencies:
|
||||||
|
supports-color: 7.2.0
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
@@ -4147,15 +4151,15 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.29.7
|
'@babel/parser': 7.29.7
|
||||||
'@babel/traverse': 7.29.7
|
'@babel/traverse': 7.29.7([email protected])
|
||||||
'@vue/compiler-sfc': 3.5.39
|
'@vue/compiler-sfc': 3.5.39
|
||||||
callsite: 1.0.0
|
callsite: 1.0.0
|
||||||
camelcase: 6.3.0
|
camelcase: 6.3.0
|
||||||
cosmiconfig: 7.1.0
|
cosmiconfig: 7.1.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3([email protected])
|
||||||
deps-regex: 0.2.0
|
deps-regex: 0.2.0
|
||||||
findup-sync: 5.0.0
|
findup-sync: 5.0.0
|
||||||
ignore: 5.3.2
|
ignore: 5.3.2
|
||||||
@@ -4338,19 +4342,19 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]0([email protected])([email protected]):
|
[email protected]4([email protected]([email protected]))([email protected])([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/eslint-plugin-next': 15.5.20
|
'@next/eslint-plugin-next': 15.5.24
|
||||||
'@rushstack/eslint-patch': 1.16.1
|
'@rushstack/eslint-patch': 1.16.1
|
||||||
'@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected])
|
'@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])
|
||||||
'@typescript-eslint/parser': 8.64.0([email protected])([email protected])
|
'@typescript-eslint/parser': 8.64.0([email protected]([email protected]))([email protected])([email protected])
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1([email protected])
|
||||||
eslint-import-resolver-node: 0.3.10
|
eslint-import-resolver-node: 0.3.10([email protected])
|
||||||
eslint-import-resolver-typescript: 3.10.1([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected]))([email protected])
|
eslint-import-resolver-typescript: 3.10.1([email protected])([email protected]([email protected]))([email protected])
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected])
|
eslint-plugin-import: 2.32.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected])([email protected]([email protected]))([email protected])
|
||||||
eslint-plugin-jsx-a11y: 6.10.2([email protected])
|
eslint-plugin-jsx-a11y: 6.10.2([email protected]([email protected]))
|
||||||
eslint-plugin-react: 7.37.5([email protected])
|
eslint-plugin-react: 7.37.5([email protected]([email protected]))
|
||||||
eslint-plugin-react-hooks: 5.2.0([email protected])
|
eslint-plugin-react-hooks: 5.2.0([email protected]([email protected]))
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -4358,52 +4362,52 @@ snapshots:
|
|||||||
- eslint-plugin-import-x
|
- eslint-plugin-import-x
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
[email protected]:
|
[email protected]([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7([email protected])
|
||||||
is-core-module: 2.16.2
|
is-core-module: 2.16.2
|
||||||
resolve: 2.0.0-next.7
|
resolve: 2.0.0-next.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
[email protected]([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected]))([email protected]):
|
[email protected]([email protected])([email protected]([email protected]))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nolyfill/is-core-module': 1.0.39
|
'@nolyfill/is-core-module': 1.0.39
|
||||||
debug: 4.4.3
|
debug: 4.4.3([email protected])
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1([email protected])
|
||||||
get-tsconfig: 4.14.0
|
get-tsconfig: 4.14.0
|
||||||
is-bun-module: 2.0.0
|
is-bun-module: 2.0.0
|
||||||
stable-hash: 0.0.5
|
stable-hash: 0.0.5
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
unrs-resolver: 1.12.2
|
unrs-resolver: 1.12.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected])
|
eslint-plugin-import: 2.32.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected])([email protected]([email protected]))([email protected])
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
[email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])(eslint-import-resolver-[email protected]([email protected](@typescript-eslint/[email protected]([email protected])(typescript@5.9.3))([email protected]))([email protected]))([email protected]):
|
[email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))(eslint-import-resolver-typescript@3.10.1)([email protected]([email protected]))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7([email protected])
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 8.64.0([email protected])([email protected])
|
'@typescript-eslint/parser': 8.64.0([email protected]([email protected]))([email protected])([email protected])
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1([email protected])
|
||||||
eslint-import-resolver-node: 0.3.10
|
eslint-import-resolver-node: 0.3.10([email protected])
|
||||||
eslint-import-resolver-typescript: 3.10.1([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected]))([email protected])
|
eslint-import-resolver-typescript: 3.10.1([email protected])([email protected]([email protected]))([email protected])
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
[email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected]):
|
[email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected])([email protected]([email protected]))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rtsao/scc': 1.1.0
|
'@rtsao/scc': 1.1.0
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
array.prototype.findlastindex: 1.2.6
|
array.prototype.findlastindex: 1.2.6
|
||||||
array.prototype.flat: 1.3.3
|
array.prototype.flat: 1.3.3
|
||||||
array.prototype.flatmap: 1.3.3
|
array.prototype.flatmap: 1.3.3
|
||||||
debug: 3.2.7
|
debug: 3.2.7([email protected])
|
||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1([email protected])
|
||||||
eslint-import-resolver-node: 0.3.10
|
eslint-import-resolver-node: 0.3.10([email protected])
|
||||||
eslint-module-utils: 2.14.0(@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])(eslint-import-resolver-[email protected]([email protected](@typescript-eslint/[email protected]([email protected])(typescript@5.9.3))([email protected]))([email protected]))([email protected])
|
eslint-module-utils: 2.14.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))(eslint-import-resolver-typescript@3.10.1)([email protected]([email protected]))([email protected])
|
||||||
hasown: 2.0.4
|
hasown: 2.0.4
|
||||||
is-core-module: 2.16.2
|
is-core-module: 2.16.2
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@@ -4415,13 +4419,13 @@ snapshots:
|
|||||||
string.prototype.trimend: 1.0.10
|
string.prototype.trimend: 1.0.10
|
||||||
tsconfig-paths: 3.15.0
|
tsconfig-paths: 3.15.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 8.64.0([email protected])([email protected])
|
'@typescript-eslint/parser': 8.64.0([email protected]([email protected]))([email protected])([email protected])
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- eslint-import-resolver-typescript
|
- eslint-import-resolver-typescript
|
||||||
- eslint-import-resolver-webpack
|
- eslint-import-resolver-webpack
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
[email protected]([email protected]):
|
[email protected]([email protected]([email protected])):
|
||||||
dependencies:
|
dependencies:
|
||||||
aria-query: 5.3.2
|
aria-query: 5.3.2
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
@@ -4431,7 +4435,7 @@ snapshots:
|
|||||||
axobject-query: 4.1.0
|
axobject-query: 4.1.0
|
||||||
damerau-levenshtein: 1.0.8
|
damerau-levenshtein: 1.0.8
|
||||||
emoji-regex: 9.2.2
|
emoji-regex: 9.2.2
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1([email protected])
|
||||||
hasown: 2.0.4
|
hasown: 2.0.4
|
||||||
jsx-ast-utils: 3.3.5
|
jsx-ast-utils: 3.3.5
|
||||||
language-tags: 1.0.9
|
language-tags: 1.0.9
|
||||||
@@ -4440,11 +4444,11 @@ snapshots:
|
|||||||
safe-regex-test: 1.1.0
|
safe-regex-test: 1.1.0
|
||||||
string.prototype.includes: 2.0.1
|
string.prototype.includes: 2.0.1
|
||||||
|
|
||||||
[email protected]([email protected]):
|
[email protected]([email protected]([email protected])):
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1([email protected])
|
||||||
|
|
||||||
[email protected]([email protected]):
|
[email protected]([email protected]([email protected])):
|
||||||
dependencies:
|
dependencies:
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
array.prototype.findlast: 1.2.5
|
array.prototype.findlast: 1.2.5
|
||||||
@@ -4452,7 +4456,7 @@ snapshots:
|
|||||||
array.prototype.tosorted: 1.1.4
|
array.prototype.tosorted: 1.1.4
|
||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
es-iterator-helpers: 1.4.0
|
es-iterator-helpers: 1.4.0
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1([email protected])
|
||||||
estraverse: 5.3.0
|
estraverse: 5.3.0
|
||||||
hasown: 2.0.4
|
hasown: 2.0.4
|
||||||
jsx-ast-utils: 3.3.5
|
jsx-ast-utils: 3.3.5
|
||||||
@@ -4475,20 +4479,20 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1([email protected])
|
'@eslint-community/eslint-utils': 4.9.1([email protected]([email protected]))
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
'@eslint/eslintrc': 2.1.4
|
'@eslint/eslintrc': 2.1.4([email protected])
|
||||||
'@eslint/js': 8.57.1
|
'@eslint/js': 8.57.1
|
||||||
'@humanwhocodes/config-array': 0.13.0
|
'@humanwhocodes/config-array': 0.13.0([email protected])
|
||||||
'@humanwhocodes/module-importer': 1.0.1
|
'@humanwhocodes/module-importer': 1.0.1
|
||||||
'@nodelib/fs.walk': 1.2.8
|
'@nodelib/fs.walk': 1.2.8
|
||||||
'@ungap/structured-clone': 1.3.3
|
'@ungap/structured-clone': 1.3.3
|
||||||
ajv: 6.15.0
|
ajv: 6.15.0
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
debug: 4.4.3
|
debug: 4.4.3([email protected])
|
||||||
doctrine: 3.0.0
|
doctrine: 3.0.0
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
eslint-scope: 7.2.2
|
eslint-scope: 7.2.2
|
||||||
@@ -5055,9 +5059,9 @@ snapshots:
|
|||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7([email protected])
|
react-dom: 19.2.7([email protected])
|
||||||
|
|
||||||
[email protected]0([email protected]([email protected]))([email protected]):
|
[email protected]4([email protected]([email protected]))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 15.5.20
|
'@next/env': 15.5.24
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
caniuse-lite: 1.0.30001805
|
caniuse-lite: 1.0.30001805
|
||||||
postcss: 8.4.31
|
postcss: 8.4.31
|
||||||
@@ -5065,14 +5069,14 @@ snapshots:
|
|||||||
react-dom: 19.2.7([email protected])
|
react-dom: 19.2.7([email protected])
|
||||||
styled-jsx: 5.1.6([email protected])
|
styled-jsx: 5.1.6([email protected])
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@next/swc-darwin-arm64': 15.5.20
|
'@next/swc-darwin-arm64': 15.5.24
|
||||||
'@next/swc-darwin-x64': 15.5.20
|
'@next/swc-darwin-x64': 15.5.24
|
||||||
'@next/swc-linux-arm64-gnu': 15.5.20
|
'@next/swc-linux-arm64-gnu': 15.5.24
|
||||||
'@next/swc-linux-arm64-musl': 15.5.20
|
'@next/swc-linux-arm64-musl': 15.5.24
|
||||||
'@next/swc-linux-x64-gnu': 15.5.20
|
'@next/swc-linux-x64-gnu': 15.5.24
|
||||||
'@next/swc-linux-x64-musl': 15.5.20
|
'@next/swc-linux-x64-musl': 15.5.24
|
||||||
'@next/swc-win32-arm64-msvc': 15.5.20
|
'@next/swc-win32-arm64-msvc': 15.5.24
|
||||||
'@next/swc-win32-x64-msvc': 15.5.20
|
'@next/swc-win32-x64-msvc': 15.5.24
|
||||||
sharp: 0.34.5
|
sharp: 0.34.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import { getEnergyKcal, getNutrientValue } from "../lib/nutrition.ts";
|
||||||
|
import { formatNutritionData, sortProducts, type Product } from "../lib/openFoodFactsApi.ts";
|
||||||
|
|
||||||
|
test("prefere energia explícita em kcal e preserva zero", () => {
|
||||||
|
assert.equal(getEnergyKcal({ "energy-kcal_100g": 539, energy_100g: 2255 }), 539);
|
||||||
|
assert.equal(getEnergyKcal({ "energy-kcal_100g": 0, energy_100g: 418.4 }), 0);
|
||||||
|
assert.equal(getNutrientValue(0), 0);
|
||||||
|
assert.equal(getNutrientValue("0"), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("converte kJ para kcal quando o campo kcal está ausente", () => {
|
||||||
|
assert.ok(Math.abs(getEnergyKcal({ energy_100g: 418.4 })! - 100) < 1e-10);
|
||||||
|
assert.ok(Math.abs(getEnergyKcal({ "energy-kj_100g": 418.4 })! - 100) < 1e-10);
|
||||||
|
assert.equal(getEnergyKcal(), null);
|
||||||
|
assert.equal(getEnergyKcal({}), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("valores ausentes, negativos e não finitos não entram nos cálculos", () => {
|
||||||
|
for (const value of [null, undefined, "", " ", "garbage", "12g", true, -1, NaN, Infinity, "Infinity"]) {
|
||||||
|
assert.equal(getNutrientValue(value), null);
|
||||||
|
}
|
||||||
|
assert.equal(getEnergyKcal({ "energy-kcal_100g": NaN, energy_100g: Infinity }), null);
|
||||||
|
assert.equal(getNutrientValue("12.5"), 12.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formata energia e nutrientes com zero sem confundir com dados ausentes", () => {
|
||||||
|
const formatted = formatNutritionData({ code: "zero", product_name: "Zero", nutriments: {
|
||||||
|
"energy-kcal_100g": 0, energy_100g: 100, fat_100g: 0,
|
||||||
|
carbohydrates_100g: 0, sodium_100g: 0,
|
||||||
|
} });
|
||||||
|
assert.equal(formatted?.energy, "0 kcal");
|
||||||
|
assert.equal(formatted?.fat, "0.0g");
|
||||||
|
assert.equal(formatted?.carbs, "0.0g");
|
||||||
|
assert.equal(formatted?.carbohydrates, "0.0g");
|
||||||
|
assert.equal(formatted?.sodium, "0.00g");
|
||||||
|
assert.equal(formatted?.proteins, "N/A");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formata kcal explícitas e converte o fallback em kJ", () => {
|
||||||
|
const base = { code: "a", product_name: "A" };
|
||||||
|
assert.equal(formatNutritionData({ ...base, nutriments: { "energy-kcal_100g": 539, energy_100g: 2255 } })?.energy, "539 kcal");
|
||||||
|
assert.equal(formatNutritionData({ ...base, nutriments: { energy_100g: 418.4 } })?.energy, "100 kcal");
|
||||||
|
assert.equal(formatNutritionData({ ...base, nutriments: { fat_100g: Infinity } })?.fat, "N/A");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ordena por kcal, preserva zero e deixa dados ausentes no fim nas duas direções", () => {
|
||||||
|
const products: Product[] = [
|
||||||
|
{ code: "unknown", product_name: "Unknown" },
|
||||||
|
{ code: "kj", product_name: "kJ", nutriments: { energy_100g: 418.4 } },
|
||||||
|
{ code: "zero", product_name: "Zero", nutriments: { "energy-kcal_100g": 0 } },
|
||||||
|
{ code: "kcal", product_name: "kcal", nutriments: { "energy-kcal_100g": 50 } },
|
||||||
|
];
|
||||||
|
assert.deepEqual(sortProducts(products, "energy").map(p => p.code), ["zero", "kcal", "kj", "unknown"]);
|
||||||
|
assert.deepEqual(sortProducts(products, "energy", "desc").map(p => p.code), ["kj", "kcal", "zero", "unknown"]);
|
||||||
|
assert.equal(products[0].code, "unknown");
|
||||||
|
});
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
|||||||
Reference in new issue
Block a user