No tienes acceso a esta clase

¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera

Curso Profesional de Next.js

Curso Profesional de Next.js

Oscar Barajas Tavares

Oscar Barajas Tavares

Obteniendo la lista de productos desde la API

16/31
Recursos

Aportes 17

Preguntas 3

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad?

o inicia sesión.

Les comparto mi solución para el reto de paginación

Es un componente que lo llamé Paginate y usa la UI de Tailwind
Para la lógica le paso estas props:
totalItems: El total de todos los elementos (llamando a la API con el offset=0 y limit=0 y luego aplicando un .length)
itemsPerPage: Los elementos a mostrar por página, sería el PRODUCT_LIMIT
setOffset: Le paso la función que actualiza el estado offsetProducts para realizar la llamada a la API (sería el resultado de la página donde estoy, menos 1 y multiplicado por los elementos que se muestran)
neighbours: La cantidad de números que se van a mostrar a los lados de la página seleccionada. Siempre se van a mostrar la misma cantidad, o menos si el total de páginas es inforior

Hago los cálculos para generar un lista con las páginas.

Utilizo una variable de estado current que se actualiza al seleccionar un página o al siguiente o anterior y ahí también es donde utilizo setOffset(), que en realidad es setOffsetProducts. Al actualizar la variable de estado offsetProducts que la paso como variable al endpoint se actualiza la url de la API, por lo que aprovecho el useEffect de useFecth pasándole como parámetro el endpoint para que llame a la API

import React, { useState } from 'react';
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid';

const Paginate = ({ totalItems, itemsPerPage, neighbours, setOffset }) => {
  const items = [];
  const [current, setCurrent] = useState(1);
  const totalPage = Math.ceil(totalItems / itemsPerPage);
  const end = Math.min(Math.max(neighbours * 2 + 2, neighbours + current + 1), totalPage + 1);
  const start = Math.min(Math.max(end - (neighbours * 2 + 1), 1), Math.max(current - neighbours, 1));

  for (let i = start; i < end; i++) {
    items.push(
      <a
        key={`Paginador-${i}`}
        onClick={() => {
          setCurrent(i);
          setOffset((i - 1) * itemsPerPage);
        }}
        href="#"
        aria-current="page"
        className={`${getClassActive(i)} relative inline-flex items-center px-4 py-2 border text-sm font-medium`}
      >
        {i}
      </a>
    );
  }

  function getClassActive(i) {
    return i === current ? 'z-10 bg-indigo-50 border-indigo-500 text-indigo-600' : 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50';
  }

  function prevPage() {
    if (current > 1) {
      setCurrent(current - 1);
      setOffset((current - 2) * itemsPerPage);
    }
  }

  function nextPage() {
    if (current < totalPage) {
      setCurrent(current + 1);
      setOffset(current * itemsPerPage);
    }
  }

  return (
    <div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
      <div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
        <div>
          <p className="text-sm text-gray-700">
            Showing <span className="font-medium">{itemsPerPage * (current - 1) + 1}</span> to{' '}
            <span className="font-medium">{current * itemsPerPage < totalItems ? current * itemsPerPage : totalItems}</span> of <span className="font-medium">{totalItems}</span> results
          </p>
        </div>
        <div>
          <nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
            <a
              onClick={() => prevPage()}
              href="#"
              className="bg-white border-gray-300 text-gray-500 hover:bg-gray-50 relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
            >
              <span className="sr-only">Previous</span>
              <ChevronLeftIcon className="h-5 w-5" aria-hidden="true" />
            </a>
            {items}
            <a
              onClick={() => nextPage()}
              href="#"
              className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
            >
              <span className="sr-only">Next</span>
              <ChevronRightIcon className="h-5 w-5" aria-hidden="true" />
            </a>
          </nav>
        </div>
      </div>
    </div>
  );
};

export default Paginate;

En el Dashboard la variable de estado y las llamadas

  const [offsetProducts, setOffsetProducts] = useState(0);

  const products = useFecth(endPoints.products.getProducts(PRODUCT_LIMIT, offsetProducts), offsetProducts);
  const totalProducts = useFecth(endPoints.products.getProducts(0, 0)).length;

Para mostrar el paginador

{totalProducts > 0 && <Paginate totalItems={totalProducts} itemsPerPage={PRODUCT_LIMIT} setOffset={setOffsetProducts} neighbours={3}></Paginate>}

En el useEffect de useFetch

  useEffect(() => {
    try {
      fecthData();
    } catch (error) {
      console.log(error);
    }
  }, [endPoint]);

