primeiro tutorial

This commit is contained in:
flavia-vic 2025-01-20 19:13:51 -04:00
commit 53da3e0a8c
15 changed files with 3724 additions and 0 deletions

31
.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
.vscode
/cypress/videos/
/cypress/screenshots/
public
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

105
README.md Normal file
View File

@ -0,0 +1,105 @@
# Pokemon Search
## Descrição
O Pokemon Search é uma aplicação que permite pesquisar e visualizar informações detalhadas sobre Pokémons usando a API [PokeAPI](https://pokeapi.co/) projeto apresenta uma interface interativa e amigável para facilitar a exploração do mundo dos Pokémons.
## Funcionalidades
- Campo de busca: Permite pesquisar Pokémons por nome ou ID.
- Histórico de pesquisas: Armazena até 5 pesquisas recentes, com opção de selecionar ou remover itens do histórico.
- Lista de Pokémons: Exibe uma lista inicial de Pokémons com imagens e nomes.
- Detalhes do Pokémon: Mostra informações como altura, peso, tipos, habilidades e estatísticas em um modal.
- Carregamento dinâmico: Exibe um indicador de carregamento enquanto busca dados da API.
- Tratamento de erros: Mostra mensagens de erro em caso de falha na busca.
## Tecnologias Utilizadas
- Vue.js: Framework JavaScript para construção da interface.
- Pinia: Gerenciamento de estado para manutenção de dados globais (e.g., lista de Pokémons, estado de carregamento, erros).
- CSS3: Estilização responsiva e interativa da aplicação.
- PokeAPI: API usada para buscar os dados dos Pokémons.
## Como Executar
1. Requisitos
Certifique-se de que você possui as seguintes ferramentas instaladas em sua máquina:
Node.js (versão 16 ou superior)
npm ou yarn
2. Clonar o Repositório
```sh
https://github.com/seu-usuario/pokemon-search.git
cd pokemon-search
```
3. Instalar Dependências
Utilize o npm ou o yarn para instalar as dependências do projeto:
```sh
npm install
# ou
yarn install
```
4. Executar o Projeto
Inicie o servidor de desenvolvimento com o comando:
```sh
npm run dev
# ou
yarn dev
```
O aplicativo estará disponível em http://localhost:3000.
Estrutura do Projeto
```sh
┌── src/
│ ├── assets/
│ ├── components/
│ ├── stores/ # Gerenciamento de estado com Pinia
│ ├── views/ # Páginas principais do aplicativo
│ └── App.vue # Componente raiz
│ └── main.js # Arquivo principal para inicialização
└── public/
└── index.html # Arquivo HTML principal
```
## Personalização
Para personalizar o projeto:
- Novas funcionalidades: Adicione novos módulos no diretório components ou stores.
- Endpoints de API: Atualize ou expanda as chamadas à API no pokemonStore.
## Melhorias Futuras
- Adicionar paginação na lista de Pokémons.
- Implementar busca de Pokémons por tipos.
- Melhorar a responsividade para dispositivos menores.
- Adicionar testes unitários e de integração.
## Contribuições
Contribuições são bem-vindas! Sinta-se à vontade para abrir um issue ou enviar um pull request.

18
index.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<link rel="stylesheet" href="css/pico.min.css">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&family=Quicksand:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8
jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

2922
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "poke-api",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.9",
"pinia": "^2.3.1",
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.0.5",
"vite-plugin-vue-devtools": "^7.6.8"
}
}

16
src/App.vue Normal file
View File

@ -0,0 +1,16 @@
<template>
<div id="app">
<Home />
</div>
</template>
<script>
import Home from './views/Home.vue';
export default {
name: 'App',
components: {
Home,
},
};
</script>

86
src/assets/base.css Normal file
View File

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

1
src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

35
src/assets/main.css Normal file
View File

@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

View File

@ -0,0 +1,43 @@
<template>
<div>
<h1>Pokémons</h1>
<button @click="loadPokemons" :disabled="isLoading">
{{ isLoading ? "Carregando..." : "Carregar Pokémons" }}
</button>
<div v-if="error" class="error">{{ error }}</div>
<ul v-if="pokemons.length">
<li v-for="(pokemon, index) in pokemons" :key="index">
{{ pokemon.name }}
</li>
</ul>
</div>
</template>
<script>
import { usePokemonStore } from '../stores/pokemonStore';
export default {
setup() {
const store = usePokemonStore();
const loadPokemons = () => {
store.fetchPokemons();
};
return {
pokemons: store.pokemons,
isLoading: store.isLoading,
error: store.error,
loadPokemons,
};
},
};
</script>
<style scoped>
.error {
color: red;
margin: 1rem 0;
}
</style>

