Hamburger Icon
Crea una app de la lista de la compra con Next.js y Tailwind CSS

Crea una app de la lista de la compra con Next.js y Tailwind CSS

En este tutorial vamos a ver cómo desarrollar una aplicación para generar nuestra lista de la compra usando Next.js y Tailwind CSS. NextJS es un framework basado en React que mejora la experiencia de desarrollo añadiendo muchas características. Tailwind CSS es un framework que nos ofrece un sistema de diseño que podemos adaptar y ampliar a nuestras necesidades. Si no has usado nunca Tailwind o un framework similar, es posible que no entiendas las clases CSS que vamos a usar, ya que se comentarán por encima.

Si nunca has usado React, te aconsejo que primero mires este tutorial básico, y hagas tu mismo alguna aplicación sencilla para habituarte. La documentación te ayudará a hacerte una idea de todo lo que puedes conseguir con React.

Qué vamos a hacer

Vamos a desarrollar esta aplicación. Tienes el código en este repositorio de GitHub.

Esta es una aplicación sencilla para crear una lista de la compra. No vamos a aprovechar ni de lejos todo lo que nos ofrece NextJS. Es más, esta aplicación se podría desarrollar sin necesidad de este framework sin problema. El único propósito aquí es que sirva de introducción.

Instalación de NextJS

La forma más sencilla de instalar NextJS es a través de NPX:

npx create-next-app nombre-proyecto

Luego puedes lanzar el servidor de desarrollo con este comando:

npm run dev

Instalación y configuración de Tailwind CSS

Para instalar Tailwind CSS tendremos que ejecutar estos dos comandos:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Después de ejecutar el segundo comando se nos habrá generado en la raíz del proyecto el archivo de configuración tailwind.config.js al que tendremos añadir este contenido:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Ahora que ya tenemos todo listo para usar Tailwind CSS, vamos a modificar el archivo styles/globals.css y eliminaremos todo su contenido para dejar simplemente esto:

@tailwind base;
@tailwind components;
@tailwind utilities;

Ahora ya podemos ponernos manos a la obra.

Preparando estructura

En el directorio pages vamos a añadir todas las páginas que contendrá la aplicación. Por defecto hay un index.js que nos servirá como punto de entrada a nuestra app.

Para organizar los componentes que vamos a desarrollar crearemos el directorio components en la raíz del proyecto. Esta carpeta puede tener el nombre que tu quieras, y dentro crear las subcarpetas que creas conveniente. En este tutorial vamos a poner aquí todos los componentes, pero en una aplicación más grande te interesa poner un poco de orden.

También crearemos el directorio apis en la raíz del proyecto. Aquí añadiremos más adelante un archivo para interactuar con el almacenamiento local del navegador para guardar datos.

Creando los componentes básicos

Para esta aplicación necesitaremos usar algunos encabezados HTML, un par de iconos y algunos botones. Para ello, vamos a crear componentes para poder reaprovecharlos en distintos lugares.

Encabezados

Vamos a crear dos encabezados, para representar los h1 y los h2. Empecemos con el primero. Vamos a crear en el directorio components el archivo heading1.js. El componente es tan sencillo como esto:

const Heading1 = ({ children }) => {
  return <h1 className="text-3xl mb-4 text-blue-500">{children}</h1>
}

export default Heading1

El componente devuelve los nodos hijo que se le pasen envueltos en una etiqueta h1. Las clases CSS usadas nos las proporciona Tailwind y nos sirven para modificar el tamaño del texto, establecer un margen inferior e indicar el color que va a tener el encabezado.

El segundo encabezado a a ser casi idéntico, lo crearemos en el archivo heading2.js:

const Heading2 = ({ children }) => {
  return <h2 className="text-2xl mb-3 text-blue-500">{children}</h2>
}

export default Heading2

La única diferencia es que hemos usado una etiqueta h2 en este caso.

Si quieres practicar un poco, te propongo un ejercicio. Crea un único componente de cabecera que reciba como parámetro, a parte de los nodos hijos, un valor entero del 1 al 6 para devolver el encabezado correspondiente según ese valor.

Iconos

