Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lootbox-frontend-challenge/.env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_API_URL=https://pokeapi.co/api/v2
24 changes: 24 additions & 0 deletions lootbox-frontend-challenge/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
8 changes: 8 additions & 0 deletions lootbox-frontend-challenge/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"endOfLine": "auto"
}
171 changes: 171 additions & 0 deletions lootbox-frontend-challenge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# 🎮 Lootbox Frontend Challenge - Pokédex

Uma aplicação frontend que consome a [PokéAPI](https://pokeapi.co/) para exibir uma **listagem de Pokémons**, permitir **buscas inteligentes** e mostrar a **comparação detalhada entre dois Pokémons** com direito a gráficos interativos.

[Veja o vídeo do app](/src/assets/players/apresentacao.mp4)

## 🚀 Funcionalidades

- **Listagem de Pokémons**
- Carrega **50 pokémons por vez** com botão _"Carregar mais"_.
- Header com **campo de busca**:
- Pesquisa em cima dos dados já carregados.
- Pesquisa diretamente na API e adiciona os resultados à listagem.

- **Detalhes do Pokémon**
- Ao clicar em um card, abre a página de detalhes.
- Mostra:
- Tipos do Pokémon.
- Estatísticas base (ataque, defesa, velocidade etc.).
- Altura e peso.

- **Comparação de Pokémons**
- Botão para selecionar outro Pokémon para comparar.
- Exibição lado a lado dos dois cards.
- **Gráfico radar interativo** para comparar atributos (ataque, defesa etc.).

---

## 🛠️ Tecnologias

Esse projeto foi criado com **Vite + React + TypeScript** e utiliza:

- [**Ant Design**](https://ant.design/) - UI Components.
- [**Ant Design Icons**](https://ant.design/components/icon/) - Ícones.
- [**TailwindCSS**](https://tailwindcss.com/) - Estilização responsiva.
- [**TanStack React Query**](https://tanstack.com/query/latest) - Gerenciamento de estado assíncrono e cache de requisições.
- [**Axios**](https://axios-http.com/) - Cliente HTTP.
- [**Zustand**](https://zustand-demo.pmnd.rs/) - Gerenciamento de estado global.
- [**React Router DOM**](https://reactrouter.com/) - Rotas da aplicação.
- [**Recharts**](https://recharts.org/) - Gráficos (usado no radar de comparação).

---

## 📂 Estrutura de Pastas

```bash
src/
├── assets/ # Imagens e arquivos estáticos
├── components/ # Componentes reutilizáveis
├── hooks/ # Hooks customizados
│ ├── models/ # Hooks personalizados - models do MVVM
│ ├── queries/ # Lógica de busca (API via React Query)
│ ├── device.ts # Hook para detectar dispositivo
│ ├── debounce.ts # Hook de debounce
│ └── image-pokemon.ts # Utilidades relacionadas a imagens
├── lib/ # Configurações globais (axios, query client)
├── models/ # Agrupamento de constantes, tipagens e stores globais
│ ├── consts/ # Constantes (ex: cores por tipo)
│ ├── types/ # Tipagens (TS)
│ │ ├── view/ # Contrato de MVVM (TS)
│ │ └── .ts # Tipagens (TS)
│ └── zustand/ # Store global
├── pages/ # Views principais (Home, Details etc.)
├── providers/ # Providers globais (React Query, Zustand etc.)
├── routes/ # Configuração das rotas
├── services/ # Serviços externos (API, integração)
├── styles/ # Estilos globais
├── tests/ # Testes com Vitest
├── views/ # Views MVVM
├── App.tsx # App principal
└── main.tsx # Entry point
```

## 🧪 Testes

O projeto possui uma pasta tests/ na raiz onde ficam os testes unitários dos hooks, stores e componentes.

- 🛠️ Bibliotecas usadas para testes

Vitest
– Framework de testes unitários moderno para Vite.

@testing-library/react
– Facilita testar componentes React de forma próxima à interação real do usuário.

@testing-library/react-hooks
– Permite testar hooks isoladamente.

jsdom
– Simula o DOM no Node.js para testes de componentes React.

- 🚀 Rodando os testes

```bash
# Rodar todos os testes uma vez
pnpm test

# Rodar em modo watch (recarrega ao salvar alterações)
pnpm test:watch

```

Scripts disponíveis no package.json:

---

## ⚙️ Como rodar o projeto

1. Clonar repositório

```bash
git clone https://github.com/luizgsv/lootbox-frontend-challenge.git
cd lootbox-frontend-challenge
```

2. Instalar dependências

```bash
pnpm install
# ou npm install
# ou yarn install
```

3. Configurar variáveis de ambiente
Crie um arquivo .env na raiz e adicione:

```bash
VITE_API_URL=https://pokeapi.co/api/v2
```

4. Rodar em desenvolvimento

```bash
pnpm dev
```

5. Build para produção

```bash
pnpm build
```

6. Preview do build

```bash
pnpm preview
```

## 📸 Screenshots

## 📐 Arquitetura (MVVM)

O projeto segue o padrão MVVM (Model–View–ViewModel), que organiza a aplicação em três camadas principais:

- Model: Responsável pela lógica de dados: definição de estruturas (tipos), regras de negócio, stores globais e constantes.

Exemplo: models/types/, models/consts/, models/zustand/.

- View: Camada de interface com o usuário. Só se preocupa em exibir dados e reagir a interações.

Exemplo: páginas em pages/ e visões/componentes em views/ e components/.

- ViewModel: Faz a ponte entre Model e View. É onde fica a lógica de apresentação: buscar dados na API, transformar informação, manipular estado e entregar já pronto para a View renderizar.

Exemplo: hooks/ (queries com React Query, Zustand e outros hooks customizados).

🔗 Essa separação ajuda a manter o código modular, testável e de fácil manutenção, evitando que a View (UI) se confunda com a lógica de negócio.

## 👨‍💻Autor

Desenvolvido por [Luiz Gustavo](https://www.linkedin.com/in/luiz-vargas/).
38 changes: 38 additions & 0 deletions lootbox-frontend-challenge/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
import { globalIgnores } from 'eslint/config';
import prettierPlugin from 'eslint-plugin-prettier'

export default tseslint.config([
globalIgnores(['dist', 'node_modules']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: globals.browser,
},
rules: {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'prettier/prettier': 'error',
},
settings: {
react: {
version: 'detect',
},
},
plugins: {
prettier: prettierPlugin
},
},
]);
13 changes: 13 additions & 0 deletions lootbox-frontend-challenge/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/pokeball.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pokémon - Luiz Vargas</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
55 changes: 55 additions & 0 deletions lootbox-frontend-challenge/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "lootbox-frontend-challenge",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint src/**/*.{ts,tsx}",
"lint:fix": "eslint src/**/*.{ts,tsx} --fix",
"format": "prettier --write src/**/*.{ts,tsx,css,scss,md}",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@ant-design/icons": "^6.0.1",
"@tailwindcss/vite": "^4.1.13",
"@tanstack/react-query": "^5.87.1",
"@tanstack/react-query-devtools": "^5.87.1",
"antd": "^5.27.3",
"axios": "^1.11.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.8.2",
"recharts": "^3.1.2",
"tailwindcss": "^4.1.13",
"zustand": "^5.0.8"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@tanstack/eslint-plugin-query": "^5.86.0",
"@testing-library/react": "^16.3.0",
"@testing-library/react-hooks": "^8.0.1",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@types/react-router-dom": "^5.3.3",
"@typescript-eslint/eslint-plugin": "^8.42.0",
"@typescript-eslint/parser": "^8.42.0",
"@vitejs/plugin-react": "^5.0.0",
"eslint": "^9.33.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"jsdom": "^26.1.0",
"prettier": "^3.6.2",
"typescript": "~5.8.3",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2",
"vitest": "^3.2.4"
}
}
Loading