Artículo original escrito por: Joy Shaheb
Artículo original: ReactJS Project – Build a Rick and Morty Character Wiki
Traducido y adaptado por: Juan C. Guaña

Hoy vamos a practicar nuestras habilidades de React JS construyendo una Wiki de personajes de Rick and Morty.

Esto es lo que construiremos hoy:

Image description

Aquí hay una [demostración en vivo del proyecto] ?‌
https://react-projects-psi.vercel.app/.

Y aquí está el repositorio de GitHub.

Los temas que cubriremos mientras construimos este proyecto son:

  • React Hooks (useState, useEffect)
  • React Components
  • Fetch API
  • Bootstrap - para estilos
  • Paginación
  • Barra de búsqueda
  • Filtrado de datos
  • Enrutamiento dinámico

También puedes ver este tutorial en YouTube si lo deseas:

Características del proyecto

Veamos una demostración de todas las funciones que crearemos durante todo el transcurso de este artículo.

Este proyecto está lleno de características interesantes para que pueda aprovechar al máximo este tutorial y ser realmente bueno escribiendo código ReactJS.

Barra de búsqueda

Construiremos esta barra de búsqueda genial para que podamos buscar a nuestros personajes favoritos.

Image description

Paginación

En total hay más de 800 caracteres. Para mostrar y administrar todos estos caracteres, implementaremos un sistema de paginación donde cada página mostrará 20 caracteres.

Image description

Filtros

Hay muchas etiquetas presentes en la API. Utilizándolas, podemos filtrar nuestros datos y buscar exactamente lo que necesitamos. Aquí está la demostración:

Image description

Enrutamiento

Implementaremos este componente para cambiar nuestras páginas y crear una barra de navegación. Usaremos la libreria llamada react-router-dom para hacer esto.

Image description

Enrutamiento dinámico

Usando react-router-dom, también agregaremos enrutamiento dinámico para que podamos aprender más sobre un personaje cuando hagamos clic en él.

Image description

Tabla de contenido

  • Configuración
  • Estructura de carpetas
  • Obtención de datos
  • Tarjetas de personaje
  • Barra de búsqueda
  • Paginación con React
  • Filtros
  • Enrutamiento con React
  • Episodios
  • Ubicación
  • Páginas dinámicas

Configuración

Image description

Antes de comenzar el proyecto, sigue estos pasos para configurarlo:

  • Crea una carpeta llamada 'react-wiki'
  • Abre esa carpeta en VS code
  • Abre tu terminal y ejecuta estos comandos uno por uno: ?
npx create-react-app .

npm install bootstrap

npm install @popperjs/core --save

npm install sass

npm install react-router-dom

npm install react-paginate --save

npm start

Para facilitar su experiencia de desarrollo, descarga estas extensiones de  VSCode:

  • ES7 React/Redux/GraphQL/React-Native snippets
  • ESLint

Estructura de carpeta

Image description

Dividiremos todo nuestro proyecto en 5 componentes:

  • Card
  • Pagination
  • Search
  • Filter
  • Navbar

Crea una carpeta llamada 'components' dentro de su carpeta 'src' y crea 5 carpetas como esta: ?

Image description

Y luego, crea estos archivos de acuerdo con los nombres de nuestros componentes. ?

Image description

App.js

Algunos otros cambios que necesitas hacer:

  • Elimina todo del archivo App.css
  • Importa los React hooks y Bootstrap en la parte superior en App.js
import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap/dist/js/bootstrap";
import React, { useState, useEffect } from "react";

A continuación, importa todos sus módulos desde componentes:

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";

Dentro del return statement, elimina todo y agrega este código: ?

<div className="App">
  <h1 className="text-center mb-3">Characters</h1>
  <div className="container">
  <div className="row">
    Filter component will be placed here
    <div className="col-lg-8 col-12">
      <div className="row">
        Card component will be placed here
      </div>
    </div>
  </div>
  </div>
</div>

index.css

Agrega estos estilos predeterminados: ?

@import url('https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&display=swap');

