ProductHunt Clone: React + Lowcode backend
Published: 2021-12-25 07:04:56

---
title: ProductHunt Clone: React + Lowcode backend
published: true
description: 
tags: 
//cover_image: https://direct_url_to_image.jpg
---

We are going to build a very basic clone of **Product Hunt** today! 

We'll be using:

- **React** for building the frontend.
- **Material UI** as our UI Library
- **Canonic** as a Backend

 📝  **Step 1: Create-react-app**

First thing first, create a new react project using - **create-react-app**

```bash
npx create-react-app product-hunt
```

You'll have the basic project setup with the boilerplate code. 

📩  **Step 2: Add the dependencies** 

Next, go to your project folder on the terminal and add all the required dependencies. As we disc

```bash
yarn add 
```

We have the project setup and all the dependencies installed. On the frontend, our design will consist of:

- A simple `Header` at the top.
- A `List` below displaying the data we are gonna get from our backend (List of all the products).

👩‍🔧  **Step 3: Build the Header**

We'll create a component - `Header` at `src/components/Header`. Create the respective `Header.js` and `index.js` files. 

Add following code to your `index.js` file

```jsx
export { default } from "./Header";
```

Coming to our `Header.js` file, we'l be using the `Box` component from Material UI to build it. Add the following code:

```bash
import React from "react";
import { Avatar, Box, Divider } from "@mui/material";

const Header = () => {
  return (
    <Box
      sx={{
        display: "flex",
        flexDirection: "column",
      }}
    >
      <Box
        sx={{
          display: "flex",
          flexDirection: "row",
          justifyContent: "space-between",
          marginBottom: "20px",
        }}
      >
        <Avatar
          sx={{
            bgcolor: "#da552f",
            width: 50,
            height: 50,
            fontWeight: 900,
            fontSize: "1.5rem",
          }}
        >
          P
        </Avatar>
      </Box>
      <Divider variant="fullWidth" />
    </Box>
  );
};

export default Header;
```

Our `Header` component is ready, let's add it to our layout to see how it looks. Head back to `App.js`, import the **Header** component:

```jsx
import "./App.css";
import { Box } from "@mui/material";
import Header from "./components/Header/Header";

function App() {
  return (
    <div className="App">
      <Box
        sx={{
          margin: "20px 30px 0px 30px",
        }}
      >
        <Header></Header>
      </Box>
    </div>
  );
}

export default App;
```

It should like this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4p8t514zlndpnu30o2d4.png)
 

📝  **Step 4: Setup Data Stream**

We'll use some dummy data as a **data source** at first to feed into our table and then replace it with the actual data coming in from the API. This way we'll be able to build our frontend properly and just have to replace the data coming into it without changing any of our frontend code.

This is how our data is gonna come in from the backend, so we'll mock it!  Let's create a javascript file to hold it `dummyData.js` at `src/` 

```jsx
export const ProductsList = [
  {
    _id: "61c67c2916f6d70009932190",
    createdAt: "2021-12-25T02:04:25.797Z",
    updatedAt: "2021-12-25T02:04:25.797Z",
    name: "Canonic",
    description: "Super charge the back to your frontend.",
    brandImage: { url: null, alt: null, name: null },
    tags: [
      { label: "Dev Tools", value: "DEV_TOOLS" },
      { label: "Productivity", value: "PRODUCTIVITY" },
    ],
    upvotes: "498",
  },
  {
    _id: "61c67d0616f6d7000993219e",
    createdAt: "2021-12-25T02:08:06.651Z",
    updatedAt: "2021-12-25T02:08:06.651Z",
    name: "Mockups by Glorify",
    description: "Bring your designs to life with stunning mockups",
    brandImage: { url: null, alt: null, name: null },
    tags: [
      { label: "Design Tools", value: "DESIGN_TOOLS" },
      { label: "Free", value: "FREE" },
    ],
    upvotes: "470",
  },
  {
    _id: "61c67d4716f6d700099321a3",
    createdAt: "2021-12-25T02:09:11.389Z",
    updatedAt: "2021-12-25T02:09:11.389Z",
    name: "Chatfully",
    description: "All-in-one chat tool for teams - texting, web chat, & more",
    brandImage: { url: null, alt: null, name: null },
    tags: [{ label: "iPhone", value: "IPHONE" }],
    upvotes: "455",
  },
  {
    _id: "61c67d6f16f6d700099321a7",
    createdAt: "2021-12-25T02:09:51.782Z",
    updatedAt: "2021-12-25T02:09:51.782Z",
    name: "Nitro",
    description: "Fast, professional translations by native speakers, open API",
    brandImage: { url: null, alt: null, name: null },
    tags: [{ label: "Web App", value: "WEB_APP" }],
    upvotes: "410",
  },
  {
    _id: "61c67d8e16f6d700099321ab",
    createdAt: "2021-12-25T02:10:22.184Z",
    updatedAt: "2021-12-25T02:10:22.184Z",
    name: "SelectStar",
    description: "Automated data catalog and discovery for modern data teams.",
    brandImage: { url: null, alt: null, name: null },
    tags: [{ label: "Productivity", value: "PRODUCTIVITY" }],
    upvotes: "326",
  },
  {
    _id: "61c67da816f6d700099321af",
    createdAt: "2021-12-25T02:10:48.456Z",
    updatedAt: "2021-12-25T02:10:48.456Z",
    name: "Eraser",
    description: "A whiteboard that lets you focus on ideas",
    brandImage: { url: null, alt: null, name: null },
    tags: [
      { label: "Productivity", value: "PRODUCTIVITY" },
      { label: "Free", value: "FREE" },
    ],
    upvotes: "301",
  },
  {
    _id: "61c67dd316f6d700099321b4",
    createdAt: "2021-12-25T02:11:31.860Z",
    updatedAt: "2021-12-25T02:11:31.860Z",
    name: "Image to Cartoon",
    description: "Best AI cartoonizer online for free",
    brandImage: { url: null, alt: null, name: null },
    tags: [
      { label: "Productivity", value: "PRODUCTIVITY" },
      { label: "Free", value: "FREE" },
    ],
    upvotes: "264",
  }
];
```

