DEV Community

Cover image for 🎉🎉 E-commerce product page in HTML, CSS and JavaScript - Frontend Mentor Challenge
Chaoo Charles
Chaoo Charles

Posted on

🎉🎉 E-commerce product page in HTML, CSS and JavaScript - Frontend Mentor Challenge

In this post we will take an existing design from Frontend Mentor, and convert the design into a HTML, CSSand JavaScript webpage.

Image slide show

The webpage will have some cool features like:

  • Image gallery
  • Image slide show
  • Adding product to cart
  • Removing product from cart
  • Cart counter
  • Mobile responsiveness
  • and more...

Interested in a video tutorial guide? I got you sorted. The following is the video:

Subscribe to my channel for more.

Starter files

To get the designs and starter files, visit the following link which will take you to the front-end mentor challenge: E-commerce product page

After downloading the starter file, just unzip the zipped folder. Open the unzipped folder with your favorite code editor. I will be using vs-code. You should now have the following starter files.

Folder Structure

  • design folder - this folder have all the designs you need to complete the challenge
  • images - these are the different images and icons you will need when working on the web page
  • index.html - A html template with content to get started with.
  • README - please read the README to get more details about the challenge and what is expected
  • style-guide - a guide on fonts and colors to be used

Head

Open the index.html file. At our head tag we will link the different css and javascript files that we will need. Remove the already existing style tag, we will use an external css file for styles called main.css. Add script tags for main.js, slider.js, and cart.js.

  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link
      rel="icon"
      type="image/png"
      sizes="32x32"
      href="./images/favicon-32x32.png"
    />

    <title>Frontend Mentor | E-commerce product page</title>
    <link rel="stylesheet" type="text/css" media="screen" href="main.css" />
    <script src="main.js" defer></script>
    <script src="slider.js" defer></script>
    <script src="cart.js" defer></script>
  </head>
Enter fullscreen mode Exit fullscreen mode

Note how we have the defer attributes at our scripts. When you include a script with the defer attribute, the browser will download the script in the background while continuing to parse and render the HTML. Once the HTML parsing is complete, the deferred scripts will be executed in the order they appear in the document.

Make sure to also create these files at the root. Note how the folder structure now have additional files. That is main.css, main.js, slider.js, and cart.js.

Folder structure 2

Nav

At index.html file we will start by working on our navbar. Below will be our html markup for the navbar. We will make sure to place our nav inside the div .container which will set a maximum width for all our content.

<body>
    <div class="container">
      <header>
        <nav class="navbar">
          <section class="nav-first">
            <svg
              class="menu-icon"
              width="16"
              height="15"
              xmlns="http://www.w3.org/2000/svg"
            >
              <path
                d="M16 12v3H0v-3h16Zm0-6v3H0V6h16Zm0-6v3H0V0h16Z"
                fill="#69707D"
                fill-rule="evenodd"
              />
            </svg>
            <div class="logo">
              <img src="images/logo.svg" alt="sneakers logo" />
            </div>
            <div class="backdrop"></div>
            <div class="nav-links">
              <svg
                class="close-icon"
                width="14"
                height="15"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path
                  d="m11.596.782 2.122 2.122L9.12 7.499l4.597 4.597-2.122 2.122L7 9.62l-4.595 4.597-2.122-2.122L4.878 7.5.282 2.904 2.404.782l4.595 4.596L11.596.782Z"
                  fill="#69707D"
                  fill-rule="evenodd"
                />
              </svg>
              <a href="#">Collections</a>
              <a href="#">Men</a>
              <a href="#">Women</a>
              <a href="#">About</a>
              <a href="#">Contact</a>
            </div>
          </section>
          <section class="nav-second">
            <div class="cart">
              <img
                class="cart-icon"
                src="images/icon-cart.svg"
                alt="cart icon"
              />
              <div class="cart-container">
                <div class="cart-title">Cart</div>
                <div class="cart-items empty">
                  <p class="cart-empty">Your cart is empty</p>
                </div>
                <button class="checkout empty">Checkout</button>
              </div>
              <div class="cart-count">
                <span class="qty">0</span>
              </div>
            </div>
            <div class="avatar">
              <img src="images/image-avatar.png" alt="avatar" />
            </div>
          </section>
        </nav>
      </header>
    </div>
  </body>