body {
  margin: 0;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.ubuntu {
  font-family: "Ubuntu" !important;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
    monospace;
}

Este es el resultado hasta ahora:

Image description

¡Felicidades! Hemos terminado con el proceso de configuración. Así que ahora comencemos a codificar.

Image description

Cómo obtener datos de la API

Para obtener datos de nuestra API, usaremos la API de personajes de Rick and Morty. Tendremos que agregar algunas cosas.

App.js ?

 let api = `https://rickandmortyapi.com/api/character/?page=1`

Nota: no uses comillas o comillas dobles alrededor de la URL, usa comillas invertidas en su lugar. ☝

Para obtener datos de esta API, usaremos nuestro hook useEffects de esta manera: ?

  useEffect(() => { }, [api]);

Estamos escribiendo el hook useEffect y poniendo el observador en api. Esto significa que, en caso de que cambie la variable api, queremos recargar datos nuevos.

Continuemos. ?

useEffect(() => {
  (async function () {
    let data = await fetch(api).then((res) => res.json());
    console.log(data);
  })();
}, [api]);

Estamos usando una función asíncrona para obtener nuestros datos sin procesar y luego los convertimos al formato JSON. Revisemos la consola para ver qué tenemos hasta ahora: ?

Image description

¿Quieres probarlo tú mismo? Cambia el número de página a 2 dentro de la API y encontrarás nuevos datos en tu consola: ?

let api = `https://rickandmortyapi.com/api/character/?page=1`

En lugar de almacenar los datos en la consola, usemos el hook useState. Esto almacenará los datos en una variable, y tendremos una clave de función para cambiar los datos de la variable cada vez que el hook useEffect obtenga nuevos datos. ?

let [fetchedData, updateFetchedData] = useState([]);
let { info, results } = fetchedData;

Además, estamos desestructurando info and result de la variable fetchedData para facilitarnos la vida. ?

La variable fetchedData almacenará los datos que obtuvimos de la API. Usaremos la función updateFetchedData para cambiar los datos cuando queramos.

Cambiemos nuestro hook useEffect: ?

useEffect(() => {
  (async function () {
    let data = await fetch(api).then((res) => res.json());
    updateFetchedData(data);
  })();
}, [api]);

Cómo hacer las tarjetas de personajes

Comencemos a construir nuestras tarjetas de personajes.?

En primer lugar, importa el componente card reemplazando el texto escrito Card component will be placed here. Luego, pasa los datos obtenidos de nuestro componente App.js a nuestro componente Card de esta manera:?

<Card results={results} />

Card.js

Ahora, primero desestructura los datos que obtuvimos de nuestro componente App.js.?

const Card = ({ results }) => {}

Luego crea una variable llamada display. Esto mantendrá todas nuestras tarjetas. Junto con esto, crearemos una declaración if else para verificar si los datos que obtuvimos de nuestra API están vacíos o no.?

const Card = ({ results }) => {
  let display;

  if (results) {}
  else{
    display = "No Characters Found :/";
  }

  return <>{display}</>;
}

Ahora, vamos a asignar nuestros results a nuestro componente card de manera que creará tarjetas para nosotros automáticamente. Pero primero, debemos desestructurar los datos que obtuvimos de nuestra API.?

if (results) {
  display = results.map((x) => {
  let { id, image, name, status, location } = x;

    return (
      <div
        key={id}
        className="col-lg-4 col-md-6 col-sm-6 col-12 mb-4 position-relative text-dark"
      >
      </div>
  );
});
}

Crea un archivo llamado Card.module.scss y agrega este código: ?

$radius: 10px;
.card {
  border: 2px solid #0b5ed7;
  border-radius: $radius;
}
.content {
  padding: 10px;
}
.img {
  border-radius: $radius $radius 0 0;
}
.badge {
  top: 10px; right: 20px;
  font-size: 17px;
}

Además, impórtalo dentro del componente Card.js : ?

import styles from "./Card.module.scss";

Ahora es el momento de crear nuestra plantilla de tarjeta y colocar los datos en sus respectivos lugares.?

<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>

Los resultados hasta ahora se ven así: ?

Image description

Por último, usaremos este código ? para que los usuarios sepan si el personaje está vivo, muerto o desconocido:

{
(() => {
  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>
    );
  }
})()}