📝  **Step 5: Create Products List**

 Create our list component - `ProductList` at `src/components/ProductList`. Create the respective `ProductList.js` and `index.js` files.  Add following code to your `index.js` file

```bash
export { default } from "./ProductList";
```

Coming to our main `ProductList.js` file, to build this component we'll be using the `List` component from *MaterialUI.* 

- We import the necessary dependencies from *MaterialUI*
- Import our DummyData
- Add a header for the List using the `Typography` component
- Iterate over the data

```bash
import React from "react";
import { Typography, Box, List, Divider } from "@mui/material";
import { ProductsList } from "../../dummyData";

const ProductList = () => {
  const products = ProductsList;

  return (
    <Box>
      <Typography
        variant="h5"
        sx={{
          marginTop: 3,
          marginBottom: 2,
          color: "#4b587c",
          "&:hover": {
            color: "#da552f",
          },
        }}
      >
        Products
      </Typography>

      <List>
        {products.map((product) => {
          return (
            <Box>
              // Show List Items Here
            </Box>
          );
        })}
      </List>
    </Box>
  );
};

export default ProductList;
```

We have the basic skeleton ready for our List, now we'll create our `ProductItem` component which will actually show the data for a particular item in the List. Create the component - `ProductItem` at `src/components/ProductList/components/ProductItem`. Create the respective `ProductItem.js` and `index.js` files.  Add following code to your `index.js` file

```jsx
export { default } from "./ProductItem";
```

Add the following code to the `ProductItem.js` file. The Item has the:

- Brand Image of the Product
- Name
- Description
- Tags Associated
- The Number of Upvotes

```jsx
import React from "react";
import { Avatar, Box } from "@mui/material";
import { ListItem, ListItemText, ListItemSecondaryAction } from "@mui/material";
import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp";

const ProductItem = ({
  name,
  description,
  tags,
  brandImage,
  upvotes = "0",
  isUpvoted = false,
  _id,
}) => {
  const [upvoted, setUpvoted] = React.useState(isUpvoted);

  const tagNames = tags.map((tag) => {
    return tag.label;
  });

  return (
    <ListItem disableGutters>
      <Avatar
        alt={name}
        src={brandImage.url ?? "notPresent"}
        sx={{ width: 80, height: 80, bgcolor: "#4b587c", marginRight: 2 }}
        variant="square"
      />
      <Box sx={{ display: "flex", flexDirection: "column" }}>
        <ListItemText
          primary={name}
          primaryTypographyProps={{
            fontSize: 16,
            fontWeight: "bold",
            letterSpacing: 0,
            color: "#21293c",
          }}
          secondary={description}
          secondaryTypographyProps={{ color: "#4b587c" }}
        ></ListItemText>
        <ListItemText
          primary={tagNames.join(" ・ ")}
          primaryTypographyProps={{
            fontSize: 11,
            fontWeight: 900,
            letterSpacing: 0,
            color: "#21293c",
          }}
        ></ListItemText>
      </Box>
      <ListItemSecondaryAction>
        // Add Upvote Button
      </ListItemSecondaryAction>
    </ListItem>
  );
};

export default ProductItem;
```

We'll create a custom `Upvote` Button component to and add it to our `ProductItem`. Create a new folder `UpvoteButton` at `src/components/ProductList/components/ProductItem/UpvoteButton`. Create the respective `UpvoteButton.js` and `index.js` files. This is how your folder structure should look like:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j8p7pl9nrsfsm2j7gltc.png)
 

Add following code to your `index.js` & `UpvoteButton.js` files respectively:

```jsx
export { default } from "./UpvoteButton";
```

