A煤n no tienes acceso a esta clase

Crea una cuenta y contin煤a viendo este curso

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 9

Preguntas 1

Ordenar por:

驴Quieres ver m谩s aportes, preguntas y respuestas de la comunidad? Crea una cuenta 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...

Comparto el componente de paginaci贸n de TailwindUI. El n煤mero m谩ximo de productos es 200, para la propiedad disabled de <button />. 馃檶

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/

Resultado del reto:
.

.
Archivos utilizados/modificado:

.
Link del repo aqui

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

Aqu铆 dejo el reto que nos dej贸 el profe. En cuanto al estilo, he estado utilizando los Tailwind CSS components de daisyUI 馃槃


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