How to create a product carousel for a TalkJS chat

In this tutorial, we are going to use HTMLPanels in TalkJS to create a product carousel. If you haven’t used HTMLPanels before, do check our official documentation. The app we are going to build uses the Sneaks-API as a backend server and displays sneakers and other products based on user input. We use HTMLPanels to display the retrieved products in a beautiful carousel. You can even click on the product name and visit the reseller website. Here’s a peek into what we’re going to build.

Prerequisites

Note that we are using the Sneaks-API as a reference backend server in this project. This could be any backend that connects to a database and houses product data. For example, you could be running a marketplace with hundreds of different products with specific categories and tags attached to each product. You can give your users information on how to use these filters in the chatbox itself and then when they type something in, use your APIs to retrieve products that satisfy the required criteria. To run this project, you need to have the following:

Running the Sneaks-API Server Locally

To run the Sneaks-API server locally, you must clone their Github repo. Then, go to index.js and uncomment these three lines:

app.listen(port, function () {
  console.log(`Sneaks app listening on port `, port);
});

Now, run the following commands: “npm install” and “npm start” from the same directory. The server should start on port 4000.

Setting up the Chatbox

Follow the TalkJS Getting Started guide to set up a working chat in minutes. The guide uses the Inbox UI mode, but this example uses a chatbox. Replace the following two lines:

var inbox = talkSession.createInbox({ selected: conversation });
  inbox.mount(document.getElementById('talkjs-container'));

With these three lines of code:

const chatbox = talkSession.createChatbox();
chatbox.select(conversation);
chatbox.mount(document.getElementById("talkjs-container"));

Right below this, we initialize our HTML Panel. The “createHtmlPanel()” function on the chatbox object returns a Promise, so we use await. The three parameters specify the location of the HTML file that contains our HTML panel, the height of the panel and whether to display it after initialization.

  const htmlPanel = await chatbox.createHtmlPanel({
    url: "./product-recommendations.html",
    height: 285,
    show: false,
  });

Now, we use the “onSendMessage()” function of the chatbox object to perform a function every time the user sends a message. We store the message and the conversation ID in two variables. First, we call the “getProducts()” method passing the message text. This returns an array of products which we pass as an argument along with the HTML panel and conversation ID to the “addProductsToHTMLPanel()” method. The first line hides the HTML panel if it's already visible.

  chatbox.onSendMessage(async function (data) {
    if(htmlPanel.isVisible())
      htmlPanel.hide();
    const messageText = data.message.text;
    const conversationId = data.conversation.id;
    const productData = await getProducts(messageText, htmlPanel);
    addProductsToHTMLPanel(productData, htmlPanel, conversationId);
  });

The HTMLPanel

The code for the HTMLPanel is mostly boilerplate. See below:

<!DOCTYPE html>


<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
    <link href="styles.css" rel="stylesheet">
</head>


<body>
    <div id="close-button">X</div>
    <div id="row"></div>
</body>


</html>

It contains two Bootstrap files and our very own stylesheet. The body contains two divs: one with the id “close-button” and one with the ID “row”. That’s it. Save it as “product-recommendations.html” in the same directory as the TalkJS JavaScript and HTML files. The rest of the product carousel is built by manipulating the DOM through JavaScript.

Getting Product Data

We have a new async function to get our product data every time the user types a message. In short, the function calls the Sneaks-API’s /search endpoint and returns the results. We only display 10 products in the carousel, so we slice the response to use only the first 10 products. Finally, we return the product array.

//Function to get products from Sneaks API
async function getProducts(messageText) {
  const product_url = `http://localhost:4000/search/${messageText}?count=10`;
  const product_url_options = {
    method: "GET",
  };


  try {
    const response = await fetch(product_url, product_url_options);
    const result = await response.json();
    const productData = result.slice(0, 10);
   
    return productData;


  } catch (error) {
    console.log(error);
  }
}

Adding Products to the HTMLPanel

This is the HTML template of the carousel that we are trying to achieve by manipulating the DOM.

<body>
  <div id="close-button">X</div>
  <div id="row">
    <div class="card">
      <img
        src=""
        class="card-image">
      <span class="brand-name"></span>
      <span class="price"></span>
      <a href="" target="_blank"></a>
    </div>
  </div>
</body>

The first if-block checks if the HTML panel DOM already has products. If it does, we clear them. Next, we have two lines of code that are very important. The first line waits for the HTML panel DOM to fully load. The next line waits for all assets (like images, scripts, etc) to fully load. We start manipulating the DOM only after this is complete.

    await htmlPanel.DOMContentLoadedPromise;
    await htmlPanel.windowLoadedPromise;

We start off by retrieving the <div> with the id “row” and iterating over the product array. For each product, we add a <div> tag, and within that we add an <img> tag, 2 <span> tags and one <a> tag with the classes mentioned above.

