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