React JS - Construindo uma wiki de personagens Ricky e Morty - parte 2
Eduardo Ramos
Posted on July 27, 2022
» Filter
Bora lá colocar filtros...
Primeiramente vamos criar uma estrutura de pasta e arquivos.
Dentro da pasta Filter teremos os arquivos: Filter.js e FilterBTN.js. Teremos também uma pasta category e dentro dela os arquivos: Gender.js, Species.js e Status.js
No arquivo App.js vamos criar mais alguns hooks para armazenar os filtros. Também vamos precisar mudar a url da api novamente, para receber os novos parâmetros.
Adicione na função App() esse código abaixo:
let [status, updateStatus] = useState("");
let [gender, updateGender] = useState("");
let [species, updateSpecies] = useState("");
e altere a api para:
let api = `https://rickandmortyapi.com/api/character/?page=${pageNumber}&name=${search}&status=${status}&gender=${gender}&species=${species}`;
Feito isso, substitua o "Filtro aparecerá aqui" por:
<Filter
pageNumber={pageNumber}
status={status}
updateStatus={updateStatus}
updateGender={updateGender}
updateSpecies={updateSpecies}
updatePageNumber={updatePageNumber}
/>
No arquivo Filter.js vamos importar as categorias criadas:
import React from "react";
import Gender from "./category/Gender";
import Species from "./category/Species";
import Status from "./category/Status";
Antes que eu me esqueça vou colocar o código de cada arquivo das categorias aqui, começando pelo Gender.js:
import React from "react";
import FilterBTN from "../FilterBTN";
const Gender = ({ updateGender, updatePageNumber }) => {
let genders = ["female", "male", "genderless", "unknown"];
return (
<div className="accordion-item">
<h2 className="accordion-header" id="headingThree">
<button
className="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
data-bs-target="#collapseThree"
aria-expanded="false"
aria-controls="collapseThree"
>
Gender
</button>
</h2>
<div
id="collapseThree"
className="accordion-collapse collapse"
aria-labelledby="headingThree"
data-bs-parent="#accordionExample"
>
<div className="accordion-body d-flex flex-wrap gap-3">
{genders.map((items, index) => {
return (
<FilterBTN
name="gender"
index={index}
key={index}
updatePageNumber={updatePageNumber}
task={updateGender}
input={items}
/>
);
})}
</div>
</div>
</div>
);
};
export default Gender;
Agora o Species.js:
import React from "react";
import FilterBTN from "../FilterBTN";
const Species = ({ updateSpecies, updatePageNumber }) => {
let species = [
"Human",
"Alien",
"Humanoid",
"Poopybutthole",
"Mythological",
"Unknown",
"Animal",
"Disease",
"Robot",
"Cronenberg",
"Planet",
];
return (
<div className="accordion-item ">
<h2 className="accordion-header" id="headingTwo">
<button
className="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
data-bs-target="#collapseTwo"
aria-expanded="false"
aria-controls="collapseTwo"
>
Species
</button>
</h2>
<div
id="collapseTwo"
className="accordion-collapse collapse"
aria-labelledby="headingTwo"
data-bs-parent="#accordionExample"
>
<div className="accordion-body d-flex flex-wrap gap-3">
{species.map((item, index) => {
return (
<FilterBTN
name="species"
index={index}
key={index}
updatePageNumber={updatePageNumber}
task={updateSpecies}
input={item}
/>
);
})}
</div>
</div>
</div>
);
};
export default Species;
E por ultimo o Status.js
import React from "react";
import FilterBTN from "../FilterBTN";
const Status = ({ updateStatus, updatePageNumber }) => {
let status = ["Alive", "Dead", "Unknown"];
return (
<div className="accordion-item">
<h2 className="accordion-header" id="headingOne">
<button
className="accordion-button"
type="button"
data-bs-toggle="collapse"
data-bs-target="#collapseOne"
aria-expanded="true"
aria-controls="collapseOne"
>
Status
</button>
</h2>
<div
id="collapseOne"
className="accordion-collapse collapse show"
aria-labelledby="headingOne"
data-bs-parent="#accordionExample"
>
<div className="accordion-body d-flex flex-wrap gap-3">
{status.map((item, index) => (
<FilterBTN
key={index}
index={index}
name="status"
task={updateStatus}
updatePageNumber={updatePageNumber}
input={item}
/>
))}
</div>
</div>
</div>
);
};
export default Status;
```
Vamos deixar o arquivo **FilterBTN.js** pronto também com o seguinte código:
```
import React from "react";
const FilterBTN = ({ input, task, updatePageNumber, index, name }) => {
return (
<div>
<style jsx>
{`
.x:checked + label {
background-color: #0b5ed7;
color: white;
}
input[type="radio"] {
display: none;
}
`}
</style>
<div className="form-check">
<input
className="form-check-input x"
type="radio"
name={name}
id={`${name}-${index}`}
/>
<label
onClick={(x) => {
task(input);
updatePageNumber(1);
}}
className="btn btn-outline-primary"
for={`${name}-${index}`}
>
{input}
</label>
</div>
</div>
);
};
export default FilterBTN;
```
Voltando ao arquivo **Filter.js** que até esse ponto possui apenas 4 linhas de código. Vamos deixá-lo assim:
```
import React from "react";
import Gender from "./category/Gender";
import Species from "./category/Species";
import Status from "./category/Status";
const Filter = ({
pageNumber,
updatePageNumber,
updateStatus,
updateGender,
updateSpecies,
}) => {
let clear = () => {
updateStatus("");
updateGender("");
updateSpecies("");
updatePageNumber(1);
window.location.reload(false);
};
return (
<div className="col-lg-3 col-12 mb-5">
<div className="text-center fw-bold fs-4 mb-2">Filtros</div>
<div
style={{ cursor: "pointer" }}
onClick={clear}
className="text-primary text-decoration-underline text-center mb-3"
>
Limpar Filtros
</div>
<div className="accordion" id="accordionExample">
<Status
updatePageNumber={updatePageNumber}
updateStatus={updateStatus}
/>
<Species
updatePageNumber={updatePageNumber}
updateSpecies={updateSpecies}
/>
<Gender
updatePageNumber={updatePageNumber}
updateGender={updateGender}
/>
</div>
</div>
);
};
export default Filter;
```
Pra finalizar e testar os filtros o **App.js** tem que estar assim:
import 'bootstrap/dist/css/bootstrap.min.css';
import "bootstrap/dist/js/bootstrap";
import React, { useState, useEffect } from "react";
import Search from "./components/Search/Search";
import Card from "./components/Card/Card";
import Pagination from "./components/Pagination/Pagination";
import Filter from "./components/Filter/Filter";
function App() {
let [pageNumber, updatePageNumber] = useState(1);
let [status, updateStatus] = useState("");
let [gender, updateGender] = useState("");
let [species, updateSpecies] = useState("");
let [fetchedData, updateFetchedData] = useState([]);
let [search, setSearch] = useState("");
let { info, results } = fetchedData;
let api = https://rickandmortyapi.com/api/character/?page=${pageNumber}&name=${search}&status=${status}&gender=${gender}&species=${species}
;
useEffect(() => {
(async function () {
let data = await fetch(api).then((res) => res.json());
updateFetchedData(data);
})();
}, [api]);
return (
Personagens
<Search setSearch={setSearch} updatePageNumber={updatePageNumber} />
<div className="container">
<div className="row">
<Filter
pageNumber={pageNumber}
status={status}
updateStatus={updateStatus}
updateGender={updateGender}
updateSpecies={updateSpecies}
updatePageNumber={updatePageNumber}
/>
<div className="col-lg-8 col-12">
<div className="row">
<Card page="/" results={results} />
</div>
</div>
</div>
</div>
<Pagination
info={info}
pageNumber={pageNumber}
updatePageNumber={updatePageNumber}
/>
);
}
export default App;
Agora vamos criar o componente com o menu de navegação.
##
##
##» Navigation
##
![fig8](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s76g5e5lgp7wk0xipsfj.png)
Dentro da pasta **_src_**, crie uma pasta **_Pages_** e dentro dela crie 2 arquivos **Episodes.js** e **Location.js**.
Agora vá para o arquivo **App.js** e importe esses arquivos que acabamos de criar:
import Episodes from "./Pages/Episodes";
import Location from "./Pages/Location";
Vamos adicionar o react-router-dom e importar alguns componentes:
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
Agora vamos criar uma função auxiliar dentro do **App.js** mesmo:
const Home = () => {
// Código aqui
}
Dentro da função **App** vamos criar um **Router** e um **Navbar** igual o código abaixo:
function App() {
return (
);
}
Agora definimos nossas rotas. Cada rota requer o caminho (path) e o componente (element):
} />
} />
} />
Feito isso, vamos criar o código da nossa **Navbar.js**.
Dentro do pasta **_Navbar_** crie um arquivo chamado **Navbar.js** e coloque o código abaixo:
import React from "react";
import { NavLink, Link } from "react-router-dom";
import "../../App.scss";
const Navbar = () => {
return (
Rick e Morty WiKi
{<code><br>
button[aria-expanded="false"] > .close {<br>
display: none;<br>
}<br>
button[aria-expanded="true"] > .open {<br>
display: none;<br>
}<br>
</code>}
className="navbar-toggler border-0"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNavAltMarkup"
aria-controls="navbarNavAltMarkup"
aria-expanded="false"
aria-label="Toggle navigation"
>
className="collapse navbar-collapse justify-content-end"
id="navbarNavAltMarkup"
>
Personagens
Episódios
activeClassName="active"
className="nav-link"
to="/location"
>
Localizações
);
};
export default Navbar;
Vamos criar mais páginas para testar a navegação. Porém antes de criarmos as próximas páginas, vamos criar um componente para auxiliar no filtro, que será incluído nelas.
Esse componente terá o papel similar aos filtros de categoria, logo vamos criá-lo dentro da pasta **_category_** que está dentro da pasta **_Filter_**. Crie um arquivo chamado **InputGroup.js**
Coloque o código abaixo:
import React from "react";
const InputGroup = ({ name, changeID, total }) => {
return (
onChange={(e) => changeID(e.target.value)}
className="form-select"
id={name}
>
Escolha...
{[...Array(total).keys()].map((x, index) => {
return (
{name} - {x + 1}
);
})}
);
};
export default InputGroup;
Dentro de **Episode.js** que está na pasta **_Pages_**, coloque o código abaixo:
import React, { useEffect, useState } from "react";
import Card from "../components/Card/Card";
import InputGroup from "../components/Filter/category/InputGroup";
const Episodes = () => {
let [results, setResults] = React.useState([]);
let [info, setInfo] = useState([]);
let { air_date, episode, name } = info;
let [id, setID] = useState(1);
let api = https://rickandmortyapi.com/api/episode/${id}
;
useEffect(() => {
(async function () {
let data = await fetch(api).then((res) => res.json());
setInfo(data);
let a = await Promise.all(
data.characters.map((x) => {
return fetch(x).then((res) => res.json());
})
);
setResults(a);
})();
}, [api]);
return (
Episode name :{" "}
{name === "" ? "Unknown" : name}
Air Date: {air_date === "" ? "Unknown" : air_date}
Episódios
);
};
export default Episodes;
O que foi feito aqui é uma página com uma nova url para a api, onde o combo representado pelo componente **InputGroup** filtra o resultado.
No arquivo **Location.js** coloque este código:
import React, { useEffect, useState } from "react";
import Card from "../components/Card/Card";
import InputGroup from "../components/Filter/category/InputGroup";
const Location = () => {
let [results, setResults] = React.useState([]);
let [info, setInfo] = useState([]);
let { dimension, type, name } = info;
let [number, setNumber] = useState(1);
let api = https://rickandmortyapi.com/api/location/${number}
;
useEffect(() => {
(async function () {
let data = await fetch(api).then((res) => res.json());
setInfo(data);
let a = await Promise.all(
data.residents.map((x) => {
return fetch(x).then((res) => res.json());
})
);
setResults(a);
})();
}, [api]);
return (
Location :
{" "}
{name === "" ? "Unknown" : name}
Dimension: {dimension === "" ? "Unknown" : dimension}
Type: {type === "" ? "Unknown" : type}
Localização
);
};
export default Location;
Perceba que fizemos a mesma coisa aqui, trocando obviamente o _episodes_ por _location_ e alterando a url da api.
Agora vamos conferir mais uma vez o arquivo **App.js** que deve estar igual o código abaixo:
import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap/dist/js/bootstrap";
import React, { useState, useEffect } from "react";
import Search from "./components/Search/Search";
import Card from "./components/Card/Card";
import Pagination from "./components/Pagination/Pagination";
import Filter from "./components/Filter/Filter";
import Navbar from "./components/Navbar/Navbar";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Episodes from "./Pages/Episodes";
import Location from "./Pages/Location";
function App() {
return (
} />
<Route path="/episodes" element={<Episodes />} />
<Route path="/location" element={<Location />} />
</Routes>
</Router>
);
}
const Home = () => {
let [pageNumber, updatePageNumber] = useState(1);
let [status, updateStatus] = useState("");
let [gender, updateGender] = useState("");
let [species, updateSpecies] = useState("");
let [fetchedData, updateFetchedData] = useState([]);
let [search, setSearch] = useState("");
let { info, results } = fetchedData;
let api = https://rickandmortyapi.com/api/character/?page=${pageNumber}&name=${search}&status=${status}&gender=${gender}&species=${species}
;
useEffect(() => {
(async function () {
let data = await fetch(api).then((res) => res.json());
updateFetchedData(data);
})();
}, [api]);
return (
Personagens
pageNumber={pageNumber}
status={status}
updateStatus={updateStatus}
updateGender={updateGender}
updateSpecies={updateSpecies}
updatePageNumber={updatePageNumber}
/>
info={info}
pageNumber={pageNumber}
updatePageNumber={updatePageNumber}
/>
);
};
export default App;
Sua aplicação deve estar similar a isso:
![fig9](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9is0rldza8zerq0rgx1e.png)
_______________
![fig10](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ft15j9u24jx4dd41bn0l.png)
___________________
![fig11](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j0rogy3ijdpln4zj59i0.png)
___________________
Antes de finalizar vamos criar uma página/componente para exibir detalhes do card que foi clicado.
Será uma página simples apenas com alguns detalhes a mais.
Futuramente iremos implementar uma modal, na evolução deste artigo, mas por ora optamos pela simplicidade de uma página comum.
Vá até a pasta **_Card_** e crie um arquivo chamado **CardDetails.js**.
Nele coloque o seguinte código:
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
const CardDetails = () => {
let { id } = useParams();
let [fetchedData, updateFetchedData] = useState([]);
let { name, location, origin, gender, image, status, species } = fetchedData;
let api = https://rickandmortyapi.com/api/character/${id}
;
useEffect(() => {
(async function () {
let data = await fetch(api).then((res) => res.json());
updateFetchedData(data);
})();
}, [api]);
return (
{name}
<img className="img-fluid" src={image} alt="" />
{(() => {
if (status === "Dead") {
return <div className="badge bg-danger fs-5">{status}</div>;
} else if (status === "Alive") {
return <div className=" badge bg-success fs-5">{status}</div>;
} else {
return <div className="badge bg-secondary fs-5">{status}</div>;
}
})()}
<div className="content">
<div className="">
<span className="fw-bold">Gender : </span>
{gender}
</div>
<div className="">
<span className="fw-bold">Location: </span>
{location?.name}
</div>
<div className="">
<span className="fw-bold">Origin: </span>
{origin?.name}
</div>
<div className="">
<span className="fw-bold">Species: </span>
{species}
</div>
</div>
</div>
</div>
);
};
export default CardDetails;
Aqui estamos criando um card, a partir de um id que foi selecionado previamente no momento do click.
A api retorna alguns dados de acordo com o id que foi informado para ela. Dessa forma conseguimos ter nossa página de detalhes funcionando.
Vamos finalizar ajustando o arquivo **App.js** para redirecionar corretamente para a rota de detalhes. Para isso inclua mais algumas rotas:
`<Route path="/:id" element={<CardDetails />} />`
`<Route path="/episodes/:id" element={<CardDetails />} />`
`<Route path="/location/:id" element={<CardDetails />} />`
Dentro do arquivo **Card.js** vamos colocar um elemento Link para encapsular a div e tornar o card "clicável". Para facilitar vou deixar aqui o código atualizado do **Card.js**:
import React from "react";
import { Link } from "react-router-dom";
import styles from "./Card.module.scss";
const Card = ({ page, results }) => {
let display;
if (results) {
display = results.map((x) => {
let { id, image, name, status, location } = x;
return (
<Link
style={{ textDecoration: "none" }}
to={`${page}${id}`}
key={id}
className="col-lg-4 col-md-6 col-sm-6 col-12 mb-4 position-relative text-dark"
>
<div
className={`${styles.card} d-flex flex-column justify-content-center`}
>
<img className={`${styles.img} img-fluid`} src={image} alt="" />
<div className={`${styles.content}`}>
<div className="fs-5 fw-bold mb-4">{name}</div>
<div className="">
<div className="fs-6 fw-normal">Last Location</div>
<div className="fs-5">{location.name}</div>
</div>
</div>
</div>
{(() => {
if (status === "Dead") {
return (
<div
className={`${styles.badge} position-absolute badge bg-danger`}
>
{status}
</div>
);
} else if (status === "Alive") {
return (
<div
className={`${styles.badge} position-absolute badge bg-success`}
>
{status}
</div>
);
} else {
return (
<div
className={`${styles.badge} position-absolute badge bg-secondary`}
>
{status}
</div>
);
}
})()}
</Link>
);
});
} else {
display = "Nenhum personagem encontrado :/";
}
return <>{display}</>;
};
export default Card;
Importe o componente **CardDetails.js** dentro do **App.js**:
`import CardDetails from "./components/Card/CardDetails";`
E pra não ter erro o seu arquivo **App.js** deverá ficar assim:
import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap/dist/js/bootstrap";
import React, { useState, useEffect } from "react";
import Search from "./components/Search/Search";
import Card from "./components/Card/Card";
import CardDetails from "./components/Card/CardDetails";
import Pagination from "./components/Pagination/Pagination";
import Filter from "./components/Filter/Filter";
import Navbar from "./components/Navbar/Navbar";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Episodes from "./Pages/Episodes";
import Location from "./Pages/Location";
function App() {
return (
} />
} />
} />
} />
} />
} />
);
}
const Home = () => {
let [pageNumber, updatePageNumber] = useState(1);
let [status, updateStatus] = useState("");
let [gender, updateGender] = useState("");
let [species, updateSpecies] = useState("");
let [fetchedData, updateFetchedData] = useState([]);
let [search, setSearch] = useState("");
let { info, results } = fetchedData;
let api = https://rickandmortyapi.com/api/character/?page=${pageNumber}&name=${search}&status=${status}&gender=${gender}&species=${species}
;
useEffect(() => {
(async function () {
let data = await fetch(api).then((res) => res.json());
updateFetchedData(data);
})();
}, [api]);
return (
Personagens
pageNumber={pageNumber}
status={status}
updateStatus={updateStatus}
updateGender={updateGender}
updateSpecies={updateSpecies}
updatePageNumber={updatePageNumber}
/>
info={info}
pageNumber={pageNumber}
updatePageNumber={updatePageNumber}
/>
);
};
export default App;
O resultado final ao clicar no card deverá ser igual a figura abaixo:
![fig13](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qzvijrb2eulump05cpak.png)
_____________________
Esse foi um post para mostrar o básico de **React**.
Resolvi fazer esse post, pois acompanhei algumas postagens e tutoriais que "ensinavam" a implementar o consumo dessa mesma api inclusive, porém sempre me senti perdido quanto a didática da postagem, pois não ficava claro onde colocar arquivos ou trecho de códigos. Pra quem está iniciando é bem complicado quando não se consegue acompanhar ou reproduzir o artigo ou tutorial. Tentei ser o mais claro e objetivo.
Em breve escreverei outros artigos incluindo **Typescript**, **Context** e outros conceitos. Espero que gostem e principalmente consigam entender e implementar.
##
_____________________
_____________________[Clique aqui para ver o resultado final](https://wiki-rick-morty-react.vercel.app/)_____________________
_____________________
##
A primeira parte do artigo está aqui:
[Parte 1](https://dev.to/dooramos/react-js-construindo-uma-wiki-de-personagens-ricky-e-morty-parte-1-4g9b)
Posted on July 27, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.