Los resultados hasta ahora: ?

Image description

Cómo construir la barra de búsqueda

Aquí hay un video de demostración de nuestro componente de barra de búsqueda: ?

Image description

Ahora, construyamos nuestra barra de búsqueda de personajes. Pero primero, necesitamos crear dos hooks useState para mantener nuestras palabras clave de búsqueda y el número de página actual. ?

App.js

let [pageNumber, updatePageNumber] = useState(1);
let [search, setSearch] = useState("");

Luego, necesitamos actualizar nuestra API con variables. Esto significa que siempre que nuestra API cambie, nuestro hook useEffect obtendrá nuevos datos de nuestra base de datos. ?

let api = `https://rickandmortyapi.com/api/character/?page=${pageNumber}&name=${search}`;

Ahora, vamos a importar nuestro componente de barra de búsqueda dentro de la declaración de devolución. Y junto con eso, vamos a pasar nuestras variables de estado recién creadas dentro de ese componente. ?

  <h1 className="text-center mb-3">Characters</h1>
  <Search setSearch={setSearch} updatePageNumber={updatePageNumber} />

Crea un archivo llamado Search.module.scss para contener los estilos de un módulo específico. Luego, haz estos ajustes: ?

Search.module.scss

.input {
  width: 40%; border-radius: 8px;
  border: 2px solid #0b5ed7;
  box-shadow: 1px 3px 9px rgba($color: #000000, $alpha: 0.25);
  padding: 10px 15px;
  &:focus { outline: none; }
}
.btn {
  box-shadow: 1px 3px 9px rgba($color: #000000, $alpha: 0.25);
}
@media (max-width: 576px) {
  .input { width: 80%; }
}

Search.js

En primer lugar, necesitamos desestructurar nuestros accesorios. Luego, crearemos una función llamada searchBtn para evitar el comportamiento predeterminado de nuestra aplicación, de esta manera: ?

import React from "react";
import styles from "./Search.module.scss";

const Search = ({ setSearch, updatePageNumber }) => {
  let searchBtn = (e) => {
    e.preventDefault();
  };
};

Luego, escribamos dentro de nuestra declaración de devolución. En primer lugar, escribamos la etiqueta de formulario para contener nuestras etiquetas de entrada y botón. ?

return (
  <form
    className={`${styles.search} d-flex flex-sm-row flex-column align-items-center justify-content-center gap-4 mb-5`}
  >
  </form>
);

Luego, creamos el botón y las etiquetas de entrada dentro de nuestra etiqueta de formulario. ?

<input
  onChange={(e) => {
    updatePageNumber(1);
    setSearch(e.target.value);
  }}
  placeholder="Search for characters"
  className={styles.input}
  type="text"
/>
<button
  onClick={searchBtn}
  className={`${styles.btn} btn btn-primary fs-5`}
>
  Search
</button>

Los resultados hasta ahora: ?

Image description

Cómo configurar la paginación con React-paginate

Aquí hay una demostración de nuestro componente React-paginate: ?

Image description

Vamos a usar este paquete para paginar nuestros datos. Entonces, importémoslo en la parte inferior: ?

App.js

<Pagination
  info={info}
  pageNumber={pageNumber}
  updatePageNumber={updatePageNumber}
/>

Pagination.js

Aquí, vamos a desestructurar nuestras propiedades y luego escribiremos en algunos estilos JSX: ?

import React, { useState, useEffect } from "react";
import ReactPaginate from "react-paginate";

const Pagination = ({ pageNumber, info, updatePageNumber }) => {}

Dentro de la declaración de devolución, escribimos los estilos en JSX así: ?

return (
<>
<style jsx>
{`
  a {
    color: white; text-decoration: none;
  }
  @media (max-width: 768px) {
    .pagination {font-size: 12px}

    .next,
    .prev {display: none}
  }
  @media (max-width: 768px) {
    .pagination {font-size: 14px}
  }
`}
</style>
</>
);

Ahora, crea esta función para manejar la función de cambio de página: ?

let pageChange = (data) => {
  updatePageNumber(data.selected + 1);
};

Para que nuestro componente de paginación sea adapte al tamaño de la pantalla, necesitamos escribir este pequeño componente:

const [width, setWidth] = useState(window.innerWidth);
const updateDimensions = () => {
  setWidth(window.innerWidth);
};
useEffect(() => {
  window.addEventListener("resize", updateDimensions);
  return () => window.removeEventListener("resize", updateDimensions);
}, []);

Muy bien a todos, excelente! Ahora vamos a usar el paquete react-paginate.

Primero, diseñemos todo usando las propiedades integradas de react-paginate para estilizar los elementos básicos:?

<ReactPaginate
  className="pagination justify-content-center my-4 gap-4"
  nextLabel="Next"
  previousLabel="Prev"
  previousClassName="btn btn-primary fs-5 prev"
  nextClassName="btn btn-primary fs-5 next"
  activeClassName="active"
  pageClassName="page-item"
  pageLinkClassName="page-link"
/>

Aquí está lo más importante: vamos a agregar las funcionalidades a nuestro componente para que funcione correctamente. ?

<ReactPaginate
  forcePage={pageNumber === 1 ? 0 : pageNumber - 1}
  marginPagesDisplayed={width < 576 ? 1 : 2}
  pageRangeDisplayed={width < 576 ? 1 : 2}
  pageCount={info?.pages}
  onPageChange={pageChange}
  //.... other props here
/>

Los resultados hasta ahora: ?

Image description

Cómo hacer el componente de filtros

Aquí hay una demostración de nuestro componente de filtros:?

Image description

Lo primero que debemos hacer es crear una estructura de carpetas para contener todos los pequeños componentes que vamos a usar. Crea estos componentes dentro de la carpeta Filter:?

Image description

App.js

Ahora, crea estos useState hooks para almacenar nuestro estado, género y especie.

let [status, updateStatus] = useState("");
let [gender, updateGender] = useState("");
let [species, updateSpecies] = useState("");

Además, necesitamos modificar nuestra variable api de acuerdo con nuestras variables de useState hook. ?

  let api = `https://rickandmortyapi.com/api/character/?page=${pageNumber}&name=${search}&status=${status}&gender=${gender}&species=${species}`;

Ahora, importaremos nuestro componente  filter dentro de nuestra aplicación, donde se colocará aquí su componente de filtro escrito. Además, pasa todas estas propiedades  requeridas: ?

<Filter
  pageNumber={pageNumber}
  status={status}
  updateStatus={updateStatus}
  updateGender={updateGender}
  updateSpecies={updateSpecies}
  updatePageNumber={updatePageNumber}
/>

Filter.js

Hagamos estos cambios en nuestro componente Filtro para que podamos lograr los resultados deseados. Primero, importa todos los componentes de nuestra categoría de esta manera:?

import React from "react";
import Gender from "./category/Gender";
import Species from "./category/Species";
import Status from "./category/Status";

Luego, desestructura las propiedades pasadas ​​y coloca un accordion que incluya un clear button:

const Filter = ({
  pageNumber, updatePageNumber,
  updateStatus, updateGender,
  updateSpecies,
}) => {

return (
<div className="col-lg-3 col-12 mb-5">
  <div className="text-center fw-bold fs-4 mb-2">Filters</div>
  <div
    style={{ cursor: "pointer" }} onClick={clear}
    className="text-primary text-decoration-underline text-center mb-3"
  > Clear Filters </div>
  <div className="accordion" id="accordionExample">
    {/* Category components will be placed here */}
  </div>
</div>
);
};

Realiza esta función para que podamos borrar nuestros filtros y actualizar la página:?

let clear = () => {
  updateStatus("");
  updateGender("");
  updateSpecies("");
  updatePageNumber(1);
  window.location.reload(false);
};

Los resultados hasta ahora se ven así ?

Image description

FilterBTN.js

Primero, creemos estos botones de filtro. También desestructuraremos las propiedades necesarias. ?

Image description
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>
);
};