Acá el componente Pagination, basado en el de TailwindUI, simplificado para usar previous y next:
Las props offset y setOffset son de un useState del componente padre (dashboard):

export default function Pagination({ offset, setOffset }) {
  const handlePrev = () => {
    if (offset <= 5) return;
    setOffset(offset - 5);
  };

  const handleNext = () => {
    setOffset(offset + 5);
  };

  return (
    <div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
      <div className="flex-1 flex justify-between">
        <button
          onClick={handlePrev}
          className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
        >
          Previous
        </button>
        <button
          onClick={handleNext}
          className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
        >
          Next
        </button>
      </div>
    </div>
  );
}

luego en el archivo useFetch.jsx, le agregue la dependencia de endpoint al useEffect:

const useFetch = (endpoint) => {
  const [data, setData] = useState([]);

  async function fetchData() {
    const res = await axios.get(endpoint);
    setData(res.data);
  }

  useEffect(() => {
    try {
      fetchData();
    } catch (error) {
      console.log(error);
    }
  }, [endpoint]);

  return data;
};

Y en el dashboard/index.jsx, modifique la parte del componente función:

const PRODUCT_LIMIT = 5;
const PRODUCT_OFFSET = 5;

export default function Dashboard() {
  const [offset, setOffset] = useState(PRODUCT_OFFSET);

  const products = useFetch(
    endPoints.products.getProducts(PRODUCT_LIMIT, offset)
  );

  return ( ...aca va el resto del archivo como estaba agregando el componente <Pagination offset={offset} setOffset={setOffset} /> antes del ultimo div...

Clase #16: Obteniendo la lista de productos desde la API 16/31 🛒


 

GET Products: 🛍️

 
En ésta clase vamos a implementar la lógica para mostrar los productos.
 
En la API (enlace: aquí), en la pestaña de Producto en el método GET, la petición requiere dos elementos: el limit qué es la cantidad de productos que queremos que se muestren y el offset que indica desde cuál posición queremos que se muestren los productos (importante recordar que la primera posición de los id es cero “0”).
 
Por ejemplo si queremos mostrar 15 productos desde el primer producto el limit sería 15 y el offset sería 0 de ésta forma: https://api.escuelajs.co/api/v1/products?limit=15&offset=0
 
En cambio si queremos mostrar 5 productos desde el sexto producto, el limit sería 5 y el offset sería 5 de ésta forma: https://api.escuelajs.co/api/v1/products?limit=5&offset=5
 
Si queremos mostrar todos los productos el limit sería 0 al igual que el offset = 0 :
https://api.escuelajs.co/api/v1/products?limit=0&offset=0
 


 

Continuando con el Proyecto: 🔨

 
En VSC, vamos a la ruta src/hooks y creamos un archivo llamado useFetch.js que nos permitirá hacer peticiones por medio de axios con un custom hooks, la lógica queda así:

import { useState, useEffect } from "react"; //Se importa desde react
import axios from "axios"; //Con axios vamos a realizar las peticiones


const useFetch = (endpoint) => {
    const [data, setData] = useState([]); //Array vacío

    async function fetchData(){
        const response = await axios.get(endpoint); //Llamado
        setData(response.data);
    }
    //useEffect permite ejecutar el llamado cuando se necesite
    useEffect(() => {
        try {
            fetchData();
        } catch (error) {
            console.log(error);
        }
    }, []); //El array debe estar vacío cuando no se usa Pagination

    return data;
};

export default useFetch;

 
Vamos al archivo index.js donde está implementado la estructura del dashboard en la ruta src/pages/dashboard ahí tenemos como defecto la llamada de atributos de people, lo que queremos es adaptarlo para que nos muestre otras características relacionadas con los producto como la imagen, el nombre de la categoría, el precio, el id y agregar opciones de Editar y Borrar productos.
 
Para la imagen, si vamos a la API y vemos la salida de cada producto aparece tres enlaces para la imagen con diferentes resoluciones, como queremos mostrar solo la primera cuando llamemos en la estructura en html debe ser desde la posición cero:

<img className="h-10 w-10 rounded-full" src={product.images[0]} alt="" />

 
Para el nombre del producto, en la API está referido como title, entonces se accede así:

<div className="text-sm font-medium text-gray-900">{product.title}</div>

 
Para obtener el nombre de la categoría, en la API nos indica que debemos entrar a category y luego a name:

<div className="text-sm text-gray-900">{product.category.name}</div>

 
Para mostrar el precio de cada producto podemos colocar el símbolo de la moneda que queremos que salga a lado izquierdo de cada monto:

<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">${product.price}</span>

 
Para mostrar el id del producto:

<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{product.id}</td>

 
La estructura total queda así:

import endPoints from "@services/api"; //Para poder usar el endPoints de services
import useFetch from "@hooks/useFetch"; //Llamar al custom hooks

//El limit y offset de GET Products
//En mayúsculas porque son valores que se cambian muy poco
const PRODUCT_LIMIT = 5;
const PRODUCT_OFFSET = 5;

export default function Dashboard() {

  //Llamar a los productos
  const products = useFetch(endPoints.products.getProducts(PRODUCT_LIMIT, offset));
  //console.log(products);

  return (
    <>
      <div className="flex flex-col">
        <div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
          <div className="py-2 align-middle inline-block min-w-full
                          sm:px-6 lg:px-8">
            <div className="shadow overflow-hidden border-b
                            border-gray-200 sm:rounded-lg">
              <table className="min-w-full divide-y divide-gray-200">
                <thead className="bg-gray-50">
                  <tr>
                    <th scope="col" className="px-6 py-3 text-left
                                                text-xs font-medium
                                                text-gray-500 uppercase
                                                tracking-wider">
                      Name
                    </th>
                    <th scope="col" className="px-6 py-3 text-left text-xs
                                                font-medium text-gray-500
                                                uppercase tracking-wider">
                      Category
                    </th>
                    <th scope="col" className="px-6 py-3 text-left text-xs
                                                font-medium text-gray-500
                                                uppercase tracking-wider">
                      Price
                    </th>
                    <th scope="col" className="px-6 py-3 text-left text-xs
                                                font-medium text-gray-500
                                                uppercase tracking-wider">
                      Id
                    </th>
                    <th scope="col" className="relative px-6 py-3">
                      <span className="sr-only">Edit</span>
                    </th>
                    <th scope="col" className="relative px-6 py-3">
                      <span className="sr-only">Delete</span>
                    </th>
                  </tr>
                </thead>
                <tbody className="bg-white divide-y divide-gray-200">
                  {products?.map((product) => (
                    <tr key={`Product-item-${product.id}`}>
                      <td className="px-6 py-4 whitespace-nowrap">
                        <div className="flex items-center">
                          <div className="flex-shrink-0 h-10 w-10">
                            <img className="h-10 w-10 rounded-full"
                              src={product.images[0]} alt="" />
                          </div>
                          <div className="ml-4">
                            <div className="text-sm font-medium
                                            text-gray-900">
                              {product.title}
                            </div>
                          </div>
                        </div>
                      </td>
                      <td className="px-6 py-4 whitespace-nowrap">
                        <div className="text-sm text-gray-900">
                          {product.category.name}
                        </div>
                      </td>
                      <td className="px-6 py-4 whitespace-nowrap">
                        <span className="px-2 inline-flex text-xs
                                        leading-5 font-semibold
                                        rounded-full bg-green-100
                                        text-green-800">
                          ${product.price}
                        </span>
                      </td>
                      <td className="px-6 py-4 whitespace-nowrap
                                      text-sm text-gray-500">
                        {product.id}
                      </td>
                      <td className="px-6 py-4 whitespace-nowrap
                                    text-right text-sm font-medium">
                        <a href="#" className="text-indigo-600
                                              hover:text-indigo-900">
                          Edit
                        </a>
                      </td>
                      <td className="px-6 py-4 whitespace-nowrap
                                    text-right text-sm font-medium">
                        <a href="#" className="text-indigo-600
                                              hover:text-indigo-900">
                          Delete
                        </a>
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </div>
        </div>
      </div>
    </>
  );
}

 
Guardamos todo y ejecutamos en consola npm run dev en local al tener un login con éxito, entra directamente al dashboard y debe mostrar el render de los productos: la tabla con la cantidad productos que indicamos en el limit y con todas las características.

herramienta que recomienda el profesor para capturar los errores https://sentry.io/

Oscar, las imagenes estan caidas.

Mi solución fue utilizar un poco la lógica que tenemos de useFetch y combinarla con lo que teníamos en el index del dashboard, para mi, fue más simple crear un useProducts y mover toda mi lógica allá, quedó así mi archivo

import { useState, useEffect } from 'react'
import endPoints from '@services/api'
import axios from 'axios'

const useProducts = () => {
  const [offset, setOffset] = useState(0)
  const [page, setPage] = useState(0)
  const PRODUCTS_LIMIT = 5
  const [products, setProducts] = useState(null)

  useEffect(() => {
    const fetchData = async () => {
      const res = await axios(endPoints.products.getProducts(PRODUCTS_LIMIT, offset))
      console.log(res)
      setProducts(res)
    }
    try {
      fetchData()
    } catch (error) {
      console.log(error)
    }
  }, [offset, PRODUCTS_LIMIT])

  return {
    page,
    products,
    setOffset,
    setPage,
    PRODUCTS_LIMIT,
  }
}

export default useProducts

A mi index solo crea la variable products e itero por los productos recibidos y agrego un footer:

export default function Dashboard() {
  const products = useProducts()
  return (
    <>
...
                <tbody className="bg-white divide-y divide-gray-200">
                  {products?.products?.data?.map((product) => (
                    <tr key={`Product-item-${product.id}`}>
                      <td className="px-6 py-4 whitespace-nowrap">
....
              <FooterPag
                page={products.page}
                setOffset={products.setOffset}
                setPage={products.setPage}
                PRODUCTS_LIMIT={products.PRODUCTS_LIMIT}
              />
            </div>
          </div>
        </div>
      </div>
    </>
  )
} 

Y el footer:

/* This example requires Tailwind CSS v2.0+ */
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid'
import { useEffect } from 'react'

export default function FooterPag({ page, setOffset, setPage, PRODUCTS_LIMIT }) {
  const handlePlusPage = () => {
    setPage(page + 1)
  }
  const handleMinusPage = () => {
    if (page >= 0) {
      setPage(page - 1)
    }
  }
  useEffect(() => {
    setOffset(PRODUCTS_LIMIT * page)
  }, [page, PRODUCTS_LIMIT, setOffset])
  return (
    <div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
      <div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-end">
        <div>
          <nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
            <button
              type="button"
              disabled={page == 0 ? true : false}
              className={`relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 ${
                page == 0 && 'opacity-60'
              }`}
              onClick={handleMinusPage}
            >
              <span className="sr-only">Previous</span>
              <ChevronLeftIcon className="h-5 w-5" aria-hidden="true" />
            </button>
            {/* Current: "z-10 bg-indigo-50 border-indigo-500 text-indigo-600", Default: "bg-white border-gray-300 text-gray-500 hover:bg-gray-50" */}
            <p
              aria-current="page"
              className="z-10 bg-indigo-50 border-indigo-500 text-indigo-600 relative inline-flex items-center px-4 py-2 border text-sm font-medium"
            >
              {page + 1}
            </p>
            <button
              type="button"
              className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
              onClick={handlePlusPage}
            >
              <span className="sr-only">Next</span>
              <ChevronRightIcon className="h-5 w-5" aria-hidden="true" />
            </button>
          </nav>
        </div>
      </div>
    </div>
  )
}

Recomiendo mucho el uso de swr como librearía de peticiones y manejo de estado con cache para Next.js. Lo he estado usando mucho ultimamente y es una maravilla: https://swr.vercel.app/

Comparto el componente de paginación de TailwindUI. El número máximo de productos es 200, para la propiedad disabled de <button />. 🙌

Para el reto de la clase hice los siguiente:

Cree el hook Paginate. tal que:

import { useState } from 'react';

const Paginate = (limit, currentOffset, total) => {
  const [offset, setOffset] = useState(currentOffset);

  const handleNext = () => {
    if (offset < total) {
      setOffset(offset + limit);
    }
  };
  const handlePrev = () => {
    if (offset > 0) {
      setOffset(offset - limit);
    }
  };
  
   const newOffset = offset;

  return {
    newOffset,
    handleNext,
    handlePrev,
  };
};

export default Paginate;

Ademas cree el componente Pagination:

import React from 'react';

export default function Pagination({ limit, offset, total, handlePrev, handleNext }) {
  return (
    <div className="flex flex-col items-center">
      <span className="text-sm text-gray-700 ">
        Showing <span className="font-semibold text-gray-900 text-gray-900">{offset + 1}</span> to <span className="font-semibold text-gray-900 text-gray-900">{offset + limit}</span> of{' '}
        <span className="font-semibold text-gray-900 text-gray-900">{total}</span>
      </span>
      <div className="inline-flex mt-2 xs:mt-0">
        <button
          type="button"
          onClick={handlePrev}
          className="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-gray-800 rounded-l hover:bg-gray-900 dark:bg-gray-800 dark:border-gray-700  dark:hover:bg-gray-700 dark:hover:text-white"
        >
          Prev
        </button>
        <button
          type="button"
          onClick={handleNext}
          className="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-gray-800 border-0 border-l border-gray-700 rounded-r hover:bg-gray-900 dark:bg-gray-800 dark:border-gray-700  dark:hover:bg-gray-700 dark:hover:text-white"
        >
          Next
        </button>
      </div>
    </div>
  );
}


Finalmente, dashboard quedó de esta forma:

import useFetch from '@hooks/useFetch';
import Paginate from '@hooks/Paginate';
import endPoints from '@services/api';
import Pagination from '@components/Pagination';

const PRODUCT_LIMIT = 5;
const PRODUCT_OFFSET = 0;

export default function Dashboard() {
  const allProducts = useFetch(endPoints.products.getProducts(0, 0));
  const totalProducts = allProducts.length;
  const paginate = Paginate(PRODUCT_LIMIT, PRODUCT_OFFSET, totalProducts);
  const handleNext = paginate.handleNext;
  const handlePrev = paginate.handlePrev;
  const offset = paginate.newOffset;
  const products = allProducts.slice(offset, offset + PRODUCT_LIMIT);

  return (
    <>
      <div className="flex flex-col">
        <div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
          <div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
            <div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
              <table className="min-w-full divide-y divide-gray-200">
                <thead className="bg-gray-50">
                  <tr>
                    <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                      Name
                    </th>
                    <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                      Category
                    </th>
                    <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                      Price
                    </th>
                    <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                      Id
                    </th>
                    <th scope="col" className="relative px-6 py-3">
                      <span className="sr-only">Edit</span>
                    </th>
                    <th scope="col" className="relative px-6 py-3">
                      <span className="sr-only">Delete</span>
                    </th>
                  </tr>
                </thead>
                <tbody className="bg-white divide-y divide-gray-200">
                  {products?.map((product) => (
                    <tr key={`Product-item-${product.id}`}>
                      <td className="px-6 py-4 whitespace-nowrap">
                        <div className="flex items-center">
                          <div className="flex-shrink-0 h-10 w-10">
                            <img className="h-10 w-10 rounded-full" src={product.images[0]} alt="" />
                          </div>
                          <div className="ml-4">
                            <div className="text-sm font-medium text-gray-900">{product.title}</div>
                          </div>
                        </div>
                      </td>
                      <td className="px-6 py-4 whitespace-nowrap">
                        <div className="text-sm text-gray-900">{product.category.name}</div>
                      </td>
                      <td className="px-6 py-4 whitespace-nowrap">
                        <span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">{product.price}</span>
                      </td>
                      <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">$ {product.id}</td>
                      <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
                        <a href="/edit" className="text-indigo-600 hover:text-indigo-900">
                          Edit
                        </a>
                      </td>
                      <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
                        <a href="/edit" className="text-indigo-600 hover:text-indigo-900">
                          Delete
                        </a>
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>

              <Pagination limit={PRODUCT_LIMIT} offset={offset} total={totalProducts} handlePrev={handlePrev} handleNext={handleNext}></Pagination>
            </div>
          </div>
        </div>
      </div>
    </>
  );
}

En mi caso no me complique mucho

  const limit = 5;
  const [offset, setOffset] = useState(0);

  const handleNext = async (offset) => {
    setOffset(offset + 5);
  };

  const handlePrev = async (offset) => {
    setOffset(offset - 5);
  };

  const products = useFetch(endPoints.products.getProducts(limit, offset));

Después en el button

// Cada uno en su respectivo button
onClick={() => handlePrev(offset)}
onClick={() => handleNext(offset)}

Por ultimo no se olviden de ir al hook useFetch y agregar la variable endpoints al arreglo de dependencias

const useFetch = (endpoint) => {
  const [data, setData] = useState([]);

  async function fetchData() {
    const response = await axios.get(endpoint);
    setData(response.data);
  }

  useEffect(() => {
    try {
      fetchData();
    } catch (error) {
      console.log(error);
    }
  }, [endpoint]);

  return data;
};

Aquí dejo el reto que nos dejó el profe. En cuanto al estilo, he estado utilizando los Tailwind CSS components de daisyUI 😄


Resultado del reto:
.

.
Archivos utilizados/modificado:

.
Link del repo aqui

Les quiero compartir mi solución a la paginación usando React-Bootstrap y TS.

Yo he creado un componente para manejar la tabla de ítems de la tabla de productos, esta ha quedado así.

import React from 'react';

type category = {
  id: number[];
  name: string[];
  image: string[];
};

type products = {
  category: category[];
  creationAt: string[];
  description: string[];
  id: string[];
  images: any[];
  price: number[];
  title: string[];
  updatedAt: string[];
};

interface Props {
  products: Array<products>;
}

const Productos = (products: Props) => {
  console.log(products.products);
  return (
    <>
      {products?.products?.map((product: products) => (
        <tr key={`Product-item-${product.id}`}>
          <td className="px-6 py-4 whitespace-nowrap">
            <div className="flex items-center">
              <div className="flex-shrink-0 h-10 w-10">
                <img className="h-10 w-10 rounded-full" src={product.images[0]} alt="" />
              </div>
              <div className="ml-4">
                <div className="text-sm font-medium text-gray-900">{product.title}</div>
                <div className="text-sm text-gray-500">{product.description}</div>
              </div>
            </div>
          </td>
          <td className="px-6 py-4 whitespace-nowrap">
            <div className="text-sm text-gray-900">${product.price}</div>
            <div className="text-sm text-gray-500"></div>
          </td>
          <td className="px-6 py-4 whitespace-nowrap">
            <span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">{product.id}</span>
          </td>
          <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{product.category.name}</td>
          <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
            <a href="#" className="text-indigo-600 hover:text-indigo-900">
              Delete
            </a>
          </td>
        </tr>
      ))}
    </>
  );
};

export default Productos;

También he creado un componente para manejar la lógica de la paginación

import React from 'react';

import { Pagination } from 'react-bootstrap';
import PageItem from 'react-bootstrap/PageItem';
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/20/solid';

interface Props {
  products: Array<any>;
  productsPerPage: number;
  totalProducts: number;
  neighbours: number;
  end: number;
  start: number;
  setPage: React.FunctionComponent;
  active: number;
}

function Paginacion(props: Props) {
  let active = props.active;
  let item = [];

  const clickHandle = (i: number) => {
    if (i > Math.round(props.totalProducts / 5)) {
    } else {
      props.setPage(i);
    }
  };

  for (let i = props.start; i < props.end - 1; i++) {
    item.push(
      <Pagination.Item key={i} active={i === active} onClick={(event) => clickHandle(i)}>
        {i}
      </Pagination.Item>
    );
  }

  return (
    <div>
      <Pagination>
        <Pagination.First onClick={(event) => clickHandle(1)} />
        <Pagination.Prev />
        {item}
        <Pagination.Next onClick={(event) => clickHandle(active + 1)} />
        <Pagination.Last onClick={(event) => clickHandle(Math.round(props.totalProducts / 5))} />
      </Pagination>
      <br />
    </div>
  );
}

export default Paginacion;

y por último, así quedó la página dashboard

import React from 'react';
import { useState } from 'react';

import useFetch from '@hooks/useFetch';
import endPoints from '@services/api';
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/20/solid';
import Productos from '@components/Productos';
import Paginacion from '@components/Paginacion';

export default function Dashboard() {
  const PRODUCT_LIMIT = 0;
  const PRODUCT_OFFSET = 0;

  const [currentPage, setCurrentPage] = useState(1);
  const productsPerPage = 5;
  const neighbours = 5;

  const products = useFetch(endPoints.products.getProducts(PRODUCT_LIMIT, PRODUCT_OFFSET));

  const indexOfLastProduct = currentPage * productsPerPage;
  const indexOfFirstProduct = indexOfLastProduct - productsPerPage;
  const currentProducts = products.slice(indexOfFirstProduct, indexOfLastProduct);
  const totalPage = Math.ceil(products.length / productsPerPage);
  const end = Math.min(Math.max(neighbours * 2 + 2, neighbours + currentPage + 1), totalPage + 1);
  const start = Math.min(Math.max(end - (neighbours * 2 + 1), 1), Math.max(currentPage - neighbours, 1));

  const setPage = (page: number) => {
    setCurrentPage(page);
  };
  return (
    <>
      <div className="flex flex-col">
        <div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
          <div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
            <div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
              <table className="min-w-full divide-y divide-gray-200">
                <thead className="bg-gray-50">
                  <tr>
                    <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                      products
                    </th>
                    <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                      Price
                    </th>
                    <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                      Id
                    </th>
                    <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                      Category
                    </th>
                    <th scope="col" className="relative px-6 py-3">
                      <span className="sr-only">Edit</span>
                    </th>
                  </tr>
                </thead>
                <tbody className="bg-white divide-y divide-gray-200">
                  <Productos products={currentProducts} />
                </tbody>
              </table>
              <Paginacion
                active={currentPage}
                setPage={setPage}
                end={end}
                start={start}
                productsPerPage={productsPerPage}
                totalProducts={products.length}
                products={currentProducts}
                neighbours={neighbours}
              />
            </div>
          </div>
        </div>
      </div>
    </>
  );
}

Si tienen un error que les indique que .map no es una función, organicen el useFetch así:

const {data:products} = useFetch(endPoints.products.getProducts(5,5));

Mi solución a la paginación

Mi solución al reto fue la siguiente:

dashboard/index.js

import useFetch from '@hooks/useFetch';
import endPoints from '@services/api';
import { useState } from 'react';
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid';
const PRODUCT_LIMIT = 5;

export default function Dashboard() {
  const [page, setPage] = useState(1);
  const products = useFetch(endPoints.products.getProducts(PRODUCT_LIMIT, (page - 1) * PRODUCT_LIMIT));
  const totalProducts = useFetch(endPoints.products.getProducts(0, 0)).length;
  return (
    <>
      <div className="flex flex-col">
        <div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
          <div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
            <div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
              <table className="min-w-full divide-y divide-gray-200">
                <thead className="bg-gray-50">
                  <tr>
                    <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                      Name
                    </th>
                    <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                      Category
                    </th>
                    <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                      Price
                    </th>
                    <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                      Id
                    </th>
                    <th scope="col" className="relative px-6 py-3">
                      <span className="sr-only">Edit</span>
                    </th>
                    <th scope="col" className="relative px-6 py-3">
                      <span className="sr-only">Delete</span>
                    </th>
                  </tr>
                </thead>
                <tbody className="bg-white divide-y divide-gray-200">
                  {products?.map((product) => (
                    <tr key={`Product-item-${product.id}`}>
                      <td className="px-6 py-4 whitespace-nowrap">
                        <div className="flex items-center">
                          <div className="flex-shrink-0 h-10 w-10">
                            <img className="h-10 w-10 rounded-full" src={product.images[0]} alt="" />
                          </div>
                          <div className="ml-4">
                            <div className="text-sm font-medium text-gray-900">{product.title}</div>
                          </div>
                        </div>
                      </td>
                      <td className="px-6 py-4 whitespace-nowrap">
                        <div className="text-sm text-gray-900">{product.category.name}</div>
                      </td>
                      <td className="px-6 py-4 whitespace-nowrap">
                        <span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">{product.price}</span>
                      </td>
                      <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{product.id}</td>
                      <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
                        <a href="/login" className="text-indigo-600 hover:text-indigo-900">
                          Edit
                        </a>
                      </td>
                      <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
                        <a href="/login" className="text-indigo-600 hover:text-indigo-900">
                          Delete
                        </a>
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
            <div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
              <div className="flex-1 flex justify-between sm:hidden">
                <a href="/login" className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
                  Previous
                </a>
                <a href="/login" className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
                  Next
                </a>
              </div>
              <div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
                <div>
                  <p className="text-sm text-gray-700">
                    Showing <span className="font-medium">{1 + (page - 1) * PRODUCT_LIMIT}</span> to <span className="font-medium">{page * PRODUCT_LIMIT}</span> of{' '}
                    <span className="font-medium">{totalProducts}</span> results
                  </p>
                </div>
                <div>
                  <nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
                    <button
                      onClick={() => {
                        page == 1 ? setPage(page) : setPage(page - 1);
                      }}
                      className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
                    >
                      <span className="sr-only">Previous</span>
                      <ChevronLeftIcon className="h-5 w-5" aria-hidden="true" />
                    </button>
                    {/* Current: "z-10 bg-indigo-50 border-indigo-500 text-indigo-600", Default: "bg-white border-gray-300 text-gray-500 hover:bg-gray-50" */}
                    <a href="/login" aria-current="page" className="z-10 bg-indigo-50 border-indigo-500 text-indigo-600 relative inline-flex items-center px-4 py-2 border text-sm font-medium">
                      {page}
                    </a>
                    <button
                      onClick={() => ((page + 1) * PRODUCT_LIMIT > totalProducts ? setPage(page) : setPage(page + 1))}
                      className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
                    >
                      <span className="sr-only">Next</span>
                      <ChevronRightIcon className="h-5 w-5" aria-hidden="true" />
                    </button>
                  </nav>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </>
  );
}

Les comparto mi solución para el reto de paginación además de compartirles un componente que cree para darle feedback al usuario cuando se estén cargando mas datos:

Mi componente de paginación es el siguiente:

import { useState, useMemo } from 'react';
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid';

type PaginationProps = {
  size: number;
  onChangePage(page: number): void;
};

export default function Pagination({
  size: maxNumberOfPages,
  onChangePage,
}: PaginationProps) {
  const [size] = useState(maxNumberOfPages);
  const [section, setSection] = useState(1);
  const [page, setPage] = useState(1);

  const items = useMemo(() => {
    const array: number[] = [];
    for (let i = 0; i < size; i++) {
      array.push(i + 1 + size * (section - 1));
    }
    return array;
  }, [size, section]);

  const onChangePageHandler = (newPage) => {
    if (newPage > size * section) {
      setSection(section + 1);
    } else if (newPage < size * section - (size - 1)) {
      setSection(section - 1);
    }
    setPage(newPage);
    onChangePage(newPage);
  };

  return (
    <div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
      <div className="flex-1 flex justify-between sm:hidden">
        <button className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
          Previous
        </button>
        <button className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
          Next
        </button>
      </div>
      <div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-end">
        <div>
          <nav
            className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px"
            aria-label="Pagination"
          >
            <button
              className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
              disabled={page - 1 < 1}
              onClick={() => onChangePageHandler(page - 1)}
            >
              <span className="sr-only">Previous</span>
              <ChevronLeftIcon className="h-5 w-5" aria-hidden="true" />
            </button>
            {items.map((item) => {
              const isActive = item === page;
              const buttonStyles = isActive
                ? 'bg-indigo-50 border-indigo-500 text-indigo-600'
                : 'bg-white border-gray-300 text-gray-500';

              return (
                <button
                  key={`pagination-button-${item}`}
                  className={
                    buttonStyles +
                    'hover:bg-gray-50 relative inline-flex items-center px-4 py-2 border text-sm font-medium'
                  }
                  onClick={() => onChangePageHandler(item)}
                >
                  {item}
                </button>
              );
            })}

            <button
              className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
              onClick={() => onChangePageHandler(page + 1)}
            >
              <span className="sr-only">Next</span>
              <ChevronRightIcon className="h-5 w-5" aria-hidden="true" />
            </button>
          </nav>
        </div>
      </div>
    </div>
  );
}

El funcionamiento es el siguiente el componente recibe un size como prop que se refiere a la cantidad de elementos por pagina que muestra, luego tengo una variable sección que utilizo para determinar cuando tengo que cambiar los numeros de pagina que se muestran en la paginación (cambiar de sección ) y por ultimo una variable que contiene la pagina actual.

El siguiente codigo es para el esqueleto de carga que se muestra cuando se estan cargando datos

primero necesitamos agregar las siguientes lo siguiente a nuestro archivo de configuración de tailwind

module.exports = {
  content: ['./src/**/*.{ts,tsx}'],
  theme: {
    extend: {
      backgroundImage: {
        'gradiente-skeleton':
          'linear-gradient(to right, rgba(255,255,255,0),rgba(255,255,255,0.5) 50% ,rgba(255,255,255,0) 80%)',
      },
      backgroundSize: {
        '50-200': '25px 200px',
      },
      animation: {
        shink: 'shink 1s infinite',
      },
      keyframes: {
        shink: {
          to: {
            backgroundPosition: '120% 0',
          },
        },
      },
    },
  },
  plugins: [],
};

El codigo del componente:

type RowSkeletonProps = {
  colCount: number;
  rowCount: number;
};

export default function RowsSkeleton({ rowCount, colCount }: RowSkeletonProps) {
  const rows = [...new Array(rowCount)];
  const cols = [...new Array(colCount)];

  return (
    <>
      {rows.map((val, index) => (
        <tr key={`Row-skeleton-${index}`}>
          {cols.map((value, index) => (
            <td
              className="px-6 py-6 whitespace-nowrap"
              key={`Col-skeleton-${index}`}
            >
              <div className="w-24 h-4 bg-gray-200 bg-repeat-y bg-left-top bg-50-200 bg-gradiente-skeleton animate-shink"></div>
            </td>
          ))}
        </tr>
      ))}
    </>
  );
}

Funciona de la siguiente manera el componente recibe dos parametros colCount y rowCount con los cual le indicamos cuantas filas y columnas renderizar.
El resultado 😀