Leverage the Spread of AI: Build an AI-Powered Prompt Sharing Web App with Next.js 13.4, MongoDB, Tailwind CSS, and NextAuth
Welcome to an exciting tutorial where we'll dive into the world of web development and create a captivating prompt sharing application where users can share their best prompts for others to use in AI applications like ChatGPT. With Next.js 13.4, MongoDB, and Tailwind CSS at our disposal, we'll embark on a journey to build a feature-rich platform that empowers users to share and explore creative prompts.
In this blog post, we'll guide you through the step-by-step process of developing this dynamic web application. From setting up the development environment to implementing robust data storage with MongoDB, we'll cover everything you need to know. With the help of Next.js, a powerful React framework, we'll create a seamless and interactive user experience, ensuring that users can easily navigate, discover, and engage with the prompts.
The aesthetics of our application will be elevated using Tailwind CSS, a utility-first CSS framework. With its extensive set of customizable styles, we'll craft visually stunning interfaces that capture the essence of creativity and inspiration.
Join us on this journey as we unlock the potential of Next.js, MongoDB, and Tailwind CSS to build a remarkable prompt sharing web application. Whether you're a seasoned developer or just starting your coding adventure, this tutorial will provide valuable insights and hands-on experience.
Get ready to inspire others and be inspired as we explore the world of prompt sharing. Let's embark on this exhilarating development adventure together!
Tutorial
For this tutorial, I have referred to the NextJS documentation on Next.js by Vercel - The React Framework (nextjs.org) and JavaScript Mastery courses on YouTube.
First, let’s create a new project using the following command in a new terminal window:
npx create-next-app@latest prompt-share-nextjs
This will then ask us for certain settings like this:
√ Would you like to use TypeScript? ... No / Yes --select No
√ Would you like to use ESLint? ... No / Yes --select No
√ Would you like to use Tailwind CSS? ... No / Yes --select Yes
√ Would you like to use `src/` directory? ... No / Yes --select No
√ Would you like to use App Router? (recommended) ... No / Yes --select Yes
? Would you like to customize the default import alias? » No / Yes --select No
Once this is done, we will change the directory using the cd command and open it in VS code editor.
Next we will install the basic dependencies which we will using in the project with the command:
npm install bcrypt mongodb mongoose next-auth
Folder Structure & Template Setup
Let’s delete the existing app folder, public folder. Now let’s create new folders for app, public, components, models (for database), styles, utils (for utility functions), and a .env file for our environment variables.
Now, let’s replace the existing code in tailwind.config.js file with the following code:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
fontFamily: {
satoshi: ['Satoshi', 'sans-serif'],
inter: ['Inter', 'sans-serif'],
},
colors: {
'primary-orange': '#FF5722',
}
},
},
plugins: [],
}
This is a configuration file for the Tailwind CSS framework. Tailwind CSS is a utility-first CSS framework that provides pre-built CSS classes to rapidly build user interfaces.
Let's break down the code:
/** @type {import('tailwindcss').Config} */: This is a JSDoc comment that specifies the type of the exported module. It tells the editor or IDE that this module exports a Tailwind CSS configuration object.module.exports = { ... }: This line exports an object containing the Tailwind CSS configuration.content: [...]: This property specifies the files that Tailwind CSS will scan to generate its utility classes. In this case, it will search for files matching the patterns "./pages//*.{js,ts,jsx,tsx,mdx}", "./components//.{js,ts,jsx,tsx,mdx}", and "./app/**/.{js,ts,jsx,tsx,mdx}".theme: { ... }: This property allows you to customize the default theme provided by Tailwind CSS. Inside theextendobject, you can add or modify theme values. In this example, the theme is being extended to include customizations for fonts and colors.fontFamily: { ... }: This sub-property allows you to define or extend font families used in your project. In this case, two font families are defined: "Satoshi" and "Inter". The first one uses the font stack ["Satoshi", "sans-serif"], and the second one uses ["Inter", "sans-serif"].colors: { ... }: This sub-property allows you to define or extend colors used in your project. In this example, a custom color named "primary-orange" is defined with the value "#FF5722".plugins: []: This property allows you to enable or configure plugins for Tailwind CSS. In this case, thepluginsarray is empty, indicating that no additional plugins are being used.
Overall, this configuration file sets up the content files to be scanned, extends the default theme with custom font families and colors, and does not use any additional plugins.
Next, we will create a globals.css file with the styles folder and paste the following code:
@import url("<https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap>");
@tailwind base;
@tailwind components;
@tailwind utilities;
/*
Note: The styles for this gradient grid background is heavily inspired by the creator of this amazing site (<https://dub.sh>) – all credits go to them!
*/
.main {
width: 100vw;
min-height: 100vh;
position: fixed;
display: flex;
justify-content: center;
padding: 120px 24px 160px 24px;
pointer-events: none;
}
.main:before {
background: radial-gradient(circle, rgba(2, 0, 36, 0) 0, #fafafa 100%);
position: absolute;
content: "";
z-index: 2;
width: 100%;
height: 100%;
top: 0;
}
.main:after {
content: "";
background-image: url("/assets/images/grid.svg");
z-index: 1;
position: absolute;
width: 100%;
height: 100%;
top: 0;
opacity: 0.4;
filter: invert(1);
}
.gradient {
height: fit-content;
z-index: 3;
width: 100%;
max-width: 640px;
background-image: radial-gradient(
at 27% 37%,
hsla(215, 98%, 61%, 1) 0px,
transparent 0%
),
radial-gradient(at 97% 21%, hsla(125, 98%, 72%, 1) 0px, transparent 50%),
radial-gradient(at 52% 99%, hsla(354, 98%, 61%, 1) 0px, transparent 50%),
radial-gradient(at 10% 29%, hsla(256, 96%, 67%, 1) 0px, transparent 50%),
radial-gradient(at 97% 96%, hsla(38, 60%, 74%, 1) 0px, transparent 50%),
radial-gradient(at 33% 50%, hsla(222, 67%, 73%, 1) 0px, transparent 50%),
radial-gradient(at 79% 53%, hsla(343, 68%, 79%, 1) 0px, transparent 50%);
position: absolute;
content: "";
width: 100%;
height: 100%;
filter: blur(100px) saturate(150%);
top: 80px;
opacity: 0.15;
}
@media screen and (max-width: 640px) {
.main {
padding: 0;
}
}
/* Tailwind Styles */
.app {
@apply relative z-10 flex justify-center items-center flex-col max-w-7xl mx-auto sm:px-16 px-6;
}
.black_btn {
@apply rounded-full border border-black bg-black py-1.5 px-5 text-white transition-all hover:bg-white hover:text-black text-center text-sm font-inter flex items-center justify-center;
}
.outline_btn {
@apply rounded-full border border-black bg-transparent py-1.5 px-5 text-black transition-all hover:bg-black hover:text-white text-center text-sm font-inter flex items-center justify-center;
}
.head_text {
@apply mt-5 text-5xl font-extrabold leading-[1.15] text-black sm:text-6xl;
}
.orange_gradient {
@apply bg-gradient-to-r from-amber-500 via-orange-600 to-yellow-500 bg-clip-text text-transparent;
}
.green_gradient {
@apply bg-gradient-to-r from-green-400 to-green-500 bg-clip-text text-transparent;
}
.blue_gradient {
@apply bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent;
}
.desc {
@apply mt-5 text-lg text-gray-600 sm:text-xl max-w-2xl;
}
.search_input {
@apply block w-full rounded-md border border-gray-200 bg-white py-2.5 font-satoshi pl-5 pr-12 text-sm shadow-lg font-medium focus:border-black focus:outline-none focus:ring-0;
}
.copy_btn {
@apply w-7 h-7 rounded-full bg-white/10 shadow-[inset_10px_-50px_94px_0_rgb(199,199,199,0.2)] backdrop-blur flex justify-center items-center cursor-pointer;
}
.glassmorphism {
@apply rounded-xl border border-gray-200 bg-white/20 shadow-[inset_10px_-50px_94px_0_rgb(199,199,199,0.2)] backdrop-blur p-5;
}
.prompt_layout {
@apply space-y-6 py-8 sm:columns-2 sm:gap-6 xl:columns-3;
}
/* Feed Component */
.feed {
@apply mt-16 mx-auto w-full max-w-xl flex justify-center items-center flex-col gap-2;
}
/* Form Component */
.form_textarea {
@apply w-full flex rounded-lg h-[200px] mt-2 p-3 text-sm text-gray-500 outline-0;
}
.form_input {
@apply w-full flex rounded-lg mt-2 p-3 text-sm text-gray-500 outline-0;
}
/* Nav Component */
.logo_text {
@apply max-sm:hidden font-satoshi font-semibold text-lg text-black tracking-wide;
}
.dropdown {
@apply absolute right-0 top-full mt-3 w-full p-5 rounded-lg bg-white min-w-[210px] flex flex-col gap-2 justify-end items-end;
}
.dropdown_link {
@apply text-sm font-inter text-gray-700 hover:text-gray-500 font-medium;
}
/* PromptCard Component */
.prompt_card {
@apply flex-1 break-inside-avoid rounded-lg border border-gray-300 bg-white/20 bg-clip-padding p-6 pb-4 backdrop-blur-lg backdrop-filter md:w-[360px] w-full h-fit;
}
.flex-center {
@apply flex justify-center items-center;
}
.flex-start {
@apply flex justify-start items-start;
}
.flex-end {
@apply flex justify-end items-center;
}
.flex-between {
@apply flex justify-between items-center;
}
Next, you can copy and paste the assets folder from the GitHub repository into the public folder with all the icons and images for this project.
Github Repo: gupta-karan1/prompt-share-nextjs (github.com)
Next, within the app directory, we will create the page.jsx file with the following starter code:
const Home = () => {
return <div>page</div>;
};
export default Home;
And we’ll also create the layout.jsx file with the following starter code:
import "@/styles/globals.css";
import Nav from "@/components/nav";
import Provider from "@components/Provider";
export const metadata = {
title: "Promptopia",
description:
"Promptopia is a place for prompt engineers to find inspiration and share their prompts with the world.",
};
const RootLayout = ({ children }) => {
return (
<html lang="en">
<body suppressHydrationWarning={true}>
<div className="main">
<div className="gradient"></div>
</div>
<main className="app">
<Nav />
{children}
</main>
</body>
</html>
);
};
export default RootLayout;
This code represents a layout component in a React application. It sets up the structure and styling for the layout of the application's pages. Let's break down the code:
import "@/styles/globals.css";: This imports a global CSS file namedglobals.cssusing the@alias. This file likely contains global styles that will be applied throughout the application.import Nav from "@/components/nav";: This imports a component namedNavfrom the@/components/navmodule. It suggests that there is anav.jsornav.jsxfile in thecomponentsdirectory, which exports theNavcomponent.import Provider from "@components/Provider";: This imports a component namedProviderfrom the@components/Providermodule. It suggests that there is aProvider.jsorProvider.jsxfile in thecomponentsdirectory, which exports theProvidercomponent.export const metadata = { ... }: This exports an object namedmetadatathat contains metadata related to the layout or the application in general. The metadata object includes properties such astitleanddescriptionwith their respective values.const RootLayout = ({ children }) => { ... }: This defines a functional component namedRootLayoutthat represents the layout for the application. It receives thechildrenprop, which represents the nested components or elements within the layout.<html lang="en">: This is an HTML tag indicating the root of the HTML document. Thelangattribute specifies the language of the document as English.<body suppressHydrationWarning={true}>: This is the HTML body tag where the content of the page resides. ThesuppressHydrationWarningattribute is set totrue, which can be used to suppress hydration warnings in certain scenarios when server-side rendering (SSR) is involved.<div className="main">: This is a div element with the CSS class name "main". It likely represents the main content area of the page.<div className="gradient"></div>: This is another div element with the CSS class name "gradient". It represents an empty div that might be used to apply a gradient effect to the layout.<main className="app">: This is a main HTML tag with the CSS class name "app". It likely represents the main content area of the application.<Nav />: This is a self-closing component tag that renders theNavcomponent imported earlier. It represents a navigation component for the layout.{children}: This is a special placeholder where the content of the page or component using this layout will be rendered. Thechildrenprop represents the nested components or elements within the layout.</body>: Closing tag for the body element.</html>: Closing tag for the HTML element.export default RootLayout;: This exports theRootLayoutcomponent as the default export, allowing it to be imported and used in other parts of the application.
Overall, this layout component sets up the basic structure of the HTML document, defines the main content areas, imports and renders additional components like Nav, and provides a placeholder for dynamic rendering of content through the children prop. It also imports a global CSS file and exports a metadata object that can be used for SEO or other purposes.
Before running the dev server, we will edit the jsconfig.json file by removing the / symbol from the paths key:
{
"compilerOptions": {
"paths": {
"@*": ["./*"]
}
}
}
The jsconfig.json file is a configuration file used in JavaScript projects to configure the behavior of the JavaScript compiler (for example, the one used in TypeScript projects). It helps define how the compiler should handle modules and paths.
In this specific jsconfig.json file, there is only one property defined: compilerOptions. The compilerOptions object is used to specify various compiler options. Within the compilerOptions object, there is one specific option defined: paths.
"paths": { "@*": ["./*"] }: This option defines path mappings for module resolution. In this case, a path mapping with the key"@*"is defined, where `` represents a wildcard."@*": This is a path alias starting with@followed by a wildcard ``. It can be used as a placeholder for any module path that matches this pattern.["./*"]: This is an array that maps the path alias to one or more actual file or directory paths. In this case, the"./*"path represents the current directory (relative path).
By defining the paths option in the jsconfig.json file, it allows the JavaScript compiler to resolve module imports using the specified path mappings. For example, if there is a module import statement like import SomeModule from '@/components/SomeModule', the compiler will replace the @ alias with the corresponding path defined in "./*". So, the import statement will be resolved to import SomeModule from './components/SomeModule'. This provides a convenient way to define and use aliases for module paths, making it easier to manage and refactor code.
Next we will build the heading section for the Home in page.jsx in the following way:
const Home = () => {
return (
<section className="w-full flex-col flex-center">
<h1 className="head_text text-center">
Discover and Share
<br className="max-md:hidden" />
<span className="orange_gradient text-center">AI-Powered Prompts </span>
</h1>
<p className="text-center desc">
Prompotopia is a an open source AI prompting tool for modern world to
discover, create and share creative prompts
</p>
</section>
);
};
export default Home;
Let's break down the code step by step:
const Home = () => { ... }: This line declares a functional component called "Home" using an arrow function syntax. Functional components are the building blocks of React applications, and they are used to encapsulate reusable UI logic.- Inside the component, there is a JSX (JavaScript XML) code block enclosed in parentheses:
( ... ). JSX is a syntax extension for JavaScript that allows you to write HTML-like code within JavaScript. <section className="w-full flex-col flex-center"> ... </section>: This JSX code represents a<section>element with the CSS classesw-full,flex-col, andflex-center. These classes are likely defining the width, flex-direction, and alignment properties for this section. The content of this section includes the heading and paragraph elements.<h1 className="head_text text-center"> ... </h1>: This JSX code represents an<h1>element with the CSS classeshead_textandtext-center. The text within this element is "Discover and Share", which will be displayed as the main heading of the section.<br className="max-md:hidden" />: This JSX code represents a line break element (<br>). It has the CSS classmax-md:hidden, which suggests that it is hidden on screens with a maximum width of a medium size (e.g., mobile screens). This allows the text to wrap to the next line on smaller screens.<span className="orange_gradient text-center">AI-Powered Prompts </span>: This JSX code represents a<span>element with the CSS classesorange_gradientandtext-center. The text within this element is "AI-Powered Prompts", which will be displayed as a part of the heading. Theorange_gradientclass likely applies a CSS style to create an orange gradient effect on this text.<p className="text-center desc"> ... </p>: This JSX code represents a<p>element with the CSS classestext-centeranddesc. The text within this element describes the purpose of "Prompotopia" as "an open source AI prompting tool for the modern world to discover, create, and share creative prompts".- The final line
export default Home;exports the component as the default export, allowing it to be imported and used in other parts of the application.
Overall, this code represents a section with a heading and a paragraph that introduces a tool called "Prompotopia", emphasizing its AI-powered prompt generation capabilities.
Building the Nav Bar Component
After this we will create the a Nav.jsx file for the top nav bar with the following code:
"use client";
import Link from "next/link";
import Image from "next/image";
import { useState, useEffect } from "react";
import { signIn, signOut, useSession, getProviders } from "next-auth/react";
const Nav = () => {
const isUserLoggedIn = true;
const [providers, setProviders] = useState(null);
const [toggleDropdown, setToggleDropdown] = useState(false);
useEffect(() => {
const setUpProviders = async () => {
const response = await getProviders();
setProviders(response);
};
setProviders();
}, []);
return (
<nav className="flex-between w-full pt-3 mb-16">
<Link href="/" className="flex gap-2 flex-center">
<Image
src="/assets/images/logo.svg"
alt="Promptopia logo"
width={30}
height={30}
className="object-contain"
/>
<p className="logo_text"> Promptopia </p>
</Link>
{/* Desktop Navigation */}
<div className="sm:flex hidden" key="desktop">
{isUserLoggedIn ? (
<div className="flex gap-3 md:gap-5">
<Link href="/create-prompt" className="black_btn">
Create Post
</Link>
<button className="outline_btn" type="button">
Sign Out
</button>
<Link href="/profile">
<Image
src="/assets/images/logo.svg"
alt="profile"
width={35}
height={35}
className="rounded-full"
/>
</Link>
</div>
) : (
<>
{providers &&
Object.value(providers).map((provider) => {
<button
type="button"
key={provider.name}
onClick={() => signIn(provider.id)}
className="black_btn"
>
Sign In
</button>;
})}
</>
)}
</div>
{/* Mobile Navigation */}
<div className="sm:hidden flex relative">
{isUserLoggedIn ? (
<div className="flex">
<Image
src="/assets/images/logo.svg"
width={35}
height={35}
className="rounded-full"
alt="profile"
onClick={() => setToggleDropdown((prev) => !prev)}
/>
{toggleDropdown && (
<div className="dropdown">
<Link
href="/profile"
className="dropdown_link"
onClick={() => setToggleDropdown(false)}
>
My Profile
</Link>
<Link
href="/create-prompt"
className="dropdown_link"
onClick={() => setToggleDropdown(false)}
>
Create Prompt
</Link>
<button
type="button"
className="mt-5 w-full black_btn"
onClick={() => {
setToggleDropdown(false);
signOut();
}}
>
Sign Out
</button>
</div>
)}
</div>
) : (
<>
{providers &&
Object.values(providers).map((provider) => (
<button
type="button"
key={provider.name}
onClick={() => signIn(provider.id)}
className="black_btn"
>
Sign In
</button>;
))}
</>
)}
</div>
</nav>
);
};
export default Nav;
The abivecode is a functional component in Next.js called "Nav". This component represents a navigation bar and handles user authentication and navigation links.
Let's break down the code step by step:
use client;: This indicates NextJS that this component will be rendered on client side.- The
importstatements at the beginning of the code import necessary modules and components from various libraries. In this case, the following imports are used:Linkfrom "next/link": This allows for client-side navigation between pages in a Next.js application.Imagefrom "next/image": This component is used to optimize and render images in a Next.js application.useStateanduseEffectfrom "react": These are hooks provided by React for managing component state and performing side effects respectively.signIn,signOut,useSession, andgetProvidersfrom "next-auth/react": These are authentication-related hooks and functions provided by the NextAuth library for managing user sessions and authentication providers.
- The component function
Navis declared using the arrow function syntax. - Inside the component, there are several variables declared using the
useStatehook:isUserLoggedIn: This variable is currently set totrueas a placeholder. It likely represents the state of whether a user is logged in or not.providers: This variable is initialized asnulland is used to store the authentication providers available for sign-in. It will be populated asynchronously using thesetProvidersfunction.toggleDropdown: This variable represents the state of whether the mobile dropdown menu is open or closed. It is initially set tofalse.
- The
useEffecthook is used to fetch the authentication providers when the component is mounted. Inside the effect, thegetProvidersfunction is called asynchronously to retrieve the available providers. The result is then set using thesetProvidersfunction. The effect runs only once when the component is mounted, as indicated by the empty dependency array[]. - The JSX code within the
returnstatement represents the structure of the navigation bar. Let's break it down:- The
navelement has the CSS classflex-between,w-full,pt-3, andmb-16, which likely define the layout and styling of the navigation bar. - The first part of the navigation bar contains a link to the homepage with the Promptopia logo and text. It uses the
Linkcomponent from Next.js to enable client-side navigation. - The second part of the navigation bar is the desktop navigation section, represented by a
divelement with the CSS classessm:flexandhidden. It contains different navigation options based on whether the user is logged in or not. - If the user is logged in, it displays links for creating a post, signing out, and a link to the user's profile. The profile image is also displayed using the
Imagecomponent from Next.js. - If the user is not logged in, it checks if the
providersvariable is populated with authentication providers. If so, it renders sign-in buttons for each provider. - The mobile navigation section is represented by another
divelement with the CSS classessm:hidden,flex, andrelative. It displays different navigation options based on the user's login status. - If the user is logged in, it displays the user's profile image and a dropdown menu. Clicking on the profile image toggles the dropdown menu visibility. The dropdown menu contains links to the user's profile and an option to create a prompt. It also includes a "Sign Out" button that triggers the
setToggleDropdownandsignOutfunctions when clicked. - If the user is not logged in, it checks if the
providersvariable is populated with authentication providers. If so, it renders sign-in buttons for each provider.
- The
- The component is exported as the default export using
export default Nav;, allowing it to be imported and used in other parts of the application.
Overall, this code represents a navigation bar component in a Next.js application that handles user authentication, displays navigation links, and provides a responsive experience for different screen sizes.
![]() |
Figure 1: Desktop Nav Bar |
![]() |
Figure 2: Mobile Nav Bar |
Setting up Google Authentication
First go to the Google Cloud console website and create a new project. Next, go to the APIs and Services from the side menu and open OAuth consent screen. Here you’ll need to set up the app with default settings. Simply enter the required fields like app name, support email and developer contact info. Now go to Credentials from the APIs and Services from the side menu and click on ‘Create Credentials’. Select web application and choose OAuth Client ID. Select a web application from the drop down and in the next screen, enter http://localhost:3000 as the URI and redirect URI.
Once this is setup, we will get our Client ID and Client Secret as two strings. Copy both of them and past them in the .env file in your project directory in VS code like this:
GOOGLE_CLIENT_ID=YOUR_CLIENT_ID
GOOGLE_CLIENT_SECRET=YOUR_CLIENT_SECRET
Next, we will create a route.js file in app>api>auth>[…nextauth] folder with the following code to enable server connection:
import NextAuth from "next-auth/next";
import GoogleProvider from "next-auth/providers/google";
console.log({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
});
const handler = NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
callbacks:{
async session({ session }) {},
async signIn({ profile }) {
try {
} catch {}
},
}
});
export { handler as GET, handler as POST };
The above code sets up the server-side configuration for NextAuth, a library used for authentication in Next.js applications. It configures Google as the authentication provider and includes some callback functions for session management and sign-in handling.
Let's break down the code step by step:
- The
importstatements at the beginning of the code import the necessary modules and providers from NextAuth.NextAuthis imported from "next-auth/next" and represents the NextAuth server-side configuration function.GoogleProvideris imported from "next-auth/providers/google" and represents the Google authentication provider for NextAuth.
- The
console.log()statement outputs an object containing the values ofprocess.env.GOOGLE_CLIENT_IDandprocess.env.GOOGLE_CLIENT_SECRETto the console. These values are environment variables that store the Google client ID and client secret required for authentication. The actual values are retrieved from the environment during runtime. - The
NextAuthfunction is invoked to create a server-side authentication configuration. The function takes an object as its argument with various configuration options. - The
providersarray within the configuration object specifies the authentication providers to be used. In this case, only the Google provider is included. It is initialized with theclientIdandclientSecretvalues obtained from the environment variables. - The
callbacksobject within the configuration object defines callback functions for session management and sign-in handling.- The
sessioncallback is an empty async function. It can be used to modify the user session object before it is saved. - The
signIncallback is an async function that takesprofileas a parameter. It is a placeholder for the sign-in handling logic. The actual implementation of the sign-in logic can be added within thetryblock.
- The
- The
handlervariable is assigned the result of theNextAuthfunction, which is the authentication configuration object. - The authentication configuration object is exported twice using the
GETandPOSTproperties. This allows the Next.js API route to handle both GET and POST requests using the same authentication configuration.GETexports thehandlerconfiguration object for GET requests.POSTexports the samehandlerconfiguration object for POST requests.
By exporting the authentication configuration, it can be used in Next.js API routes to handle authentication-related requests.
Overall, this code sets up NextAuth with Google as the authentication provider and includes callbacks for session management and sign-in handling. It exports the authentication configuration object to be used in Next.js API routes.
Setting up MongoDB Connection
Now we will go to MongoDB Atlas | Multi-cloud Developer Data Platform | MongoDB website and create a new shared database and a new cluster within it. Then we will go to the Database Access option from the side menu and copy the user password from the Edit option. We will use this password in the URI string to connect with the database. We will also go to the Network Access option from the side menu and click on ADD IP ADDRESS → ALLOW ALL IP ADDRESSES TO ACCESS. This will allow the access from anywhere. Lastly, we’ll go to the Database option from the side menu and click on the Connect button which will give us a connection URI. We will copy the URI and paste it in our .env file to access it in our other files.
Next we will create a database.js file in the utils directory with the following code:
import mongoose from "mongoose";
let isConnected = false; //track connection
export const connectToDB = async () => {
mongoose.set("strictQuery", true); //strict mode for queries. if we don't do this we will get warnings in the console.
if (isConnected) {
console.log("MOngoDB is already connected");
return;
}
try {
await mongoose.connect(process.env.MONGODB_URI, {
dbName: "shareprompt",
useNewUrlParser: true,
useUnifiedTopology: true,
});
isConnected = true;
} catch (error) {
console.log(error);
}
};
The above code sets up a connection to a MongoDB database using Mongoose, an Object Data Modeling (ODM) library for MongoDB and Node.js. It includes a function called connectToDB that establishes the database connection.
Let's break down the code step by step:
- The
importstatement at the beginning of the code imports themongoosemodule, which is the MongoDB ODM library for Node.js. - The variable
isConnectedis declared and initialized tofalse. This variable is used to track whether the database connection has been established or not. - The
connectToDBfunction is declared as an asynchronous function. - The
mongoose.set()method is called to set the "strictQuery" option totrue. This enables strict mode for queries, ensuring that any undefined or invalid fields in queries will result in an error instead of being silently ignored. This helps in debugging and avoiding potential issues. - The function checks the
isConnectedvariable. If it istrue, it means that the connection is already established, and a message is logged to the console. The function then returns without attempting to establish a new connection. - If the
isConnectedvariable isfalse, indicating that the connection has not been established, the function attempts to connect to the MongoDB database. - Inside a
try-catchblock, themongoose.connect()method is called with theprocess.env.MONGODB_URIvalue. This value is expected to contain the URI of the MongoDB database, which is retrieved from the environment variables. - The
mongoose.connect()method accepts an options object as the second argument, where thedbName,useNewUrlParser, anduseUnifiedTopologyoptions are specified.dbNamespecifies the name of the database to connect to.useNewUrlParseris set totrueto use the new MongoDB connection string parser.useUnifiedTopologyis set totrueto use the new MongoDB Server Discovery and Monitoring engine.
- If the connection is established successfully, the
isConnectedvariable is set totrue. - If an error occurs during the connection attempt, the error is caught in the
catchblock, and an error message is logged to the console. - The
connectToDBfunction does not return anything.
By calling the connectToDB function, you can establish a connection to the MongoDB database using the configuration provided in the function. The connection is only established if it hasn't been established before, preventing multiple connections.
Next, we will import this utility function into the route.js file and implement the following code:
import NextAuth from "next-auth/next";
import GoogleProvider from "next-auth/providers/google";
import { connectToDB } from "@utils/database";
import User from "@models/user";
// console.log({
// clientId: process.env.GOOGLE_CLIENT_ID,
// clientSecret: process.env.GOOGLE_CLIENT_SECRET,
// });
const handler = NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
callbacks: {
async session({ session }) {
const sessionUser = await User.findOne({ email: session.user.email });
session.user.id = sessionUser._id.toString();
return session;
},
async signIn({ profile }) {
try {
await connectToDB();
//check if user exists in the database
const userExists = await User.findOne({ email: profile.email });
//if not, create a new user
if (!userExists) {
await User.create({
email: profile.email,
username: profile.name.replace(" ", "").toLowerCase(),
image: profile.picture,
});
}
return true;
} catch (error) {
console.log(error);
return false;
}
},
},
});
export { handler as GET, handler as POST };
The above code sets up the server-side configuration for NextAuth with Google as the authentication provider. It includes callbacks for session management and sign-in handling, as well as importing and using a database connection function and a User model.
Let's break down the code step by step:
- The
importstatements at the beginning of the code import necessary modules, providers, and utilities:NextAuthis imported from "next-auth/next" and represents the NextAuth server-side configuration function.GoogleProvideris imported from "next-auth/providers/google" and represents the Google authentication provider for NextAuth.connectToDBis imported from "@utils/database" and represents a function to establish a connection to the MongoDB database.Useris imported from "@models/user" and represents the User model used to interact with the database.
- The
handlervariable is declared and assigned the result of theNextAuthfunction, which is the authentication configuration object. - The authentication configuration object is created by invoking the
NextAuthfunction and passing an object as its argument. - The
providersarray within the configuration object specifies the authentication providers to be used. In this case, only the Google provider is included. It is initialized with theclientIdandclientSecretvalues obtained from the environment variables. - The
callbacksobject within the configuration object defines callback functions for session management and sign-in handling.- The
sessioncallback is an async function that receives thesessionobject as a parameter. Inside the function, a query is made to find the corresponding user in the database based on the email stored in the session. The retrieved user's ID is then assigned tosession.user.id, and the modified session object is returned. - The
signIncallback is an async function that receives theprofileobject as a parameter. Inside the function, a database connection is established by calling theconnectToDBfunction. The function then checks if the user exists in the database based on the provided email. If the user does not exist, a new user is created in the database using the information from theprofileobject. Finally, the function returnstrueto indicate successful sign-in.
- The
- The authentication configuration object is exported twice using the
GETandPOSTproperties. This allows the Next.js API route to handle both GET and POST requests using the same authentication configuration.GETexports thehandlerconfiguration object for GET requests.POSTexports the samehandlerconfiguration object for POST requests.
By exporting the authentication configuration, it can be used in Next.js API routes to handle authentication-related requests.
Overall, this code sets up NextAuth with Google as the authentication provider and includes callbacks for session management and sign-in handling. It also imports and uses a database connection function and a User model to interact with the database. The authentication configuration object is exported for use in Next.js API routes.
Now, within the try catch block, we will need to write the functions that will run create a new user within our database. For this we need to create a model based on which the document for our user will be created. Within the models directory, we will create a user.js file with the following code:
import { Schema, model, models } from "mongoose";
const UserSchema = new Schema({
email: {
type: String,
unique: [true, "Email already exists"],
required: [true, "Email is required"],
},
username: {
type: String,
required: [true, "Username is required"],
match: [
/^(?=.{8,20}$)(?![_.])(?!.*[_.]{2})[a-zA-Z0-9._]+(?<![_.])$/,
"Username invalid, it should contain 8-20 alphanumeric letters and be unique!",
],
},
image: {
type: String,
},
});
// The 'models' object is provided by the mongoose library and stores all the registered models.
// If a model named 'User' already exists in the 'models' object, it assigns that existing model to the 'User' variable.
//If a model named 'User' doesn't exist in the 'models' object, the 'model' function is called to create a new model.
// THe newly created model is then assigned to the 'User' variable.
const User = models.User || model("User", UserSchema);
export default User;
The above code defines a Mongoose schema for a user and exports it as a model named "User". It also utilizes the models object provided by Mongoose to check if a model with the name "User" already exists. If it does, it assigns the existing model to the "User" variable. If it doesn't exist, it creates a new model using the model function and assigns it to the "User" variable.
Let's break down the code step by step:
- The
importstatement at the beginning of the code imports the necessary modules from Mongoose:Schemarepresents the Mongoose schema class used to define the structure of a document.modelrepresents the Mongoose model function used to create a model based on a schema.modelsrepresents the object provided by Mongoose that stores all the registered models.
- The
UserSchemavariable is declared and assigned a new instance of theSchemaclass. This schema defines the structure and validation rules for a user document.- The
emailfield is defined as a string with theuniqueoption set totrueto ensure email uniqueness. It is also marked asrequiredto enforce the presence of an email value. - The
usernamefield is defined as a string with specific validation rules using a regular expression (match). It should contain alphanumeric characters, be between 8 and 20 characters long, and have no consecutive underscores or periods. It is also marked asrequired. - The
imagefield is defined as a string and does not have any validation rules.
- The
- The
Uservariable is declared and assigned the existing "User" model if it exists in themodelsobject. If not, themodelfunction is called to create a new model named "User" based on theUserSchema. - The
export default User;statement exports the "User" model as the default export of the module, allowing it to be imported and used in other parts of the application.
Overall, this code defines a Mongoose schema for a user and exports it as a model named "User". By using this model, you can interact with the MongoDB collection associated with the user data, perform CRUD (Create, Read, Update, Delete) operations, and enforce the defined schema and validation rules.
We must checkout the Next Auth documentation to understand how the entire process works from Getting Started | NextAuth.js (next-auth.js.org)
To make sure the authentication works properly in production, we will need to enter the following environment variables:
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_URL_INTERNAL=http://localhost:3000
NEXTAUTH_SECRET=YOUR_SECRET_KEY
The Secret key can be generated with the following command using the Git Bash terminal to generate a unique key:
openssl rand -base64 32
If you don’t have git bash, you can use the following website to run openssl commands within the web page: OpenSSL - CrypTool Portal which will give us a unique string and we cna paste it on .env folder.
Next, we will implement the following code for NextJs configuration to enable mongoose in next.config.js file:
**/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true,
serverComponentsExternalPackages: ["mongoose"],
},
images: {
domains: ["lh3.googleusercontent.com"],
},
webpack(config) {
config.experiments = {
...config.experiments,
topLevelAwait: true,
};
return config;
},
};
module.exports = nextConfig;**
The above code exports a Next.js configuration object named nextConfig. This configuration object specifies experimental features, image domains, and webpack settings for a Next.js application.
Let's break down the code step by step:
- The
/** @type {import('next').NextConfig} */comment at the beginning of the code is a type annotation comment that provides type information for thenextConfigobject. It indicates that the object conforms to theNextConfigtype from the Next.js library. - The
nextConfigobject is declared and initialized with configuration options for the Next.js application. - The
experimentalproperty within thenextConfigobject specifies experimental features for Next.js.appDiris set totrueto enable the experimental support for a separateappdirectory, where components can be placed.serverComponentsExternalPackagesis an array that includes the package name(s) that should be treated as external dependencies when using Next.js server components. In this case, the "mongoose" package is listed.
- The
imagesproperty within thenextConfigobject configures the image domains that Next.js should allow when using thenext/imagecomponent. Thedomainsarray includes the domain "lh3.googleusercontent.com", indicating that images from this domain should be allowed and optimized by Next.js. - The
webpackfunction within thenextConfigobject allows customization of the Webpack configuration for the Next.js application.- The function receives the
configobject representing the default Webpack configuration. - The
experimentsproperty of theconfigobject is spread using the spread operator to retain the default experimental features. - The
topLevelAwaitexperimental feature is enabled by setting it totrue, allowing the usage of top-levelawaitin the application code.
- The function receives the
- The
webpackfunction returns the modifiedconfigobject. - The
module.exportsstatement exports thenextConfigobject as the configuration for the Next.js application.
Overall, this code configures experimental features, image domains, and webpack settings for a Next.js application. It enables experimental features like the app directory, specifies allowed image domains, and modifies the Webpack configuration to include the topLevelAwait feature. The resulting nextConfig object is exported as the configuration for the Next.js application.
Now will run the development server with the command:
npm run dev
If everything in the backend has worked in order, this should compile successfully in the terminal.
Now we will implement the user logic from our authentication data to display the Sign In feature within the Nav.jsx component:
"use client";
import Link from "next/link";
import Image from "next/image";
import { useState, useEffect } from "react";
import { signIn, signOut, useSession, getProviders } from "next-auth/react";
const Nav = () => {
// const isUserLoggedIn = true;
const { data: session } = useSession();
const [providers, setProviders] = useState(null);
const [toggleDropdown, setToggleDropdown] = useState(false);
useEffect(() => {
const setUpProviders = async () => {
const response = await getProviders();
setProviders(response);
};
setUpProviders();
}, []);
return (
<nav className="flex-between w-full pt-3 mb-16">
<Link href="/" className="flex gap-2 flex-center">
<Image
src="/assets/images/logo.svg"
alt="Promptopia logo"
width={30}
height={30}
className="object-contain"
/>
<p className="logo_text"> Promptopia </p>
</Link>
{/* Desktop Navigation */}
<div className="sm:flex hidden">
{session?.user ? (
<div className="flex gap-3 md:gap-5">
<Link href="/create-prompt" className="black_btn">
Create Post
</Link>
<button className="outline_btn" type="button" onClick={signOut}>
Sign Out
</button>
<Link href="/profile">
<Image
src={session?.user?.image}
alt="profile"
width={35}
height={35}
className="rounded-full"
/>
</Link>
</div>
) : (
<>
{providers &&
Object.values(providers).map((provider) => (
<button
type="button"
key={provider.name}
onClick={() => signIn(provider.id)}
className="black_btn"
>
Sign In
</button>
))}
</>
)}
</div>
{/* Mobile Navigation */}
<div className="sm:hidden flex relative">
{session?.user ? (
<div className="flex">
<Image
src={session?.user?.image}
width={35}
height={35}
className="rounded-full"
alt="profile"
onClick={() => setToggleDropdown((prev) => !prev)}
/>
{toggleDropdown && (
<div className="dropdown">
<Link
href="/profile"
className="dropdown_link"
onClick={() => setToggleDropdown(false)}
>
My Profile
</Link>
<Link
href="/create-prompt"
className="dropdown_link"
onClick={() => setToggleDropdown(false)}
>
Create Prompt
</Link>
<button
type="button"
className="mt-5 w-full black_btn"
onClick={() => {
setToggleDropdown(false);
signOut();
}}
>
Sign Out
</button>
</div>
)}
</div>
) : (
<>
{providers &&
Object.values(providers).map((provider) => {
return(
<button
type="button"
key={provider.name}
onClick={() => signIn(provider.id)}
className="black_btn"
>
Sign In
</button>)
})}
</>
)}
</div>
</nav>
);
};
export default Nav;
The above code represents a navigation bar component in a Next.js application that utilizes the NextAuth library for client-side authentication. Let's go through the code and understand its functionality:
- The line
"use client";is a comment indicating that the code should be executed on the client-side. - Several modules and hooks are imported from Next.js and NextAuth:
Linkis imported from "next/link" and is used for client-side navigation between pages.Imageis imported from "next/image" and is used for optimizing and rendering images.useStateanduseEffectare imported from "react" and are used for managing component state and performing side effects.signIn,signOut,useSession, andgetProvidersare imported from "next-auth/react" and are NextAuth-specific hooks and functions for managing authentication.
- The
Navcomponent is defined as a functional component. - The
useSessionhook is used to fetch the session data, and the resultingsessionobject is destructured fromdataproperty. Thesessionobject contains information about the authenticated user. - Two state variables,
providersandtoggleDropdown, are declared using theuseStatehook. Theprovidersstate variable is used to store the authentication providers available, and thetoggleDropdownstate variable is used to toggle the visibility of the mobile navigation dropdown menu. - The
useEffecthook is used to fetch the available authentication providers when the component mounts. It calls thegetProvidersfunction asynchronously and sets the retrieved providers in theprovidersstate variable. - The
returnstatement renders the JSX code representing the navigation bar. - The navigation bar consists of two sections: desktop navigation and mobile navigation.
- The desktop navigation section is rendered when the screen size is larger (using CSS classes
sm:flex hidden).- If the
session?.usercondition is truthy, indicating that the user is authenticated, it renders a set of elements for an authenticated user. This includes a "Create Post" link, a "Sign Out" button, and a link to the user's profile with an image rendered using theImagecomponent. - If the
session?.usercondition is falsy, indicating that the user is not authenticated, it renders a set of elements for a non-authenticated user. This includes buttons for each available authentication provider retrieved fromproviders. These buttons allow the user to sign in with the respective provider.
- If the
- The mobile navigation section is rendered when the screen size is smaller (using CSS classes
sm:hidden flex relative).
- If the
session?.usercondition is truthy, it renders a set of elements for an authenticated user. This includes an image representing the user's profile, which can be clicked to toggle the visibility of the dropdown menu. When the dropdown is visible (toggleDropdownis true), it renders links to the user's profile and a "Create Prompt" link, as well as a "Sign Out" button. - If the
session?.usercondition is falsy, it renders a set of elements for a non-authenticated user. This includes buttons for each available authentication provider retrieved fromproviders.
- The
Navcomponent is exported as the default export of the module, allowing it to be imported and used in other parts of the application.
Overall, this code represents a responsive navigation bar component that displays different options based on the user's authentication status. It uses NextAuth hooks and functions for authentication management and Next.js components for navigation and image rendering.
Now, if we click on the Sign In, it will give us an error saying, ‘Access blocked: This app’s request is invalid’. To resolve this, we need to add the following URL to the credential settings, within the Authorised redirect URIs: http://localhost:3000/api/auth/callback/google
We need to look into the documentation at Google | NextAuth.js (next-auth.js.org) to find the configuration required for NExtJS to work with the authentication providers.
Now, the authentication should work with sign in options for our google accounts and when we hit sign in from one of them, we will get the create post and sign out buttons on our nav bar.
We can also see the user data displayed under Collections within the MongoDB Atlas Dashboard.
Create New Prompt Page Setup
For this, we will create a new folder called ‘create-prompt’ within the app directory such that the file base routing system of NextJS identifies it and is directly accessible via http://localhost:3000/create-prompt without any need to write code for custom routing or to download any react router external package. This file based routing system is by far the most useful feature introduced in NextJS which makes routing very intuitive and effortless.
Within the create-prompt folder, we will create a page.jsx file with the following code:
"use client";
import { useState } from "react";
import { useSession } from "next-auth/react"; // let's us know if we're signed in or not
import { useRouter } from "next/navigation"; // let's us redirect the user
import Form from "@components/Form";
const CreatePrompt = () => {
const router = useRouter();
const { data: session } = useSession();
const [submitting, setSubmitting] = useState(false);
const [post, setPost] = useState({
prompt: "",
tag: "",
});
const createPrompt = async (e) => {
e.preventDefault(); // prevents the page from refreshing
setSubmitting(true);
try {
const response = await fetch("/api/prompt/new", {
method: "POST",
body: JSON.stringify({
prompt: post.prompt,
userId: session?.user.id,
tag: post.tag,
}),
});
if (response.ok) {
router.push("/");
}
} catch (error) {
console.log(error);
} finally {
setSubmitting(false);
}
};
return (
<Form
type="Create"
post={post}
setPost={setPost}
submitting={submitting}
handleSubmit={createPrompt}
/>
);
};
export default CreatePrompt;
The above code represents a component named CreatePrompt in a Next.js application. This component is responsible for rendering a form to create a new prompt and handle the form submission.
Let's go through the code and understand its functionality:
- The line
"use client";is a comment indicating that the code should be executed on the client-side. - Several modules and hooks are imported from Next.js:
useStateis imported from "react" and is used for managing component state.useSessionis imported from "next-auth/react" and is a NextAuth-specific hook that provides information about the authenticated user.useRouteris imported from "next/router" and is used for client-side navigation.
- The
CreatePromptcomponent is defined as a functional component. - The
useRouterhook is used to access the router object, which provides methods for navigation. - The
useSessionhook is used to fetch the session data, and the resultingsessionobject is destructured fromdataproperty. Thesessionobject contains information about the authenticated user. - Two state variables,
submittingandpost, are declared using theuseStatehook. Thesubmittingstate variable is used to track whether the form is currently being submitted, and thepoststate variable is used to store the form data (prompt and tag). - The
createPromptfunction is defined as an asynchronous function that handles the form submission.- It prevents the default form submission behavior using
e.preventDefault(). - It sets the
submittingstate variable totrueto indicate that the form is being submitted. - It makes a POST request to the "/api/prompt/new" endpoint, sending the form data (prompt, userId, and tag) as the request payload.
- If the response is successful (
response.ok), it navigates the user to the home page usingrouter.push("/"). - If an error occurs, it logs the error to the console.
- Finally, it sets the
submittingstate variable back tofalseto indicate that the form submission is complete.
- It prevents the default form submission behavior using
- The
returnstatement renders aFormcomponent with the following props:typeis set to "Create" to indicate that it is a form for creating a prompt.postandsetPostare used to pass thepoststate variable and its setter function to theFormcomponent.submittingis used to indicate whether the form is currently being submitted.handleSubmitis set to thecreatePromptfunction to handle the form submission.
- The
CreatePromptcomponent is exported as the default export of the module, allowing it to be imported and used in other parts of the application.
Overall, this code represents a form component for creating a new prompt. It uses the useSession hook to check the user's authentication status and the useRouter hook for client-side navigation. The form data is sent to the server-side API endpoint for processing.
Next, we will write the code for the Form component in the Form.jsx file in the components directory:
import Link from "next/link";
const Form = ({ type, post, setPost, submitting, handleSubmit }) => {
return (
<section className="w-full max-w-full flex-start flex-col">
<h1 className="head_text text-left">
<span className="blue_gradient">{type} Post</span>
</h1>
<p className="desc text-left max-w-md">
{type} and share amazing prompts with the world and let your imagination
run wild with any AI-powered platform.
</p>
<form
onSubmit={handleSubmit}
className="mt-10 w-full max-w-2xl flex flex-col gap-7 glassmorphism"
>
<label>
<span className="font-satoshi font-semibold text-base text-gray-700">
Your AI Prompt
</span>
<textarea
value={post.prompt}
onChange={(e) => setPost({ ...post, prompt: e.target.value })}
placeholder="Write your prompt here"
required
className="form_textarea"
/>
</label>
<label>
<span className="font-satoshi font-semibold text-base text-gray-700">
Tag{" "}
<span className="font-normal">
(#product, #web-development, #idea)
</span>
</span>
<input
value={post.tag}
onChange={(e) => setPost({ ...post, tag: e.target.value })}
placeholder="#tag"
required
className="form_input"
/>
</label>
<div className="flex-end mx-3 mb-5 gap-4">
<Link href="/" className="text-gray-500 text-sm">
Cancel
</Link>
<button
type="submit"
disabled={submitting}
className="px-5 py-1.5 text-sm bg-primary-orange rounded-full text-white"
>
{submitting ? `${type}...` : type}
</button>
</div>
</form>
</section>
);
};
export default Form;
The above code represents a Form component that is used to render a form for creating a post or prompt in the application. Let's go through the code and understand its functionality:
- The
Linkcomponent is imported from "next/link" to create links within the application. - The
Formcomponent is defined as a functional component that receives several props:type: Represents the type of form, either "Create" or "Update".post: Represents the current post object, containing the prompt and tag values.setPost: A function to update the post object with new values.submitting: A boolean indicating whether the form is currently being submitted.handleSubmit: A function to handle the form submission.
- The component returns JSX code representing the form:
- The form is wrapped in a
sectionelement with CSS classes for styling. - It includes a heading displaying the form type using the
typeprop. - It includes a description paragraph related to the form type.
- Inside the
formelement, theonSubmitevent is set to thehandleSubmitfunction. - The form is styled using CSS classes for layout and appearance.
- The form contains two form fields: a textarea for the prompt and an input for the tag.
- The textarea and input fields have event handlers (
onChange) that update thepostobject when the values change. - The form also includes a cancel link and a submit button.
- The cancel link uses the
Linkcomponent to navigate back to the home page when clicked. - The submit button is disabled when
submittingis true and displays the appropriate text based on the form state.
- The form is wrapped in a
- The
Formcomponent is exported as the default export of the module, allowing it to be imported and used in other parts of the application.
Overall, this code represents a reusable form component that can be used for creating or updating posts. It renders the form inputs, handles user input changes, and triggers the form submission when the user clicks the submit button.
Creating Prompt Schema Model for MongoDB
Next, we will create a prompt.js file in the models directory to define the data structure for the data we will receive from the mongodb database in the following way:
// the model file is for mongodb to know how the data is structured and what to expect from the data that is being sent to it.
import { Schema, model, models } from "mongoose";
const PromptSchema = new Schema({
creator: {
type: Schema.Types.ObjectId,
ref: "User",
},
prompt: {
type: String,
required: [true, "Prompt is required"],
},
tag: {
type: String,
required: [true, "Tag is required"],
},
});
const Prompt = models.Prompt || model("Prompt", PromptSchema);
export default Prompt;
The provided code represents a Mongoose model file used for defining the structure and behavior of the "Prompt" data in MongoDB. Let's go through the code and understand its functionality:
- The
Schema,model, andmodelsare imported from the "mongoose" library.Schemarepresents the schema definition for the data model.modelis used to create a new Mongoose model based on the schema.modelsis an object provided by Mongoose that stores all registered models.
- The
PromptSchemais defined using theSchemaconstructor from Mongoose.- The
PromptSchemadefines the structure of the "Prompt" data in MongoDB. - It has three fields:
creator: Represents the creator of the prompt. It is a reference to the "User" model using theObjectIdtype.prompt: Represents the prompt content. It is a string and is required.tag: Represents the tag associated with the prompt. It is a string and is required.
- The
- The
Promptmodel is defined using themodels.Promptormodel("Prompt", PromptSchema)syntax.- The
models.Promptpart checks if the "Prompt" model is already registered in themodelsobject. If it exists, it assigns the existing model to thePromptvariable. If not, it proceeds to the next part. - The
model("Prompt", PromptSchema)part creates a new model named "Prompt" using themodelfunction from Mongoose. It uses thePromptSchemadefined earlier as the schema for the model.
- The
- The
Promptmodel is exported as the default export of the module, allowing it to be imported and used in other parts of the application.
Overall, this code represents the Mongoose model definition for the "Prompt" data in MongoDB. It defines the structure of the data and provides a convenient way to interact with the database collection that stores the prompts. The model can be imported and used to perform CRUD (Create, Read, Update, Delete) operations on the "Prompt" data.
Creating POST request function
Once this schema is created, we will create a route.js file in the api > prompt > new directory to make the api calls to the database in the following way:
import { connectToDB } from "@utils/database";
import Prompt from "@models/prompt";
export const POST = async (req, res) => {
const { userId, prompt, tag } = await req.json();
try {
await connectToDB(); // a lambda function that connects to the database which will die after the function is done running
const newPrompt = await Prompt.create({
creator: userId,
prompt,
tag,
});
await newPrompt.save();
return new Response(JSON.stringify(newPrompt), {
status: 201,
});
} catch (error) {
return new Response("Failed to create a new prompt", { status: 500 });
}
};
The above code represents a request handler function for creating a new prompt. It is typically used in the context of an API route in a Next.js application. Let's go through the code and understand its functionality:
- The
connectToDBfunction is imported from the "@utils/database" module. This function is responsible for establishing a connection to the MongoDB database. - The
Promptmodel is imported from the "@models/prompt" module. This model represents the Mongoose model for the "Prompt" data in MongoDB. - The
POSTfunction is defined as an asynchronous function that takesreq(request) andres(response) as parameters. - Inside the function, the request body is destructured to extract the
userId,prompt, andtagvalues. - The function then attempts to create a new prompt using the
Prompt.createmethod.- The
Prompt.createmethod creates a new instance of thePromptmodel with the provided data. - The
creator,prompt, andtagfields of the new prompt are set using the extracted values from the request body. - The
awaitkeyword is used to wait for the asynchronous operation to complete.
- The
- After creating the new prompt, the
newPrompt.save()method is called to save the prompt to the database. - If the prompt is successfully saved, a
Responseobject is returned with the created prompt as the JSON response body.- The
Responseconstructor is used to create a new response object. - The
JSON.stringifymethod is used to convert the prompt object to a JSON string. - The
statusproperty of the response is set to 201 (Created) to indicate a successful creation.
- The
- If any error occurs during the process of creating or saving the prompt, a
Responseobject is returned with an appropriate error message and a status of 500 (Internal Server Error).
Overall, this code represents the logic for creating a new prompt and saving it to the MongoDB database. It uses the connectToDB function to establish a database connection, creates a new instance of the Prompt model, and saves it to the database. The response is then returned based on the success or failure of the operation.
Once this route is created, we will now test with npm run dev and type in a new prompt and submit it. For the moment it will submit the data to our MongoDB database collection and return us to the home page without any feed because we haven’t built it yet.
![]() |
| Figure 3: Create Post Form |
Displaying Prompts Feed
Next, we will write the code for the Feed.jsx component to get and display the data on the home page in the following way:
"use client";
import { useState, useEffect } from "react";
import PromptCard from "./PromptCard";
const PromptCardList = ({ data, handleTagClick }) => {
return (
<div className="mt-16 prompt_layout">
{data.map((post) => {
return (
<PromptCard
key={post._id}
post={post}
handleTagClick={handleTagClick}
/>
);
})}
</div>
);
};
const Feed = () => {
const [searchText, setSearchText] = useState("");
const [posts, setPosts] = useState([]);
const handleSearchChange = (e) => {
setSearchText(e.target.value);
};
useEffect(() => {
const fetchPosts = async () => {
const response = await fetch("/api/prompt");
const data = await response.json();
setPosts(data);
};
fetchPosts();
}, []);
return (
<section className="feed">
<form className="relative w-full flex-center">
<input
type="text"
placeholder="Search for a tag or a username"
value={searchText}
onChange={handleSearchChange}
className="search_input"
/>
</form>
<PromptCardList data={posts} handleTagClick={() => {}} />
</section>
);
};
export default Feed;
The above code represents a component called "Feed" that is responsible for rendering a feed of prompt cards and providing a search functionality. Let's go through the code and understand its functionality:
- The code begins by importing the necessary dependencies, including the React hooks
useStateanduseEffect, and thePromptCardcomponent. - The
PromptCardListcomponent is defined as a separate component that takes two props:dataandhandleTagClick. It renders a list ofPromptCardcomponents based on thedataarray. - The
PromptCardListcomponent iterates over thedataarray using themapmethod and renders aPromptCardcomponent for each item in the array. Thekeyprop is set to the_idof the post, and thepostandhandleTagClickprops are passed to eachPromptCardcomponent. - The
Feedcomponent is defined as a functional component. - Inside the
Feedcomponent, two state variables are declared using theuseStatehook:searchTextandposts. - The
handleSearchChangefunction is defined to handle changes in the search input. It updates thesearchTextstate variable based on the value entered in the input field. - The
useEffecthook is used to fetch the posts data from the server when the component mounts.- The
fetchPostsfunction is defined as an asynchronous function that makes a GET request to the "/api/prompt" endpoint to fetch the posts data. - The response data is converted to JSON using
response.json()and stored in thedatavariable. - The
setPostsfunction is called to update thepostsstate variable with the fetched data.
- The
- The
returnstatement contains the JSX code to render the component.- The JSX code includes a form with a search input field that updates the
searchTextstate variable when its value changes. - The
PromptCardListcomponent is rendered, passing thepostsdata as thedataprop and an empty function as thehandleTagClickprop. - The component is wrapped in a
<section>element with the class name "feed".
- The JSX code includes a form with a search input field that updates the
- Finally, the
Feedcomponent is exported as the default export of the module, allowing it to be imported and used in other parts of the application.
Overall, this code represents a component that renders a feed of prompt cards and provides a search functionality. It fetches the posts data from the server, updates the state with the fetched data, and renders the PromptCardList component with the posts data.
Creating GET request function
Next, in order to make the fetch api call successful we will create a route.js file in api > prompt with the following code for GET request:
import { connectToDB } from "@utils/database";
import Prompt from "@models/prompt";
export const GET = async (request) => {
try {
await connectToDB(); // a lambda function that connects to the database which will die after the function is done running
const prompts = await Prompt.find({}).populate("creator");
return new Response(JSON.stringify(prompts), {
status: 200,
});
} catch (error) {
return new Response("Failed to get prompts", { status: 500 });
}
};
The above code represents a request handler function for retrieving prompts from the database. It is typically used in the context of an API route in a Next.js application. Let's go through the code and understand its functionality:
- The
connectToDBfunction is imported from the "@utils/database" module. This function is responsible for establishing a connection to the MongoDB database. - The
Promptmodel is imported from the "@models/prompt" module. This model represents the Mongoose model for the "Prompt" data in MongoDB. - The
GETfunction is defined as an asynchronous function that takes arequestparameter. - Inside the function, the database connection is established using the
connectToDBfunction. - The
Prompt.find({})method is called to find all prompts in the database.- The
Prompt.find({})method returns a Mongoose query that retrieves all documents from the "Prompt" collection. - The
populate("creator")method is used to populate the "creator" field of each prompt with the corresponding user data. It references the "User" model through the "creator" field in the "Prompt" schema.
- The
- The
awaitkeyword is used to wait for the asynchronous operation of fetching prompts to complete. - If the prompts are successfully fetched, a
Responseobject is returned with the prompts as the JSON response body.- The
Responseconstructor is used to create a new response object. - The
JSON.stringifymethod is used to convert the prompts array to a JSON string. - The
statusproperty of the response is set to 200 (OK) to indicate a successful retrieval.
- The
- If any error occurs during the process of fetching prompts, a
Responseobject is returned with an appropriate error message and a status of 500 (Internal Server Error).
Overall, this code represents the logic for retrieving prompts from the database. It establishes a database connection, queries the "Prompt" collection for all prompts, populates the "creator" field with user data, and returns the prompts as a JSON response. The response is then returned based on the success or failure of the operation.
Once the API function is done, we will write the logic to display the prompts within the PromptCard.jsx component in the following way:
"use client";
import { useState } from "react";
import Image from "next/image";
import { useSession } from "next-auth/react";
import { useRouter, usePathname } from "next/navigation";
const PromptCard = ({ post, handleTagClick, handleEdit, handleDelete }) => {
const [copied, setCopied] = useState("");
const handleCopy = () => {
setCopied(post.prompt);
navigator.clipboard.writeText(post.prompt);
setTimeout(() => {
setCopied("");
}, 3000);
};
return (
<div className="prompt_card">
<div className="flex justify-between items-start gap-5">
<div className="flex-1 flex justify-start items-center gap-5 cursor-pointer">
<Image
src={post.creator.image}
alt="user_image"
width={40}
height={40}
className="rounded-full object-contain"
/>
<div className="flex flex-col">
<h3 className="font-satoshi font-semibold text-gray-900">
{post.creator.username}
</h3>
<p className="font-inter text-sm text-gray-500">
{post.creator.email}
</p>
</div>
</div>
<div className="copy_btn" onClick={handleCopy}>
<Image
src={
copied === post.prompt
? "/assets/icons/tick.svg"
: "/assets/icons/copy.svg"
}
width={12}
height={12}
/>
</div>
</div>
<p className="my-4 font-satoshi text-sm text-gray-700">
{post.prompt}
<p
className="font-inter text-sm blue_gradient cursor-pointer"
onClick={() => handleTagClick && handleTagClick(post.tag)}
>
{post.tag}
</p>
</p>
</div>
);
};
export default PromptCard;
The above code represents a React component called "PromptCard" that renders a card displaying information about a prompt. Let's go through the code and understand its functionality:
- The necessary dependencies are imported, including
useState,Imagefrom Next.js,useSessionfrom next-auth/react, anduseRouterandusePathnamefrom next/navigation. - The "PromptCard" component is defined as a functional component that takes several props:
post,handleTagClick,handleEdit, andhandleDelete. These props represent the prompt data and various event handlers. - Inside the component, the
useStatehook is used to define a state variable calledcopied, which will keep track of whether the prompt text has been copied to the clipboard. - The
handleCopyfunction is defined to handle the copy action. It sets thecopiedstate to the prompt text, uses thenavigator.clipboard.writeTextmethod to write the prompt text to the clipboard, and resets thecopiedstate after 3 seconds. - The JSX code is used to render the prompt card.
- The card contains a user section and a copy button section.
- The user section displays the creator's image, username, and email.
- The copy button section displays an image that changes depending on whether the prompt text has been copied or not. The
handleCopyfunction is called when the copy button is clicked. - The prompt text and tag are displayed in the card. The tag is clickable and triggers the
handleTagClickevent handler if it is provided.
- Finally, the
PromptCardcomponent is exported as the default export of the module, allowing it to be imported and used in other parts of the application.
Overall, this code represents a component that renders a prompt card with the creator's information, the prompt text, and a copy button. It provides the functionality to copy the prompt text to the clipboard and triggers an event handler when the tag is clicked.
![]() |
| Figure 5: Prompts on Home Page |
Creating the Profile Page
For this we will create a profile folder within the app directory. Within that we’ll create a page.jsx file with the following code:
"use client";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import Profile from "@components/Profile";
const MyProfile = () => {
const { data: session } = useSession();
const [posts, setPosts] = useState([]);
useEffect(() => {
const fetchPosts = async () => {
const response = await fetch(`/api/users/${session?.user.id}/posts`);
const data = await response.json();
setPosts(data);
};
if (session?.user.id) {
fetchPosts();
}
}, []);
const handleEdit = () => {};
const handleDelete = async () => {};
return (
<Profile
name="My"
desc="Welcome to your personalized profile page!"
data={posts}
handleEdit={handleEdit}
handleDelete={handleDelete}
/>
);
};
export default MyProfile;
The above code represents a React component called "MyProfile" that displays the profile page of the currently logged-in user. Let's go through the code and understand its functionality:
- The necessary dependencies are imported, including
useState,useEffectfrom React,useSessionfrom next-auth/react, anduseRouterfrom next/navigation. - The "MyProfile" component is defined as a functional component.
- Inside the component, the
useSessionhook is used to retrieve the session data, specifically thedataproperty. - The
useStatehook is used to define a state variable calledposts, which will hold the user's posts. - The
useEffecthook is used to fetch the user's posts when the component mounts.- The
fetchPostsfunction is defined as an asynchronous function that makes a request to the API endpoint/api/users/${session?.user.id}/poststo retrieve the user's posts. - The response is converted to JSON using
response.json(). - The retrieved data is set to the
postsstate variable usingsetPosts.
- The
- The
handleEditandhandleDeletefunctions are defined. These functions can be used as event handlers to edit or delete a post. - The
MyProfilecomponent renders theProfilecomponent.- The
Profilecomponent receives props such asname,desc,data,handleEdit, andhandleDelete. - The
nameprop is set to "My" to indicate that this is the user's own profile. - The
descprop provides a welcome message or description for the profile page. - The
dataprop is set to thepostsstate variable, which contains the user's posts. - The
handleEditandhandleDeletefunctions are passed as props to allow editing and deleting of posts.
- The
- Finally, the
MyProfilecomponent is exported as the default export of the module, allowing it to be imported and used in other parts of the application.
Overall, this code represents a component that displays the profile page of the currently logged-in user. It fetches the user's posts and renders the Profile component with the necessary props. It also provides event handlers for editing and deleting posts.
Creating GET request for each user
Next, we will create a dynamic route to enable the fetch request to get the user specific posts. We will create a route.js file in api > users > [id] > posts with the following code:
import { connectToDB } from "@utils/database";
import Prompt from "@models/prompt";
export const GET = async (request, { params }) => {
try {
await connectToDB(); // a lambda function that connects to the database which will die after the function is done running
const prompts = await Prompt.find({ creator: params.id }).populate(
"creator"
);
return new Response(JSON.stringify(prompts), {
status: 200,
});
} catch (error) {
return new Response("Failed to get prompts", { status: 500 });
}
};
The above code snippet defines an asynchronous function named GET that handles a GET request to retrieve prompts created by a specific user. Let's break down the code:
- The
connectToDBfunction is imported from the@utils/databasemodule. This function establishes a connection to the database. - The
Promptmodel is imported from the@models/promptmodule. This model represents the prompts collection in the database. - The
GETfunction accepts two parameters:requestandparams.requestrepresents the incoming request object, andparamscontains the URL parameters. - Inside the
GETfunction, there is a try-catch block to handle any potential errors. - The
await connectToDB()statement ensures that the database connection is established before proceeding with the database operation. - The
Prompt.find({ creator: params.id })query is used to find all prompts where thecreatorfield matches theidvalue provided in the URL parameters. This query searches for prompts created by a specific user. - The
populate("creator")method is used to populate thecreatorfield with the corresponding user data. This allows retrieving the complete user object associated with each prompt. - The retrieved prompts are stored in the
promptsvariable. - The function returns a
Responseobject with a status of 200 (OK) and the JSON stringifiedpromptsas the response body. - In case of an error, a
Responseobject with a status of 500 (Internal Server Error) and an error message is returned.
This code essentially retrieves prompts created by a specific user from the database and sends them as a JSON response.
Next, we will write the logic to present the prompts within the Profile.jsx component in the following way:
import PromptCard from "./PromptCard";
const Profile = ({ name, desc, data, handleEdit, handleDelete }) => {
return (
<section className="w-full">
<h1 className="head_text text-left">
<span className="blue_gradient">{name} Profile</span>
</h1>
<p className="desc text-left">{desc}</p>
<div className="mt-10 prompt_layout">
{data.map((post) => {
return (
<PromptCard
key={post._id}
post={post}
handleEdit={() => handleEdit && handleEdit(post)}
handleDelete={() => handleDelete && handleDelete(post)}
/>
);
})}
</div>
</section>
);
};
export default Profile;
The above code snippet defines a React functional component named Profile that displays a user's profile information and a list of prompts associated with the user. Let's break down the code:
- The
PromptCardcomponent is imported. - The
Profilecomponent is defined as a functional component that accepts several props:name,desc,data,handleEdit, andhandleDelete. - Inside the component, there is a JSX structure that represents the profile section.
- The
nameprop is used to display the name in the heading of the profile section. - The
descprop is used to display the description text in the paragraph below the heading. - The
dataprop is an array of prompts associated with the user. It is mapped over using themapfunction to generate a list ofPromptCardcomponents. - Each
PromptCardcomponent is rendered with the following props:key: A unique identifier for each prompt.post: The prompt object containing information about the prompt.handleEdit: An optional function to handle the edit action on the prompt.handleDelete: An optional function to handle the delete action on the prompt.
- The rendered list of
PromptCardcomponents is enclosed in a<div>element with the classprompt_layout. - The
Profilecomponent is exported as the default export.
In summary, the Profile component is responsible for rendering the user's profile information and a list of prompts associated with the user, using the PromptCard component for each prompt in the list. The handleEdit and handleDelete functions are optional and can be provided to handle edit and delete actions on the prompts, respectively.
![]() |
| Figure 6: User Profile with their own Prompts |
Implementing Edit and Delete Prompt
First we will implement the edit and delete buttons in PromptCard.jsx component in the following way:
"use client";
import { useState } from "react";
import Image from "next/image";
import { useSession } from "next-auth/react";
import { useRouter, usePathname } from "next/navigation";
const PromptCard = ({ post, handleTagClick, handleEdit, handleDelete }) => {
const { data: session } = useSession();
const router = useRouter();
const pathName = usePathname();
const [copied, setCopied] = useState("");
const handleCopy = () => {
setCopied(post.prompt);
navigator.clipboard.writeText(post.prompt);
setTimeout(() => {
setCopied("");
}, 3000);
};
return (
<div className="prompt_card">
<div className="flex justify-between items-start gap-5">
<div className="flex-1 flex justify-start items-center gap-5 cursor-pointer">
<Image
src={post.creator.image}
alt="user_image"
width={40}
height={40}
className="rounded-full object-contain"
/>
<div className="flex flex-col">
<h3 className="font-satoshi font-semibold text-gray-900">
{post.creator.username}
</h3>
<p className="font-inter text-sm text-gray-500">
{post.creator.email}
</p>
</div>
</div>
<div className="copy_btn" onClick={handleCopy}>
<Image
src={
copied === post.prompt
? "/assets/icons/tick.svg"
: "/assets/icons/copy.svg"
}
width={12}
height={12}
/>
</div>
</div>
<p className="my-4 font-satoshi text-sm text-gray-700">
{post.prompt}
<p
className="font-inter text-sm blue_gradient cursor-pointer"
onClick={() => handleTagClick && handleTagClick(post.tag)}
>
#{post.tag}
</p>
</p>
{/* check if the session user is same as post creator and if they are on the profile page */}
{session?.user.id === post.creator._id && pathName === "/profile" && (
<div className="mt-5 flex justify-end gap-6 flex-between border-t border-gray-100 pt-3">
<p
className="font-inter text-sm text-gray-500 cursor-pointer"
onClick={handleDelete}
>
Delete
</p>
<p
className="font-inter text-sm text-green-700 cursor-pointer"
onClick={handleEdit}
>
Edit Prompt
</p>
</div>
)}
</div>
);
};
export default PromptCard;
The code snippet provided defines an updated version of the PromptCard component. Let's review the changes:
- The
useRouterandusePathnamehooks from thenext/navigationpackage are imported. - The
useRouterhook is used to access the current router instance, and theusePathnamehook is used to get the current pathname. - The
PromptCardcomponent now includes additional functionality based on the user's session and the current pathname. - The
useSessionhook is used to access the session data. - The
routervariable is assigned the router instance using theuseRouterhook. - The
pathNamevariable is assigned the current pathname using theusePathnamehook. - Inside the JSX structure, an additional condition is added to check if the session user ID matches the post creator's ID and if the current pathname is "/profile". This condition is used to determine whether to display the edit and delete options for the prompt.
- If the condition is met, the edit and delete options are rendered in a
<div>element with the classes "mt-5 flex justify-end gap-6 flex-between border-t border-gray-100 pt-3". The options are displayed as<p>elements with appropriate event handlers (handleDeleteandhandleEdit). - The updated
PromptCardcomponent is exported as the default export.
These changes enable the display of edit and delete options only for the creator of the prompt when viewing the profile page.
Creating API function for GET, PATCH, and DELETE request for each post
First, we will create a route.js file in api > prompt > [id] directory where will write three sections of code for each different request.
import { connectToDB } from "@utils/database";
import Prompt from "@models/prompt";
//GET (read)
export const GET = async (request, { params }) => {
try {
await connectToDB(); // a lambda function that connects to the database which will die after the function is done running
const prompt = await Prompt.findById(params.id).populate("creator");
if (!prompt) return new Response("Prompt not found", { status: 404 });
return new Response(JSON.stringify(prompt), {
status: 200,
});
} catch (error) {
return new Response("Failed to get prompt", { status: 500 });
}
};
//PATCH (update)
export const PATCH = async (request, { params }) => {
try {
const { prompt, tag } = await request.json();
await connectToDB(); // a lambda function that connects to the database which will die after the function is done running
const existingPrompt = await Prompt.findById(params.id);
if (!existingPrompt)
return new Response("Prompt not found", { status: 404 });
existingPrompt.prompt = prompt;
existingPrompt.tag = tag;
await existingPrompt.save();
return new Response(JSON.stringify(existingPrompt), {
status: 200,
});
} catch (error) {
return new Response("Failed to update prompt", { status: 500 });
}
};
//DELETE (delete)
export const DELETE = async (request, { params }) => {
try {
await connectToDB(); // a lambda function that connects to the database which will die after the function is done running
await Prompt.findByIdAndRemove(params.id);
return new Response("Prompt deleted", { status: 200 });
} catch (error) {
return new Response("Failed to delete prompt", { status: 500 });
}
};
The code snippet above includes three new API route handlers for CRUD operations on prompts. Let's review each handler:
GET: This handler is used to retrieve a single prompt by its ID. It first connects to the database using theconnectToDBfunction. Then it retrieves the prompt from the database usingPrompt.findById(params.id). If the prompt is not found, it returns a response with a status of 404 (Not Found). If the prompt is found, it populates the "creator" field with the corresponding user data usingpopulate("creator"). Finally, it returns a response with the prompt data and a status of 200 (OK).PATCH: This handler is used to update a prompt by its ID. It first extracts the updated prompt and tag from the request's JSON payload usingrequest.json(). Then it connects to the database usingconnectToDB. Next, it retrieves the existing prompt from the database usingPrompt.findById(params.id). If the prompt is not found, it returns a response with a status of 404 (Not Found). If the prompt is found, it updates the prompt and tag fields with the new values. Finally, it saves the changes usingexistingPrompt.save()and returns a response with the updated prompt data and a status of 200 (OK).DELETE: This handler is used to delete a prompt by its ID. It first connects to the database usingconnectToDB. Then it finds and removes the prompt from the database usingPrompt.findByIdAndRemove(params.id). Finally, it returns a response with a status of 200 (OK) to indicate that the prompt was successfully deleted.
These handlers provide the necessary functionality to perform read, update, and delete operations on prompts in the database.
Next we will add the code to navigate to the edit prompt page within the page.jsx within the profile page in the following way:
"use client";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import Profile from "@components/Profile";
const MyProfile = () => {
const router = useRouter();
const { data: session } = useSession();
const [posts, setPosts] = useState([]);
useEffect(() => {
const fetchPosts = async () => {
const response = await fetch(`/api/users/${session?.user.id}/posts`);
const data = await response.json();
setPosts(data);
};
if (session?.user.id) {
fetchPosts();
}
}, []);
const handleEdit = (post) => {
router.push(`/update-prompt?id=${post._id}`);
};
const handleDelete = async (post) => {};
return (
<Profile
name="My"
desc="Welcome to your personalized profile page!"
data={posts}
handleEdit={handleEdit}
handleDelete={handleDelete}
/>
);
};
export default MyProfile;
The updated code snippet includes the client-side code for the "MyProfile" page. Let's review the changes:
useRouter: TheuseRouterhook from thenext/navigationmodule is imported to allow for programmatic navigation.handleEdit: ThehandleEditfunction is modified to navigate to the "Update Prompt" page when invoked. It uses therouter.pushmethod to navigate to the specified URL, which includes the prompt ID as a query parameter.handleDelete: ThehandleDeletefunction is declared but left empty. You can add the necessary logic to delete a prompt when this function is invoked.- Rendering: The
Profilecomponent is rendered with the appropriate props. Thenameprop is set to "My", indicating that it's the user's own profile page. Thedescprop provides a welcome message. Thedataprop is set to thepostsstate, which contains the user's prompts. ThehandleEditandhandleDeletefunctions are passed as props to theProfilecomponent.
These updates enable the "MyProfile" page to display the user's prompts and allow for editing prompts by navigating to the "Update Prompt" page.
Next, we will create the update-prompt directory within the app directory within which we will write the following code (replica of the page.jsx from the create-prompt page):
"use client";
import { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation"; // let's us redirect the user
import Form from "@components/Form";
const EditPrompt = () => {
const router = useRouter();
// const { data: session } = useSession();
const searchParams = useSearchParams();
const promptId = searchParams.get("id");
const [submitting, setSubmitting] = useState(false);
const [post, setPost] = useState({
prompt: "",
tag: "",
});
useEffect(() => {
const getPromptDetails = async () => {
const response = await fetch(`/api/prompt/${promptId}`);
const data = await response.json();
setPost({
prompt: data.prompt,
tag: data.tag,
});
};
if (promptId) {
getPromptDetails();
}
}, [promptId]);
const updatePrompt = async (e) => {
e.preventDefault(); // prevents the page from refreshing
setSubmitting(true);
if (!promptId) {
return alert("Prompt ID is missing!");
}
try {
const response = await fetch(`/api/prompt/${promptId}`, {
method: "PATCH",
body: JSON.stringify({
prompt: post.prompt,
tag: post.tag,
}),
});
if (response.ok) {
router.push("/");
}
} catch (error) {
console.log(error);
} finally {
setSubmitting(false);
}
};
return (
<Form
type="Edit"
post={post}
setPost={setPost}
submitting={submitting}
handleSubmit={updatePrompt}
/>
);
};
export default EditPrompt;
The updated code snippet includes the client-side code for the "EditPrompt" page. Let's review the changes:
useRouter: TheuseRouterhook from thenext/navigationmodule is imported to allow for programmatic navigation.useSearchParams: TheuseSearchParamshook is imported to access the query parameters in the URL.promptId: ThepromptIdis obtained from the query parameters using theuseSearchParamshook.getPromptDetails: ThegetPromptDetailsfunction is modified to fetch the details of the prompt based on thepromptId. The retrieved data is used to populate thepoststate with the prompt details.updatePrompt: TheupdatePromptfunction is modified to send a PATCH request to update the prompt. The request includes the updated prompt details from thepoststate.- Rendering: The
Formcomponent is rendered with the appropriate props. Thetypeprop is set to "Edit" to indicate that it's an edit form. Thepoststate is passed as thepostprop to pre-fill the form fields with the existing prompt details. ThesetPostfunction is passed as thesetPostprop to update thepoststate. Thesubmittingstate is passed as thesubmittingprop to manage the form submission status. TheupdatePromptfunction is passed as thehandleSubmitprop to handle the form submission.
These updates enable the "EditPrompt" page to retrieve the existing prompt details, display them in the form, and update the prompt with the edited information when submitted.
Now when we click on the edit post button, we will get the create post form but now with the previously entered data.
![]() |
Figure 7: Edit Post Form with previous data |
![]() |
Figure 8: Updated post from edit function |
Next we will implement the delete post functionality within the page.jsx file within the profile page in the following way:
"use client";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import Profile from "@components/Profile";
const MyProfile = () => {
const router = useRouter();
const { data: session } = useSession();
const [posts, setPosts] = useState([]);
useEffect(() => {
const fetchPosts = async () => {
const response = await fetch(`/api/users/${session?.user.id}/posts`);
const data = await response.json();
setPosts(data);
};
if (session?.user.id) {
fetchPosts();
}
}, []);
const handleEdit = (post) => {
router.push(`/update-prompt?id=${post._id}`);
};
const handleDelete = async (post) => {
const hasConfirmed = confirm("Are you sure you want to delete this post?");
if (hasConfirmed) {
try {
await fetch(`/api/prompt/${post._id.toString()}`, {
method: "DELETE",
});
const filteredPosts = posts.filter((p) => p._id !== post._id);
setPosts(filteredPosts);
} catch (error) {
console.log(error);
}
}
};
return (
<Profile
name="My"
desc="Welcome to your personalized profile page!"
data={posts}
handleEdit={handleEdit}
handleDelete={handleDelete}
/>
);
};
export default MyProfile;
The updated code snippet includes the client-side code for the "MyProfile" page. Let's review the changes:
handleDelete: ThehandleDeletefunction is modified to prompt the user for confirmation before deleting a post. If the user confirms, a DELETE request is sent to the server to delete the post. If the request is successful, the deleted post is filtered out from thepostsstate to update the UI.- Rendering: The
Profilecomponent is rendered with the appropriate props. Thenameprop is set to "My" to indicate that it's the user's profile. Thedescprop provides a welcome message. Thedataprop is set to thepostsstate to display the user's posts. ThehandleEditandhandleDeletefunctions are passed as props to allow for editing and deleting posts.
These updates enable the "MyProfile" page to fetch the user's posts, display them in the profile, and provide options to edit and delete each post.
Conclusion
Congratulations! You've successfully completed the journey of building a dynamic prompt sharing web application using Next.js 13.4, MongoDB, and Tailwind CSS. Throughout this tutorial, we've explored the power and versatility of these technologies, allowing you to create a captivating platform for users to unleash their creativity.
By leveraging the capabilities of Next.js, we've built a fast, server-side rendered application that provides a seamless user experience. The integration with MongoDB has enabled us to store and retrieve prompt data efficiently, ensuring a robust and scalable solution. And with the help of Tailwind CSS, we've crafted visually appealing interfaces that enhance the overall aesthetic of the application.
But this is just the beginning! With the foundation laid out, there are endless possibilities for you to further enhance and customize the prompt sharing web application. You can consider adding additional features such as user authentication, social sharing capabilities, or even integrating AI-powered functionalities to generate prompts automatically.
In the near future, we will build the search functionality that will allow users to search by tags, usernames, and even prompts. Then we will develope a feature where user specific and tag specific prompts will be displayed on different pages. Stay tuned!
Remember, the key to success is continuous learning and exploration. Keep experimenting with new ideas, technologies, and design patterns to take your application to new heights. The web development landscape is constantly evolving, and by staying curious and open-minded, you'll stay ahead of the curve.
We hope this tutorial has sparked your creativity and inspired you to build more innovative applications. Now it's your turn to create a thriving community of prompt sharers and empower others to unlock their imagination.
Thank you for joining us on this exciting journey. Happy coding and prompt sharing!