Ahora, colocamos el componente de entrada principal con etiquetas debajo del style jsx: ?

<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>

Status.js

Image description

Escribe este código de inicio dentro de Status.js:

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>
  );
};

Luego, crea los botones de estado mapeando nuestro arreglos de datos. ?

Debajo de la etiqueta h2 final, colócalos dentro ?, lo que nos ayudará a mapear automáticamente los datos y crear nuestros botones de estado:

<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>

Hora de probar en Filter.js

Escribe estas líneas dentro del Filter.js para comprobar si el componente funciona o no: ?

<Status
  updatePageNumber={updatePageNumber}
  updateStatus={updateStatus}
/>

Los resultados hasta ahora: ?

Image description

Species.js

Image description

Escribe estos códigos de inicio dentro de Species.js:

import FilterBTN from "../FilterBTN";

const Species = ({ updateSpecies, updatePageNumber }) => {
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>
)}

Ahora, crea un arreglo para contener todos nuestros posibles datos de especies: ?

  let species = [
    "Human", "Alien", "Humanoid",
    "Poopybutthole", "Mythological", "Unknown",
    "Animal", "Disease","Robot","Cronenberg","Planet",
  ];

Y luego, creamos los botones Species mapeando nuestro arreglo de datos de esta manera: ?

<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>