Enter fullscreen mode Exit fullscreen mode

Initial CSS

At main.css we will start by importing Kumbh Sans font from google fonts. We then remove the default margin and padding from all the elements and set box-sizing to border-box. This will tell css to include the padding and border of the elements to their height and width. This way we will not have unexpected sizes of our elements.

We also target :root to setup css variables for our colors from the style guide file. The following will be our initial css.

@import url("https://fonts.googleapis.com/css2?family=Kumbh+Sans:wght@400;700&display=swap");

*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

:root {
  --orange: hsl(26, 100%, 55%);
  --pale-orange: hsl(25, 100%, 94%);
  --very-dark-blue: hsl(220, 13%, 13%);
  --dark-grayish-blue: hsl(219, 9%, 45%);
  --grayish-blue: hsl(220, 14%, 75%);
  --light-grayish-blue: hsl(223, 64%, 98%);
  --white: hsl(0, 0%, 100%);
  --black: hsl(0, 0%, 0%);
  --black-with-opacity: hsla(0, 0%, 0%, 0.75);
}

html {
  font-family: "Kumbh Sans", sans-serif;
}

a {
  text-decoration: none;
  color: var(--dark-grayish-blue);
}

body {
  min-height: 100vh;
  min-width: 100vw;
  position: relative;
}

.container {
  max-width: 1120px;
  min-height: 100vh;
  padding: 0 5px;
  margin: auto;
}
Enter fullscreen mode Exit fullscreen mode

Nav CSS

To layout our navbar we will just use css flex box. Notice how we had divided the navbar into two sections. .nav-first and .nav-second. On the first section we style the logo and links while on the second section we will style our cart and profile. Since the cart styles are more detailed, we will have a separate section for it below.

/* Navbar */
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-top: 26px;
  border-bottom: 1px solid var(--grayish-blue);
  margin-bottom: 85px;
  position: relative;
}

.nav-first {
  display: flex;
  align-items: center;
  gap: 50px;
  padding-bottom: 30px;
}

.nav-first .menu-icon {
  display: none;
}

.nav-first .backdrop {
  display: none;
}

.nav-links .close-icon {
  display: none;
}

.nav-links {
  display: flex;
  align-items: center;
  gap: 30px;
}

.nav-links a {
  position: relative;
}

.nav-links a:hover {
  color: var(--black);
}

.nav-links a:hover::after {
  content: "";
  position: absolute;
  background-color: var(--orange);
  width: 100%;
  height: 3px;
  left: 0;
  bottom: -47px;
}

.nav-second {
  display: flex;
  align-items: center;
  gap: 45px;
  padding-bottom: 30px;
}

.logo img {
  height: 22px;
}

.avatar img {
  height: 50px;
  width: 50px;
}
Enter fullscreen mode Exit fullscreen mode

Cart CSS

.cart-container will hold our cart items. .empty is a dynamic class which we will add or remove it depending on whether we have items in cart or not by making use of javascript. By default the cart will be empty. Note how .cart-container has a display of none. We will add an active class with javascript whenever we click the cart icon in order to open the cart. If you check the html that we currently have, we don't have a .cart-item. We will create the cart-item at our javascript file and make it a child of .cart-items.

/* Cart */
.cart {
  position: relative;
}

.cart-icon {
  cursor: pointer;
}

.cart-container {
  right: -95px;
  top: 50px;
  z-index: 9;
  position: absolute;
  width: 360px;
  min-height: 260px;
  background: white;
  box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
  display: none;
}

.cart-container.active {
  display: flex;
  flex-direction: column;
}

.cart-title {
  padding: 25px 20px;
  font-weight: 700;
  border-bottom: 1px solid var(--grayish-blue);
}

.cart .cart-items {
  padding: 25px 20px;
  display: flex;
  flex-direction: column;
  gap: 25px;
}

.cart .cart-items.empty {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 185px;
  font-weight: 700;
}

.cart .cart-items.empty .cart-empty {
  color: var(--grayish-blue);
  display: inline-block;
}

.cart .cart-items .cart-empty {
  display: none;
}

