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
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
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";
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;
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;
It should like this:
šĀ 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",
}
];
šĀ 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";
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;
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";
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;
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:
Add following code to your index.js
& UpvoteButton.js
files respectively:
export { default } from "./UpvoteButton";
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:
// Import the button component
import UpvoteButton from "./Upvote Button";
...
<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:
import ProductItem from "./components/Product Item";
Add it to our List Component:
...
<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.
import ProductList from "./components/Product List/ProductList";
...
<Box
sx={{
margin: "20px 30px 0px 30px",
}}
>
<Header></Header>
<ProductList></ProductList>
</Box>
...
When you refresh your page, it should look like this:
š©āš§Ā 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.
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.
š©āš§Ā 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
);
}, []);
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'sonClick
.
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);
};
...
// 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.
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.
Top comments (0)