Desenvolvendo uma pokédex (web app)

Desenvolvendo uma pokédex (web app) logo HTML logo CSS logo JavaScript logo Bootstrap logo jQuery logo PokéAPI logo Chrome DevTools

Nesta situação-problema veremos parte do contexto de SOA na prática! Para isto consumiremos um Web service REST, por meio de uma RESTful API! Veremos, o quão simples, pode ser isso. Vamos lá?!

Para quem é fã de uma das mais consagradas franquias da Nintendo, isto é, Pokémon, gostará muito desta proposta. Nesta utilizaremos a PokéAPI v2, uma API pública e muito simples de se trabalhar.

Antes de iniciarmos, é importante entendermos como funciona a REST API em dois simples passos:

  1. Em linhas gerais, nós (o cliente) fazemos um HTTP request, por meio do método GET ao servidor.
  2. O servidor, por sua vez, nos devolve um HTTP response, com um código de erro, caso nossa solicitação apresente algum problema, ou o código 200 (HTTP success), juntamente com os dados requeridos (JSON object (já parseado em JavaScript object)).

Nesta API temos o endpoint (endereço de request) https://pokeapi.co/api/v2/{query} e também a query, que é o nosso critério de pesquisa. Para pesquisar um pokémon por nome ou id, por exemplo, utilizamos como query a sintaxe: pokemon/{id|name}. Para maiores detalhes, podemos sempre recorrer à documentação.

Com estas informações em mente, vejamos uma implementação simplificada do consumo da REST API, utilizando um plugin JS, o jQuery.

<!DOCTYPE html>
<html>
  <head>
    <title>Pokédex</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- Incorpora o jQuery ao site -->
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script>
    
      function getPokemon(valor){
        // Faz um HTTP Request, por meio de uma GET call (assíncrona)
        // para um endpoint definido
        // Aguarda como dados vindos do response um JSON object
        $.ajax({
          url: "https://pokeapi.co/api/v2/pokemon/" + valor,
          method: "get",
          dataType: "json"
        })
        // Se o servidor retornar SUCCESS
        .done(function(data){
          // imprime os dados do response no console do browser
          console.log(data);
        })
        // Se o servidor retornar ERRO
        .fail(function(jqXHR){
          // imprime o HTTP error number
          console.log("Erro HTTP: " + jqXHR.status);
        });                     
      } 
      
    </script>
  </head>
  <body>
    <!-- Aciona um evento de clique chamando a getPokemon function 
    por meio de um valor de id -->
    <button type="button" onclick="getPokemon('150')">getPokemonById</button>
    <!-- Aciona um evento de clique chamando a getPokemon function 
    por meio de um valor de nome -->
    <button type="button" onclick="getPokemon('muk')">getPokemonByName</button>
  </body>
</html>

Bom, nossa tarefa é a partir do código fornecido, criar uma página web que tenha um input, do tipo text para receber nome|id de pokémons, e um button de pesquisa. Quando o usuário realizar a pesquisa, por meio, do response, devemos imprimir na página o id, o nome, o tipo e uma imagem que ilustre o pokémon. Caso exista erro, devemos imprimir na página, o correspondente HTTP error. O layout é livre.

E aí, pronto para o desafio?

A resolução

Considerando o código já fornecido, faremos nele alguns melhoramentos, respeitando, no entanto, aquilo que foi solicitado. Bom, para iniciarmos, criamos uma estrutura de diretórios, separarando cada linguagem com sua respectiva funcionalidade em arquivos correspondentes. Algo como:

p1/
  assets/
    css/
      style.css
    js/
      script.js
  index.html

Aqui, de imediato, podemos identificar as linguagens que formam o tripé da Programação Front-end, isto é, o HTML, onde é trabalhado a estrutura da página, o CSS, com o estilo/formatação desta e o JavaScript, também conhecido como JS, onde estará a programação, de fato.

Em um primeiro momento preenchemos nosso arquivo HTML com o código já fornecido, excluindo deste os trechos de código JS, bem como as tags button.

Nota: É convencionado que manter múltiplas linguagens em um mesmo arquivo é uma má prática de programação! Assim sendo, evitaremos!

Chamamos neste arquivo os arquivos CSS e JS, por meio das tags link e script, respectivamente. Além disso, adicionamos o jQuery localmente, a fim de evitar um request externo. Para isso, fizemos o download do jQuery v3.6.0 (compressed version), adicionando-o ao diretório /assets/js/.

