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 el token de la API

13/31
Recursos

Aportes 22

Preguntas 3

Ordenar por:

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

o inicia sesión.

Para los errores utilicé un estado “errorLogin” para mostrar el error y otro estado “loading” para mostrar un icono en el botón y desahabilitar al enviar el form

const [errorLogin, setErrorLogin] = useState(null);
const [loading, setLoading] = useState(false);

En el submit un catch y seteo el error y el estado de loading para mostrar un icono

  const submitHandler = (event) => {
    event.preventDefault();
    const email = emailRef.current.value;
    const password = passwordRef.current.value;
    setErrorLogin(null);
    setLoading(true);
    auth
      .signIn(email, password)
      .then(() => {
        route.push('/dashboard');
      })
      .catch(function (error) {
        if (error.response?.status === 401) {
          setErrorLogin('Usuario o password incorrecto.');
        } else if (error.request) {
          setErrorLogin('Tenemos un problema');
        } else {
          setErrorLogin('Algo salió mal.');
        }
        setLoading(false);
      });
  };

El si hay un error muestro el div con el mensaje

{errorLogin && (
  <div class="p-3 mb-3 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800" role="alert">
    <span class="font-medium">Error!</span> {errorLogin}
  </div>
)}

Si se envía el formulario muestro un icono dentro del button

{loading && (
  <span class="flex absolute h-4 w-4 top-0 right-0 -mt-1 -mr-1">
    <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-indigo-300 opacity-75"></span>
    <span class="relative inline-flex rounded-full h-4 w-4 bg-indigo-400"></span>
  </span>
)}

Y establezco disabled el button
disabled={loading}

para los errores cree dos constantes utilizando useState en el useAuth.js

const [error, setError]=useState();

  return {
    user,
    error,
    setError,
    signIn,
  };

En el login page, en el auth.signIn

    auth.signIn(email, password).then(
      () => {
        console.log('Login success');
      },
      (reason) => {
        console.log('Login Failed');
        console.error(reason);
        auth.setError('Invalid Username or Password');
      }
    );

y antes de cerrar el form para mostrar el estado del error agrege

            {auth.error ? (
              <div className="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800" role="alert">
                <span className="font-medium">Login Failed!</span> {auth.error}
              </div>
            ) : null}

Clase #13: Obteniendo el token de la API 13/31 🔐

 

¿Qué son las cookies? 🍪

 

Las cookies son pequeños fragmentos de texto que los sitios web que visitas envían al navegador. Permiten que los sitios web recuerden información sobre tu visita, lo que puede hacer que sea más fácil volver a visitar los sitios y hacer que estos te resulten más útiles. Fuente: aquí

 

Continuando con el proyecto: 🔨

 
En VSC en la ruta src/hooks en el archivo useAuth.js quitamos el console.log de signIn y agregamos la estructura de la cookie:

if(access_token){
	Cookie.set('token', access_token.access_token, {expires: 5}); 
	//expires permite que después de un tiempo definido podamos eliminar la información almacenada y pueda volver a logear
}

Una vez guardado, vamos a la consola, lo corremos con npm run dev, al ir al inspector de web, en Application sale una cuadro donde muestra el token generado, ésto quiere decir que la cookie guardó el token.

¿Qué son las Cookies 🍪?

 

Las Cookies es un archivo con datos creado y guardado en el ordenador de un usuario cuando visita paginas web que usan Cookies.

 

Este pequeño archivo contiene cierta información de usuario y/o otros tipo para su posterior rehúso del mismo.

 

¿Porque se llaman Cookies 🍪?

El nombre tan peculiar proviene del clásico cuento de hadas “Hansel y Gretel” en el cuento los hermanos dejan rastros de dulces y basándose en esta analogía se refiere al mismo rastros que dejamos con las visitas a los sitios web que visitamos y dejan en el ordenador estos archivos.

 

¿Para que sirven las Cookies 🍪?

 

Las Cookies almacenan distintos tipos de información de los usuarios con el objetivo de; identificarlo, recordar ciertas preferencias, ofrecerles contenido, etc.

Utilice el modal que estaba ya en el proyecto c: quedo así

