In this media jam series, we will build a clone of Product Hunt, a website to share and discover new products with Next.js. In addition, we will use Auth0 for authentication, Airtable for storing product data, and Cloudinary for storing product images.
In part 1, we will configure the initial Next.js app with Auth0, Cloudinary, and Airtable. Then, we will fetch the data from Airtable and display them on the homepage. We will also discuss how to add new products to our app.
If you want to jump right into the code, check out the GitHub Repo here.
You can refer to the second part here: How to build a Product Hunt Clone with Next.js - Part 2 of 2
CodeSandbox
Since the preview in the CodeSandbox is embedded inside another page, you will see the products but you will not be able to log in via the preview. Instead, to explore and play navigate to deployed or live version, https://qc1kx.sse.codesandbox.io/.
How to Setup and Install Next.js
We will use Create Next App to initialize a Next.js project quickly. In your project's root directory, run the following commands in the terminal.
1npx create-next-app product-hunt-clone2cd product-hunt-clone3npm run dev
The last command, npm run dev
, will start the development server on your system's port 3000.
You can close the server by hitting the CTRL+C in the terminal.
In this tutorial, you will use Tailwind CSS to style your Product Hunt Clone app. Run the following command in the terminal to install Tailwind CSS.
1npm install -D tailwindcss@latest postcss@latest autoprefixer@latest @tailwindcss/forms2npx tailwindcss init -p
The last command will create a tailwind.config.js
and postcss.config.js
file in your project's root directory.
Update the tailwind.config.js
file like this.
1module.exports = {2 purge: {3 content: ["./pages/**/*.js", "./components/**/*.js"],4 options: {},5 },6 darkMode: false, // or 'media' or 'class'7 theme: {},8 plugins: [require("@tailwindcss/forms")],9};
@tailwindcss/forms
- A @tailwindcss plugin that provides a basic reset for form styles. You will use this to create the form for adding/updating new products.
Update styles/global.css
file like this.
1@tailwind base;2@tailwind components;3@tailwind utilities;
You can refer to https://tailwindcss.com/docs/guides/nextjs#install-tailwind-via-npm for detailed instructions on setting up a Next.js app with Tailwind CSS.
In this tutorial, you will use Auth0 for all your authentication and authorization needs. In addition, you will use the @auth0/nextjs-auth0
SDK for quick and easy installation and configuration.
Create an account on Auth0.com if you haven't already.
Run the following command in the project's terminal to install @auth0/nextjs-auth0
SDK.
1npm install @auth0/nextjs-auth0
On your Auth0 dashboard, create a Regular Web Application and name it Product Hunt Clone.
Under Settings-->Application URIs, configure the following URLs for your application:
- Allowed Callback URLs: http://localhost:3000/api/auth/callback
- Allowed Logout URLs: http://localhost:3000/
Remember to click on the SAVE CHANGES button afterward.
In your project, create a new file named .env.local
by running the following command.
1touch .env.local
Copy and paste the Client ID, Client Secret, and Domain of your application from your Auth0 dashboard inside the .env.local
file.
1# A long secret value used to encrypt the session cookie2AUTH0_SECRET='F(5^&wMh&nDvbr!!&jxJ01TYjh4reUcs'34# The base url of your application5AUTH0_BASE_URL='http://localhost:3000'67# The url of your Auth0 tenant domain8AUTH0_ISSUER_BASE_URL='https://YOUR_AUTH0_DOMAIN.auth0.com'910# Your Auth0 application's Client ID11AUTH0_CLIENT_ID='YOUR_AUTH0_CLIENT_ID'1213# Your Auth0 application's Client Secret14AUTH0_CLIENT_SECRET='YOUR_AUTH0_CLIENT_SECRET'
Create a Dynamic API Route handler at /pages/api/auth/[...auth0].js
by running the following command.
1cd pages/api2mkdir auth3touch auth/[...auth0].js
Add following code to the [...auth0].js
file.
1import { handleAuth } from '@auth0/nextjs-auth0';2export default handleAuth();
This will create the following urls: /api/auth/login
, /api/auth/callback
, /api/auth/logout
and /api/auth/me
.
Wrap your pages/_app.js
component in the UserProvider
component.
1// pages/_app.js2import "../styles/globals.css";3import { UserProvider } from "@auth0/nextjs-auth0";45function MyApp({ Component, pageProps }) {6 return (7 <UserProvider>8 <Component {...pageProps} />9 </UserProvider>10 );11}1213export default MyApp;
You will use Cloudinary, a media management platform for web and mobile developers, to store the media related to products like images or thumbnails.
Create a free account on Cloudinary if you haven't already.
After creating the account, head over to your dashboard and create a Product Hunt Clone folder; to store the product's images, take note of your Cloudinary's Cloud name, API Key, and API Secret.
Paste this Cloud name, API Key and API Secret in your .env.local
file along with other credentials.
1NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME = 'YOUR_CLOUDINARY_CLOUD_NAME'2CLOUDINARY_API_KEY = 'YOUR_CLOUDINARY_API_KEY'3CLOUDINARY_API_SECRET = 'YOUR_CLOUDINARY_API_SECRET'
Next.js has built-in support for loading environment variables from .env.local
into process.env
.
By default, all environment variables loaded through .env.local
are only available in the Node.js environment. This means that they won't be exposed to the browser. However, using the NEXT_PUBLIC_
prefix exposes the environment variable to the browser. You can read more about it here.
Run the following command in the terminal to install the Cloudinary React SDK and Cloudinary Node.js SDK.
1npm install cloudinary-react cloudinary
The next step is to configure your product's database, i.e., Airtable. Then, finally, create an Airtable account, if you haven't already.
After creating an account, create a base
named Product Hunt Clone, and inside this base, create a table named products
with the following schema.
Name
- Name of the product - Single line textDescription
- Short description of the product - Long textLink
- Link to the product website - URLSub
- Unique Identifier to identify the creator of product - Single line textPublicId
- Public Id of the Image stored on Cloudinary - Single line text
After creating the schema, make sure to add at least one entry to the table. You can copy the sample data from here. In the next part, you will update the schema to add upvotes of the product.
Head over to your Airtable account settings and copy your API Key.
Head over to https://airtable.com/api and choose the Product Hunt Clone base.
On your base's API documentation page, copy the base ID of the Product Hunt Clone base.
Paste both Airtable API Key and Airtable Base ID in your
.env.local
file.
1AIRTABLE_API_KEY = ''2AIRTABLE_BASE_ID = ''
You can refer to the .env.local
file of this project here.
Run the following command in the terminal to install the Airtable JS official API client, airtable
.
1npm install airtable
There are a few other dependencies that you will need in this project. Run the following command to install them.
1npm install react-dropzone
react-dropzone
- React hook to create a HTML5-compliant drag-drop zone for files. You can read more about this package here.
How To Create the Navbar Component
In this section, you will create the Navbar component, which will display the application name and a Login/SignUp button.
Run the following command in the project's root directory to create the Navbar.js
file inside the components
directory.
1mkdir components2cd components3touch Navbar.js
Add the following code to the Navbar.js
file.
1import React from "react";2import { useUser } from "@auth0/nextjs-auth0";3import Link from "next/link";45export default function Navbar() {6 const { user} = useUser();78 return (9 <div className="relative bg-white">10 <div className="max-w-7xl mx-auto px-2 sm:px-3">11 <div className="flex flex-col md:flex-row justify-between items-center border-b-2 border-gray-100 py-6 md:space-x-10 ">12 <div className="flex justify-start lg:w-0 lg:flex-1">13 <Link href="/">14 <a>15 <img16 className="h-10 w-auto sm:h-12"17 src="https://res.cloudinary.com/singhashutoshk/image/upload/v1622888633/product-hunt-clone/b2beawbhpjcy7kwnipo4.svg"18 alt=""19 />20 </a>21 </Link>22 <Link href="/">23 <a>24 <h1 className="leading-normal text-3xl md:text-4xl font-serif tracking-tight font-bold ml-1 text-gray-800">25 <span className="block text-purple-600 xl:inline">26 Product Hunt Clone27 </span>28 </h1>29 </a>30 </Link>31 </div>32 {user ? (33 <Link href="/products/insert">34 <a className="inline-flex font-medium text-gray-500 bg-transparent text-indigo-500 font-semibold px-3 py-2 my-1 border border-indigo-500 rounded">35 <svg36 xmlns="http://www.w3.org/2000/svg"37 className="h-6 w-6 hover:bg-white"38 fill="none"39 viewBox="0 0 24 24"40 stroke="#6366F1"41 >42 <path43 strokeLinecap="round"44 strokeLinejoin="round"45 strokeWidth={2}46 d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"47 />48 </svg>49 Add Product50 </a>51 </Link>52 ) : null}5354 {user ? (55 <div className=" flex items-center justify-end md:flex-1 lg:w-0">56 <Link href="/user/profile">57 <a className="whitespace-nowrap text-base font-medium text-gray-500 hover:text-gray-900">58 My Profile59 </a>60 </Link>6162 <Link href="/api/auth/logout">63 <a className="ml-8 whitespace-nowrap inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-base font-medium text-white bg-red-400 hover:bg-red-600">64 Log Out65 </a>66 </Link>67 </div>68 ) : (69 <Link href="/api/auth/login">70 <a className="ml-8 whitespace-nowrap inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-base font-medium text-white bg-purple-600 hover:bg-purple-700">71 Login72 </a>73 </Link>74 )}75 </div>76 </div>77 </div>78 );79}
You start by importing useUser()
hook from @auth0/nextjs-auth0
. The useUser()
hook will give you the UserProfile
object from the server-side session by requesting it from the HandleProfile
API Route handler.
The user
object from userUser()
hook contains the current user's information and defaults to undefined
if there is no user. Here is how it looks like.
1{2 "nickname": "lelouchB",3 "name": "ASHUTOSH KUMAR SINGH",4 "picture": "https://avatars.githubusercontent.com/u/45850882?v=4",5 "updated_at": "2021-06-06T05:52:11.117Z",6 "email": "ashutoshksingh@outlook.com",7 "sub": "github|45960883"8}
The sub
in the user
object is the unique identifier to identify the creator of the product.
In the Navbar.js
file, you use this user
object with the ternary operator to either show the Login
button or the Logout
button. The Login
and Logout
buttons are links with href
equal to /api/auth/login
and /api/auth/logout
routes.
Along with the Logout
button, you show a link, My Profile
, which takes the user to /user/profile
page. This page shows the user their name and their products. Also, in the Navbar, you create a link to the /products/insert
page. This page contains a form to add new products to the database.
You will create the /user/profile
page in part 2 and /products/insert
page later in this jam.
The next step is to import and use the Navbar
component to index.js
.
1// index.js2import Head from "next/head";3import { useEffect, useState } from "react";4import Navbar from "../components/Navbar";5import { useUser } from "@auth0/nextjs-auth0";67export default function Home() {8 const { user, error, isLoading } = useUser();910 if (isLoading) {11 return (12 <div className="mx-auto my-64 text-gray-800 text-center text-3xl">13 Loading14 </div>15 );16 }1718 return (19 <div className="contianer px-3">20 <Head>21 <title>Product Hunt Clone</title>22 <link rel="icon" href="/favicon.ico" />23 </Head>24 <Navbar />25 </div>26 );27}
Start the development server by running the npm run dev
command. Next, navigate to http://localhost:3000/ in your browser.
Here is how your app will look.
Click on the Login
button and either log in with Google OAuth or register a new user on the Auth0 Login Page.
Here is how the Navbar will look after you have logged in.
The next step is to create a container or a Card component that will be used to display the products with their name, image, and description.
How To Create the Product Component
In this section, you will create the Product component inside the components
directory.
Run the following command to create the Product
component.
1cd components2touch Product.js
Add the following command to the Product.js
file.
1import React from "react";2import { Image, Transformation } from "cloudinary-react";3import { useUser } from "@auth0/nextjs-auth0";4import { useRouter } from "next/router";56export default function Product({7 name,8 id,9 publicId,10 description,11 link,12 check,13}) {14 const router = useRouter();15 const { user, error, isLoading } = useUser();1617 const deleteThisProduct = async (e) => {18 await fetch("/api/deleteProduct", {19 method: "DELETE",20 body: JSON.stringify({ id, publicId }),21 headers: {22 "Content-Type": "application/json",23 },24 });25 router.reload();26 };2728 return (29 <div className="max-w-md mx-auto my-4 bg-white rounded-xl shadow-xl overflow-hidden md:max-w-2xl">30 <div className="md:flex">31 <div className="md:flex-shrink-0">32 <Image33 className="h-48 w-full object-cover md:w-48"34 publicId={publicId}35 cloudName={process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}36 alt={name}37 format="webp"38 secure39 >40 <Transformation width="800" gravity="auto" crop="fill" />41 </Image>42 </div>43 <div className="p-8 w-full">44 <div className="flex justify-between items-start">45 <a46 href={link}47 className="block mt-1 text-xl font-semibold leading-tight font-medium text-indigo-700 hover:underline"48 >49 {name}50 </a>51 </div>52 <p className="mt-2 text-gray-600 w-10/12">{description} </p>53 {check && (54 <div className="flex justify-end">55 <button56 className="mx-1 h-6 w-6"57 onClick={() =>58 router.push({59 pathname: "/update/[id]",60 query: {61 id: id,62 },63 })64 }65 >66 <svg67 xmlns="http://www.w3.org/2000/svg"68 className="h-6 w-6"69 fill="none"70 viewBox="0 0 24 24"71 stroke="gray"72 >73 <path74 strokeLinecap="round"75 strokeLinejoin="round"76 strokeWidth={2}77 d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"78 />79 </svg>80 </button>81 <button onClick={deleteThisProduct} className="px-1">82 <svg83 xmlns="http://www.w3.org/2000/svg"84 className="h-6 w-6"85 fill="none"86 viewBox="0 0 24 24"87 stroke="gray"88 >89 <path90 strokeLinecap="round"91 strokeLinejoin="round"92 strokeWidth={2}93 d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"94 />95 </svg>96 </button>97 </div>98 )}99 </div>100 </div>101 </div>102 );103}
Most of the above code is Tailwind CSS styling. You use the props passed to this component and display them on the page. Here are the props that are passed to this component.
name
- Name of the Productid
- Id of the productpublicId
-public_id
of the image, used with the Image component fromcloudinary-react
to display the product's image.description
- Short description of the Product.link
- URL to the product.check
- A boolean. You check whether the current user is the product's creator or not and pass eithertrue
orfalse
. It compares thesub
in the product with thesub
of the current user.
The above code uses the check
boolean to show the edit
and delete
buttons to the creator of the product. The edit
button takes the user to /update/[id]
page. The id
of the product is passed in the query of the route with the router
object. You can read more about the router
object here.
1<button2 className="mx-1 h-6 w-6"3 onClick={() =>4 router.push({5 pathname: "/update/[id]",6 query: {7 id: id,8 },9 })10 }11>12 // SVG Code13</button>
On the edit
or /update/[id]
page, you will use the product's id
to update it in the Airtable database.
When the delete
button is clicked, the deleteThisProduct
is triggered, sending a DELETE
request to /api/deleteProduct
route.
In the request body of this DELETE
request, you send both the id
and publicId
of the image. When a product is deleted, its data should be deleted from both Airtable and Cloudinary.
You will create this API route in the last section.
1const deleteThisProduct = async (e) => {2 await fetch("/api/deleteProduct", {3 method: "DELETE",4 body: JSON.stringify({ id, publicId }),5 headers: {6 "Content-Type": "application/json",7 },8 });9 router.reload();10 };
The router.reload()
functions reloads the current URL and is equivalent to window.location.reload()
.
Here is how the Product
component looks like.
How To Fetch and Display the Products
In this section, you will fetch the products from Airtable and display them on the app using the Product
component.
Create a new file named api.js
inside the lib
directory by running the following command.
1mkdir lib2cd lib3touch api.js
You will create all the functions to fetch, update or delete records from Airtable in this file.
Add the following code to api.js
file.
1const Airtable = require("airtable");23const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base(4 process.env.AIRTABLE_BASE_ID5);67const table = base("products");89export const getAllProducts = async () => {10 const data = await table11 .select({12 view: "Grid view",13 })14 .firstPage();15 return data.map((record) => {16 return { id: record.id, ...record.fields };17 });18};
You start by creating an instance of airtable
and then access the Product Hunt Clone
base bypassing the base ID and API key stored in the .env.local
file to this instance.
You then access the table products
bypassing "products"
to the Product Hunt Clone
base.
You create and export an asynchronous function named getAllProducts
which fetches all the products using the .select({view:"Grid view"})
and .firstPage()
methods to be displayed on the landing page. You can read more about these methods on your base's documentation page from where you copied the base ID.
Here is how the data array looks like.
1[2 Record {3 _table: Table {4 _base: [Base],5 id: null,6 name: 'products',7 // Functions8 },9 id: 'rec6Jv8UmrQk1ZoMc',10 _rawJson: {11 id: 'rec6Jv8UmrQk1ZoMc',12 fields: [Object],13 createdTime: '2021-06-04T07:37:01.000Z'14 },15 fields: {16 Sub: 'google-oauth2|1053578586999545372338041',17 Description: 'Final Space API is a RESTful API based on the animated television show Final Space. ',18 Name: 'Final Space API',19 PublicId: 'product-hunt-clone/qfadnlboijexxvuqan08',20 Link: 'https://finalspaceapi.com/'21 },22 // Functions23 }24]
You map over the data
array to destructure each record in the array to return only the id and the fields.
Here is how the array
looks like after destructuring. Less messy and easy to use.
1[{2 id: 'rec6Jv8UmrQk1ZoMc',3 Sub: 'google-oauth2|1053578586999545372338041',4 Description: 'Final Space API is a RESTful API based on the animated television show Final Space. ',5 Name: 'Final Space API',6 PublicId: 'product-hunt-clone/qfadnlboijexxvuqan08',7 Link: 'https://finalspaceapi.com/'8}]
The next step is to import and use the getAllProducts()
function in index.js
file.
Update index.js
file like this.
1import Head from "next/head";2import Product from "../components/Product";3import Navbar from "../components/Navbar";4import { useUser } from "@auth0/nextjs-auth0";5import { getAllProducts } from "../lib/api";67export default function Home({ products }) {8 const { user, error, isLoading } = useUser();910 if (isLoading) {11 return (12 <div className="mx-auto my-64 text-gray-800 text-center text-3xl">13 Loading14 </div>15 );16 }1718 return (19 <div className="contianer px-3">20 <Head>21 <title>Product Hunt Clone</title>22 <link rel="icon" href="/favicon.ico" />23 </Head>24 <Navbar />25 <span className="mb-4"></span>26 {products.length>0 &&27 products.map((product) => (28 <Product29 key={product.id}30 id={product.id}31 name={product.Name}32 link={product.Link}33 publicId={product.PublicId}34 description={product.Description}35 check={user && product.Sub === user.sub ? true : false}36 />37 ))}38 </div>39 );40}4142export async function getServerSideProps() {43 const products = await getAllProducts();44 return {45 props: { products },46 };47}
In the above code, you create and export async function getServerSideProps()
. This function uses the getAllProducts()
function to fetch the products from Airtable and pass it as props to the Home
component. You can read more about getServerSideProps()
function here.
You map over the products
array and pass its data to the Product
component. Notice the check
prop of the Product
component. You check whether the sub
of product is equal to the sub
of the current user.
1check={user && product.Sub === user.sub ? true : false}
Navigate to http://localhost:3000/ in your browser. Here is how your app will look.
How To Add New Products
In this section, you will create the form for adding new products. Run the following command to create the product/insert
page.
1cd pages2mkdir products3cd products4touch insert.js
Before proceeding further, copy the code for the
insert.js
file from GitHub and paste it to theinsert.js
file.Raw Link: https://raw.githubusercontent.com/lelouchB/product-hunt-clone-part-1/main/pages/products/insert.js
You might notice that in the insert.js
file, you are exporting the AddNewProduct
component with the withPageAuthRequired()
function. This function protects the page it is used with and redirects any anonymous user to the login page.
You will need to be logged in to access the /products/insert
page in simple words, which makes sense to discourage spam inserts in the database. You can read more about this method here.
1export default withPageAuthRequired(function AddNewProduct() {2 // Code3 }4)
In the insert.js
file, you are ultimately making a POST request to the /api/createProduct
API route with the product's name, link, description, and the data URL of the image.
The next step is to accept data from the user. You can split this task into two parts. The first is to store the textual data, i.e., name
, link
, description
, and the second, to create a Drag N Drop area to accept the image and then create a data URL of that image. You will also show the preview of the image to the user.
- You initialize three states, namely
name
,link
, anddescription
. These states stores the name, link, and description of the product using thevalue
property andonChange()
event handler.
1<input2 type="text"3 name="product_name"4 id="product_name"5 value={name}6 onChange={(e) => setName(e.target.value)}7 autoComplete="given-name"8 className="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"9 required10/>
- Creating Drag N Drop is rather tricky compared to storing the textual data. You will use the
react-dropzone
library installed in the first section for the drag n drop component.
1const { getRootProps, getInputProps } = useDropzone({2 accept: "image/*",3 multiple: false,4 onDrop: (acceptedFiles) => {5 const file = acceptedFiles[0];6 const reader = new FileReader();7 reader.readAsDataURL(file);8 reader.onloadend = () => {9 setUploadImage(reader.result);10 };11 reader.onerror = () => {12 console.error("Something has happend.");13 };1415 setFiles(16 acceptedFiles.map((file) =>17 Object.assign(file, {18 preview: URL.createObjectURL(file),19 })20 )21 );22 },23});
In the above code, you use the useDropzone
hook to configure your dropzone. You set accept
property to image only and the multiple
property to false
, which means you can only upload one image.
Inside the onDrop
property, you use the FileReader API's .readAsDataURL()
method to convert the image to data URL. This data URL is stored inside the uploadImage
state.
Here is how the data URL of an image looks like. This data URL has been trimmed. You can see the original data URL here.
1data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB2oAAAOMCAYAAACM7zM................
You use the .createObjectURL()
method to convert the image to Object URL stored inside the file
state and is later used to preview the image.
Here is how the Object URL of an image looks like.
1blob:http://localhost:3000/6d511357-c6fa-43ce-9374-0f5ddb7c5445
This file
state is used to preview the selected image to the user.
1<aside className=" flex flex-row flex-wrap justify-center mt-2">2 <span className="flex min-w-0 overflow-hidden">3 <img src={file} className="block w-auto h-32 w-32" />4 </span>5</aside>
When the Add New Product
button is clicked, the handleSubmit
function is triggered which sends the data in a POST request to /api/createProduct
route.
1const createProduct = async ({ name, description, link, uploadImage }) => {2 try {3 await fetch("/api/createProduct", {4 method: "POST",5 body: JSON.stringify({ name, description, link, uploadImage }),6 headers: {7 "Content-Type": "application/json",8 },9 });10 } catch (err) {11 console.error(err);12 }13};1415const handleSubmit = async (e) => {16 await e.preventDefault();17 await setName("");18 await setDescription("");19 await setLink("");20 await setUploadImage("");21 await setFile("");22 await setLoading(true);23 await createProduct({ name, description, link, uploadImage });24 await router.push("/");25};
While the data is being added to Airtable, the loading
state is set to true
, which makes the form disappear and a message
Uploading! New Product is being added.
is shown to the user. After a successful POST request, the user is redirected to the landing page.
The next step is to create the /api/createProduct
route or createProduct.js
file inside the pages/api
directory. Run the following command in the terminal to create the file.
1touch pages/api/createProduct.js
Add the following code to the createProduct.js
file.
1import { withApiAuthRequired, getSession } from "@auth0/nextjs-auth0";2const cloudinary = require('cloudinary').v2;3var Airtable = require('airtable');45cloudinary.config({6 cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,7 api_key: process.env.CLOUDINARY_API_KEY,8 api_secret: process.env.CLOUDINARY_API_SECRET,9});1011const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base(12 process.env.AIRTABLE_BASE_ID13);1415export const config = {16 api: {17 bodyParser: {18 sizeLimit: "3mb",19 },20 },21};2223export default withApiAuthRequired(async function handler(req, res) {24 const session = getSession(req, res);25 const sub = await session.user.sub;2627 if (req.method !== "POST") {28 return res.status(405);29 }30 const { name, description, link, uploadImage } = req.body;3132 const image = await cloudinary.uploader.upload(33 uploadImage,34 { folder: "product-hunt-clone" },35 (error, result) => {36 if (error) {37 console.error("An error has occured while uploading the image");38 }39 console.log("Image was uploaded successfully");40 }41 );42const public_id = image.public_id;4344 base('products').create([45 {46 "fields": {47 "Name": name,48 "Description": description,49 "Link": link,50 "Sub": sub,51 "PublicId": public_id52 }53 }54 ], function(err, records) {55 if (err) {56 console.error(err);57 return res.status(500).json({ msg: "Something went wrong." });58 }59 });60 return res.status(200).json({ msg: "Product Added" });6162});
You might notice that similar to the /product/insert
route, this route is also protected, and anonymous users cannot access it. The only difference is that this route is protected with the withApiAuthRequired()
function and not the withPageAuthRequired()
function. You can read more about the withApiAuthRequired()
function here.
You will also notice that in this API route you export a config
object from this API route. This limits the request body size to 3mb.
1export const config = {2 api: {3 bodyParser: {4 sizeLimit: "3mb",5 },6 },7};
To store the public_id
of the image in Airtable, you first need to upload the image to Cloudinary and then use the public_id
sent from Cloudinary to create a new record in Airtable.
You create an instance of cloudinary
and pass the Cloud Name, API Key, and API Secret, stored in the .env.local
file, to the instance. You then use the uploader.upload()
function to upload the data URL of the image inside the product-hunt-clone
folder in your Cloudinary account. You store the public_id
of the uploaded image in the public_id
variable.
You create an instance of airtable
similar to the one created in the lib/api.js
file and create a new record using create()
method.
1base("products").create([2 {3 fields: {4 Name: name,5 Description: description,6 Link: link,7 Sub: sub,8 PublicId: public_id,9 },10 },11])
One thing to notice is how you are getting the user's sub
since it was not sent in the request body along with other data. The sub
is accessed using the getSession()
function from @auth0/nextjs-auth0
SDK.
1const session = getSession(req, res);2const sub = await session.user.sub;
Head over to http://localhost:3000/products/insert in the browser or click on Add Product
button in the Navbar.
Here is how this page will look.
Add some dummy data and click on Add New Product
button.
Once the new product has been added in the Airtable, you will be redirected to the landing page.
How To Delete a Product
You have already created the delete
button. which triggers deleteThisProduct()
function, in the Product
component. The deleteThisProduct
function makes a DELETE request to the /api/deleteProduct
API route.
Run the following command to create the deleteProduct.js
file in the pages/api
directory.
1touch pages/api/deleteProduct.js
Add the following code to deleteProduct.js
file.
1import { withApiAuthRequired } from "@auth0/nextjs-auth0";2const cloudinary = require("cloudinary").v2;3var Airtable = require("airtable");45cloudinary.config({6 cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,7 api_key: process.env.CLOUDINARY_API_KEY,8 api_secret: process.env.CLOUDINARY_API_SECRET,9});1011const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base(12 process.env.AIRTABLE_BASE_ID13);1415export default withApiAuthRequired(async function handler(req, res) {16 if (req.method !== "DELETE") {17 return res.status(405);18 }19 const { id, publicId } = req.body;2021 try {22 const deletedProduct = await base("products").destroy(id);23 const deleteImageFromCloudinary = cloudinary.uploader.destroy(24 publicId,25 function (error, result) {26 if (error) {27 console.log(error);28 }29 }30 );31 return res.status(200).json({ msg: "Product Deleted" });32 } catch (err) {33 console.error(err);34 res.status(500).json({ msg: "Something went wrong." });35 }36});
In the above cove, you use the destroy(id)
method to delete the record from Airtable.
You use the uploader.destroy()
method to destroy or delete the image from Cloudinary. After successful deletion, the Product Deleted
message is sent as a response to the frontend.
Conclusion
In this media jam, we configure our initial Next.js app and created Navbar
and Product
components. We also fetched and displayed the products on the home page. Finally, we made a form to add new products to the database.
In the next part, we will
- Create the
My Profile
page. - Create the
/update/[id]
page to update the product. - Create the
upvote
button in theProduct
component to allow logged-in users to upvote a product.
You can refer to the second part here: How to build a Product Hunt Clone with Next.js - Part 2 of 2
Here are a few resources that you might find helpful.
Happy coding!