.cart-item {
  display: flex;
  align-items: center;
  gap: 20px;
}

.cart-item img {
  height: 50px;
  border-radius: 5px;
}

.cart-item {
  color: var(--dark-grayish-blue);
}

.cart-item .total-price {
  color: var(--black);
  font-weight: 700;
}

.checkout.empty {
  display: none;
}

.checkout {
  height: 56px;
  margin: 27px 23px;
  border: none;
  color: var(--white);
  background-color: var(--orange);
  border-radius: 10px;
  font-weight: 700;
}

.checkout:hover {
  cursor: pointer;
}

.cart-count {
  cursor: pointer;
  position: absolute;
  top: -8px;
  right: -10px;
  background-color: var(--orange);
  color: var(--white);
  min-width: 25px;
  min-height: 17px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 700;
}

.delete-item {
  border: none;
  background: none;
  cursor: pointer;
}
Enter fullscreen mode Exit fullscreen mode

Side Nav

To style the side navbar, add the following at the bottom of our css file. We have created a breakpoint at which the side nav will become active.

/* Mobile */

@media (max-width: 755px) {
  .navbar {
    margin-bottom: 0;
    border-bottom: none;
  }

  .nav-first,
  .nav-second {
    gap: 30px;
    padding-bottom: 10px;
  }

  .nav-first .menu-icon {
    cursor: pointer;
    display: inline-block;
  }

  .nav-links {
    display: none;
  }

  .nav-links.active {
    display: flex;
    flex-direction: column;
    position: absolute;
    top: 0;
    left: -5px;
    max-width: 220px;
    width: 100%;
    height: 100vh;
    background: var(--white);
    align-items: start;
    z-index: 15;
    padding: 25px 30px;
  }

  .nav-first .backdrop.active {
    background: var(--black-with-opacity);
    width: 100vw;
    height: 100vh;
    display: block;
    position: absolute;
    top: 0;
    left: -5px;
    z-index: 11;
  }

  .nav-links.active .close-icon {
    display: inline-block;
    margin-bottom: 30px;
    cursor: pointer;
  }

  .nav-links a {
    font-weight: 700;
    color: black;
  }

  .nav-links.active a:hover::after {
    bottom: -5px;
  }
}
Enter fullscreen mode Exit fullscreen mode

To open and close the navbar, let's add some javascript. At main.js, add the following code:

const menuIcon = document.querySelector(".menu-icon");
const backdrop = document.querySelector(".backdrop");
const navLinks = document.querySelector(".nav-links");
const closeIcon = document.querySelector(".close-icon");

menuIcon.addEventListener("click", () => {
  backdrop.classList.add("active");
  navLinks.classList.add("active");
});

closeIcon.addEventListener("click", () => {
  backdrop.classList.remove("active");
  navLinks.classList.remove("active");
});

backdrop.addEventListener("click", () => {
  backdrop.classList.remove("active");
  navLinks.classList.remove("active");
});

Enter fullscreen mode Exit fullscreen mode

Image Gallery, Lightbox and Product Details

At our index.html just right after our header, we will add a main section with the html markup for our gallery, lightbox and product description.