En esta aplicación usaremos dos iconos: el check para marcar un ítem como hecho y una basura para eliminarlo de la lista. Queremos un componente al que le podamos indicar qué icono tiene que mostrar además de pasarle la función que debe ejecutarse cuando se le haga clic. Podemos hacer algo así:

const Icon = ({ icon, onClick }) => {
  const icons = {
    check: "M864 128l-480 480-224-224-160 160 384 384 640-640z",
    trash:
      "M192 1024h640l64-704h-768zM640 128v-128h-256v128h-320v192l64-64h768l64 64v-192h-320zM576 128h-128v-64h128v64z",
  }
  return (
    <svg
      className="fill-blue-300 hover:fill-blue-500 cursor-pointer"
      onClick={onClick}
      width="24"
      height="24"
      viewBox="0 0 1024 1024"
    >
      <path d={icons[icon]}></path>
    </svg>
  )
}

export default Icon

En el componente declaramos un objeto literal con las llaves check y trash. En su valor guardaremos el path que usaremos en el SVG que formará el icono. En la propia etiqueta svg añadiremos las clases CSS para indicar el color por defecto y al pasar el cursor por encima, además de otra clase para que cambie el cursor con el efecto hover.

También llamaremos la función que llega como parámetro en el evento onClick y estableceremos las dimensiones del icono.

Botones

Queremos usar dos tipos de botón. La diferencia entre los tipos será sencillamente a nivel de estilos. Queremos un botón de tipo principal y otro secundario. También queremos que al hacer clic en el botón se ejecute alguna función:

const Button = ({ handleClick, children, btnType = false }) => {
  const btnStyles =
    btnType === "secondary"
      ? "text-blue-500 bg-gray-100 hover:bg-gray-200"
      : "text-gray-100 bg-blue-500 hover:bg-gray-100 hover:text-blue-500"

  return (
    <button
      onClick={handleClick}
      className={`mb-2 py-1 px-2 block text-sm border border-blue-500 rounded ${btnStyles} hover:shadow-sm`}
    >
      {children}
    </button>
  )
}

export default Button

El botón recibirá como parámetros la función que debe ejecutarse al hacerle clic, el tipo de botón que debe mostrar y los nodos hijos.

Lo primero de todo es definir los estilos que usaremos según el tipo de botón. Por defecto será un botón más llamativo, mientras que si indicamos el tipo secundario, será un botón más discreto. Según el tipo, guardamos en la variable btnStyles las clases para dar un aspecto u otro.

Este componente es sencillo, simplemente devolverá el botón indicando la función que le pasemos en el evento onClick.

Creando los componentes con más funcionalidad

Formulario para añadir elementos a la lista

Para añadir elementos a la lista de la compra necesitaremos un formulario con una caja de texto para añadir elementos. El formulario se compondrá de:

  • Una caja de texto
  • Un botón para añadir un nuevo elemento
  • Un botón para borrar la lista completa

Ante todo definiremos un componente que representará nuestra caja de texto:

const InputText = ({ label, name, placeholder, onChange, value }) => {
  return (
    <>
      {label && (
        <label
          htmlFor={name}
          className="block mb-2 text-sm font-medium text-blue-500"
        >
          {label}
        </label>
      )}
      <input
        id={name}
        name={name}
        type="text"
        value={value}
        placeholder={placeholder}
        onChange={onChange}
        className="mb-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded outline-blue-500 block w-full p-2.5 placeholder-gray-400"
      />
    </>
  )
}

export default InputText

Como parámetros de entrada recibirá una equiqueta para el campo, un nombre para el input, un valor de placeholder para la caja de texto, el valor que hay dentro de la caja de texto y la función que debe ejecutarse cuando dicho valor cambie.

Solo queremos mostrar la etiqueta si se ha indicado un valor para la misma. En JSX, una forma habitual de conseguir el renderizado condicional es usando esta técnica:

{
  label && (
    <label
      htmlFor={name}
      className="block mb-2 text-sm font-medium text-blue-500"
    >
      {label}
    </label>
  )
}

De modo que, al haber una operación lógica AND, solo mostrará la etiqueta label si contiene algún valor. Por lo demás, no hay mucho misterio. En el input indicamos los atributos que nos vienen de entrada al componente y añadimos algunas clases CSS.

Una vez definido la caja de texto, ya podemos crear el componente que hará de formulario:

import InputText from "./inputText"
import Button from "./button"

const AddItemForm = ({
  handleSubmit,
  handleClearList,
  inputValue,
  setInputValue,
}) => {
  const handleInputChange = (e) => {
    setInputValue(e.target.value)
  }

  return (
    <form>
      <InputText
        label="Añadir elemento"
        name="grocery-item-name"
        placeholder="Escribre el nombre del elemento..."
        onChange={handleInputChange}
        value={inputValue}
      />
      <span className="flex flex-col">
        <Button handleClick={handleSubmit}>Añadir</Button>
        <Button handleClick={handleClearList} btnType="secondary">
          Limpiar Lista
        </Button>
      </span>
    </form>
  )
}

export default AddItemForm

Lo primero diferente aquí es que en este componente vamos a usar por primera vez otros componentes ya definidos. De modo que antes de nada debemos importarlos. Luego, para este componente pasaremos como parámetros de entrada la función que debe ejecutarse al enviar el formulario, la función que permite limpiar la lista de elementos, el valor de la caja de texto y la función que permite modificar ese valor.

En este componente definiremos una función establecer qué sucede cuando se modifica el valor de la caja de texto:

const handleInputChange = (e) => {
  setInputValue(e.target.value)
}

Esta función recibe como parámetro el evento que se dispara al modificar la caja de texto, que usaremos para recoger el valor actual introducido en el input por el usuario y se lo pasaremos, a su misma vez, a la función que hemos recibido en el componente para actualizar el valor del input.

Luego, el formulario en sí está construido de esta forma:

return (
  <form>
    <InputText
      label="Añadir elemento"
      name="grocery-item-name"
      placeholder="Escribre el nombre del elemento..."
      onChange={handleInputChange}
      value={inputValue}
    />
    <span className="flex flex-col">
      <Button handleClick={handleSubmit}>Añadir</Button>
      <Button handleClick={handleClearList} btnType="secondary">
        Limpiar Lista
      </Button>
    </span>
  </form>
)

Dentro de la etiqueta form añadiremos el input, llamando al componente InputText que hemos creado antes y que ya hemos importado. Le pasaremos los parámetros que necesita y que hemos comentado antes. En el parámetro onChange pasaremos esta función que acabamos de definir.

Justo debajo de la caja de texto, añadiremos los dos botones comentados antes: el que sirve para añadir un elemento a la lista y el que la elimina. Al primer botón le pasaremos en el parámetro handleClick la función que define qué sucede cuando enviamos el formulario, que crearemos más adelante.

Al segundo botón le pasaremos la función que limpia la lista y que también definiremos más adelante. Además, a este segundo botón será de tipo secundario para que destaque menos.

En este componente hemos hecho muchas cosas, aunque parezca que no. Lo importante con lo que debes quedarte es que estamos pasando la lógica de la aplicación top to down, de arriba a abajo. Habrá otro componente que determinará el funcionamiento de estas funciones que nos hemos limitado a pasar entre componentes.

La lista de elementos

La lista de elementos no tiene ninguna complejidad. Ahora bien, cada uno de los elementos de la lista tiene más funcionalidad, de modo que empezaremos por el componente que renderizará un elemento de la lista de la compra.

De cada elemento, queremos saber si está completado o no y si el cursor del cursor está encima o no. Esto último lo queremos porque mientras el cursor esté encima del elemento mostraremos dos iconos: uno para marcar el elemento como hecho y otro para eliminarlo.

Como parámetros de entrada del componente tendremos el elemento de la lista, una función para eliminar el elemento y otra función para modificar su estado.

import { useState } from "react"
import Icon from "./icon"