Nota: Chamamos nossos arquivos de script como os últimos elementos da tag body! Assim garantimos que nossas JS functions sejam invocadas apenas após o carregamento total da página!

Antes de "desenhar" de fato, a interface, adicionamos também ao projeto mais uma tecnologia, a saber, o:

  • Bootstrap v5.1.3: um framework web, que tem como finalidades, o auxílio no desenvolvimento de código responsivo, também chamado de RWD (Responsive Web Design), o fornecimento de Componentes (HTML estilizados) e de Ícones (vetorizados), entre outras coisas.

Realizamos o download (Compiled CSS and JS version) e descompactamos o arquivo. Deste, migramos os arquivos bootstrap.min.css e bootstrap.min.css.map para o diretório /assets/css/ e os arquivos bootstrap.min.js e bootstrap.min.js.map para o diretório /assets/js/. Feito isso, linkamos cada um dos arquivos citados à página index.html.

Criamos também um novo diretório, o /assets/image/, e neste adicionamos uma imagem (16x16) para uso como favicon.

Ao fim, nossa estrutura de diretórios e arquivos ficou com a seguinte aparência:

p1/
  assets/
    css/
      bootstrap.min.css
      bootstrap.min.css.map
      style.css
    image/
      favicon.png
    js/
      bootstrap.min.js
      bootstrap.min.js.map
      jquery-3.6.0.min.js
      script.js
  index.html

Já nosso arquivo index.html:

<!DOCTYPE html>
<html>
  <head>
    <title>Pokédex</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="assets/css/bootstrap.min.css" rel="stylesheet">
    <link href="assets/css/style.css" rel="stylesheet">    
    <link href="assets/image/favicon.png" rel="icon" type="image/png">    
  </head>
  <body>  
    <script src="assets/js/jquery-3.6.0.min.js"></script>
    <script src="assets/js/bootstrap.min.js"></script>
    <script src="assets/js/script.js"></script>  
  </body>
</html>

Antes de prosseguirmos, vamos compreender minimamente o funcionamento do Bootstrap com as seguintes informações:

  1. Requer que todo código que for utilizá-lo esteja contido em um container. Este container é representado pela class container-fluid.
  2. Utiliza um sistema de grades, permitindo "fatiar" uma linha, (representada pela class row) em até 12 colunas (representada pela class col). Coluna(s) devem obrigatoriamente estar contidas em linha(s), e linha(s) em container(s).
  3. Este "fatiamento" pode ainda ser feito para diferentes resoluções, utilizando classes específicas, a saber:
    • extra extra large (>=1400px): col-xxl-
    • extra large (>=1200px): col-xl-
    • large (>=992px): col-lg-
    • medium (>=768px): col-md-
    • small (>=576px): col-sm-
    • extra small (<576px): col-

Para maiores detalhes, podemos consultar a documentação do Bootstrap, na seção Grid System.

A partir daí, incluímos, o código abaixo entre as tags body, observando atentamente os blocos de comentários, entendendo assim com maior clareza o funcionamento do framework.

<!-- cria o CONTAINER -->
<body class="container-fluid">
  <!-- cria a LINHA 1 -->    
  <div class="row">
    <!-- cria a COLUNA 1 -->      
    <div class="col-lg-9 col-md-8 col-sm-6 col-12">
      <!-- Na RESOLUÇÃO large, esta DIV ocupa 9/12 partes -->
      <!-- Na RESOLUÇÃO medium, esta DIV ocupa 8/12 partes -->
      <!-- Na RESOLUÇÃO small, esta DIV ocupa 6/12 partes -->
      <!-- Na RESOLUÇÃO extra small, esta DIV ocupa 12/12 partes -->  
    </div>
    <!-- cria a COLUNA 2 -->      
    <div class="col-lg-3 col-md-4 col-sm-6 col-12">
      <!-- Na RESOLUÇÃO large, esta DIV ocupa 3/12 partes -->
      <!-- Na RESOLUÇÃO medium, esta DIV ocupa 4/12 partes -->
      <!-- Na RESOLUÇÃO small, esta DIV ocupa 6/12 partes -->
      <!-- Na RESOLUÇÃO extra small, esta DIV ocupa 12/12 partes -->
    </div>
  </div>
  <!-- cria a LINHA 2 -->
  <div class="row"></div>
  <script src="assets/js/jquery-3.6.0.min.js"></script>
  <script src="assets/js/bootstrap.min.js"></script>
  <script src="assets/js/script.js"></script>  
