gotangible / components / NftGridItem.jsx
NftGridItem.jsx
Raw
import React, { useState, useEffect, useRef, Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { shirt, artPrint } from "../data/icons";
import { useShoppingCart } from "use-shopping-cart";
import { toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";

const ToastCheckIcon = () => {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      className="h-12 w-12"
      fill="none"
      viewBox="0 0 24 24"
      stroke="#5cd4ac"
      strokeWidth={2}
    >
      <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
    </svg>
  );
};

const NftGridItem = (props) => {
  const { asset } = props;
  const { addItem } = useShoppingCart();

  const [modalOpen, setModalOpen] = useState(false);
  const cancelButtonRef = useRef(null);
  const [cartButtonClicked, setCartButtonClicked] = useState(false);
  const handleCartButtonClicked = () => {
    setCartButtonClicked(true);
    setTimeout(() => {
      setCartButtonClicked(false);
    }, 3000);
  };

  const [availableProducts, setAvailableProducts] = useState([
    {
      code: "shirt",
      price_id: "price_1KyHUJAkSLqoMR6qTyXE8z8E",
      sku: "pc-shirt-0001",
      name: "T-Shirt",
      icon: shirt,
      available_sizes: ["XS", "S", "M", "L", "XL", "2XL"],
      available_colors: ["Black", "White", "Grey", "Blue", "Red", "Green"],
      price: 3000,
      cart_data: {
        name: "Shirt Test",
        description: "A test shirt",
        id: "price_1KyHUJAkSLqoMR6qTyXE8z8E",
        currency: "USD",
        image: asset.image_url,
        product_metadata: {
          size: "XS",
          color: "Black",
        },
      },
    },
    {
      code: "print",
      price_id: "price_1KyHUlAkSLqoMR6qLnzQqdP8",
      sku: "pc-print-0001",
      name: "Fine Art Print",
      icon: artPrint,
      available_sizes: ["O/S"],
      available_colors: ["O/S"],
      price: 10000,
      cart_data: {
        name: "Shirt Test",
        description: "A test shirt",
        id: "price_1KyHUJAkSLqoMR6qTyXE8z8E",
        currency: "USD",
        image: asset.image_url,
        product_metadata: {
          size: "O/S",
          color: "O/S",
        },
      },
    },
  ]);
  const [availableColors, setAvailableColors] = useState(
    availableProducts[0].available_colors
  );
  const [availableSizes, setAvailableSizes] = useState(
    availableProducts[0].available_sizes
  );

  const [selectedProductIndex, setSelectedProductIndex] = useState(0);
  const [selectedColorIndex, setSelectedColorIndex] = useState(0);
  const [selectedSizeIndex, setSelectedSizeIndex] = useState(0);

  const [selectedProduct, setSelectedProduct] = useState(availableProducts[0]);
  const [selectedColor, setSelectedColor] = useState(availableColors[0]);
  const [selectedSize, setSelectedSize] = useState(availableSizes[0]);

  const sanitize = (value) => {
    if (!value) return "";
    return value.replace(/[\W_]+/g, "");
  };

  const formatSku = (collection, name, code, size, color) => {
    let sku = `pc-${sanitize(collection)}-${sanitize(name)}-${sanitize(code)}-${sanitize(color)}-${sanitize(size)}`;
    return sku.toLowerCase();
  };

  // The cart data contains information about the currently selected item
  // This is the data that will be added to the cart object upon clicking the Add to Cart button
  const [cartData, setCartData] = useState({
    name: `${asset.name} ${selectedProduct.name}, ${selectedColor}, Size ${selectedSize}`,
    description: `A ${selectedColor} ${selectedProduct.name} in Size ${selectedSize} featuring the artwork of ${asset.name} from the ${asset.collection.name} collection.`,
    sku: formatSku(
      asset.collection.name,
      asset.name,
      availableProducts[0].code,
      availableSizes[0],
      availableColors[0]
    ),
    currency: "USD",
    image: asset.image_url,
    price: selectedProduct.price,
    product_metadata: {
      code: selectedProduct.code,
      size: selectedSize,
      color: selectedColor,
    },
  });

  const handleResize = () => {
    let imageContainers = document.querySelectorAll(".image-container");
    for (let i = 0; i < imageContainers.length; i++) {
      let imageContainer = imageContainers[i];
      if (imageContainer) {
        imageContainer.setAttribute(
          "style",
          `height:${imageContainer.offsetWidth}px`
        );
      }
      let image = imageContainer.querySelector(".asset-image");
      if (image) {
        image.setAttribute(
          "style",
          `max-height:${imageContainer.offsetWidth - 24}px`
        );
      }
    }
  };

  const handleOpenModal = () => {
    setModalOpen(true);
  };
  const handleCloseModal = () => {
    setModalOpen(false);
  };

  const handleSelectProduct = (selectedIndex) => {
    setSelectedProductIndex(selectedIndex);
    setSelectedProduct(availableProducts[selectedIndex]);

    // Update sizes to reflect the newly selected product
    setAvailableSizes(availableProducts[selectedIndex].available_sizes);
    setSelectedSizeIndex(0);
    setSelectedSize(availableProducts[selectedIndex].available_sizes[0]);

    // Update colors to reflect the newly selected product
    setAvailableColors(availableProducts[selectedIndex].available_colors);
    setSelectedColorIndex(0);
    setSelectedColor(availableProducts[selectedIndex].available_colors[0]);

    // Update the cart data
    setCartData({
      name: `${asset.name} ${availableProducts[selectedIndex].name}, ${availableProducts[selectedIndex].available_colors[0]}, Size ${availableProducts[selectedIndex].available_sizes[0]}`,
      description: `A ${availableProducts[selectedIndex].available_colors[0]} ${availableProducts[selectedIndex].name} in Size ${availableProducts[selectedIndex].available_sizes[0]} featuring the artwork of ${asset.name} from the ${asset.collection.name} collection.`,
      sku: formatSku(
        asset.collection.name,
        asset.name,
        availableProducts[selectedIndex].code,
        availableProducts[selectedIndex].available_sizes[0],
        availableProducts[selectedIndex].available_colors[0]
      ),
      currency: "USD",
      image: asset.image_url,
      price: availableProducts[selectedIndex].price,
      product_metadata: {
        code: availableProducts[selectedIndex].code,
        size: availableProducts[selectedIndex].available_sizes[0],
        color: availableProducts[selectedIndex].available_colors[0],
      },
    });
  };
  const handleSelectSize = (selectedIndex) => {
    setSelectedSizeIndex(selectedIndex);
    setSelectedSize(availableSizes[selectedIndex]);

    // Update the cart data
    setCartData({
      name: `${asset.name} ${selectedProduct.name}, ${selectedColor}, Size ${availableSizes[selectedIndex]}`,
      description: `A ${selectedColor} ${selectedProduct.name} in Size ${availableSizes[selectedIndex]} featuring the artwork of ${asset.name} from the ${asset.collection.name} collection.`,
      sku: formatSku(
        asset.collection.name,
        asset.name,
        selectedProduct.code,
        availableSizes[selectedIndex],
        selectedColor
      ),
      currency: "USD",
      image: asset.image_url,
      price: selectedProduct.price,
      product_metadata: {
        code: selectedProduct.code,
        size: availableSizes[selectedIndex],
        color: selectedColor,
      },
    });
  };
  const handleSelectColor = (selectedIndex) => {
    setSelectedColorIndex(selectedIndex);
    setSelectedColor(availableColors[selectedIndex]);

    // Update the cart data
    setCartData({
      name: `${asset.name} ${selectedProduct.name}, ${availableColors[selectedIndex]}, Size ${selectedSize}`,
      description: `A ${availableColors[selectedIndex]} ${selectedProduct.name} in Size ${selectedSize} featuring the artwork of ${asset.name} from the ${asset.collection.name} collection.`,
      sku: formatSku(
        asset.collection.name,
        asset.name,
        selectedProduct.code,
        selectedSize,
        availableColors[selectedIndex]
      ),
      currency: "USD",
      image: asset.image_url,
      price: selectedProduct.price,
      product_metadata: {
        code: selectedProduct.code,
        size: selectedSize,
        color: availableColors[selectedIndex],
      },
    });
  };

  // Listen for changes to the window size
  // This allows us to dynamically resize images, etc. based on current window size
  useEffect(() => {
    handleResize();
    window.addEventListener("resize", handleResize);
  }, []);

  const modalMarkup = (
    <>
      <Transition.Root show={modalOpen} as={Fragment}>
        <Dialog
          as="div"
          className="fixed inset-0 z-10 overflow-y-auto"
          initialFocus={cancelButtonRef}
          onClose={setModalOpen}
        >
          <div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
            <Transition.Child
              as={Fragment}
              enter="ease-out duration-300"
              enterFrom="opacity-0"
              enterTo="opacity-100"
              leave="ease-in duration-200"
              leaveFrom="opacity-100"
              leaveTo="opacity-0"
            >
              <Dialog.Overlay className="fixed inset-0 bg-zinc-700 bg-opacity-75 transition-opacity" />
            </Transition.Child>

            {/* This element is to trick the browser into centering the modal contents. */}
            <span
              className="hidden sm:inline-block sm:h-screen sm:align-middle"
              aria-hidden="true"
            >
              &#8203;
            </span>
            <Transition.Child
              as={Fragment}
              enter="ease-out duration-300"
              enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
              enterTo="opacity-100 translate-y-0 sm:scale-100"
              leave="ease-in duration-200"
              leaveFrom="opacity-100 translate-y-0 sm:scale-100"
              leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
            >
              <div className="relative inline-block w-full transform overflow-hidden rounded-lg bg-zinc-900 text-left align-bottom shadow-xl transition-all sm:my-8 sm:max-w-lg sm:align-middle md:max-w-2xl lg:max-w-4xl xl:max-w-6xl">
                <div className="relative rounded-lg bg-zinc-900 shadow">
                  <div className="relativemx-auto p-12">
                    <div className=" flex flex-wrap items-center justify-center">
                      <div
                        className="absolute top-5 right-5 z-50 cursor-pointer text-zinc-700 hover:text-zinc-500"
                        onClick={handleCloseModal}
                      >
                        <svg
                          xmlns="http://www.w3.org/2000/svg"
                          className="h-6 w-6"
                          fill="none"
                          viewBox="0 0 24 24"
                          stroke="currentColor"
                          strokeWidth={2}
                        >
                          <path
                            strokeLinecap="round"
                            strokeLinejoin="round"
                            d="M6 18L18 6M6 6l12 12"
                          />
                        </svg>
                      </div>
                      <div className="image-container align-center relative flex justify-center p-3 lg:w-1/2">
                        <div className="my-auto max-h-full rounded-2xl">
                          <img
                            className="asset-image rounded-xl object-contain object-center"
                            src={asset.image_url}
                            alt={asset.name}
                          />
                        </div>
                      </div>
                      <div className="mt-6 w-full px-6 lg:mt-0 lg:w-1/2 lg:py-6 lg:pl-10">
                        <h2 className="title-font my-2 mr-2 text-sm font-bold tracking-widest text-[#5cd4ac]">
                          {asset.collection.name.toUpperCase()}
                        </h2>
                        <h1 className="title-font my-2 mr-2 text-3xl font-medium text-white">
                          {asset.name}
                        </h1>
                        <p className="my-2 mr-2 leading-relaxed text-zinc-100">
                          {asset.description}
                        </p>
                        <hr className="mr-2 border-zinc-500" />
                        <h2 className="title-font mt-4 mb-2 mr-2 text-sm font-bold text-white">
                          Choose your product
                        </h2>
                        <div className="flex flex-row items-center justify-center">
                          {availableProducts.map((product, index) => {
                            const active =
                              selectedProductIndex == index ? true : false;
                            return (
                              <div
                                className={`mr-2 flex w-full cursor-pointer select-none items-center justify-center rounded-lg border-2 border-zinc-400 py-2 text-sm text-white ${
                                  active
                                    ? "border-[#5cd4ac] bg-[#5cd4ac]"
                                    : "hover:border-white"
                                }`}
                                onClick={() => handleSelectProduct(index)}
                              >
                                {product.icon}
                              </div>
                            );
                          })}
                        </div>
                        <h2 className="title-font mt-4 mb-2 text-sm font-bold text-white">
                          Choose your size
                        </h2>
                        <div className="flex flex-row items-center justify-center">
                          {availableSizes.map((size, index) => {
                            const active =
                              selectedSizeIndex == index ? true : false;
                            return (
                              <div
                                className={`mr-2 flex w-full cursor-pointer select-none items-center justify-center rounded-lg border-2 border-zinc-400 py-2 text-sm text-white ${
                                  active
                                    ? "border-[#5cd4ac] bg-[#5cd4ac]"
                                    : "hover:border-white"
                                }`}
                                onClick={() => handleSelectSize(index)}
                              >
                                {size}
                              </div>
                            );
                          })}
                        </div>
                        <h2 className="title-font mt-4 mb-2 text-sm font-bold text-white">
                          Choose your color
                        </h2>
                        <div className="mb-2 flex flex-row items-center justify-center">
                          {availableColors.map((color, index) => {
                            const active =
                              selectedColorIndex == index ? true : false;
                            return (
                              <div
                                className={`mr-2 flex w-full cursor-pointer select-none items-center justify-center rounded-lg border-2 border-zinc-400 py-2 text-sm text-white ${
                                  active
                                    ? "border-[#5cd4ac] bg-[#5cd4ac]"
                                    : "hover:border-white"
                                }`}
                                onClick={() => handleSelectColor(index)}
                              >
                                {color}
                              </div>
                            );
                          })}
                        </div>
                        <div className="mt-8 flex flex-row items-center justify-center">
                          {!cartButtonClicked ? (
                            <button
                              className="snipcart-add-item mr-2 flex w-full cursor-pointer select-none items-center justify-center rounded-lg border-2 border-[#5cd4ac] bg-[#5cd4ac] py-2 text-base text-white hover:border-[#35c897] hover:bg-[#35c897]"
                              onClick={() => {
                                addItem(cartData);
                                handleCartButtonClicked();
                                notify();
                              }}
                            >
                              Add to cart
                            </button>
                          ) : (
                            <span className="mr-2 flex w-full cursor-pointer select-none items-center justify-center rounded-lg border-2 border-[#5cd4ac] bg-[#5cd4ac] py-2 text-base text-white hover:border-[#35c897] hover:bg-[#35c897]">
                              <svg
                                xmlns="http://www.w3.org/2000/svg"
                                className="h-6 w-6"
                                fill="none"
                                viewBox="0 0 24 24"
                                stroke="currentColor"
                                strokeWidth={2}
                              >
                                <path
                                  strokeLinecap="round"
                                  strokeLinejoin="round"
                                  d="M5 13l4 4L19 7"
                                />
                              </svg>
                              Added!
                            </span>
                          )}
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </Transition.Child>
          </div>
        </Dialog>
      </Transition.Root>
    </>
  );

  const notify = () => {
    toast.success("Added to cart!", { icon: ToastCheckIcon });
  };

  return (
    <>
      {modalMarkup}
      <div
        className="relative max-w-sm cursor-pointer overflow-hidden rounded-2xl border-2 border-transparent bg-zinc-700 bg-opacity-30 backdrop-blur-lg backdrop-filter hover:border-[#5cd4ac]"
        onClick={() => setModalOpen(true)}
      >
        <div className="image-container align-center relative flex justify-center p-3">
          <div className="my-auto max-h-full rounded-2xl">
            <img
              className="asset-image rounded-xl object-contain object-center"
              src={asset.image_url}
              alt={asset.name}
            />
          </div>
        </div>
        <div className="px-4 py-4">
          <div className="mb-2 text-base font-bold text-white">
            {asset.name}
          </div>
          <p className="text-xs font-bold text-gray-400">
            {asset.collection.name}
          </p>
        </div>
      </div>
    </>
  );
};

export default NftGridItem;