const GroceryListItem = ({ item, handleDeleteItem, handleToggleItem }) => {
  /* 
    item = {
      name: "some name", 
      done: true/false,
    }
  */
  const [itemDone, setItemDone] = useState(item.done)
  const [itemHover, setItemHover] = useState(false)

  let doneStyle = itemDone
    ? "line-through border-blue-100 bg-gray-50"
    : "border-blue-500"
  let hoverStyle = itemHover ? "bg-blue-100" : "bg-blue-50"

  const toggleItemHover = () => {
    const newItemHover = !itemHover
    setItemHover(newItemHover)
  }

  const handleCheck = () => {
    const newItemDone = !itemDone
    setItemDone(newItemDone)
    handleToggleItem(item)
  }

  const deleteItem = () => {
    handleDeleteItem(item)
  }

  return (
    <li
      onMouseEnter={toggleItemHover}
      onMouseLeave={toggleItemHover}
      onTouchStart={handleCheck}
      className={`flex justify-between p-2 mb-2 text-blue-500 rounded border ${hoverStyle} ${doneStyle}`}
    >
      <span>{item.name}</span>
      {itemHover && (
        <span className="flex space-x-4">
          <Icon icon="check" onClick={handleCheck} />{" "}
          <Icon icon="trash" onClick={deleteItem} />
        </span>
      )}{" "}
    </li>
  )
}

export default GroceryListItem

En este componente usaremos el componente de icono que hemos definido al principio, así que lo importamos. También importamos el hook useState, ya que necesitaremos guardar el estado de si un elemento está completado o no y si si debe mostrar las opciones para completarlo o eliminarlo.

Esto lo hacemos así:

const [itemDone, setItemDone] = useState(item.done)
const [itemHover, setItemHover] = useState(false)

let doneStyle = itemDone
  ? "line-through border-blue-100 bg-gray-50"
  : "border-blue-500"
let hoverStyle = itemHover ? "bg-blue-100" : "bg-blue-50"

const toggleItemHover = () => {
  const newItemHover = !itemHover
  setItemHover(newItemHover)
}

const handleCheck = () => {
  const newItemDone = !itemDone
  setItemDone(newItemDone)
  handleToggleItem(item)
}

Con useState declaramos los dos estados que necesitamos itemDone e itemHover. Definimos un par de variables que establecerán los estilos para los elementos cuando estén completado y para cuando el cursor enté encima.

También definimos dos funciones que actualizarán estos estados. La función toggleItemHover modificará el estado que usamos para saber si el cursor está encima del elemento o no.

La función handleCheck modificará el estado del elemento y ejecutará la función que le viene dada como parámetro para actualizar el elemento en el origen de datos (usaremos el almacenamiento local del navegador, pero eso lo verás luego).

También hemos definido la función deleteItem para poder llamar la función de entrada en el componente handleDeleteItem pasando como parámetro el elemento que debemos eliminar, y que definiremos en breve.

El renderizado del componente es así:

return (
  <li
    onMouseEnter={toggleItemHover}
    onMouseLeave={toggleItemHover}
    onTouchStart={handleCheck}
    className={`flex justify-between p-2 mb-2 text-blue-500 rounded border ${hoverStyle} ${doneStyle}`}
  >
    <span>{item.name}</span>
    {itemHover && (
      <span className="flex space-x-4">
        <Icon icon="check" onClick={handleCheck} />{" "}
        <Icon icon="trash" onClick={deleteItem} />
      </span>
    )}{" "}
  </li>
)

Un elemento li con los eventos onMouseEnter y onMouseLeave que ejecutan la función toggleItemHover que hemos definido antes y que nos permiten modificar el estado cuando el cursor pasa por encima y cuando sale. También definimos el evento onTouchStart para que desde un móvil podamos marcar el elemento como hecho al tocarlo.

Mostraremos el nombre del elemento y, solo cuando pasemos el cursor por encima, mostraremos también los dos iconos que hemos creado antes en el componente de icono. Uno será el check para ejecutar la función handleCheck y marcar el elemento como completado y otro será la basura para ejecutar la función deleteItem para eliminar el elemento.

Con esto ya tenemos el componente que representa un elemento de la lista de la compra, ya solo nos falta el componente de la lista completa, que recibe como parámetros un array con todos los elementos y las funciones para eliminar un elemento y para modificar su estado:

import GroceryListItem from "./groceryListItem"

const GroceryList = ({ groceries, handleDeleteItem, handleToggleItem }) => {
  return (
    <ul className="grid grid-cols-2 gap-2 lg:grid-cols-3">
      {groceries.map((item) => (
        <GroceryListItem
          key={item.name}
          item={item}
          handleDeleteItem={handleDeleteItem}
          handleToggleItem={handleToggleItem}
        />
      ))}
    </ul>
  )
}

export default GroceryList