</body>

Bom, esta é a estrutura básica de uma aplicação com Bootstrap. Agora sigamos:

  1. Na primeira coluna da primeira linha incluímos uma imagem e um título para nossa aplicação.
  2. Na segunda coluna da mesma linha, incluímos um Bootstrap Form Item, do tipo Input group > Button addons, disponível na seção Forms.
  3. Na tag button, incluímos um Bootstrap Icon, do tipo Search (Copy HTML), disponível na seção Icons.
<div class="col-lg-9 col-md-8 col-sm-6 col-12">
  <img alt="" src="assets/image/favicon.png">                    
  <h2>Pokédex</h2>
</div>          
<div class="col-lg-3 col-md-4 col-sm-6 col-12">
  <!-- Bootstrap Form Item: Input group > Button addons -->
  <div class="input-group">
    <input class="form-control" placeholder="Pokémon: name|id" type="text">
    <button class="btn" type="button">
      <!-- Bootstrap Icon: Search (Copy HTML) -->
      <svg xmlns="http://www.w3.org/2000/svg" width="16" 
      ="16" fill="currentColor" class="bi bi-search" viewBox="0 0 16 16">
        <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
      </svg>
    </button>      
  </div>
</div>

Agora estilizamos nossa página, atualizando o arquivo style.css com as seguintes configurações:

.container-fluid {
    font-family: "Courier New", Courier, monospace;    
    padding: 0;
}

.row {
    margin: 0;
}

.row:first-child,
.form-control,
.form-control:focus,
.btn {
    background-color: #24292E;    
}

.row:first-child,
.row > div {
    padding: 10px;
}

.row:first-child div {
    align-items: center;
    display: flex;
}

.row div img {
    float: left;
}

h2 {    
    margin-bottom: 0;
    margin-left: 10px;
}

h2,
.form-control,
.form-control:focus,
svg {
    color: #FFF;
}

.form-control::placeholder {
    color: #999;
}

.form-control,
.form-control:focus,
.btn {
    border: solid 1px #999;
}

.form-control:focus,
.btn:focus {
    box-shadow: none;
}

.btn {
    border-radius: 0 4px 4px 0;
}

Bom, com isto feito, nossa interface ficou com a seguinte aparência:

Interface simplificada, feita com o framework Bootstrap

Figura 1 - Interface simplificada, feita com o framework Bootstrap.
Fonte: Autor (CC BY-NC-SA 4.0)

Bom, e os pokémons? Hora de codificá-los! Para isso, no arquivo script.js colamos a getPokemon(valor) function fornecida inicialmente, conforme o código abaixo:

function getPokemon(valor){
  $.ajax({
    url: "https://pokeapi.co/api/v2/pokemon/" + valor,
    method: "get",
    dataType: "json"
  })        
  .done(function(data){
    console.log(data);
  })
  .fail(function(jqXHR){
    console.log("Erro HTTP: " + jqXHR.status);
  });                     
}

No arquivo index.html, na tag input, adicionamos um id com o valor pokemon.

<input class="form-control" id="pokemon" placeholder="Pokémon: name|id" type="text">

Já na tag button, chamamos a function no evento de clique.

<button class="btn" onclick="getPokemon('150')" type="button">

Observamos na tag button que o argumento que a function recebe, indicado entre aspas simples é informado diretamente. No entanto, espera-se que este argumento, seja na verdade, aquilo que o usuário digitar. Para tal, temos que passar como argumento, ao invés do nome ou o id do pokémon, uma instrução que capture o que for digitado na tag input. Para isso, usamos o jQuery, observando os seguintes passos:

  1. Obtenha o valor do elemento input: $("#id-do-elemento-input").val() => $("#pokemon").val()
  2. A instrução $("#pokemon").val() é passada como argumento da getPokemon(), que por sua vez, é cercada por aspas duplas do atributo onclick, isto faz, com que as aspas utilizadas na instrução devam ser escapadas. Este escape, pode ser feito substituindo as aspas duplas por aspas simples.

Como resultado disso, temos:

<button class="btn" onclick="getPokemon($('#pokemon').val())" type="button">

Com a alteração feita, abrimos a index.html no Google Chrome e testamos nossa aplicação, pesquisando um pokémon pelo nome (gyarados, neste exemplo). Com o Console aberto visualizamos o response obtido:

Resposta de um request feito à PokéAPI impresso no Console (do Google Chrome)

Figura 2 - Resposta de um request feito à PokéAPI impresso no Console.
Fonte: Autor (CC BY-NC-SA 4.0)

Com isto, nossa tarefa agora, é imprimir na página os dados pedidos inicialmente, a saber, o id, o nome, o tipo e uma imagem que represente o pokémon. Mas como fazer isso? Bom, sigamos os passos:

  1. Obtenha o valor dos campos por meio de seu path
  2. Para descobrir o path de determinado campo, basta navegar até ele, em seguida clicar com o botão direito do mouse, e escolher a opção Copy property path.
  3. Por fim, concatene object e path, usando o ponto final (.) como separador: object.path => data.path

Seguindo, obtemos os seguintes resultados:

  • id: data.id
  • nome: data.name
  • tipo: data.types[i].type.name, onde i, representa a posição de um elemento em um Array.
  • imagem: data.sprites.other.dream_world.front_default

Com isto já temos os valores para imprimir. Pois bem, no arquivo index.html adicionamos um id com o valor localContent à nossa segunda row:

<div class="row" id="localContent"></div>

Agora, dentro de localContent, devemos criar a seguinte estrutura:

<!-- Para pokémons de tipo ÚNICO -->
<div class="col-12 TIPO-0">
  <div>TIPO-0</div>
</div>
<div class="center">
  <img alt="" src="IMAGEM">
  <div>ID - NOME</div>
</div>

<!-- Para pokémons de tipo MÚLTIPLO -->
<div class="col-6 TIPO-0">
  <div>TIPO-0</div>
</div>
<div class="col-6 TIPO-1">
  <div>TIPO-1</div>
</div>
<div class="center">
  <img alt="" src=" IMAGEM">
  <div>ID - NOME</div>
</div>

Mas devemos fazê-lo usando os dados obtidos da API, assim no arquivo script.js editamos o bloco done da getPokemon, substituindo a instrução console.log(data) por:

// Cria um atributo
let content = "";
// Recupera e armazena o elemento localContent
const localContent = $("#localContent");
// Se for um pokémon de tipo MÚLTIPLO.
if (data.types.length > 1) {
  // content recebe o bloco de tags TIPO escapadas e agrupadas.
  // O escape neste caso é feito adicionando uma barra inversa antes de cada aspa dupla.
  // O TIPO é substituído pelos resultados da API, isto é, data.types[i].type.name.
  content = "<div class=\"col-6 " + data.types[0].type.name + "\"><div>" + data.types[0].type.name + "</div></div><div class=\"col-6 " + data.types[1].type.name + "\"><div>" + data.types[1].type.name + "</div></div>";
} else {
  // Se for do tipo ÚNICO, obtenha-o da primeira posição, isto é, 0.
  content = "<div class=\"col-12 " + data.types[0].type.name + "\"><div>" + data.types[0].type.name + "</div></div>";
}
// Adiciona à content o restante do bloco de tags.
// IMAGEM, ID e NOME são substituídos pelos correspondentes resultados da API.
content = content.concat("<div class=\"center\"><img alt=\"\" src=" + data.sprites.other.dream_world.front_default + "><div>" + data.id + " - " + data.name + "</div></div>");
// Imprime na localContent o content
localContent.html(content);

Editamos também o arquivo style.css, modificando o seletor .container-fluid por

.container-fluid {
    font-family: "Courier New", Courier, monospace;    
    padding: 0;
    overflow: hidden;
}

e o .row:first-child div por

.row:first-child div,
.center {
    align-items: center;
    display: flex;
}

Além disso, adicionamos ao mesmo arquivo estas novas configurações:

h2,
#localContent div {
    font-weight: bold;
}

#localContent,
#localContent > div,
.center img {
    height: 65%;
}

#localContent,
.center img {
    position: absolute;    
}

#localContent,
.center img {
    width: 100%;
}

#localContent div {
    color: #24292E;
    font-size: 32px;
    text-align: center;
}

#localContent > div,
.center img {
    padding: 20px;
}

.center {
    justify-content: center;    
}

.center img {
    max-height: 400px;
    top: 20%;
}

/* TIPOS de pokémon */

.bug {
    background-color: #A8B820;    
}

.dark {
    background-color: #705848;
}

.dragon {
    background-color: #7038F8; 
}

.electric {
    background-color: #F8D030;    
}

.fairy {
    background-color: #EE99AC;
}