Hora de probar en Filter.js

Escriba estas líneas dentro de Filter.js para verificar si el componente funciona o no: ?

<Species
  updatePageNumber={updatePageNumber}
  updateSpecies={updateSpecies}
/>

Los resultados hasta ahora: ?

Image description

Gender.js

Image description

Escribe este código de inicio:?

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>
);
};

Debajo de la etiqueta h2 final, coloca este código dentro? que nos ayudará a mapear automáticamente los datos y crear nuestros botones de género:

<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>

Hora de probar en Filter.js

Escribe estas líneas dentro de Filter.js para comprobar si el componente funciona o no: ?

<Gender
  updatePageNumber={updatePageNumber}
  updateGender={updateGender}
/>

Los resultados hasta ahora: ?

Image description

Cómo configurar React Router

Aquí hay una demostración de nuestro componente de navegación: ?

Image description

Comencemos a codificar!

Primero, crea una carpeta llamada Pages dentro de la carpeta src. Contendrá 2 archivos: Episodes.j y Location.js. Algo como esto:?

Image description

App.js

Importa su componente de páginas recién creado en App.js: ?

import Episodes from "./Pages/Episodes";
import Location from "./Pages/Location";

Para declarar el enrutador y definir todo tipo de rutas de archivos, debemos importar react-router-dom en App.js, incluidos sus componentes principales. ?

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";

Ahora, crea un nuevo componente funcional llamado Home dentro del archivo App.js. Luego, corta todo desde el componente App y colóquelo dentro del componente Home: ?

const Home = () => {
  // Everything you've written so far
}

Dentro de la función del componente App, crea un nuevo componente Router y colóquelo dentro del componente Navbar. ?

function App() {
  return (
    <Router>
      <div className="App">
        <Navbar />
      </div>
    </Router>
  );
}

Ahora, necesitamos definir todas nuestras rutas. Recuerda, Routes es una colección de Route. La ruta es la ruta real del archivo.

Cada ruta requiere 2 cosas: el path al que conducirá la aplicación y el element que se cargará. ?

<Routes>
  <Route path="/" element={<Home />} />

  <Route path="/episodes" element={<Episodes />} />

  <Route path="/location" element={<Location />} />
</Routes>

¡Hasta ahora todo bien! Ahora, hagamos el componente de la barra de navegación. Primero, importa 2 componentes desde react-router-dom. Luego, escribe esta clase principal de arranque que incluya el nombre de la marca para contener nuestro componente de páginas de la barra de navegación:?

import { NavLink, Link } from "react-router-dom";

const Navbar = () => {
return (
  <nav className="navbar navbar-expand-lg navbar-light bg-light mb-4">
    <div className="container">
      <Link to="/" className="navbar-brand fs-3 ubuntu">
        Rick & Morty <span className="text-primary">WiKi</span>
      </Link>
      <style jsx>{`
        button[aria-expanded="false"] > .close {
          display: none;
        }
        button[aria-expanded="true"] > .open {
          display: none;
        }
      `}</style>
    </div>
  </nav>
);
};