Como puedes ver es sencillo. Necesitamos importar el componente que hemos creado en el paso anterior y mostrar la lista de elementos.

Esto lo hacemos a través de la función map disponible en los arrays. Por cada elemento de la lista llamamos al componente GroceryListItem pasándole como parámetros el propio elemento y las funciones para eliminar y cambiar el estado del mismo.

También es necesario indicar a través del atributo key un identificador para el elemento, esto lo necesita React para tener un control sobre lo que ha renderizado en el DOM.

Y un componente para dominarlos a todos 💍

Finalmente, queremos un componente que nos muestre todo lo anterior de forma organizada. Un componente que haga de gestor, donde se gestione el estado de el valor que hay en la caja de texto y la lista de elementos.

Querremos mostrar a la izquierda el formulario para mostrar elementos y a la derecha la lista. Además, queremos tener un espacio para informar al usuario si hace algo raro, como intentar añadir repetidos en la lista o para indicar qué elemento se ha añadido o eliminado recientemente. Esto lo conseguimos creando este componente para mostrar un mensaje de alerta:

const ResultMessage = ({ messageType, children }) => {
  const messageStyle =
    messageType === "ok"
      ? "text-blue-500"
      : messageType === "error"
      ? "text-red-500"
      : "text-gray-900"

  return <div className={`${messageStyle}`}>{children}</div>
}

export default ResultMessage

Este componente recibe como parámetros el tipo, que será siempre "ok" o "error" y los nodos hijos. Usamos el tipo para establecer los estilos de la caja div que mostrará el mensaje.

Antes de ponernos con el componente gestor, necesitaremos un mecanismo para guardar la lista de elementos en algún lugar. Si cerramos el navegador queremos que al volverlo a abrir se acuerde. Para ello usaremos el almacenamiento local del propio navegador. En la carpeta apis que hemos creado al principio en la raíz del proyecto, crearemos el archivo localStorage y crearemos un par de funciones para gestionar el almacenamiento:

const getLocalStorageData = () => {
  if (typeof window !== "undefined") {
    const groceryList = localStorage.getItem("groceryList")
    return groceryList ? JSON.parse(groceryList) : []
  }
  return []
}

const setLocalStorageData = (newGroceryList) => {
  if (typeof window !== "undefined" && Array.isArray(newGroceryList)) {
    localStorage.setItem("groceryList", JSON.stringify(newGroceryList))
  }
}

export { getLocalStorageData, setLocalStorageData }

La función getLocalStorageData mira si existe en el almacenamiento local una lista ya existente. De ser así, la devolvemos. La función setLocalStorageData guarda en el almacenamiento local la lista de la compra que le pasemos. Los datos se guardaran como texto pero en formato JSON.

Ahora que tenemos controlado esto, ya podemos ponernos con el componente gestor. Los que vamos a importar en el componente es esto:

import { useState, useEffect } from "react"
import GroceryList from "./groceryList"
import Heading2 from "./heading2"
import AddItemForm from "./addItemForm"
import ResultMessage from "./resultMessage"
import { getLocalStorageData, setLocalStorageData } from "../apis/localStorage"

A parte del hook useState importamos también el hook useEffect, ahora veremos por qué. También importamos los componentes que vamos a necesitar y las funciones para gestionar el almacenamiento.

El estado del componente se controla con estas variables:

const [inputValue, setInputValue] = useState("")
const [groceryList, setGroceryList] = useState([])
const [resultMessage, setResultMessage] = useState("")
const [messageType, setMessageType] = useState("ok")

useEffect(() => {
  setGroceryList(getLocalStorageData())
}, [])

El valor de la caja de texto, la lista de elementos, el mensaje que se muestra en cada momento en la caja de alertas y el tipo de mensaje. Hemos usado también el hook useEffect con el que indicamos a React que, después del renderizado, debe establecer el valor inicial de la lista de la compra a partir de la información que pueda haber guardada en el almacenamiento local.

Esto queremos que se ejecute solo depués del primer renderizado, por eso pasamos un array vacío como segundo parámetro. En este array podríamos indicar las propiedades del componente que disparan este efecto.

Ahora toca definir todas esas funciones que hasta ahora hemos ido usando alegremente pero que todavía no habíamos definido. Empezamos con la función que debe ejecutarse al procesar el formulario:

const handleSubmit = (e) => {
  e.preventDefault()

  let msg = ""
  let newGroceryList = [...groceryList]
  const itemExist = newGroceryList.find((item) => item.name === inputValue)
  if (itemExist) {
    setMessageType("error")
    msg = `¡Ya hay ${inputValue} en la lista!`
  } else {
    setMessageType("ok")
    msg = `Añadido ${inputValue} en la lista`

    newGroceryList.push({ name: inputValue, done: false })
    setGroceryList(newGroceryList)
    setLocalStorageData(newGroceryList)
  }
  setResultMessage(msg)
  setInputValue("")
}

Esta función recibe como parámetro el evento que se dispara con el envío del formulario, que usaremos para ejecutar el preventDefault para que no haga absolutamente nada y tengamos el control sobre lo que sucederá.

Lo que tenemos que hacer es añadir un nuevo elemento a la lista de la compra, pero solo si no existe ya. Por eso primero miramos si existe y en caso de que no, añadimos el nuevo elemento y actualizamos tanto el estado como el almacenamiento local con la nueva lista. También mostraremos al usuario un mensaje informativo sobre la operación.

Otras de las funciones que necesitamos crear son handleClearList para limpiar la lista y handleDeleteItem para eliminar un elemento:

const handleClearList = (e) => {
  e.preventDefault()
  setGroceryList([])
  setLocalStorageData([])
}

const handleDeleteItem = (item) => {
  const newGroceryList = groceryList.filter(
    (listItem) => listItem.name != item.name
  )
  setGroceryList(newGroceryList)
  setLocalStorageData(newGroceryList)

  setMessageType("ok")
  const msg = `Eliminado ${item.name} de la lista`
  setResultMessage(msg)
}

En la primera función nos limitamos a establecer una lista vacía tanto en el estado del componente como en el almacenamiento local.En la segunda función, buscamos el elemento que queremos eliminar por su nombre y creamos una nueva lista sin él.

Finalmente, la última función que nos falta definir es handleToggleItem, que permite cambiar el estado de un elemento de la lista:

const handleToggleItem = (item) => {
  const newItem = groceryList.find((listItem) => listItem.name === item.name)
  newItem.done = !newItem.done
  const newGroceryList = [...groceryList]
  setGroceryList(newGroceryList)
  setLocalStorageData(newGroceryList)

  setMessageType("ok")
  const action = newItem.done ? "Tachado" : "Destachado"
  const msg = `${action} ${newItem.name} de la lista`
  setResultMessage(msg)
}

Para ello buscamos el elemento por nombre, luego modificamos su estado y actualizamos la lista de elementos igual que antes.

El renderizado es este:

return (
  <>
    <div className="flex flex-col space-y-8 md:grid md:items-start md:gap-8 md:space-y-0 md:grid-cols-2 lg:grid-cols-3">
      <section className="p-4 border-2 border-blue-500 rounded shadow-md md:col-span-1 lg:col-span-1">
        <Heading2>Añade elementos a la lista</Heading2>
        <AddItemForm
          handleSubmit={handleSubmit}
          handleClearList={handleClearList}
          inputValue={inputValue}
          setInputValue={setInputValue}
        />
        <ResultMessage messageType={messageType}>{resultMessage}</ResultMessage>
      </section>
      {groceryList.length > 0 && (
        <section className="p-4 border-2 border-blue-500 rounded shadow-md md:col-span-1 lg:col-span-2">
          <Heading2>Tu lista actual</Heading2>
          <GroceryList
            groceries={groceryList}
            handleDeleteItem={handleDeleteItem}
            handleToggleItem={handleToggleItem}
          />
        </section>
      )}
    </div>
  </>
)

Mostramos a la izquierda el formulario y los mensajes de alerta y a la derecha la lista de la compra, que solo se muestra si hay algún elemento en ella. Fíjate como en el componente AddItemForm pasamos las funciones que hemos visto antes al definir el componente, y hacemos algo análogo con el componente de la lista de la compra GroceryList.

Este es el resultado final del componente gestor:

import { useState, useEffect } from "react"
import GroceryList from "./groceryList"
import Heading2 from "./heading2"
import AddItemForm from "./addItemForm"
import ResultMessage from "./resultMessage"
import { getLocalStorageData, setLocalStorageData } from "../apis/localStorage"