.fighting {
    background-color: #C03028;
}

.fire {
    background-color: #F08030;
}

.flying {
    background-color: #A890F0;    
}

.ghost {
    background-color: #705898;
}

.grass {
    background-color: #78C850;
}

.ground {
    background-color: #E0C068;     
}

.ice {
    background-color: #98D8D8;
}

.normal {
    background-color: #A8A878;    
}

.poison {
    background-color: #A040A0;
}

.psychic {
    background-color: #F85888;
}

.rock {
    background-color: #B8A038;
}

.steel {
    background-color: #B8B8D0;
}

.water {
    background-color: #6890F0;
}

Feito isso testamos e obtemos:

Impressão do pokémon Gyarados na página, com seu id, nome e tipo(s) definidos.

Figura 3 - Impressão do pokémon Gyarados na página, com seu id, nome e tipo(s) definidos.
Fonte: Autor (CC BY-NC-SA 4.0)

Agora editamos nosso arquivo script.js, movendo a instrução const localContent = $("#localContent"); para fora da function, fazendo que o atributo localContent tenha agora escopo global, podendo assim ser acessado por outras functions do mesmo arquivo.

Agora editamos o bloco fail, da getPokemon para imprimir uma mensagem personalizada, caso o pokémon digitado não exista, substituindo a instrução .fail(function (jqXHR) { console.log("Erro HTTP: " + jqXHR.status); }); por:

.fail(function(){ 
  localContent.html("<div class=\"center\">\"There is no data! There are still pokémons to be identified.\" (Pokémon, I Choose You!)</div>");
});

Bom, considerando que o usuário, pode clicar no button de pesquisa, sem, no entanto, preencher o input #pokemon, criamos uma nova function, em nosso arquivo script.js:

function isEmpty(){
  // Verifica se o input está vazio
  if($.trim($("#pokemon").val()) === ""){
    // imprime mensagem de erro  
    localContent.html("<div class=\"center\">Enter the name|id of the pokémon to perform the search.</div>");
  } else {
    // senão chama a getPokemon passando como argumento o valor do input
    getPokemon($("#pokemon").val());
  }
}

E atualizamos o button, com a nova function:

<button class="btn" onclick="isEmpty()" type="button">

Por fim, consideramos também, que o usuário pode digitar o nome de um pokémon, como "GyaRaDos", por exemplo, e isso nos retornará um erro, ao invés do referido pokémon, isso porque esta API, é case-sensitive, isto é, ela diferencia maiúsculas de minúsculas. Para solucionar o problema, devemos transformar todo o texto digitado pelo usuário em letras minúsculas, antes do request à API. Para isso, usamos a função nativa JS, toLowerCase(), modificando nossa isEmpty() function do seguinte modo:

getPokemon($("#pokemon").val().toLowerCase());

Com isto feito, finalizamos esta resolução! Se gostaram, por favor, deixem seus COMENTÁRIOS, nos SIGAM e principalmente COMPARTILHEM para que assim, seja possível alcançar mais pessoas, que como vocês, curtem Programação, com conteúdos PRÁTICOS e GRATUITOS!

Para ver e/ou baixar o código-fonte desta resolução, ou mesmo melhorá-lo, visitem o repositório no GitHub. Vejam também a nossa demo.

Pokédex Demo

Figura 4 - Pokédex Demo.
Fonte: Autor (CC BY-NC-SA 4.0)

Até a próxima!

Cite este material

FERNANDES, Fábio. Desenvolvendo uma pokédex (web app). aprendaCodar, 26 de abril de 2022. Disponível em: <https://aprendacodar.blogspot.com/2022/04/desenvolva-uma-pokedex-web-app-com.html>. Acesso em:

Fábio Fernandes

Graduado em Ciência da Computação e Especialista em Análise de Dados com BI e Big Data. Instrutor, Desenvolvedor e Produtor de Conteúdo. Apaixonado por Tecnologia e pelo compartilhamento de conhecimentos.

2 Comentários

  1. Germano Rangel11/6/22 20:59

    Maravilhosa matéria, aprendi bastante, tinha certa dificuldade com este assunto, mas, acompanhando passo-a-passo, finalmente consegui entender. Parabéns!

    ResponderExcluir
  2. Que legal, Germano!! Fico feliz que tenha obtido conhecimento! Continue sempre evoluindo!

    ResponderExcluir
Postar um comentário
Postagem Anterior Próxima Postagem