Escribe este código para generar nuestro menú de hamburguesas responsivo:?

<button
  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"
>
  <span class="fas fa-bars open text-dark"></span>
  <span class="fas fa-times close text-dark"></span>
</button>

Escribe este código para generar nuestros enlaces de barra de navegación en los que se puede hacer clic. Ten en cuenta que estamos usando el componente de react-router-dom para vincular a la URL del componente de nuestra página: ?

<div
  className="collapse navbar-collapse justify-content-end"
  id="navbarNavAltMarkup"
> <div className="navbar-nav fs-5">
    <NavLink to="/" className="nav-link">
      Characters
    </NavLink>
    <NavLink to="/episodes" className="nav-link">
      Episode
    </NavLink>
    <NavLink
      activeClassName="active" className="nav-link"
      to="/location" >Location</NavLink>
  </div>
</div>

App.css

Además, escribe estos estilos si deseas que la barra de navegación se vea bien y que sus usuarios sepan exactamente en qué página se encuentran actualmente: ?

.active {
  color: #0b5ed7 !important;
  font-weight: bold;
  border-bottom: 3px solid #0b5ed7;
}

Luego, dentro de Navbar.js, importa los estilos de esta manera: ?

import "../../App.css";

El resultado hasta ahora: ?

Image description

Episodios

Image description

Para crear esta página, necesitamos 2 componentes: el primero es el componente card, que ya está construido, y el segundo es el componente Input Group a través del cual podemos cambiar nuestro número de episodio.

InputGroup.js

Image description

Nuestros grupos de entrada solo estarán disponibles en el componente Episodes y Location para que podamos cambiar el número de episodio y la ubicación para buscar personajes. ¡Empecemos! ?

Dentro de la carpeta category de la carpeta Filter, crea un nuevo archivo llamado InputGroup.js y escriba este código de inicio, incluido el sistema de desestructuración de accesorios: ?

const InputGroup = ({ name, changeID, total }) => {
return <div className="input-group mb-3">
  <select
  onChange={(e) => changeID(e.target.value)}
  className="form-select"
  id={name}
  ></select>
</div>
};

Luego, creemos nuestra opción de grupo de entrada. Escriba este código dentro de su etiqueta select: ?

<option value="1">Choose...</option>
{[...Array(total).keys()].map((x, index) => {
  return (
    <option value={x + 1}>
      {name} - {x + 1}
    </option>
  );
})}

Episodes.js

Dentro de este archivo, importe estos componentes:?

import React, { useEffect, useState } from "react";
import Card from "../components/Card/Card";
import InputGroup from "../components/Filter/category/InputGroup";

Ahora, crea estos estados para que podamos retener y cambiar información crucial para obtener datos de nuestra api:?

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}`;
}

Escriba este código para obtener datos de nuestra API: ?

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]);

Ahora, escribamos el código para representar los resultados en nuestra pantalla. Primero, mostremos el nombre del episodio y la fecha de emisión de esta manera: ?

return (
<div className="container">
  <div className="row mb-3">
    <h1 className="text-center mb-3">
      Episode name :{" "}
      <span className="text-primary">{name === "" ? "Unknown" : name}</span>
    </h1>
    <h5 className="text-center">
      Air Date: {air_date === "" ? "Unknown" : air_date}
    </h5>
  </div>
</div>
);

Los resultados hasta ahora se ven así: ?

Image description

Ahora, mostremos las tarjetas de personajes y los grupos de entrada: ?

<div className="row">
  <div className="col-lg-3 col-12 mb-4">
    <h4 className="text-center mb-4">Pick Episode</h4>
    <InputGroup name="Episode" changeID={setID} total={51} />
  </div>
  <div className="col-lg-8 col-12">
    <div className="row">
      <Card results={results} />
    </div>
  </div>
</div>

Los resultados hasta ahora: ?

Image description

Location

Image description

Hacer este componente es similar a hacer una página de episodes. Primero, copia todo desde la página de episodes y realiza estos pocos cambios:?

  let [results, setResults] = useState([]);
  let [info, setInfo] = useState([]);
  let { dimension, type, name } = info;
  let [number, setNumber] = useState(1);

  let api = `https://rickandmortyapi.com/api/location/${number}`;