const GroceriesManager = () => {
  const [inputValue, setInputValue] = useState("")
  const [groceryList, setGroceryList] = useState([])
  const [resultMessage, setResultMessage] = useState("")
  const [messageType, setMessageType] = useState("ok")

  useEffect(() => {
    setGroceryList(getLocalStorageData())
  }, [])

  const handleSubmit = (e) => {
    e.preventDefault()

    let msg = ""
    let newGroceryList = [...groceryList]
    const itemExist = newGroceryList.find((item) => item.name === inputValue)
    if (itemExist) {
      setMessageType("error")
      msg = `¡Ya hay ${inputValue} en la lista!`
    } else {
      setMessageType("ok")
      msg = `Añadido ${inputValue} en la lista`

      newGroceryList.push({ name: inputValue, done: false })
      setGroceryList(newGroceryList)
      setLocalStorageData(newGroceryList)
    }
    setResultMessage(msg)
    setInputValue("")
  }

  const handleClearList = (e) => {
    e.preventDefault()
    setGroceryList([])
    setLocalStorageData([])
  }

  const handleDeleteItem = (item) => {
    const newGroceryList = groceryList.filter(
      (listItem) => listItem.name != item.name
    )
    setGroceryList(newGroceryList)
    setLocalStorageData(newGroceryList)

    setMessageType("ok")
    const msg = `Eliminado ${item.name} de la lista`
    setResultMessage(msg)
  }

  const handleToggleItem = (item) => {
    const newItem = groceryList.find((listItem) => listItem.name === item.name)
    newItem.done = !newItem.done
    const newGroceryList = [...groceryList]
    setGroceryList(newGroceryList)
    setLocalStorageData(newGroceryList)

    setMessageType("ok")
    const action = newItem.done ? "Tachado" : "Destachado"
    const msg = `${action} ${newItem.name} de la lista`
    setResultMessage(msg)
  }

  return (
    <>
      <div className="flex flex-col space-y-8 md:grid md:items-start md:gap-8 md:space-y-0 md:grid-cols-2 lg:grid-cols-3">
        <section className="p-4 border-2 border-blue-500 rounded shadow-md md:col-span-1 lg:col-span-1">
          <Heading2>Añade elementos a la lista</Heading2>
          <AddItemForm
            handleSubmit={handleSubmit}
            handleClearList={handleClearList}
            inputValue={inputValue}
            setInputValue={setInputValue}
          />
          <ResultMessage messageType={messageType}>
            {resultMessage}
          </ResultMessage>
        </section>
        {groceryList.length > 0 && (
          <section className="p-4 border-2 border-blue-500 rounded shadow-md md:col-span-1 lg:col-span-2">
            <Heading2>Tu lista actual</Heading2>
            <GroceryList
              groceries={groceryList}
              handleDeleteItem={handleDeleteItem}
              handleToggleItem={handleToggleItem}
            />
          </section>
        )}
      </div>
    </>
  )
}

export default GroceriesManager

Último paso, modificar el index.js

Finalmente falta modificar el archivo index.js que hay en la carpeta pages y añadir lo siguiente:

import Head from "next/head"
import Heading1 from "../components/heading1"
import GroceriesManager from "../components/groceriesManager"

export default function Home() {
  return (
    <div className="container mx-auto p-4">
      <Head>
        <title>Grocery list app</title>
        <meta name="description" content="Grocery list app with NextJS" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className="">
        <Heading1>Tu lista de la compra</Heading1>
        <GroceriesManager />
      </main>
    </div>
  )
}

Fíjate que importamos un componente llamado Head. Este nos lo da hecho NextJS y nos permite añadir metadatos a la página. El resto del componente deberías entenderlo en este punto.

Conclusión

Si has seguido el tutorial desde el principio, felicidades por llegar hasta aquí. Seguramente en muchos momentos ha habido cosas que no han cobrado sentido hasta llegar al final, donde definimos la funcionalidad y la mayor parte de la lógica de la aplicación. Esta es solo una forma de desarrollar aplicaciones: empezar por lo más básico e ir subiendo hacia arriba hasta lo más complejo.