//Function to add products to the HTML panel
async function addProductsToHTMLPanel(productData, htmlPanel, conversationId){
  if (htmlPanel.window.document.getElementById("row").hasChildNodes()) {
    htmlPanel.window.document.getElementById("row").innerHTML = "";
  }
 
  await htmlPanel.DOMContentLoadedPromise;
  await htmlPanel.windowLoadedPromise;  
  
  const row = htmlPanel.window.document.getElementById("row");


  productData.forEach((product) => {
    const cardDiv = htmlPanel.window.document.createElement("div");
    cardDiv.classList.add("card");


    const brandName = htmlPanel.window.document.createElement("span");
    brandName.classList.add("brand-name");
    brandName.innerHTML = product.brand;


    const cardImageLink = htmlPanel.window.document.createElement("a");
    cardImageLink.href = product.resellLinks["stockX"];
    cardImageLink.target = "_blank";
    cardImageLink.innerHTML = product.shoeName;


    const cardImage = htmlPanel.window.document.createElement("img");
    cardImage.addEventListener("error", () => {
      console.log(cardImage.src);
      cardImage.src = "./static/no_image.png";
    });
    cardImage.src = product.thumbnail;
    cardImage.classList.add("card-image");


    const retailPrice = htmlPanel.window.document.createElement("span");
    retailPrice.classList.add("price");
    retailPrice.innerHTML = "RRP: $" + product.lowestResellPrice["stockX"];


    cardDiv.appendChild(cardImage);
    cardDiv.appendChild(brandName);
    cardDiv.appendChild(retailPrice);
    cardDiv.appendChild(cardImageLink);
    row.appendChild(cardDiv);
  });
  sendReply(conversationId);  
  const closeButton = htmlPanel.window.document.getElementById("close-button");
    closeButton.addEventListener("click", () => {
      htmlPanel.hide();
  });
  htmlPanel.show();
}

Each of these is populated with the data from the product array. For the close button, we add an event listener for the “click” event and call the htmlPanel.hide() method when clicked.

Posting a Reply

You might be wondering why we sent the conversation ID to the function. Well, that is to post a reply to the chat from our shopping assistant. Once the user types in a product name, the shopping assistant replies back with a positive affirmation. This is just to improve the useability of the chatbox itself. We use another function called “sendReply()” to achieve it.

//Function to post a success reply to the chat
async function sendReply(conversationId) {
  const data = [
    {
      text: "Awesome. Here are our top recommendations! If you want to view other products, simply type the name of the product you want.",
      sender: "654321",
      type: "UserMessage",
    },
  ];
  const postReplyURL = `http://localhost:4000/postSuccessReply/${conversationId}`;
  try {
    const response = await fetch(postReplyURL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(data),
    });
    console.log(response);
  } catch (error) {
    console.log(error);
  }
}


This function calls the Sneaks-API backend server along with the conversation ID and the message data to POST. It doesn’t have an endpoint to resolve this, so we must add our own endpoint to achieve it.

Go to the Sneaks-API folder and navigate to routes/sneaks.routes.js. Add this import to the first line:

const bodyParser = require("body-parser");

At the very end, add a new route.

  //Posts a success reply to a conversation
  app.post(
    "/postSuccessReply/:conversationId", bodyParser.json(), (req, res) => {
      const conversationId = req.params.conversationId;
      sneaks.postSuccessReply(conversationId, req.body, (error, conversation) => {
        if (error) {
          console.log(error);
          res.send("No conversation found");
        } else {
          res.json(conversation);
        }
      });
    }
  );

Now, navigate to controllers/sneaks.controllers.js, and add the following:

  async postSuccessReply(conversationId, data) {
    const talkJSURL = `${process.env.TALKJS_URL}${process.env.TALKJS_APP_ID}/conversations/${conversationId}/messages`;
    const options = {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.SECRET_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(data),
    };
 
    try {
      const response = await fetch(talkJSURL, options);
      return response;
    } catch (error) {
      console.log(error);
    }
  }

We are using a .env file to store the TALKJS_URL, TALKJS_APP_ID and SECRET_KEY. You must retrieve the app ID and secret key from your dashboard and store it in the .env file. Here is how it should look:

TALKJS_APP_ID=Paste your app ID here
TALKJS_URL=https://api.talkjs.com/v1/
SECRET_KEY=Paste your secret key here

One thing to note here is that the Sneaks-API project uses an older version of Express that requires an additional dependency called “body-parser” to parse through the request and response. You must install this by executing the command “npm install body-parser --save”.

Adding our Styles

This is the last part of the tutorial. Create a file named “styles.css” and use the CSS below as a starting point. Place it in the same directory as the rest of the files.

#row {
  align-items: stretch;
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
}


.card {
  float: left;
  width: 40%;
  padding: 0.4rem;
  margin-bottom: 2rem;
  flex-basis: 33.333%;
  flex-grow: 0;
  flex-shrink: 0;
  margin: 0.4rem 0.4rem;
  border: 2px solid rgb(230, 230, 230);
  border-radius: 8%;
  margin: 10px 4px;
  transition: .6s ease;
  background-color: rgb(233, 233, 233);
}


.card:hover {
  transform: scale(1.05);
}


.card-image {
  float: left;
  width: 7.3rem;
  height: 5.5rem;
  vertical-align: middle;
  border-radius: 8%;
  border: 2px solid rgb(216, 216, 216);
}


a {
  font-size: 85%;
  text-decoration: none;
  color: black;
}


.brand-name {
  text-align: left;
  text-transform: capitalize;
  font-weight: bold;
}


.price {
  font-size: 85%;
  text-decoration: none;
  color: black;
}


#close-button{
  padding: 0px 0.2rem;
  border-radius: 1.5rem;
  display: inline-block;
  margin-left: 0.4rem;
  cursor: pointer;
  color: red;
}

Conclusion

That's the end of our sort-of long tutorial. The great thing about TalkJS is how customizable it is right out of the box. It is built with this idea in mind, and you can achieve pretty much anything your use case or requirement demands. You can find the source code for this tutorial on our Github repository.