<section class="main">
        <div class="default gallery">
          <div class="main-img">
            <img
              class="active"
              src="images/image-product-1.jpg"
              alt="product-img"
            />
            <img src="images/image-product-2.jpg" alt="product-img" />
            <img src="images/image-product-3.jpg" alt="product-img" />
            <img src="images/image-product-4.jpg" alt="product-img" />
          </div>
          <div class="thumb-list">
            <div class="active">
              <img
                src="images/image-product-1-thumbnail.jpg"
                alt="product-img"
              />
            </div>
            <div>
              <img
                src="images/image-product-2-thumbnail.jpg"
                alt="product-img"
              />
            </div>
            <div>
              <img
                src="images/image-product-3-thumbnail.jpg"
                alt="product-img"
              />
            </div>
            <div>
              <img
                src="images/image-product-4-thumbnail.jpg"
                alt="product-img"
              />
            </div>
          </div>
        </div>

        <div class="lightbox">
          <div class="gallery">
            <div class="main-img">
              <!-- icons -->
              <span class="icon-close">
                <svg width="14" height="15" xmlns="http://www.w3.org/2000/svg">
                  <path
                    d="m11.596.782 2.122 2.122L9.12 7.499l4.597 4.597-2.122 2.122L7 9.62l-4.595 4.597-2.122-2.122L4.878 7.5.282 2.904 2.404.782l4.595 4.596L11.596.782Z"
                    fill="#69707D"
                    fill-rule="evenodd"
                  />
                </svg>
              </span>
              <span class="icon-prev">
                <svg width="12" height="18" xmlns="http://www.w3.org/2000/svg">
                  <path
                    d="M11 1 3 9l8 8"
                    stroke="#1D2026"
                    stroke-width="3"
                    fill="none"
                    fill-rule="evenodd"
                  />
                </svg>
              </span>
              <span class="icon-next">
                <svg width="13" height="18" xmlns="http://www.w3.org/2000/svg">
                  <path
                    d="m2 1 8 8-8 8"
                    stroke="#1D2026"
                    stroke-width="3"
                    fill="none"
                    fill-rule="evenodd"
                  />
                </svg>
              </span>

              <!-- main images -->
              <img
                class="active"
                src="images/image-product-1.jpg"
                alt="product-img"
              />
              <img src="images/image-product-2.jpg" alt="product-img" />
              <img src="images/image-product-3.jpg" alt="product-img" />
              <img src="images/image-product-4.jpg" alt="product-img" />
            </div>
            <div class="thumb-list">
              <div class="active">
                <img
                  src="images/image-product-1-thumbnail.jpg"
                  alt="product-img"
                />
              </div>
              <div>
                <img
                  src="images/image-product-2-thumbnail.jpg"
                  alt="product-img"
                />
              </div>
              <div>
                <img
                  src="images/image-product-3-thumbnail.jpg"
                  alt="product-img"
                />
              </div>
              <div>
                <img
                  src="images/image-product-4-thumbnail.jpg"
                  alt="product-img"
                />
              </div>
            </div>
          </div>
        </div>

        <div class="content">
          <h3>SNEAKER COMPANY</h3>
          <h2 class="product-name">Fall Limited Edition Sneakers</h2>
          <p class="product-desc">
            These low-profile sneakers are your perfect casual wear companion.
            Featuring a durable rubber outer sole, they’ll withstand everything
            the weather can offer.
          </p>
          <div class="price-info">
            <div class="price">
              <span class="current-price">$125.00</span>
              <span class="discount">50%</span>
            </div>
            <div class="prev-price">$250.00</div>
          </div>
          <div class="add-to-cart-container">
            <div class="counter">
              <button class="minus">
                <svg
                  width="12"
                  height="4"
                  xmlns="http://www.w3.org/2000/svg"
                  xmlns:xlink="http://www.w3.org/1999/xlink"
                >
                  <defs>
                    <path
                      d="M11.357 3.332A.641.641 0 0 0 12 2.69V.643A.641.641 0 0 0 11.357 0H.643A.641.641 0 0 0 0 .643v2.046c0 .357.287.643.643.643h10.714Z"
                      id="a"
                    />
                  </defs>
                  <use fill="#FF7E1B" fill-rule="nonzero" xlink:href="#a" />
                </svg>
              </button>
              <span class="count">0</span>
              <button class="plus">
                <svg
                  width="12"
                  height="12"
                  xmlns="http://www.w3.org/2000/svg"
                  xmlns:xlink="http://www.w3.org/1999/xlink"
                >
                  <defs>
                    <path
                      d="M12 7.023V4.977a.641.641 0 0 0-.643-.643h-3.69V.643A.641.641 0 0 0 7.022 0H4.977a.641.641 0 0 0-.643.643v3.69H.643A.641.641 0 0 0 0 4.978v2.046c0 .356.287.643.643.643h3.69v3.691c0 .356.288.643.644.643h2.046a.641.641 0 0 0 .643-.643v-3.69h3.691A.641.641 0 0 0 12 7.022Z"
                      id="b"
                    />
                  </defs>
                  <use fill="#FF7E1B" fill-rule="nonzero" xlink:href="#b" />
                </svg>
              </button>
            </div>
            <button class="add-to-cart">
              <span>
                <svg width="22" height="20" xmlns="http://www.w3.org/2000/svg">
                  <path
                    d="M20.925 3.641H3.863L3.61.816A.896.896 0 0 0 2.717 0H.897a.896.896 0 1 0 0 1.792h1l1.031 11.483c.073.828.52 1.726 1.291 2.336C2.83 17.385 4.099 20 6.359 20c1.875 0 3.197-1.87 2.554-3.642h4.905c-.642 1.77.677 3.642 2.555 3.642a2.72 2.72 0 0 0 2.717-2.717 2.72 2.72 0 0 0-2.717-2.717H6.365c-.681 0-1.274-.41-1.53-1.009l14.321-.842a.896.896 0 0 0 .817-.677l1.821-7.283a.897.897 0 0 0-.87-1.114ZM6.358 18.208a.926.926 0 0 1 0-1.85.926.926 0 0 1 0 1.85Zm10.015 0a.926.926 0 0 1 0-1.85.926.926 0 0 1 0 1.85Zm2.021-7.243-13.8.81-.57-6.341h15.753l-1.383 5.53Z"
                    fill="#69707D"
                    fill-rule="nonzero"
                  />
                </svg>
              </span>
              <span>Add to cart</span>
            </button>
          </div>
        </div>
      </section>