```jsx
import { Button } from "@mui/material";
import { styled } from "@mui/material/styles";

const UpvoteButton = styled(Button, {
  shouldForwardProp: (prop) => prop !== "upvoted",
})(({ upvoted }) => ({
  fontWeight: "bold",
  ...(upvoted && {
    color: "#da552f",
    borderColor: "#da552f",
    "&:hover": {
      backgroundColor: "#fff",
      borderColor: "#da552f",
    },
  }),
  ...(!upvoted && {
    color: "#767676",
    borderColor: "#767676",
    "&:hover": {
      backgroundColor: "#fff",
      borderColor: "#da552f",
    },
  }),
}));

export default UpvoteButton;
```

Head back to `ProductItem.js` file, import our new button component and add it:

```jsx
// Import the button component
import UpvoteButton from "./Upvote Button";
```

```jsx
...

<ListItemSecondaryAction>
    <UpvoteButton
        upvoted={upvoted}
        variant="outlined"
        disableRipple={true}
        startIcon={<ArrowDropUpIcon />}
    >
        {upvotes}
    </UpvoteButton>
</ListItemSecondaryAction>

...
```

Let quickly add our `ProductItem` to our List component to see how it looks. Head back to `ProductList.js` file and import our component:

```jsx
import ProductItem from "./components/Product Item";
```

Add it to our List Component:

```jsx
...

<List>
    {products.map((product) => {
        return (
        <Box>
            <ProductItem {...product}></ProductItem>
            <Divider />
        </Box>
        );
    })}
</List>

...
```

Our `ProductList` component is ready, let's add it to our layout's to see how it looks. Head back to `App.js`, import `ProductList` & update the `<App>` component.

```jsx
import ProductList from "./components/Product List/ProductList";
```

```jsx
...

<Box
    sx={{
        margin: "20px 30px 0px 30px",
    }}
    >
    <Header></Header>
    <ProductList></ProductList>
</Box>

...
```

When you refresh your page, it should look like this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/daf0yhx8womgqpwormi8.png)
 

👩‍🔧  **Step 6: Setup Backend**

Let's head to ***Canonic*** and find the `ProductHunt` sample project from the Marketplace. You can either:

- Use this sample project to and continue, or
- Clone it and Deploy 🚀 . This will then use your data from your own project.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pv3c6fkgu5niusc9jywb.png) 

Once you deploy, the APIs will automatically be generated. Head on to the Docs and copy the `/Products` endpoint of the Products Table. This is the Get API that will fetch us the data from the database.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/epjcsqzos9bwnrgl6mzl.png)
 

👩‍🔧 **Step 7: Let's Integrate** 

Now, we need to hit the copied API endpoint to our backend and get all the products data. Head to your `ProductList.js` file and add the following code:

```jsx
// Replace the dummy data with the data coming in from the backend
const [products, setProducts] = React.useState([]);

// Fetch the list of products and see the data
React.useEffect(() => {
  fetch(`https://product-hunt-18dcc2.can.canonic.dev/api/products`)
    .then((res) => res.json())
    .then((json) => json?.data)
    .then((products) =>
      Array.isArray(products) ? setProducts(products) : null
    );
}, []);
```

Now when you'll refresh your page, it'll make the API call to your backend endpoint, fetch the data and display!

🔽 **Step 8: Integrate upvote functionality !** 

Let's Head over to your `ProductItem.js` file and trigger the upvote request on our backend.

- Create a function `handleUpvote` which will make the API to our backend to record the upvote
- Link the `handleUpvote` function to our upvote button's `onClick`.

Add the following code to do that:

```jsx
// Add handleUpvote function inside our ProductItem component
...

  const handleUpvote = () => {
    setUpvoted(!upvoted);
    fetch(`https://product-hunt-18dcc2.can.canonic.dev/api/upvotes`, {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        input: {
          product: _id,
        },
      }),
    })
      .then((res) => res.json())
      .then((json) => json?.data);
  };

...
```

```jsx
// Add the handleUpvote to the onClick of our Upvote Button

...

<ListItemSecondaryAction>
    <UpvoteButton
        upvoted={upvoted}
        variant="outlined"
        disableRipple={true}
        onClick={handleUpvote}
        startIcon={<ArrowDropUpIcon />}
    >
        {upvotes} 
    </UpvoteButton>
</ListItemSecondaryAction>

...
```

And with that, you have successfully made a basic Product Hunt clone for your project. 💃🕺

Congratulations! 🎉

---

If you want, you can also duplicate this project from Canonic's sample app and easily get started by customizing it as per your experience. Check it out [app.canonic.dev](http://app.canonic.dev/).

You can also check out our other guides [here](https://dev.to/canonic).

Join us on discord to discuss or share with our community. Write to us for any support requests at [support@canonic.dev](mailto:support@canonic.dev). Check out our [website](http://www.canonic.dev/) to know more about Canonic.