== Obteniendo el token de la API ==

  1. Se hace la validacion si tenemos el access_token:
  2. Agregamos la informacion obtenida a una cookie con Cookies.set(), en el cual le pasaremos 3 parametros, el primero sera el nombre (string), el segundo sera el valor (access_token) , y el tercero sera la duracion ({expire}).
  • useAuth.js
function useProviderAuth() {
    const [user, setUser] = useState(null);

    const signIn = async (email, password) => {
        const options = {
            headers: {
                accept: '*/*',
                'Content-Type': 'application/json',
            },
        };
        //==Lectura del AccessToken que viene desde la api, para posteriormente agregarla a una cookie
        const { data: access_token } = await axios.post(endPoints.auth.login, { email, password }, options);
        if (access_token) {
            Cookies.set('token', access_token.access_token, { expires: 5 })
        }
    };
  1. Se agrego la logica de error en el login, se creo un estado error en useAuth.js:
 const [error , setError] = useState(false);
  1. Luego se agrego la logica en en handleSubmit del login page:
  const handleSubmit = (event) => {
    event.preventDefault();
    const user = userRef.current.value;
    const password = passwordRef.current.value;
    auth.signIn(user, password).then(() => {
      auth.setError(false);
      console.log("LoginSucces")
    },
      (err) => {
        console.log("Error Login");
        console.error(err);
        auth.setError(true);
      }
    );
  };
  1. Luego se agrego un div al final del formulario con la logica al obtener el error:
 {auth.error ? (
              <div className="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800" role="alert">
                <span className="font-medium">Login Failed!</span> {auth.error}
              </div>
            ) : null}

Resultado

Para la retroalimentación de errores use los toast de tailwind y el hook de useState

En loginPage

import { useRef, useState } from 'react';
import { LockClosedIcon } from '@heroicons/react/solid';
// import custom hook
import { useAuth } from '@hooks/useAuth';
// import components
import Toast from '@components/Toast';
import Spinner from '@components/Spinner';

export default function LoginPage() {
  const emailRef = useRef(null);
  const passwordRef = useRef(null);
  // In this case auth return the user and a function to sign in
  const auth = useAuth();

  // state of spinner
  const [loading, setLoading] = useState(false);
  // state of error
  const [error, setError] = useState(null);

  const submitHandler = (e) => {
    // prevent the form from submitting
    e.preventDefault();
    setLoading(true);
    setError(false);
    const email = emailRef.current.value;
    const password = passwordRef.current.value;

    // do something with the data, in this case the user signIn
    auth
      .signIn(email, password)
      .then(() => {
        console.log('signed in');
        setLoading(false);
      })
      .catch((err) => {
        setError(true);
        setLoading(false);
        console.log('Error signing: ', err);
      });
  };

  return (
    <>
      <div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
        <div className="max-w-md w-full space-y-8">
          <div>
            <img className="mx-auto h-12 w-auto" src="https://tailwindui.com/img/logos/workflow-mark-indigo-600.svg" alt="Workflow" />
            <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Sign in to your account</h2>
          </div>
          <form className="mt-8 space-y-6" onSubmit={submitHandler}>
            <input type="hidden" name="remember" defaultValue="true" />
            <div className="rounded-md shadow-sm -space-y-px">
              <div>
                <label htmlFor="email-address" className="sr-only">
                  Email address
                </label>
                <input
                  id="email-address"
                  name="email"
                  type="email"
                  autoComplete="email"
                  required
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder="Email address"
                  ref={emailRef}
                />
              </div>
              <div>
                <label htmlFor="password" className="sr-only">
                  Password
                </label>
                <input
                  id="password"
                  name="password"
                  type="password"
                  autoComplete="current-password"
                  required
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder="Password"
                  ref={passwordRef}
                />
              </div>
            </div>

            <div className="flex items-center justify-between">
              <div className="flex items-center">
                <input id="remember-me" name="remember-me" type="checkbox" className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded" />
                <label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900">
                  Remember me
                </label>
              </div>

              <div className="text-sm">
                <a href="/reset" className="font-medium text-indigo-600 hover:text-indigo-500">
                  Forgot your password?
                </a>
              </div>
            </div>

            <div>
              <button
                type="submit"
                className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
              >
                <span className="absolute left-0 inset-y-0 flex items-center pl-3">
                  <LockClosedIcon className="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" aria-hidden="true" />
                </span>
                Sign in
              </button>
            </div>
          </form>
          {loading && <Spinner />}
          {error && <Toast type="error" title="Error" message="Your email or password is incorrect, try again" time="now" />}
        </div>
      </div>
    </>
  );
}

El toast

import React from 'react';

export default function Toast({ type, title, message, time }) {
  return (
    <div className="flex flex-col justify-center">
      {/* info toast */}
      {type === 'info' && (
        <div
          className="bg-blue-600 shadow-lg mx-auto w-96 max-w-full text-sm pointer-events-auto bg-clip-padding rounded-lg block mb-3"
          id="static-example"
          role="alert"
          aria-live="assertive"
          aria-atomic="true"
          data-mdb-autohide="false"
        >
          <div className="bg-blue-600 flex justify-between items-center py-2 px-3 bg-clip-padding border-b border-blue-500 rounded-t-lg">
            <p className="font-bold text-white flex items-center">
              <svg
                aria-hidden="true"
                focusable="false"
                data-prefix="fas"
                data-icon="info-circle"
                className="w-4 h-4 mr-2 fill-current"
                role="img"
                
                viewBox="0 0 512 512"
              >
                <path
                  fill="currentColor"
                  d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"
                ></path>
              </svg>
              MDBootstrap
            </p>
            <div className="flex items-center">
              <p className="text-white opacity-90 text-xs">11 mins ago</p>
              <button
                type="button"
                className="btn-close btn-close-white box-content w-4 h-4 ml-2 text-white border-none rounded-none opacity-50 focus:shadow-none focus:outline-none focus:opacity-100 hover:text-white hover:opacity-75 hover:no-underline"
                data-mdb-dismiss="toast"
                aria-label="Close"
              ></button>
            </div>
          </div>
          <div className="p-3 bg-blue-600 rounded-b-lg break-words text-white">Hello, world! This is a toast message.</div>
        </div>
      )}

      {/* success toast */}
      {type === 'success' && (
        <div
          className="bg-green-500 shadow-lg mx-auto w-96 max-w-full text-sm pointer-events-auto bg-clip-padding rounded-lg block mb-3"
          id="static-example"
          role="alert"
          aria-live="assertive"
          aria-atomic="true"
          data-mdb-autohide="false"
        >
          <div className="bg-green-500 flex justify-between items-center py-2 px-3 bg-clip-padding border-b border-green-400 rounded-t-lg">
            <p className="font-bold text-white flex items-center">
              <svg
                aria-hidden="true"
                focusable="false"
                data-prefix="fas"
                data-icon="check-circle"
                className="w-4 h-4 mr-2 fill-current"
                role="img"
                
                viewBox="0 0 512 512"
              >
                <path
                  fill="currentColor"
                  d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"
                ></path>
              </svg>
              MDBootstrap
            </p>
            <div className="flex items-center">
              <p className="text-white opacity-90 text-xs">11 mins ago</p>
              <button
                type="button"
                className="btn-close btn-close-white box-content w-4 h-4 ml-2 text-white border-none rounded-none opacity-50 focus:shadow-none focus:outline-none focus:opacity-100 hover:text-white hover:opacity-75 hover:no-underline"
                data-mdb-dismiss="toast"
                aria-label="Close"
              ></button>
            </div>
          </div>
          <div className="p-3 bg-green-500 rounded-b-lg break-words text-white">Hello, world! This is a toast message.</div>
        </div>
      )}
      {/* danger toast */}

      {type === 'danger' && (
        <div
          className="bg-yellow-500 shadow-lg mx-auto w-96 max-w-full text-sm pointer-events-auto bg-clip-padding rounded-lg block mb-3"
          id="static-example"
          role="alert"
          aria-live="assertive"
          aria-atomic="true"
          data-mdb-autohide="false"
        >
          <div className="bg-yellow-500 flex justify-between items-center py-2 px-3 bg-clip-padding border-b border-yellow-400 rounded-t-lg">
            <p className="font-bold text-white flex items-center">
              <svg
                aria-hidden="true"
                focusable="false"
                data-prefix="fas"
                data-icon="exclamation-triangle"
                className="w-4 h-4 mr-2 fill-current"
                role="img"
               
                viewBox="0 0 576 512"
              >
                <path
                  fill="currentColor"
                  d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
                ></path>
              </svg>
              MDBootstrap
            </p>
            <div className="flex items-center">
              <p className="text-white opacity-90 text-xs">11 mins ago</p>
              <button
                type="button"
                className="btn-close btn-close-white box-content w-4 h-4 ml-2 text-white border-none rounded-none opacity-50 focus:shadow-none focus:outline-none focus:opacity-100 hover:text-white hover:opacity-75 hover:no-underline"
                data-mdb-dismiss="toast"
                aria-label="Close"
              ></button>
            </div>
          </div>
          <div className="p-3 bg-yellow-500 rounded-b-lg break-words text-white">Hello, world! This is a toast message.</div>
        </div>
      )}
      {/* error toast */}
      {type === 'error' && (
        <div
          className="bg-red-600 shadow-lg mx-auto w-96 max-w-full text-sm pointer-events-auto bg-clip-padding rounded-lg block mb-3"
          id="static-example"
          role="alert"
          aria-live="assertive"
          aria-atomic="true"
          data-mdb-autohide="false"
        >
          <div className="bg-red-600 flex justify-between items-center py-2 px-3 bg-clip-padding border-b border-red-500 rounded-t-lg">
            <p className="font-bold text-white flex items-center">
              <svg
                aria-hidden="true"
                focusable="false"
                data-prefix="fas"
                data-icon="times-circle"
                className="w-4 h-4 mr-2 fill-current"
                role="img"
               
                viewBox="0 0 512 512"
              >
                <path
                  fill="currentColor"
                  d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm121.6 313.1c4.7 4.7 4.7 12.3 0 17L338 377.6c-4.7 4.7-12.3 4.7-17 0L256 312l-65.1 65.6c-4.7 4.7-12.3 4.7-17 0L134.4 338c-4.7-4.7-4.7-12.3 0-17l65.6-65-65.6-65.1c-4.7-4.7-4.7-12.3 0-17l39.6-39.6c4.7-4.7 12.3-4.7 17 0l65 65.7 65.1-65.6c4.7-4.7 12.3-4.7 17 0l39.6 39.6c4.7 4.7 4.7 12.3 0 17L312 256l65.6 65.1z"
                ></path>
              </svg>
              {title != null ? title : 'MDBootstrap'}
            </p>
            <div className="flex items-center">
              <p className="text-white opacity-90 text-xs">{time != null ? time : '11 mins ago'}</p>
              <button
                type="button"
                className="btn-close btn-close-white box-content w-4 h-4 ml-2 text-white border-none rounded-none opacity-50 focus:shadow-none focus:outline-none focus:opacity-100 hover:text-white hover:opacity-75 hover:no-underline"
                data-mdb-dismiss="toast"
                aria-label="Close"
              ></button>
            </div>
          </div>
          <div className="p-3 bg-red-600 rounded-b-lg break-words text-white">{message != null ? message : 'Hello, world! This is a toast message.'}</div>
        </div>
      )}
    </div>
  );
}

Y el spinner

const Spinner = () => {
  return (
    <div class="flex justify-center items-center">
      <div class="spinner-border animate-spin inline-block w-8 h-8 border-4 rounded-full" role="status">
        <span class="visually-hidden">.</span>
      </div>
    </div>
  );
};

export default Spinner;

para el reto, lo que hice fue crear un estado local en el componente LoginPage

const [loginError, setLoginError] = useState(false);

modifique la función handleSubmit que se dispara al intentar loguearnos, cuando el usuario y contraseña son correctos el valor de este estado local será seteado a true, caso contrario, será seteado a false

const handleSubmit = (e)=> {
  e.preventDefault();
  const email = emailRef.current.value;
  const password = passwordRef.current.value;
  
  auth.signIn(email,password)
    .then(res => {
      setLoginError(false);
      console.log('login success');
    })
    .catch(err => {
      setLoginError(true);
      console.log('login error');
    })
}

por ultimo, hice una validación con un ternario en el render de este componente LoginPage

export default function LoginPage() {
  const [loginError, setLoginError] = useState(false);

  ....
   ..

  return (
    <>
      ....
      {loginError 
                ? <p className='text-center'>Invalid username or password</p>
                : ''
              }
      ....
    </>
  );
}

el código completo quedaría de esta forma
src/components/LoginPage.jsx:

import { useRef, useState } from 'react';
import { LockClosedIcon } from '@heroicons/react/24/solid';
import { useAuth } from '@hooks/useAuth';

export default function LoginPage() {
  const [loginError, setLoginError] = useState(false);

  const emailRef = useRef(null)
  const passwordRef = useRef(null)
  const auth = useAuth();

  const handleSubmit = (e)=> {
    e.preventDefault();
    const email = emailRef.current.value;
    const password = passwordRef.current.value;
    
    auth.signIn(email,password)
      .then(res => {
        setLoginError(false);
        console.log('login success');
      })
      .catch(err => {
        setLoginError(true);
        console.log('login error');
      })
  }

  return (
    <>
      <div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
        <div className="max-w-md w-full space-y-8">
          <div>
            <img className="mx-auto h-12 w-auto" src="https://tailwindui.com/img/logos/workflow-mark-indigo-600.svg" alt="Workflow" />
            <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Sign in to your account</h2>
          </div>
            <form className="mt-8 space-y-6" onSubmit={handleSubmit} >
              <input type="hidden" name="remember" defaultValue="true" />
              <div className="rounded-md shadow-sm -space-y-px">
                <div>
                  <label htmlFor="email-address" className="sr-only">
                    Email address
                  </label>
                  <input
                    id="email-address"
                    name="email"
                    type="email"
                    autoComplete="email"
                    required
                    className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                    placeholder="Email address"
                    ref={emailRef}
                  />
                </div>
                <div>
                  <label htmlFor="password" className="sr-only">
                    Password
                  </label>
                  <input
                    id="password"
                    name="password"
                    type="password"
                    autoComplete="current-password"
                    required
                    className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                    placeholder="Password"
                    ref={passwordRef}
                  />
                </div>
              </div>

              <div className="flex items-center justify-between">
                <div className="flex items-center">
                  <input id="remember-me" name="remember-me" type="checkbox" className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded" />
                  <label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900">
                    Remember me
                  </label>
                </div>

                <div className="text-sm">
                  <a href="#" className="font-medium text-indigo-600 hover:text-indigo-500">
                    Forgot your password?
                  </a>
                </div>
              </div>

              <div>
                <button
                  type="submit"
                  className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
                >
                  <span className="absolute left-0 inset-y-0 flex items-center pl-3">
                    <LockClosedIcon className="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" aria-hidden="true" />
                  </span>
                  Sign in
                </button>
              </div>
              {loginError 
                ? <p className='text-center'>Invalid username or password</p>
                : ''
              }
            </form>
        </div>
      </div>
    </>
  );
}

Mientras estemos trabajando con el login, me parece util tener quedamos los valores que iran en email y password para evitar estar escribiendolo repetidas veces. 😃

const [value,setValue] = useState({email:'admin@mail.com',password:'admin123'})

Así que cree ese estado, y se los pase a los inputs.

Obviamente es algo que se debe de borrar antes de subir a producción

Para el manejo de errores, agregué un estado:

const [error, setError] = useState(null);

Y una función para modificar el estado:

const loginError = (message) =>{
	setError(message)
}

Tanto el estado como la función los agregué al return:

return {
	user,
        signIn,
        error,
        loginError
};

En la promesa de singIn, agregué el bloque catch:

 auth.signIn(email, password)
    .then(() =>{
      console.log('Login success');
    })
    .catch(() =>{
      auth.loginError('Wrong credentials!')
    });

Y finalmente, antes de la etiqueta de cierre </form>, agregué:

{
	auth.error && <p>{auth.error}</p>
}

Buenas, daré una alternativa en caso de que quieran hacer una alerta sencilla, les recomiendo usar “react-hot-toast”, es muy sencilla y es compatible con tailwind, aqui les dejo su web:

https://react-hot-toast.com/docs

[ RETO ]
instalé la librería Sweet Alert

link: https://sweetalert2.github.io/#download
npm install sweetalert2

Importé la librería

import Swal from 'sweetalert2’
agregué el método catch

Cuando se meten credenciales incorrectas muestra el error

Mi solución al reto de la clase:

import { useRef, useState } from 'react';
import { LockClosedIcon } from '@heroicons/react/24/solid';
import { useAuth } from '@hooks/useAuth';

export default function LoginPage() {
  const emailRef = useRef(null);
  const passwordRef = useRef(null);
  const auth = useAuth(null);
  const [loginStatus, setLoginStatus] = useState(null);
  const submitHandler = (event) => {
    event.preventDefault();
    const email = emailRef.current.value;
    const password = passwordRef.current.value;
    auth
      .signIn(email, password)
      .then((response) => {
        console.log('login successful');
        setLoginStatus(201);
      })
      .catch((err) => {
        if (err.response?.status === 401) {
          setLoginStatus(err.response?.status);
        }
      });
  };
  return (
    <>
      <div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
        <div className="max-w-md w-full space-y-8">
          <div>
            <img className="mx-auto h-12 w-auto" src="https://tailwindui.com/img/logos/workflow-mark-indigo-600.svg" alt="Workflow" />
            <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Sign in to your account</h2>
          </div>
          <form className="mt-8 space-y-6" onSubmit={submitHandler}>
            <input type="hidden" name="remember" defaultValue="true" />
            <div className="rounded-md shadow-sm -space-y-px">
              <div>
                <label htmlFor="email-address" className="sr-only">
                  Email address
                </label>
                <input
                  id="email-address"
                  name="email"
                  type="email"
                  autoComplete="email"
                  required
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder="Email address"
                  ref={emailRef}
                />
              </div>
              <div>
                <label htmlFor="password" className="sr-only">
                  Password
                </label>
                <input
                  id="password"
                  name="password"
                  type="password"
                  autoComplete="current-password"
                  required
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder="Password"
                  ref={passwordRef}
                />
              </div>
            </div>

            {loginStatus === 401 && (
              <div class="p-3 mb-3 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800" role="alert">
                <span class="font-medium">Incorrect email or password</span>
              </div>
            )}

            <div className="flex items-center justify-between">
              <div className="flex items-center">
                <input id="remember-me" name="remember-me" type="checkbox" className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded" />
                <label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900">
                  Remember me
                </label>
              </div>

              <div className="text-sm">
                <a href="/forgot" className="font-medium text-indigo-600 hover:text-indigo-500">
                  Forgot your password?
                </a>
              </div>
            </div>

            <div>
              <button
                type="submit"
                className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
              >
                <span className="absolute left-0 inset-y-0 flex items-center pl-3">
                  <LockClosedIcon className="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" aria-hidden="true" />
                </span>
                Sign in
              </button>
            </div>
          </form>
        </div>
      </div>
    </>
  );
}

Primero pensé en un modal pero me pareció algo intrusivo para un login fail así que lo mantuve simple desplegando texto abajo si falla

añadí unos estados:

const [error, setError] = useState(false)
const [loading, setLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState('')

Adapté la función de envío:

const handleSubmit = (event) => {
    event.preventDefault()
    setError(false)
    setLoading(true)
    const email = emailRef.current.value
    const password = passwordRef.current.value

    auth.signIn(email, password).then(() => {
      setLoading(false)
      console.log('Login success...')
    })
    .catch((err) => {
      setLoading(false)
      setError(true)
      const { response } = err
      setErrorMessage(response.statusText)
    })
  }

Y añadí estados al botón así como el div y el diseño de los input:

<div>
                <label htmlFor="email-address" className="sr-only">
                  Email address
                </label>
                <input
                  id="email-address"
                  name="email"
                  type="email"
                  autoComplete="email"
                  required
                  className={!!!error
                    ?
                      "appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                    :
                      "appearance-none rounded-none relative block w-full px-3 py-2 border border-red-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-red-500 focus:border-red-500 focus:z-10 sm:text-sm"
                  }
                  placeholder="Email address"
                  ref={emailRef}
                  />
              </div>
              <div>
                <label htmlFor="password" className="sr-only">
                  Password
                </label>
                <input
                  id="password"
                  name="password"
                  type="password"
                  autoComplete="current-password"
                  required
                  className={!!!error
                    ?
                      "appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                    :
                      "appearance-none rounded-none relative block w-full px-3 py-2 border border-red-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-red-500 focus:border-red-500 focus:z-10 sm:text-sm"
                  }
                  placeholder="Password"
                  ref={passwordRef}
                />
              </div>
<button
                type="submit"
                className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
                >
                <span className="absolute left-0 inset-y-0 flex items-center pl-3">
                  {!!loading 
                    ? 
                      <ArrowPathIcon
                        className='h-5 w-5 text-indigo-400 animate-spin ease-in-out'
                        aria-hidden="true" 
                      />
                    : !! error ?
                      <ExclamationTriangleIcon
                        className="h-5 w-5 text-indigo-400"
                        aria-hidden="true"
                      />
                    :
                      <LockClosedIcon
                        className="h-5 w-5 text-indigo-500 group-hover:text-indigo-400"
                        aria-hidden="true"
                      />
                  }
                </span>
                Sign in
              </button>
const [loginError, setLoginError] = useState(false);

.signIn(email, password)
      .then(() => {
        console.log('login success');
      })
      .catch((error) => {
        console.log(error.message);
        setLoginError(true);
        setTimeout(() => {
          setLoginError(false);
        }, 2000);
      });
{loginError && (
              <div className="font-medium text-red-600 hover:text-red-500">
                <span>loginError</span>
              </div>
            )}

Solucion Login Request failed

        await axios.post(endPoints.auth.login, { email, password }, options)
            .then(value => Cookies.set('token', value.data.access_token, {expires: 5}))
            .catch(error => {
                console.error(error)
                // show a errorModal
            });

Mi solución para mostrar un mensaje de error o exito al usuario

useAuth.js

const signIn = async (email, password) => { 
    const options = {
      headers: {
        accept: '*/*',
        'Content-Type': 'application/json',
      },
    };
    const { data } = await axios.post(endPoints.auth.login, { email, password }, options);
    
    if (data.access_token) {
      Cookie.set('token', data.access_token, {expires: 5});
    }
    return data;
  }

LoginPage.js

const handleSubmit = (e) => {
  e.preventDefault();
  const email = emailRef.current.value;
  const password = passwordRef.current.value;
  
  auth.signIn(email, password).then((response) => {
    if (response.access_token) {
      setLoginSuccess(200);
    }
  }).catch((error) => {
    setLoginSuccess(error.response.status);
  });
};

{loginSuccess === 401 && (
  <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
    <strong className="block sm:inline">Email or password is incorrect.</strong>
  </div>
)}

{loginSuccess === 200 && (
  <div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative" role="alert">
    <strong className="block sm:inline">Login success.</strong>
  </div>
)}	

Lo hice con SweetAlert2 y algo del router de next.js

Acá los imports

import { useRouter } from 'next/router';
import { useSate, useRef } from 'react';
import Swal from 'sweetalert2';

import { useAuth } from '@hooks/useAuth';

import { LockClosedIcon } from '@heroicons/react/solid';

Y acá lo anterior al return

  const passwordRef = useRef(null);
  const emailRef = useRef(null);
  const router = useRouter();
  const auth = useAuth();

  const submitHandler = (event) => {
    event.preventDefault();
    const password = passwordRef.current.value;
    const email = emailRef.current.value;

    auth
      .singIn(email, password)
      .then(() => {
        router.push('/dashboard');
        Swal.fire({
          title: 'Welcome',
          icon: 'success',
          confirmButtonText: 'Okay',
        });
      })
      .catch((error) => {
        if (error.response?.status === 401) {
          Swal.mixin({
            toast: true,
            title: 'Error!',
            text: `This user doesn't exist, it migth be an error on your user or password`,
            icon: 'error',
            confirmButtonText: 'Okay',
          });
        } else if (error.resquest) {
          Swal.mixin({
            toast: true,
            title: 'Ups...',
            text: 'Something went wrong, we are experimenting some toubles, try it later',
            icon: 'error',
            confirmButtonText: 'Okay',
          });
        }
      });
  };

Por si a alguien le interesa, para instarlar SweetAlert, usas

npm install sweetalert2

Utilizé el componente modal que nos ofrece tailwind UI y lo adapté un poco

Componente LoginError

Añadí un catch en LoginPage.jsx

si quieren ver su cookie actual en cualquier navegador ctrl+shit+c, ejecutar un
alert(document.cookie)