Enter fullscreen mode Exit fullscreen mode

Before the mobile responsiveness, we will style the gallery and the product details. By default the lightbox(image slideshow) will be hidden, and we will add the active to it upon clicking the .main-img.

/* Main */
.main {
  display: flex;
  gap: 125px;
  min-height: 570px;
  align-items: center;
  padding: 0 50px;
}

/* Image gallery */
.gallery {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 30px;
}

.gallery .main-img img {
  display: none;
}

.gallery .main-img img.active {
  display: inline-block;
  max-width: 445px;
  max-height: 445px;
  width: 100%;
  height: 100%;
  border-radius: 20px;
  cursor: pointer;
}

.gallery .thumb-list {
  display: flex;
  justify-content: space-between;
  max-width: 445px;
  width: 100%;
}

.gallery .thumb-list div {
  max-width: 90px;
  max-height: 90px;
  margin: 0 2px;
}

.gallery .thumb-list img {
  width: 100%;
  height: 100%;
  border-radius: 10px;
  cursor: pointer;
}

.gallery .thumb-list img:hover {
  opacity: 50%;
}

.gallery .thumb-list .active img {
  opacity: 30%;
}

.gallery .thumb-list .active {
  border: 2px solid var(--orange);
  border-radius: 13px;
  margin: 0;
}

/* lightbox */
.lightbox {
  display: none;
  position: absolute;
  top: 0;
  left: 0;
  height: 100vh;
  width: 100vw;
  z-index: 10;
  background: var(--black-with-opacity);
  align-items: center;
  justify-content: center;
}

.lightbox.active {
  display: flex;
}

.lightbox.active .gallery {
  max-width: 445px;
}

.lightbox .main-img {
  position: relative;
}

.lightbox .icon-prev,
.lightbox .icon-next {
  position: absolute;
  height: 60px;
  width: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--white);
  border-radius: 50%;
}

.icon-prev:hover,
.icon-next:hover {
  cursor: pointer;
}

/* .icon-prev svg path {
  fill: var(--orange);
} */

.icon-prev {
  top: 50%;
  transform: translate(-50%, -50%);
}

.icon-next {
  top: 50%;
  right: 0;
  transform: translate(50%, -50%);
}

.icon-close svg path {
  fill: var(--white);
}

.icon-close svg path:hover {
  cursor: pointer;
  fill: var(--orange);
}

.icon-close {
  position: absolute;
  right: 0;
  top: -40px;
}

/* Content */

.content {
  flex: 1;
}
.content h3 {
  font-size: 16px;
  color: var(--orange);
}

.content h2 {
  font-size: 37px;
  margin: 20px 0 40px 0;
}

.content p {
  font-size: 16px;
  color: var(--dark-grayish-blue);
  margin-bottom: 30px;
}

.price {
  display: flex;
  align-items: center;
  gap: 15px;
}