7
src/main.js Normal file
View File

@ -0,0 +1,7 @@
import { createApp } from 'vue';
import App from './App.vue';
import { createPinia } from 'pinia';
const app = createApp(App);
app.use(createPinia());
app.mount('#app');

View File

@ -0,0 +1,39 @@
import { defineStore } from 'pinia';
import axios from 'axios';
export const usePokemonStore = defineStore('pokemon', {
state: () => ({
pokemons: [],
loading: false,
error: null,
}),
actions: {
async fetchPokemons(limit = 10) {
this.loading = true;
this.error = null;
try {
const response = await axios.get(`https://pokeapi.co/api/v2/pokemon?limit=${limit}`);
this.pokemons = response.data.results;
} catch (err) {
this.error = err.message;
} finally {
this.loading = false;
}
},
async fetchPokemonDetails(name) {
this.loading = true;
this.error = null;
try {
const response = await axios.get(`https://pokeapi.co/api/v2/pokemon/${name}`);
return response.data;
} catch (err) {
this.error = err.message;
return null;
} finally {
this.loading = false;
}
},
},
});

374
src/views/Home.vue Normal file
View File

@ -0,0 +1,374 @@
<template>
<div class="container">
<h1 class="title">Pokemon Search</h1>
<!-- Search Field -->
<div class="search-container">
<!-- Campo de entrada de texto com binding bidirecional com "searchQuery" e evento para disparar a busca ao pressionar Enter -->
<input
type="text"
v-model="searchQuery"
placeholder="Search for a Pokémon..."
class="search-input"
@keyup.enter="handleSearch"
/>
<!-- Botão de pesquisa que chama a função "handleSearch" -->
<button class="search-button" @click="handleSearch">Search</button>
</div>
<!-- Histórico de Pesquisas -->
<div v-if="recentSearches.length" class="history-container">
<ul class="history-dropdown">
<!-- Exibe as pesquisas recentes, permitindo selecionar ou remover pesquisas anteriores -->
<li v-for="(item, index) in recentSearches" :key="index">
<span @click="fetchPokemonDetails(item)">{{ item }}</span>
<button class="remove-btn" @click="removeSearchHistory(index)">×</button>
</li>
</ul>
</div>
<!-- Lista de Pokémons -->
<div class="pokemon-grid-container">
<!-- Exibe a lista de Pokémons apenas se não estiver carregando e houver dados -->
<ul v-if="!loading && pokemons.length" class="pokemon-grid">
<li
v-for="(pokemon, index) in pokemons"
:key="pokemon.name"
class="pokemon-card"
@click="fetchPokemonDetails(pokemon.name)"
>
<!-- Imagem e nome do Pokémon -->
<img
:src="`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${index + 1}.png`"
alt="Pokemon Image"
/>
<span>{{ pokemon.name }}</span>
</li>
</ul>
</div>
<!-- Modal para exibir detalhes do Pokémon -->
<div v-if="pokemonDetails" class="modal" @click.self="closeModal">
<div class="modal-content">
<h2>{{ pokemonDetails.name }}</h2>
<img :src="pokemonDetails.sprites.front_default" alt="Pokemon Image" />
<p>Altura: {{ pokemonDetails.height * 10 }} cm</p> <!-- Converte de decímetros para centímetros -->
<p>Peso: {{ pokemonDetails.weight * 0.1 }} kg</p> <!-- Converte de hectogramas para quilogramas -->
<p>Types:
<span v-for="type in pokemonDetails.types" :key="type.type.name">{{ type.type.name }}</span>
</p>
<p>Abilities:
<span v-for="ability in pokemonDetails.abilities" :key="ability.ability.name">
{{ ability.ability.name }}
</span>
</p>
<p>Stats:</p>
<ul>
<li v-for="stat in pokemonDetails.stats" :key="stat.stat.name">
{{ stat.stat.name }}: {{ stat.base_stat }}
</li>
</ul>
<button @click="closeModal">Close</button>
</div>
</div>
<!-- Indicador de Carregamento -->
<div v-if="loading" class="loader">Loading...</div>
<!-- Mensagem de Erro -->
<div v-if="error" class="error-message">
Error: {{ error }}
</div>
</div>
</template>
<script>
import { usePokemonStore } from "@/stores/pokemonStore";
export default {
name: "Home",
data() {
return {
searchQuery: "", // Armazena o texto da pesquisa
recentSearches: [], // Histórico das pesquisas recentes
pokemonDetails: null, // Detalhes do Pokémon selecionado
};
},
computed: {
// Obtém a lista de Pokémons do store
pokemons() {
const store = usePokemonStore();
return store.pokemons;
},
// Indica se está carregando os dados
loading() {
const store = usePokemonStore();
return store.loading;
},
// Armazena mensagens de erro do store
error() {
const store = usePokemonStore();
return store.error;
},
},
methods: {
// Gerencia a lógica para buscar um Pokémon
handleSearch() {
if (this.searchQuery.trim()) {
// Adiciona ao histórico de pesquisas, removendo duplicatas e limitando a 5 itens
this.recentSearches = [
this.searchQuery,
...this.recentSearches.filter((item) => item !== this.searchQuery),
].slice(0, 5);
// Busca os detalhes do Pokémon
this.fetchPokemonDetails(this.searchQuery);
this.searchQuery = ""; // Limpa o campo de pesquisa
}
},
// Remove um item do histórico de pesquisas
removeSearchHistory(index) {
this.recentSearches.splice(index, 1);
},
// Busca os detalhes de um Pokémon específico
async fetchPokemonDetails(name) {
try {
const store = usePokemonStore();
this.pokemonDetails = await store.fetchPokemonDetails(name);
} catch (error) {
console.error("Erro ao buscar detalhes do Pokémon:", error);
}
},
// Fecha o modal de detalhes do Pokémon
closeModal() {
this.pokemonDetails = null;
},
},
// Ao montar o componente, carrega a lista inicial de Pokémons
mounted() {
const store = usePokemonStore();
store.fetchPokemons();
},
};
</script>
<style scoped>
body {
background-color: #fffafc;
font-family: "Poppins", sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.container {
max-width: 800px;
width: 100%;
background-color: #fff0f5;
padding: 30px;
margin-top: 80px;
border-radius: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
text-align: center;
overflow: hidden;
}
.title {
color: #ff6f91;
font-size: 3rem;
margin-bottom: 20px;
}
.search-container {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.search-input {
flex: 1;
padding: 12px;
border: 2px solid #ffb3c1;
border-radius: 20px;
outline: none;
transition: border-color 0.3s ease;
height: 44px;
}
.search-input:focus {
border-color: #ff6f91;
box-shadow: 0 0 8px rgba(255, 111, 145, 0.5);
}
.search-button {
padding: 0 20px;
height: 44px;
background-color: #ff6f91;
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
}
.search-button:hover {
background-color: #ff4f7a;
transform: scale(1.05);
}
.history-container {
margin-bottom: 20px;
}
.modal {
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: #ffe4e9;
padding: 20px;
border-radius: 15px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
text-align: center;
max-width: 400px;
width: 100%;
}
.modal-content h2 {
color: #ff6f91;
font-size: 2rem;
margin-bottom: 10px;
}
.modal-content p {
color: #555;
margin: 10px 0;
}
.modal-content ul {
color: #555;
margin: 10px 0;
}
.modal-content img {
max-width: 100%;
margin-bottom: 20px;
}
.modal-content button {
padding: 10px 20px;
background-color: #ff6f91;
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s ease, transform 0.2s ease;
}
.modal-content button:hover {
background-color: #ff4f7a;
transform: scale(1.05);
}
.history-dropdown {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid #ddd;
border-radius: 10px;
background-color: #ffe4e9;
}
.history-dropdown li {
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.history-container {
margin-bottom: 20px;
max-height: 200px;
overflow-y: auto;
border-radius: 10px;
padding-right: 10px;
}
.history-container::-webkit-scrollbar {
width: 8px;
}
.history-container::-webkit-scrollbar-thumb {
background-color: #ff6f91;
border-radius: 10px;
transition: background-color 0.3s ease;
}
.history-container::-webkit-scrollbar-track {
background-color: #ffe4e9;
border-radius: 10px;
}
.history-container::-webkit-scrollbar-thumb:hover {
background-color: #ff4f7a;
}
.remove-btn {
background: none;
border: none;
color: #ff7f7f;
font-size: 1rem;
cursor: pointer;
}
.pokemon-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
width: 100%;
}
.pokemon-card {
background-color: #ffe4e9;
border: 2px solid #ffb3c1;
border-radius: 15px;
text-align: center;
padding: 10px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.pokemon-card:hover {
transform: scale(1.1);
box-shadow: 0 6px 12px rgba(255, 111, 145, 0.4);
background-color: #ff6f91;
}
.loader {
font-size: 1.5rem;
color: #ff6f91;
}
.error-message {
color: #ff4f7a;
font-size: 1.2rem;
margin-top: 20px;
}
</style>

18
vite.config.js Normal file
View File

@ -0,0 +1,18 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})