Introduction
This jam is the final part of a two-part series on creating a Product Hunt Clone in Next.js. In the first part, we configured the initial Next.js app with Auth0, Cloudinary, and Airtable. Then, we fetched and displayed the products on the homepage. We also discussed how to add and delete products in our app.
In this part, we will build the User Profile page where users can see their products and add the functionality to update the products. We will also discuss how to upvote a product.
If you want to jump right into the code, check out the GitHub Repo here.
You can refer to the first part here: How to build a Product Hunt Clone with Next.js - Part 1 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 version, https://d00re.sse.codesandbox.io/.
How to create User Profile page
In this section, you will create the My Profile
page, showing the current user's information and their products. You have already created the link to the My Profile
page in the Navbar component, i.e., /user/profile
.
You can split the My Profile
page into two parts. First, you need to show the user information, and second, you need to show the products created by that user. You can easily extract user information from the user
object of the useUser()
hook. For the second part, you will need to create a function in the lib/api.js
file to get the products where the Sub
of product is equal to the current user's sub
.
Create and export a function named getProductsByUserSub()
by adding the following code to lib/api.js
file.
1export const getProductsByUserSub = async (sub) => {2 const data = await table3 .select({4 filterByFormula: `Sub = "${sub}"`,5 })6 .firstPage();7 return data.map((record) => {8 return { id: record.id, ...record.fields };9 });10};
In the above code, you pass the sub
of the user as an argument to the function and, with the help of the filterByFormula
method, filter the records where the Sub
of product is equal to the current user's sub
.
The next step is to create the profile.js
file under the pages/user
directory. Run the following command in the terminal.
1mkdir pages/user2touch pages/user/profile.js
Add the following code to the user/profile.js
file.
1// pages/user/profile.js2import Head from "next/head";3import { useUser, withPageAuthRequired, getSession } from "@auth0/nextjs-auth0";4import Product from "../../components/Product";5import Navbar from "../../components/Navbar";6import {getProductsByUserSub} from "../../lib/api"78export default function Profile({products}) {9 const { user, error, isLoading } = useUser();1011 if (isLoading) {12 return (13 <div className="mx-auto my-64 text-gray-800 text-center text-3xl">14 Loading15 </div>16 );17 }18 if (error) return <div>{error.message}</div>;1920 return (21 <div className="contianer px-2">22 <Head>23 <title> User: {user.name}</title>24 <link rel="icon" href="/favicon.ico" />25 </Head>26 <Navbar />27 <div className="flex space-x-4 px-4 my-10 justify-around">28 <div className="h-36 w-96 rounded-3xl shadow-lg relative flex flex-col29 items-center justify-between md:items-start p-5 transition-all duration-150">30 <img31 className="rounded-full w-20 h-20 shadow-sm absolute -top-832 transform md:scale-110 duration-700"33 src={user.picture}34 alt={user.name}35 />3637 <div className=" align-middle text-2xl font-semibold text-gray-80038 text-center m-auto md:m-0 md:mt-8">39 {user.name}40 </div>41 <ul className="text-lg text-gray-600 font-light md:block">42 <li>Products: {products ? products.length : 0}</li>43 </ul>44 </div>45 </div>4647 {products.length>0 ? (48 products.map((product) => (49 <Product50 key={product.id}51 id={product.id}52 name={product.Name}53 link={product.Link}54 description={product.Description}55 publicId={product.PublicId}56 check={true}57 />58 ))59 ) : (60 <div className="mx-auto my-12 text-gray-800 text-center text-3xl">61 You are yet to create your first Product.62 </div>63 )}64 </div>65 );66}6768export const getServerSideProps = withPageAuthRequired({69 returnTo: "/api/auth/login",70 async getServerSideProps(ctx) {71 const sub = await getSession(ctx.req).user.sub;72 const data = await getProductsByUserSub(sub);73 return {74 props: {75 products: data,76 },77 };78 },79});
In the above code, you are using Next.js getServerSideProps
function to fetch the products created by the current user with the help of the getProductsByUserSub()
function.
You will notice that you have wrapped the getServerSideProps()
function with withPageAuthRequired()
method from the @auth0/nextjs-auth0
SDK. This withPageAuthRequired
protects the My Profile
page from anonymous logins and redirects them to the login page. You can read more about this method here.
The getProductsByUserSub()
function requires the user's sub
to fetch the products, accessed using the getSession()
method. The getSession()
method gets the user's session from the request. You can read more about this method here.
The records from Airtable are passed as props to the Profile
component. In this component, you use the user
object to show the current user's name and profile picture.
You also show the total number of products created by the user by calculating the length of the products
array. You then map over the products
array, and similar to the index.js
file, show the products on the page using the Product
component.
Since the products fetched from Airtable are created by the current user, you pass true
in the check
prop of the Product
component.
There is also a default message if the current user has not created any products.
Here is how the My Profile
page will look if the user has not created any product.
Here is the My Profile
page after the user has created a product.
How to update a Product
In this section, you will create the update/[id]
dynamic route to edit/update a product. In this jam, you will update only the textual data of the product and not its image.
To update the product, you will create a form with Name
, Link
, and Description
fields. Then, you will fetch the selected product and prefill the form with its data so that the user can see and update the product accordingly.
You might remember that in the Product
component, you were sending the id
of the product in the query parameter.
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>
You will use this id
in the query to fetch the corresponding product from Airtable.
Create and export functions named getProductById()
and updateProduct()
in lib/api.js
file.
1export const getProductById = async (id) => {2 const data = await table.find(id);3 return { id: data.id, ...data.fields };4};56export const updateProduct = async ({ id, name, link, description }) => {7 const data = await table.update(8 id,9 {10 Name: name,11 Description: description,12 Link: link,13 },1415 function (err, records) {16 if (err) {17 console.error(err);18 }19 }20 );21};
In the getProductById
function, you use Airtable's find()
method to retrieve the product based on its id.
In the updateProduct
function, you use Airtable's update
method to update the product. You pass the product's id
to be updated as the first argument, followed by an object containing the fields to be updated, i.e., Name
, Description
, and Link
. The remaining fields like Sub
and PublicId
remain unchanged.
Run the following command to create the [id].js
under the pages/update
directory.
1mkdir pages/update2touch pages/update/[id].js
Add the following code to [id].js
file.
1import React, { useState } from "react";2import Head from "next/head";3import { useUser, withPageAuthRequired } from "@auth0/nextjs-auth0";4import { getProductById } from "../../lib/api";5import { useRouter } from "next/router";6import Navbar from "../../components/Navbar";78const UpdateProduct = ({ product }) => {9 const router = useRouter();1011 const [name, setName] = useState(product.Name);12 const [link, setLink] = useState(product.Link);13 const [description, setDescription] = useState(product.Description);14 const { user, error, isLoading } = useUser();1516 if (isLoading) return <div>Loading</div>;17 if (error) return <div>{error.message}</div>;1819 const handleSubmit = async (e) => {20 await e.preventDefault();21 const data = await fetch("/api/updateProduct", {22 method: "POST",23 body: JSON.stringify({ id: router.query.id, name, description, link }),24 headers: {25 "Content-Type": "application/json",26 }27 });28 await router.replace("/");29 };3031 if (user) {32 return (33 <div>34 <Head>35 <title> Update Product : {product.Name} </title>36 <link rel="icon" href="/favicon.ico" />37 </Head>38 <Navbar />39 <div className="md:grid justify-items-center md:gap-6">40 <div className="mt-0 md:mt-0 md:col-span-2">41 <h3 className="text-4xl text-center font-normal leading-normal mt-0 text-indigo-800">42 Update Product: {product.Name}43 </h3>44 <form>45 <div className="shadow-md sm:rounded-md sm:overflow-hidden">46 <div className="px-4 py-5 bg-white space-y-6 sm:p-6">47 <div className="grid grid-cols-3 gap-6">48 <div className="col-span-3 sm:col-span-2">49 <label50 htmlFor="product_name"51 className="block text-sm font-medium text-gray-700"52 >53 Name of the Product54 </label>55 <div className="mt-1 flex rounded-md shadow-sm">56 <input57 type="text"58 name="product_name"59 id="product_name"60 value={name}61 onChange={(e) => setName(e.target.value)}62 autoComplete="given-name"63 className="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"64 />65 </div>66 </div>67 </div>6869 <div>70 <label71 htmlFor="product_description"72 className="block text-sm font-medium text-gray-700"73 >74 Description75 </label>76 <div className="mt-1">77 <textarea78 id="product_description"79 name="product_description"80 rows={3}81 value={description}82 onChange={(e) => setDescription(e.target.value)}83 className="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"84 placeholder="Brief description for your Product."85 />86 </div>87 </div>88 <div className="grid grid-cols-3 gap-6">89 <div className="col-span-3 sm:col-span-2">90 <label91 htmlFor="product_link"92 className="block text-sm font-medium text-gray-700"93 >94 Link95 </label>96 <div className="mt-1 flex rounded-md shadow-sm">97 <input98 type="text"99 name="product_link"100 id="product_link"101 value={link}102 onChange={(e) => setLink(e.target.value)}103 className="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-md sm:text-sm border-gray-300"104 placeholder="www.example.com"105 />106 </div>107 </div>108 </div>109 </div>110 <div className="px-4 py-2 bg-gray-50 text-right sm:px-6">111 <button112 type="submit"113 onClick={handleSubmit}114 className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"115 >116 Update117 </button>118 </div>119 </div>120 </form>121 </div>122 </div>123 </div>124 );125 }126};127128export default UpdateProduct;129130export const getServerSideProps = withPageAuthRequired({131 returnTo: "/api/auth/login",132 async getServerSideProps(ctx) {133 const data = await getProductById(ctx.params.id);134135 return {136 props: {137 product: data,138 },139 };140 },141});
You use the same code for the form as in the product/insert.js
file.
Similar to /user/profile.js
file, you use the getServerSideProps()
function wrapped with the withPageAuthRequired
method to redirect any anonymous user to the login page.
You access the product's id
using the params
object and pass it to the getProductById()
function. Finally, the product fetched is sent to the UpdateProduct
function as a prop.
You define three states for name, description, and link of the product and set their default value to their corresponding value in the product
object.
Then, in the UpdateProduct
function's return
statement, you create a two-way data bind between the user input and the states.
Once the user has updated the product and clicks the Update
button, the handleSubmit()
function is triggered, sending a POST request to /api/updateProduct
API route with the id
, name
, description
, and link
of the product in the request body.
Run the following commands to create the /api/updateProduct
API route.
1touch pages/api/updateProduct.js
Add the following code to updateProduct.js
file.
1import { withApiAuthRequired } from "@auth0/nextjs-auth0";2import {updateProduct} from "../../lib/api"34export default withApiAuthRequired(async function handler(req, res) {5 if (req.method !== "POST") {6 return res.status(405);7 }8 const { name, description, link, id } = req.body;910 await updateProduct({ id, name, description, link });11 return res.status(200).json({ msg: "Product Updated" });1213});
You protect this API route with the withApiAuthRequired()
method from @auth0/nextjs-auth0
SDK. Then, you import the updateProduct
function from the lib/api.js
file and pass the arguments sent in the request body to it. Finally, after the product has been updated successfully, Product Updated
is sent as a response.
Here is a GIF showing the Update Product feature in action.
How to upvote a Product
In this section, you will create the upvote feature in Products. Like the actual Product Hunt, a user should be able to upvote a product only once and should be able to remove their upvote from the product. For tutorial purposes, we will call the process of removing the upvote - downvote.
Note: You are not actually downvoting a product; you are removing your upvote from it.
For every upvote, you need to keep track of the id
of the product and the unique identifier of a user, i.e., the user's sub
. You can store the id
and sub
together to represent an upvote.
By checking if the user's sub
is already associated with a product, i.e., a pair of the current user's sub
and the product id
, you can make sure each user votes for a product only once. Then, when the user removes their upvote, you can delete this product id
and sub
pair.
If you were using a NoSQL database like MongoDB, you could have created a new field in the schema and store the sub
of the current user in an array when the upvote button is clicked. This array would contain the sub
of all the users who upvoted a particular product. And to downvote a product, you could remove that user's sub
from the array.
The resultant product
object would look similar to this.
1{2 "id": "rec176WGNlRJGN0mg",3 "Name": "Dev Desks",4 "Description": "The place where Devs share Desks! ",5 "PublicId": "product-hunt-clone/wji2dipmt3cssvvwbr06",6 "Link": "https://devdesks.io/",7 "Sub": "google-oauth2|10535765999545372338041",8 "Votes": [9 "google-oauth2|10535765999545372338041",10 "google-oauth2|1053576593456372338041",11 "google-oauth2|105357659995452323041"12 ]13}
But, you cannot create an array object in Airtable, so its not possible to store the votes in the same manner as in a NoSQL database.
Here are a few workarounds for this limitation:
- You can create a
Long text
field in the schema and store thesub
as strings separated by commas and then use thesplit(',')
andjoin()
methods to convert them into an array. Here you will have to make sure that when the user upvotes a product, you add to the existing string to retain the previous votes. - You can create another table in the same base named
votes
, and for each vote, you can store bothsub
and theid
of the product in it. You can then filter out the votes of a product using its id.
You will use the second method in this jam and create a new table named votes
in the Product Hunt Clone
base.
Head over to your Airtable account and open the Product Hunt Clone
base. Create a new table named votes
with the following schema.
ProductId
- Id of the product upvoted.Sub
- Sub of the user who upvoted the product.
You can refer to the votes
table of this project here.
Each record in the votes
table represents a vote. So, for example, if there is one product and three users in your Product Hunt Clone app. And each user upvotes the product; there will be three records in the votes
table and one record in the products
table.
The next step is to create the getVotesByProductId
, upvoteProduct
and downvoteProduct
functions in lib/api.js
. Add the following code to lib/api.js
file.
1export const getVotesByProductId = async (productId) => {2 const data = await base("votes")3 .select({4 view: "Grid view",5 filterByFormula: `ProductId = "${productId}"`,6 })7 .firstPage();89 return data.map((vote) => {10 return { id: vote.id, ...vote.fields };11 });12};13export const upvoteProduct = async ({ productId, sub }) => {14 const data = await base("votes").create({15 ProductId: productId,16 Sub: sub,17 });18};1920export const downvoteProduct = async ({ id }) => {21 const data = await base("votes").destroy(id);22};
The getVotesByProductId()
functions filter out the records based on the product id
. This means that the response returned from this function is the votes of a single product.
The upvoteProduct
function creates a new record in the votes
table with the current user's sub and the product's id
upvoted. The productId
passed to the upvoteProduct
function is the id
of the product.
The downvoteProduct
function deletes the vote or record from the votes
table. The id
passed to the downvoteProduct
function is the id
of the vote. You might ask how to know which vote or record to delete from the votes
table. Or which vote id
should be passed in the downvoteProduct
function.
Since each user can only vote once, if the user has upvoted a product, then the response returned from the getVotesByProductId
function will have an object containing the sub
of that user. You can filter out that object from all the objects in the array. That object will contain the id
of the vote to be deleted.
The next step is to create the API routes for these functions. Create the getVotesByProductId/[id]
, upvote
and downvote
API routes in the api
directory.
1touch pages/api/upvote.js2touch pages/api/downvote.js3mkdir pages/api/getVotesByProductId4touch pages/api/getVotesByProductId/[id].js
Add the following code to getVotesByProductId/[id].js
file.
1import { getVotesByProductId } from "../../../lib/api";23export default async function handler(req, res) {4 if (req.method !== "GET") {5 return res.status(405);6 }78 const { id } = req.query;910 try {11 const votes = await getVotesByProductId(id );12 return res.status(200).send(votes);13 } catch (err) {14 console.error(err);15 res.status(500).json({ msg: "Something went wrong." });16 }17}
You use the id
of the product extracted from the request query and pass it to the getVotesByProductId
function. The data returned from the getVotesByProductId
function is sent as the response.
Add the following code to upvote.js
file.
1import { upvoteProduct } from "../../lib/api";2import { withApiAuthRequired, getSession } from "@auth0/nextjs-auth0";34export default withApiAuthRequired(async function handler(req, res) {5 const session = getSession(req, res);6 const sub = await session.user.sub;78 if (req.method !== "PUT") {9 return res.status(405);10 }11 const { id } = req.body;1213 try {14 const upvotedProduct = await upvoteProduct({ productId:id, sub });15 return res.status(200).json({ msg: "Upvoted" });16 } catch (err) {17 console.error(err);18 res.status(500).json({ msg: "Something went wrong." });19 }20});
In the above code, the product's id
is extracted from the request body while the sub
is accessed using the getSession()
method from @auth0/nextjs-auth0
SDK. Finally, both the id
and sub
are passed to the upvoteProduct
function.
Add the following code to downvote.js
file.
1import { downvoteProduct } from "../../lib/api";2import { withApiAuthRequired } from "@auth0/nextjs-auth0";34export default withApiAuthRequired(async function handler(req, res) {56 if (req.method !== "PUT") {7 return res.status(405);8 }9 const { id } = req.body;1011 try {12 const downvotedProduct = await downvoteProduct({ id });13 return res.status(200).json({ msg: "Downvoted" });14 } catch (err) {15 console.error(err);16 res.status(500).json({ msg: "Something went wrong." });17 }18});
Every vote in the votes
table is a record. You delete a vote is deleted in the same way a product was deleted in the first part of the series, i.e., you use the destroy
method and pass the id
of the vote to it.
You will fetch the votes using the getVotesByProductId/[id]
API route in the Product
component. But, you cannot use the getServerSideProps()
method in the Product
component.
You will need to install the swr
package to fetch the votes. You can read more about swr
package here.
Run the following command in the terminal.
1npm install swr
Update the components/Product.js
file like this.
1import React from "react";2import { Image, Transformation } from "cloudinary-react";3import { useUser } from "@auth0/nextjs-auth0";4import { useRouter } from "next/router";5import useSWR from "swr";6import Vote from "./Vote";78export default function Product({9 name,10 id,11 publicId,12 description,13 link,14 check,15}) {16 const router = useRouter();17 const { user, error, isLoading } = useUser();18 const { data, mutate } = useSWR(`/api/getVotesByProductId/${id}`);1920 const deleteThisProduct = async (e) => {21 await fetch("/api/deleteProduct", {22 method: "DELETE",23 body: JSON.stringify({ id, publicId }),24 headers: {25 "Content-Type": "application/json",26 },27 });28 await router.reload();29 };3031 return (32 <div className="max-w-md mx-auto my-4 bg-white rounded-xl shadow-xl overflow-hidden md:max-w-2xl">33 <div className="md:flex">34 <div className="md:flex-shrink-0">35 <Image36 className="h-48 w-full object-cover md:w-48"37 publicId={publicId}38 cloudName={process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}39 alt={name}40 format="webp"41 secure42 >43 <Transformation width="800" gravity="auto" crop="fill" />44 </Image>45 </div>46 <div className="p-8 w-full">47 <div className="flex justify-between items-start">48 <a49 href={link}50 className="block mt-1 text-xl font-semibold leading-tight font-medium text-indigo-700 hover:underline"51 >52 {name}53 </a>54 <Vote votes={data} refreshVotes={mutate} productId={id} />55 </div>56 <p className="mt-2 text-gray-600 w-10/12">{description} </p>5758 {check && (59 <div className=" flex justify-end">60 <button61 className="mx-1 h-6 w-6"62 onClick={() =>63 router.push({64 pathname: "/update/[id]",65 query: {66 id: id,67 },68 })69 }70 >71 <svg72 xmlns="http://www.w3.org/2000/svg"73 className="h-6 w-6"74 fill="none"75 viewBox="0 0 24 24"76 stroke="gray"77 >78 <path79 strokeLinecap="round"80 strokeLinejoin="round"81 strokeWidth={2}82 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"83 />84 </svg>85 </button>86 <button onClick={deleteThisProduct} className="px-1">87 <svg88 xmlns="http://www.w3.org/2000/svg"89 className="h-6 w-6"90 fill="none"91 viewBox="0 0 24 24"92 stroke="gray"93 >94 <path95 strokeLinecap="round"96 strokeLinejoin="round"97 strokeWidth={2}98 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"99 />100 </svg>101 </button>102 </div>103 )}104 </div>105 </div>106 </div>107 );108}
You use the useSWR
hook to fetch the data from the getVotesByProductId/[id]
API route.
1const { data, mutate } = useSWR(`/api/getVotesByProductId/${id}`);
The data fetched is stored in the data
variable. This data
array is passed to the Votes
component along with id
of the product and mutate
function.
1<Vote votes={data} refreshVotes={mutate} productId={id} />
The mutate
in the above code is a function that is pre-bound to the SWR's key or /api/getVotesByProductId/${id}
. This function updates the data
array without any need to refresh the page. This means that if you upvote a product and call this function, the data
array will be updated to include the latest addition without any reload of the whole page. You can read more about mutation here.
The next step is to create the Vote
component in the components
directory. Run the following command to create the Vote
component.
1touch components/Vote.js
Add the following code to the Vote.js
file.
1import React from "react";2import { useUser } from "@auth0/nextjs-auth0";3import { useRouter } from "next/router";45export default function Vote({ votes, refreshVotes, productId }) {6 const router = useRouter();7 const { user, error, isLoading } = useUser();89 const voteOfThisUser = user && votes ? votes.filter(function(vote){10 return vote.Sub == user.sub;11 }) : null;1213 const upvoteThisProduct = async (e) => {14 await e.preventDefault();15 if (!user) {16 router.push("/api/auth/login");17 } else {18 await fetch("/api/upvote", {19 method: "PUT",20 body: JSON.stringify({ id: productId }),21 headers: {22 "Content-Type": "application/json",23 },24 });25 refreshVotes();26 }27 };2829 const downvoteThisProduct = async (e) => {30 await e.preventDefault();31 await fetch("/api/downvote", {32 method: "PUT",33 body: JSON.stringify({ id: voteOfThisUser[0].id }),34 headers: {35 "Content-Type": "application/json",36 },37 });38 refreshVotes();3940 };41 if (!votes || isLoading || error) return <div>...</div>4243 return (44 <div className="flex">45 {user && votes.some((elem) => elem.Sub === user.sub) ? (46 <button47 className="flex border shadow-sm border-purple-800 rounded-md p-1"48 onClick={downvoteThisProduct}49 >50 <svg51 xmlns="http://www.w3.org/2000/svg"52 className="h-6 w-6"53 viewBox="0 0 20 20"54 fill="purple"55 >56 <path57 fillRule="evenodd"58 d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.707l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L9 9.414V13a1 1 0 102 0V9.414l1.293 1.293a1 1 0 001.414-1.414z"59 clipRule="evenodd"60 />61 </svg>62 <p className="text-purple-700">63 { votes.length}64 </p>65 </button>66 ) : (67 <button68 className="flex border border-gray p-1 shadow-sm rounded-md"69 onClick={upvoteThisProduct}70 >71 <svg72 xmlns="http://www.w3.org/2000/svg"73 className="h-6 w-6"74 fill="none"75 viewBox="0 0 24 24"76 stroke="gray"77 >78 <path79 strokeLinecap="round"80 strokeLinejoin="round"81 strokeWidth={2}82 d="M9 11l3-3m0 0l3 3m-3-3v8m0-13a9 9 0 110 18 9 9 0 010-18z"83 />84 </svg>85 <p className="text-gray-600">86 { votes.length}87 </p>88 </button>89 )}90 </div>91 );92}
In the above function, you show the number of votes using the length of the votes
array. By default, a user is shown the upvote
button, which triggers the upvoteThisProduct
function. This function checks if the user is logged in or not. If an anonymous user tries to upvote a product, they are redirected to the login page.
Otherwise, if the user is logged in, then a POST request is sent to the /api/upvote
API route with the product's id
in the request body. After which, the refreshVotes
or mutate
function is triggered to update the count of the votes.
If the user has already upvoted the product, they are shown the downvote
button, the colored version of the upvote
button. This button triggers the downvoteThisProduct
function, sending a POST request to the api/downvote
API route with the id
of the vote in the request body. After the POST request, the refreshVotes
function is triggered.
Here is how the record to be deleted is filtered from the votes
array.
1const voteOfThisUser =2 user && votes3 ? votes.filter(function (vote) {4 return vote.Sub == user.sub;5 })6 : null;
Here is a GIF showing the upvote and downvote in action.
How to delete votes along with product
When a user deletes the product, the votes associated with that product should also get deleted. In this section, you will update the deleteThisProduct
function in the Product
component and the deleteProduct
API route.
Since there can be any number of votes associated with a product, you will first need to create an array of the id
of all such votes.
Modify deleteThisProduct
function in Product
component like this.
1const deleteThisProduct = async (e) => {2 const votesId =3 data &&4 data.map((vote) => {5 return vote.id;6 });78 await fetch("/api/deleteProduct", {9 method: "DELETE",10 body: JSON.stringify({ id, publicId, votesId }),11 headers: {12 "Content-Type": "application/json",13 },14 });15 await router.reload();16 };
In the above code, you map over the votes
array to create a new array containing only the id
of votes. Then, you pass this voteId
array in the request body of the /api/deleteProduct
DELETE request.
Update api/deleteProduct.js
file this.
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, votesId } = req.body;2021 try {22 const deletedProduct = await base("products").destroy(id);23 const deletedVotes = await base("votes").destroy(votesId)2425 const deleteImageFromCloudinary = cloudinary.uploader.destroy(26 publicId,27 function (error, result) {28 if (error) {29 console.log(error);30 }31 }32 );33 return res.status(200).json({ msg: "Product Deleted" });34 } catch (err) {35 console.error(err);36 res.status(500).json({ msg: "Something went wrong." });37 }38});
You delete the votes using the destroy
method and pass the votesId
array to it. So now, when the user deletes a product, all the votes are deleted along with it.
Conclusion
In this media jam, we created a Profile page where the user can see the products created by them. Then, we built the dynamic route to update the product. Finally, we discussed how to upvote a product and how to delete votes along with the product.
You can follow this jam and create your own unique version of this project. There are many features and functionality that you can add to this project.
Here are a few ideas to get you started:
- In this project, the user cannot update the product's image. You can add the feature to update the product's image. You can either upload a new image to Cloudinary and update Airtable with the
public_id
of this new image. Or you can replace the existing image while retaining the originalpublic_id
. - You can follow the same steps to create a YouTube or Instagram Clone.
- Style the app using UI libraries like Chakra UI, Material UI, etc.
Here are a few resources that you might find helpful.
Happy coding!