Ahora cambie solo una palabra clave en el hook useEffect, de characters a residents, así:?

useEffect(() => {
      // Other codes are here
      data.residents.map((x) => {
      })
    // Other codes are here
}, [api]);

En la declaración de devolución, realiza estos cambios: ?

return (
<h1 className="text-center mb-3">
  Location :
  <span className="text-primary">
    {name === "" ? "Unknown" : name}
  </span>
</h1>
<h5 className="text-center">
  Dimension: {dimension === "" ? "Unknown" : dimension}
</h5>
<h6 className="text-center">Type: {type === "" ? "Unknown" : type}</h6>
);

Y finalmente, realiza estos cambios para nuestro componente InputGroup: ?

<InputGroup name="Location" changeID={setNumber} total={126} />

Los resultados hasta ahora?

Image description

Páginas dinámicas

Este es el último paso de nuestro proyecto. Nuestro objetivo principal es saber más sobre un personaje específico cuando hacemos clic en la tarjeta. Vamos a utilizar el sistema de módulos dinámicos de react-router-dom, algo como esto:?

Image description

CardDetails.js

Para lograr nuestros objetivos, necesitamos crear un componente separado para mostrar más detalles sobre el personaje. Crearemos un nuevo archivo llamado CardDetails.js dentro de la carpeta Card.

Primero, importemos los componentes esenciales:

import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";

Luego, necesitamos almacenar nuestra api y usar algunos hooks useState. Usaremos el useParams hook para obtener la identificación de la URL:?

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}`;
}

Usaremos este hook useEffect para obtener datos de nuestra API: ?

useEffect(() => {
  (async function () {
    let data = await fetch(api).then((res) => res.json());
    updateFetchedData(data);
  })();
}, [api]);

Ahora, escribamos este código que generará el nombre y la imagen de nuestro personaje: ?

return (
<div className="container d-flex justify-content-center mb-5">
  <div className="d-flex flex-column gap-3">
    <h1 className="text-center">{name}</h1>
    <img className="img-fluid" src={image} alt="" />
  </div>
</div>
)

Ahora, escribe este código en caso de que quieras mostrar el estado actual de cada personaje:?

{(() => {
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>;
}
})()}

A continuación, escribe este código para mostrar toda la información sobre el personaje: ?

<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>

App.js

A continuación, debemos definir nuestras rutas para que nuestro componente de enrutador dinámico funcione correctamente. Primero, importe y luego agregue este código:?

import CardDetails from "./components/Card/CardDetails";
// other codes are here

<Routes>
  <Route path="/:id" element={<CardDetails />} />
  <Route path="/episodes/:id" element={<CardDetails />} />
  <Route path="/location/:id" element={<CardDetails />} />
</Routes>

Ahora, desplázate hacia abajo dentro de tu App.js y haz esta pequeña modificación ? para que se refiera a la página de inicio:

<Card page="/" results={results} />

Card.js

Vaya al componente de su tarjeta y realiza estos cambios: ?

  • Primero, desestructura los nuevos accesorios e importa el Link desde react-router-dom
import { Link } from "react-router-dom";

const Card = ({ page, results }) => {}
  • A continuación, envuelve todo dentro de la declaración de devolución dentro de una etiqueta de enlace:
<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"
>
{/* Other codes are here */}
</Link>

Episodes.js

En este archivo, solo ajuste esta pequeña línea: ?

<Card page="/episodes/" results={results} />

Location.js

Al igual que en Episodes.js, solo ajusta esta pequeña línea: ?

<Card page="/location/" results={results} />

Los resultados: ✨✨✨

Image description

Conclusión

Image description

¡Felicidades por leer hasta el final! Ahora puedes usar React JS y Bootstrap de manera fácil y eficiente para manejar proyectos.

También aprendiste cómo obtener datos de una API y mapear los resultados. No solo eso, también tienes un proyecto para mostrarle a tu reclutador local.

Aquí está tu medalla por leer hasta el final. ❤️

Las sugerencias y críticas son muy apreciadas ❤️

Alt Text