.current-price {
  font-weight: 700;
  font-size: 25px;
}

.discount {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 6px;
  border-radius: 10%;
  height: 25px;
  background-color: var(--pale-orange);
  font-weight: 700;
  color: var(--orange);
}

.prev-price {
  margin: 10px 0 35px 0;
  font-size: 18px;
  color: var(--grayish-blue);
  font-weight: 700;
  text-decoration: line-through;
}

.add-to-cart-container {
  display: flex;
  align-items: center;
  gap: 15px;
}

.counter {
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-radius: 15px;
  width: 150px;
  height: 55px;
  background: var(--light-grayish-blue);
}

.counter button {
  width: 50px;
  height: 100%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
}

.counter .count {
  font-weight: 700;
}

.add-to-cart {
  color: var(--white);
  background-color: var(--orange);
  border: 0px;
  height: 55px;
  width: 100%;
  border-radius: 10px;
  font-weight: 700;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 15px;
  cursor: pointer;
  padding: 0 5px;
}

.add-to-cart svg path {
  fill: var(--white);
}
Enter fullscreen mode Exit fullscreen mode

Gallery and Slider Logic

Now let's add logic for our gallery and lightbox. At slider.js add the following code. In the following code we have selected all the mainImages and all the thumbnails, both for gallery and lightbox. Their logic will be similar apart from the slider. The goal is to show a certain image upon clicking it's thumbnail.

const mainImages = document.querySelectorAll(".default .main-img img");
const thumbnails = document.querySelectorAll(".default .thumb-list div");
const lightboxMainImages = document.querySelectorAll(".lightbox .main-img img");
const lightboxThumbnails = document.querySelectorAll(
  ".lightbox .thumb-list div"
);
const lightbox = document.querySelector(".lightbox");
const iconClose = document.querySelector(".icon-close");
const iconPrev = document.querySelector(".icon-prev");
const iconNext = document.querySelector(".icon-next");

let currentImageIndex = 0;

const changeImage = (index, mainImages, thumbnails) => {
  mainImages.forEach((img) => {
    img.classList.remove("active");
  });
  thumbnails.forEach((thumb) => {
    thumb.classList.remove("active");
  });

  mainImages[index].classList.add("active");
  thumbnails[index].classList.add("active");
  currentImageIndex = index;
};

thumbnails.forEach((thumb, index) => {
  thumb.addEventListener("click", () => {
    changeImage(index, mainImages, thumbnails);
  });
});

lightboxThumbnails.forEach((thumb, index) => {
  thumb.addEventListener("click", () => {
    changeImage(index, lightboxMainImages, lightboxThumbnails);
  });
});

mainImages.forEach((img, index) => {
  img.addEventListener("click", () => {
    lightbox.classList.add("active");
    changeImage(index, lightboxMainImages, lightboxThumbnails);
  });
});

iconPrev.addEventListener("click", () => {
  if (currentImageIndex <= 0) {
    changeImage(mainImages.length - 1, lightboxMainImages, lightboxThumbnails);
  } else {
    changeImage(currentImageIndex - 1, lightboxMainImages, lightboxThumbnails);
  }
});

iconNext.addEventListener("click", () => {
  if (currentImageIndex >= mainImages.length - 1) {
    changeImage(0, lightboxMainImages, lightboxThumbnails);
  } else {
    changeImage(currentImageIndex + 1, lightboxMainImages, lightboxThumbnails);
  }
});

iconClose.addEventListener("click", () => {
  lightbox.classList.remove("active");
});
Enter fullscreen mode Exit fullscreen mode

Let's break the above code down. We select the thumbnails and loop over each of them. This allows us to add click event listener to each of it, pass a callback function and then call changeImage function.

thumbnails.forEach((thumb, index) => {
  thumb.addEventListener("click", () => {
    changeImage(index, mainImages, thumbnails);
  });
});
Enter fullscreen mode Exit fullscreen mode

The changeImage function uses the thumbnail index to change the active image. First we remove the .active class from all mainImages and thumbnails and then we use the passed index to add the .active class to only specific mainImage and it's thumbnail.
This logic will be similar for our lighbox when we change images by clicking on thumbnails.

