DEV Community

Simranjot for Canonic Inc.

Posted on

ProductHunt Clone: React + Lowcode backend

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

npx create-react-app product-hunt
Enter fullscreen mode Exit fullscreen mode

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

yarn add 
Enter fullscreen mode Exit fullscreen mode

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

export { default } from "./Header";
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

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:

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;
Enter fullscreen mode Exit fullscreen mode

It should like this:

Image description

šŸ“Ā  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/

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",
  }
];
Enter fullscreen mode Exit fullscreen mode

šŸ“Ā  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

export { default } from "./ProductList";
Enter fullscreen mode Exit fullscreen mode

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
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;
Enter fullscreen mode Exit fullscreen mode

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

export { default } from "./ProductItem";
Enter fullscreen mode Exit fullscreen mode

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
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;
Enter fullscreen mode Exit fullscreen mode

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

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

export { default } from "./UpvoteButton";
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode

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

// Import the button component
import UpvoteButton from "./Upvote Button";
Enter fullscreen mode Exit fullscreen mode
...

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

...
Enter fullscreen mode Exit fullscreen mode

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

import ProductItem from "./components/Product Item";
Enter fullscreen mode Exit fullscreen mode

Add it to our List Component:

...

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

...
Enter fullscreen mode Exit fullscreen mode

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.

import ProductList from "./components/Product List/ProductList";
Enter fullscreen mode Exit fullscreen mode
...

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

...
Enter fullscreen mode Exit fullscreen mode

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

Image description

šŸ‘©ā€šŸ”§Ā  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

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

šŸ‘©ā€šŸ”§Ā 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:

// 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
    );
}, []);
Enter fullscreen mode Exit fullscreen mode

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:

// 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);
  };

...
Enter fullscreen mode Exit fullscreen mode
// 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>

...
Enter fullscreen mode Exit fullscreen mode

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.

You can also check out our other guidesĀ here.

Join us on discord to discuss or share with our community. Write to us for any support requests atĀ support@canonic.dev. Check out ourĀ websiteĀ to know more about Canonic.

Latest comments (0)