const changeImage = (index, mainImages, thumbnails) => {
  mainImages.forEach((img) => {
    img.classList.remove("active");
  });
  thumbnails.forEach((thumb) => {
    thumb.classList.remove("active");
  });

  mainImages[index].classList.add("active");
  thumbnails[index].classList.add("active");
  currentImageIndex = index;
};
Enter fullscreen mode Exit fullscreen mode

Open Lightbox

To open the lightbox, we looped over the mainImages and added the click event to each of them. Upon clicking the main Image, we add an active class to the lightbox. This will open the lightbox. Now to make sure that the clicked image is the one that show on the lightbox, we also call our changeImage function and pass the index of the clicked image. This will update the lightbox to the current image.

mainImages.forEach((img, index) => {
  img.addEventListener("click", () => {
    lightbox.classList.add("active");
    changeImage(index, lightboxMainImages, lightboxThumbnails);
  });
});
Enter fullscreen mode Exit fullscreen mode

Next and Prev Images

Here we reused our changeImage function. The main thing to note is the correct calculation of the index. To simply explain it, for next we simply add 1 to the index. But if we get to the end, we move the count of the index back to 0. And for previous we decrease the index by 1 and if we get to the first image, we move the count of the index to the last image. We make use of the .length property to know the last image.

iconPrev.addEventListener("click", () => {
  if (currentImageIndex <= 0) {
    changeImage(mainImages.length - 1, lightboxMainImages, lightboxThumbnails);
  } else {
    changeImage(currentImageIndex - 1, lightboxMainImages, lightboxThumbnails);
  }
});

iconNext.addEventListener("click", () => {
  if (currentImageIndex >= mainImages.length - 1) {
    changeImage(0, lightboxMainImages, lightboxThumbnails);
  } else {
    changeImage(currentImageIndex + 1, lightboxMainImages, lightboxThumbnails);
  }
});
Enter fullscreen mode Exit fullscreen mode

Cart Logic

Now add the following code at cart.js. At addToCartBtn we will add a click event listener. upon clicking the button we will extract the product details from the DOM including the name, price and image. The product quantity will already be available at our count variable. We will then call addItemToCart with the product details as parameters.

At addItemToCart we will create a cart item, notice how the information is dynamic depending on the details we got at addItemToCart as arguments, e.g the name, price etc

We also create a custom quantity attribute which will be helpful when calculating the total cart quantity at updateTotalCartQty function.

cartItem.dataset.quantity = count;

After creating the carItem, we then append it as a child of cartItems. This is how the item will simply be added to cart.

const countEl = document.querySelector(".count");
const minus = document.querySelector(".minus");
const plus = document.querySelector(".plus");
const cartIcon = document.querySelector(".cart-icon");
const cartContainer = document.querySelector(".cart-container");
const addToCartBtn = document.querySelector(".add-to-cart");
const cartItems = document.querySelector(".cart-items");
const checkout = document.querySelector(".checkout");
const cartCount = document.querySelector(".cart-count");

let count = 0;
let totalCartQty = 0;

const updateCount = (newCount) => {
  count = newCount;
  countEl.textContent = count;
};

minus.addEventListener("click", () => {
  if (count > 0) {
    updateCount(count - 1);
  }
});

plus.addEventListener("click", () => {
  updateCount(count + 1);
});

cartCount.addEventListener("click", () => {
  cartContainer.classList.toggle("active");
});

const updateTotalCartQty = () => {
  const cartItemsList = document.querySelectorAll(".cart-item");
  totalCartQty = 0;
  cartItemsList.forEach((item) => {
    totalCartQty += parseInt(item.dataset.quantity);
  });

  cartCount.innerHTML = `<span class="qty">${totalCartQty}</span>`;
};

// add item to cart

const addItemToCart = (name, price, imageSrc) => {
  const totalPrice = count * price;

  const cartItem = document.createElement("div");
  cartItem.classList.add("cart-item");
  cartItem.dataset.quantity = count;
  cartItem.innerHTML = `
      <img src="${imageSrc}" alt="${name}" />
      <div class="item-details">
        <div>${name}</div>
        <div>
            <p>
                $${price.toFixed(2)} x ${count} 
                <span class='total-price'>$${totalPrice.toFixed(2)}</span>
            </p>
        </div>
        </div>
        <button class="delete-item"> 
            <svg width="14" height="16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M0 2.625V1.75C0 1.334.334 1 .75 1h3.5l.294-.584A.741.741 0 0 1 5.213 0h3.571a.75.75 0 0 1 .672.416L9.75 1h3.5c.416 0 .75.334.75.75v.875a.376.376 0 0 1-.375.375H.375A.376.376 0 0 1 0 2.625Zm13 1.75V14.5a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 1 14.5V4.375C1 4.169 1.169 4 1.375 4h11.25c.206 0 .375.169.375.375ZM4.5 6.5c0-.275-.225-.5-.5-.5s-.5.225-.5.5v7c0 .275.225.5.5.5s.5-.225.5-.5v-7Zm3 0c0-.275-.225-.5-.5-.5s-.5.225-.5.5v7c0 .275.225.5.5.5s.5-.225.5-.5v-7Zm3 0c0-.275-.225-.5-.5-.5s-.5.225-.5.5v7c0 .275.225.5.5.5s.5-.225.5-.5v-7Z" id="a"/></defs><use fill="#C3CAD9" fill-rule="nonzero" xlink:href="#a"/></svg>
        </button>
    `;

  cartItems.appendChild(cartItem);

  updateTotalCartQty();

  if (cartItems.classList.contains("empty")) {
    cartItems.classList.remove("empty");
    checkout.classList.remove("empty");
  }

  // attach an event listener to the delete button

  const deleteButton = cartItem.querySelector(".delete-item");
  deleteButton.addEventListener("click", (event) => {
    const cartItem = event.target.closest(".cart-item");
    removeItemFromCart(cartItem);
  });
};

addToCartBtn.addEventListener("click", () => {
  if (count === 0) return;
  const productName = document.querySelector(".main .product-name").textContent;
  const productPriceEl = document.querySelector(".main .current-price");
  const productPrice = parseFloat(productPriceEl.textContent.replace("$", ""));
  const productImg = document
    .querySelector(".default.gallery .main-img img")
    .getAttribute("src");

  addItemToCart(productName, productPrice, productImg);
  cartContainer.classList.add("active");

  updateCount(0);
});

// remove item from cart

const removeItemFromCart = (cartItem) => {
  cartItem.remove();
  updateTotalCartQty();

  if (cartItems.children.length === 1) {
    cartItems.classList.add("empty");
    checkout.classList.add("empty");
  }
};

Enter fullscreen mode Exit fullscreen mode

Mobile responsiveness

Let's make the rest of the page responsive. At our breakpoint @media (max-width: 755px) after .nav-links.active, add the following.


/* main */
  .main {
    flex-direction: column;
    gap: 20px;
    padding: 0;
  }

  .main .default {
    display: none;
  }

  .lightbox {
    display: flex;
    position: relative;
    height: auto;
    width: auto;
    background: none;
  }

  .main .thumb-list {
    display: none;
  }

  .main .icon-prev {
    left: 50px;
    height: 45px;
    width: 45px;
  }

  .main .icon-next {
    right: 50px;
    height: 45px;
    width: 45px;
  }

  .gallery .main-img img.active {
    max-width: none;
    max-height: none;
    width: 100vw;
    height: auto;
    border-radius: 0;
  }

  .content {
    padding: 0 20px;
  }

  .content h2 {
    margin: 10px 0;
    font-size: 30px;
  }

  .price-info {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 30px;
  }

  .prev-price {
    margin: 0;
  }

  .add-to-cart-container {
    flex-direction: column;
  }

  .counter {
    width: 100%;
  }

  .counter button {
    width: 40%;
  }

  .cart-container {
    z-index: 20;
    right: -85px;
    top: 40px;
  }
Enter fullscreen mode Exit fullscreen mode

That's it devs, incase of any confusion you can watch the video tutorial I pinned above and the source code is also available for free on my github: https://github.com/chaoocharles/ecommerce-product-page-html-css-javascript

Do me a favor now and subscribe to my channel 😊: https://www.youtube.com/c/chaoocharles

Top comments (0)