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 theextend
object, 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, theplugins
array 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.css
using the@
alias. This file likely contains global styles that will be applied throughout the application.import Nav from "@/components/nav";
: This imports a component namedNav
from the@/components/nav
module. It suggests that there is anav.js
ornav.jsx
file in thecomponents
directory, which exports theNav
component.import Provider from "@components/Provider";
: This imports a component namedProvider
from the@components/Provider
module. It suggests that there is aProvider.js
orProvider.jsx
file in thecomponents
directory, which exports theProvider
component.export const metadata = { ... }
: This exports an object namedmetadata
that contains metadata related to the layout or the application in general. The metadata object includes properties such astitle
anddescription
with their respective values.const RootLayout = ({ children }) => { ... }
: This defines a functional component namedRootLayout
that represents the layout for the application. It receives thechildren
prop, 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. Thelang
attribute specifies the language of the document as English.<body suppressHydrationWarning={true}>
: This is the HTML body tag where the content of the page resides. ThesuppressHydrationWarning
attribute 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 theNav
component 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. Thechildren
prop 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 theRootLayout
component 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_text
andtext-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_gradient
andtext-center
. The text within this element is "AI-Powered Prompts", which will be displayed as a part of the heading. Theorange_gradient
class 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-center
anddesc
. 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
import
statements at the beginning of the code import necessary modules and components from various libraries. In this case, the following imports are used:Link
from "next/link": This allows for client-side navigation between pages in a Next.js application.Image
from "next/image": This component is used to optimize and render images in a Next.js application.useState
anduseEffect
from "react": These are hooks provided by React for managing component state and performing side effects respectively.signIn
,signOut
,useSession
, andgetProviders
from "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
Nav
is declared using the arrow function syntax. - Inside the component, there are several variables declared using the
useState
hook:isUserLoggedIn
: This variable is currently set totrue
as a placeholder. It likely represents the state of whether a user is logged in or not.providers
: This variable is initialized asnull
and is used to store the authentication providers available for sign-in. It will be populated asynchronously using thesetProviders
function.toggleDropdown
: This variable represents the state of whether the mobile dropdown menu is open or closed. It is initially set tofalse
.
- The
useEffect
hook is used to fetch the authentication providers when the component is mounted. Inside the effect, thegetProviders
function is called asynchronously to retrieve the available providers. The result is then set using thesetProviders
function. The effect runs only once when the component is mounted, as indicated by the empty dependency array[]
. - The JSX code within the
return
statement represents the structure of the navigation bar. Let's break it down:- The
nav
element 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
Link
component from Next.js to enable client-side navigation. - The second part of the navigation bar is the desktop navigation section, represented by a
div
element with the CSS classessm:flex
andhidden
. 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
Image
component from Next.js. - If the user is not logged in, it checks if the
providers
variable is populated with authentication providers. If so, it renders sign-in buttons for each provider. - The mobile navigation section is represented by another
div
element 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
setToggleDropdown
andsignOut
functions when clicked. - If the user is not logged in, it checks if the
providers
variable 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
import
statements at the beginning of the code import the necessary modules and providers from NextAuth.NextAuth
is imported from "next-auth/next" and represents the NextAuth server-side configuration function.GoogleProvider
is 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_ID
andprocess.env.GOOGLE_CLIENT_SECRET
to 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
NextAuth
function is invoked to create a server-side authentication configuration. The function takes an object as its argument with various configuration options. - The
providers
array within the configuration object specifies the authentication providers to be used. In this case, only the Google provider is included. It is initialized with theclientId
andclientSecret
values obtained from the environment variables. - The
callbacks
object within the configuration object defines callback functions for session management and sign-in handling.- The
session
callback is an empty async function. It can be used to modify the user session object before it is saved. - The
signIn
callback is an async function that takesprofile
as a parameter. It is a placeholder for the sign-in handling logic. The actual implementation of the sign-in logic can be added within thetry
block.
- The
- The
handler
variable is assigned the result of theNextAuth
function, which is the authentication configuration object. - The authentication configuration object is exported twice using the
GET
andPOST
properties. This allows the Next.js API route to handle both GET and POST requests using the same authentication configuration.GET
exports thehandler
configuration object for GET requests.POST
exports the samehandler
configuration 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
import
statement at the beginning of the code imports themongoose
module, which is the MongoDB ODM library for Node.js. - The variable
isConnected
is declared and initialized tofalse
. This variable is used to track whether the database connection has been established or not. - The
connectToDB
function 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
isConnected
variable. 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
isConnected
variable isfalse
, indicating that the connection has not been established, the function attempts to connect to the MongoDB database. - Inside a
try-catch
block, themongoose.connect()
method is called with theprocess.env.MONGODB_URI
value. 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
, anduseUnifiedTopology
options are specified.dbName
specifies the name of the database to connect to.useNewUrlParser
is set totrue
to use the new MongoDB connection string parser.useUnifiedTopology
is set totrue
to use the new MongoDB Server Discovery and Monitoring engine.
- If the connection is established successfully, the
isConnected
variable is set totrue
. - If an error occurs during the connection attempt, the error is caught in the
catch
block, and an error message is logged to the console. - The
connectToDB
function 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
import
statements at the beginning of the code import necessary modules, providers, and utilities:NextAuth
is imported from "next-auth/next" and represents the NextAuth server-side configuration function.GoogleProvider
is imported from "next-auth/providers/google" and represents the Google authentication provider for NextAuth.connectToDB
is imported from "@utils/database" and represents a function to establish a connection to the MongoDB database.User
is imported from "@models/user" and represents the User model used to interact with the database.
- The
handler
variable is declared and assigned the result of theNextAuth
function, which is the authentication configuration object. - The authentication configuration object is created by invoking the
NextAuth
function and passing an object as its argument. - The
providers
array within the configuration object specifies the authentication providers to be used. In this case, only the Google provider is included. It is initialized with theclientId
andclientSecret
values obtained from the environment variables. - The
callbacks
object within the configuration object defines callback functions for session management and sign-in handling.- The
session
callback is an async function that receives thesession
object 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
signIn
callback is an async function that receives theprofile
object as a parameter. Inside the function, a database connection is established by calling theconnectToDB
function. 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 theprofile
object. Finally, the function returnstrue
to indicate successful sign-in.
- The
- The authentication configuration object is exported twice using the
GET
andPOST
properties. This allows the Next.js API route to handle both GET and POST requests using the same authentication configuration.GET
exports thehandler
configuration object for GET requests.POST
exports the samehandler
configuration 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
import
statement at the beginning of the code imports the necessary modules from Mongoose:Schema
represents the Mongoose schema class used to define the structure of a document.model
represents the Mongoose model function used to create a model based on a schema.models
represents the object provided by Mongoose that stores all the registered models.
- The
UserSchema
variable is declared and assigned a new instance of theSchema
class. This schema defines the structure and validation rules for a user document.- The
email
field is defined as a string with theunique
option set totrue
to ensure email uniqueness. It is also marked asrequired
to enforce the presence of an email value. - The
username
field 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
image
field is defined as a string and does not have any validation rules.
- The
- The
User
variable is declared and assigned the existing "User" model if it exists in themodels
object. If not, themodel
function 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 thenextConfig
object. It indicates that the object conforms to theNextConfig
type from the Next.js library. - The
nextConfig
object is declared and initialized with configuration options for the Next.js application. - The
experimental
property within thenextConfig
object specifies experimental features for Next.js.appDir
is set totrue
to enable the experimental support for a separateapp
directory, where components can be placed.serverComponentsExternalPackages
is 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
images
property within thenextConfig
object configures the image domains that Next.js should allow when using thenext/image
component. Thedomains
array includes the domain "lh3.googleusercontent.com", indicating that images from this domain should be allowed and optimized by Next.js. - The
webpack
function within thenextConfig
object allows customization of the Webpack configuration for the Next.js application.- The function receives the
config
object representing the default Webpack configuration. - The
experiments
property of theconfig
object is spread using the spread operator to retain the default experimental features. - The
topLevelAwait
experimental feature is enabled by setting it totrue
, allowing the usage of top-levelawait
in the application code.
- The function receives the
- The
webpack
function returns the modifiedconfig
object. - The
module.exports
statement exports thenextConfig
object 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:
Link
is imported from "next/link" and is used for client-side navigation between pages.Image
is imported from "next/image" and is used for optimizing and rendering images.useState
anduseEffect
are imported from "react" and are used for managing component state and performing side effects.signIn
,signOut
,useSession
, andgetProviders
are imported from "next-auth/react" and are NextAuth-specific hooks and functions for managing authentication.
- The
Nav
component is defined as a functional component. - The
useSession
hook is used to fetch the session data, and the resultingsession
object is destructured fromdata
property. Thesession
object contains information about the authenticated user. - Two state variables,
providers
andtoggleDropdown
, are declared using theuseState
hook. Theproviders
state variable is used to store the authentication providers available, and thetoggleDropdown
state variable is used to toggle the visibility of the mobile navigation dropdown menu. - The
useEffect
hook is used to fetch the available authentication providers when the component mounts. It calls thegetProviders
function asynchronously and sets the retrieved providers in theproviders
state variable. - The
return
statement 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?.user
condition 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 theImage
component. - If the
session?.user
condition 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?.user
condition 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 (toggleDropdown
is true), it renders links to the user's profile and a "Create Prompt" link, as well as a "Sign Out" button. - If the
session?.user
condition is falsy, it renders a set of elements for a non-authenticated user. This includes buttons for each available authentication provider retrieved fromproviders
.
- The
Nav
component 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:
useState
is imported from "react" and is used for managing component state.useSession
is imported from "next-auth/react" and is a NextAuth-specific hook that provides information about the authenticated user.useRouter
is imported from "next/router" and is used for client-side navigation.
- The
CreatePrompt
component is defined as a functional component. - The
useRouter
hook is used to access the router object, which provides methods for navigation. - The
useSession
hook is used to fetch the session data, and the resultingsession
object is destructured fromdata
property. Thesession
object contains information about the authenticated user. - Two state variables,
submitting
andpost
, are declared using theuseState
hook. Thesubmitting
state variable is used to track whether the form is currently being submitted, and thepost
state variable is used to store the form data (prompt and tag). - The
createPrompt
function is defined as an asynchronous function that handles the form submission.- It prevents the default form submission behavior using
e.preventDefault()
. - It sets the
submitting
state variable totrue
to 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
submitting
state variable back tofalse
to indicate that the form submission is complete.
- It prevents the default form submission behavior using
- The
return
statement renders aForm
component with the following props:type
is set to "Create" to indicate that it is a form for creating a prompt.post
andsetPost
are used to pass thepost
state variable and its setter function to theForm
component.submitting
is used to indicate whether the form is currently being submitted.handleSubmit
is set to thecreatePrompt
function to handle the form submission.
- The
CreatePrompt
component 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
Link
component is imported from "next/link" to create links within the application. - The
Form
component 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
section
element with CSS classes for styling. - It includes a heading displaying the form type using the
type
prop. - It includes a description paragraph related to the form type.
- Inside the
form
element, theonSubmit
event is set to thehandleSubmit
function. - 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 thepost
object when the values change. - The form also includes a cancel link and a submit button.
- The cancel link uses the
Link
component to navigate back to the home page when clicked. - The submit button is disabled when
submitting
is true and displays the appropriate text based on the form state.
- The form is wrapped in a
- The
Form
component 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
, andmodels
are imported from the "mongoose" library.Schema
represents the schema definition for the data model.model
is used to create a new Mongoose model based on the schema.models
is an object provided by Mongoose that stores all registered models.
- The
PromptSchema
is defined using theSchema
constructor from Mongoose.- The
PromptSchema
defines 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 theObjectId
type.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
Prompt
model is defined using themodels.Prompt
ormodel("Prompt", PromptSchema)
syntax.- The
models.Prompt
part checks if the "Prompt" model is already registered in themodels
object. If it exists, it assigns the existing model to thePrompt
variable. If not, it proceeds to the next part. - The
model("Prompt", PromptSchema)
part creates a new model named "Prompt" using themodel
function from Mongoose. It uses thePromptSchema
defined earlier as the schema for the model.
- The
- The
Prompt
model 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
connectToDB
function is imported from the "@utils/database" module. This function is responsible for establishing a connection to the MongoDB database. - The
Prompt
model is imported from the "@models/prompt" module. This model represents the Mongoose model for the "Prompt" data in MongoDB. - The
POST
function 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
, andtag
values. - The function then attempts to create a new prompt using the
Prompt.create
method.- The
Prompt.create
method creates a new instance of thePrompt
model with the provided data. - The
creator
,prompt
, andtag
fields of the new prompt are set using the extracted values from the request body. - The
await
keyword 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
Response
object is returned with the created prompt as the JSON response body.- The
Response
constructor is used to create a new response object. - The
JSON.stringify
method is used to convert the prompt object to a JSON string. - The
status
property 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
Response
object 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
useState
anduseEffect
, and thePromptCard
component. - The
PromptCardList
component is defined as a separate component that takes two props:data
andhandleTagClick
. It renders a list ofPromptCard
components based on thedata
array. - The
PromptCardList
component iterates over thedata
array using themap
method and renders aPromptCard
component for each item in the array. Thekey
prop is set to the_id
of the post, and thepost
andhandleTagClick
props are passed to eachPromptCard
component. - The
Feed
component is defined as a functional component. - Inside the
Feed
component, two state variables are declared using theuseState
hook:searchText
andposts
. - The
handleSearchChange
function is defined to handle changes in the search input. It updates thesearchText
state variable based on the value entered in the input field. - The
useEffect
hook is used to fetch the posts data from the server when the component mounts.- The
fetchPosts
function 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 thedata
variable. - The
setPosts
function is called to update theposts
state variable with the fetched data.
- The
- The
return
statement contains the JSX code to render the component.- The JSX code includes a form with a search input field that updates the
searchText
state variable when its value changes. - The
PromptCardList
component is rendered, passing theposts
data as thedata
prop and an empty function as thehandleTagClick
prop. - 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
Feed
component 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
connectToDB
function is imported from the "@utils/database" module. This function is responsible for establishing a connection to the MongoDB database. - The
Prompt
model is imported from the "@models/prompt" module. This model represents the Mongoose model for the "Prompt" data in MongoDB. - The
GET
function is defined as an asynchronous function that takes arequest
parameter. - Inside the function, the database connection is established using the
connectToDB
function. - 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
await
keyword is used to wait for the asynchronous operation of fetching prompts to complete. - If the prompts are successfully fetched, a
Response
object is returned with the prompts as the JSON response body.- The
Response
constructor is used to create a new response object. - The
JSON.stringify
method is used to convert the prompts array to a JSON string. - The
status
property 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
Response
object 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
,Image
from Next.js,useSession
from next-auth/react, anduseRouter
andusePathname
from 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
useState
hook is used to define a state variable calledcopied
, which will keep track of whether the prompt text has been copied to the clipboard. - The
handleCopy
function is defined to handle the copy action. It sets thecopied
state to the prompt text, uses thenavigator.clipboard.writeText
method to write the prompt text to the clipboard, and resets thecopied
state 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
handleCopy
function 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
handleTagClick
event handler if it is provided.
- Finally, the
PromptCard
component 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
,useEffect
from React,useSession
from next-auth/react, anduseRouter
from next/navigation. - The "MyProfile" component is defined as a functional component.
- Inside the component, the
useSession
hook is used to retrieve the session data, specifically thedata
property. - The
useState
hook is used to define a state variable calledposts
, which will hold the user's posts. - The
useEffect
hook is used to fetch the user's posts when the component mounts.- The
fetchPosts
function is defined as an asynchronous function that makes a request to the API endpoint/api/users/${session?.user.id}/posts
to retrieve the user's posts. - The response is converted to JSON using
response.json()
. - The retrieved data is set to the
posts
state variable usingsetPosts
.
- The
- The
handleEdit
andhandleDelete
functions are defined. These functions can be used as event handlers to edit or delete a post. - The
MyProfile
component renders theProfile
component.- The
Profile
component receives props such asname
,desc
,data
,handleEdit
, andhandleDelete
. - The
name
prop is set to "My" to indicate that this is the user's own profile. - The
desc
prop provides a welcome message or description for the profile page. - The
data
prop is set to theposts
state variable, which contains the user's posts. - The
handleEdit
andhandleDelete
functions are passed as props to allow editing and deleting of posts.
- The
- Finally, the
MyProfile
component 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
connectToDB
function is imported from the@utils/database
module. This function establishes a connection to the database. - The
Prompt
model is imported from the@models/prompt
module. This model represents the prompts collection in the database. - The
GET
function accepts two parameters:request
andparams
.request
represents the incoming request object, andparams
contains the URL parameters. - Inside the
GET
function, 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 thecreator
field matches theid
value provided in the URL parameters. This query searches for prompts created by a specific user. - The
populate("creator")
method is used to populate thecreator
field with the corresponding user data. This allows retrieving the complete user object associated with each prompt. - The retrieved prompts are stored in the
prompts
variable. - The function returns a
Response
object with a status of 200 (OK) and the JSON stringifiedprompts
as the response body. - In case of an error, a
Response
object 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
PromptCard
component is imported. - The
Profile
component 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
name
prop is used to display the name in the heading of the profile section. - The
desc
prop is used to display the description text in the paragraph below the heading. - The
data
prop is an array of prompts associated with the user. It is mapped over using themap
function to generate a list ofPromptCard
components. - Each
PromptCard
component 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
PromptCard
components is enclosed in a<div>
element with the classprompt_layout
. - The
Profile
component 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
useRouter
andusePathname
hooks from thenext/navigation
package are imported. - The
useRouter
hook is used to access the current router instance, and theusePathname
hook is used to get the current pathname. - The
PromptCard
component now includes additional functionality based on the user's session and the current pathname. - The
useSession
hook is used to access the session data. - The
router
variable is assigned the router instance using theuseRouter
hook. - The
pathName
variable is assigned the current pathname using theusePathname
hook. - 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 (handleDelete
andhandleEdit
). - The updated
PromptCard
component 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 theconnectToDB
function. 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
: TheuseRouter
hook from thenext/navigation
module is imported to allow for programmatic navigation.handleEdit
: ThehandleEdit
function is modified to navigate to the "Update Prompt" page when invoked. It uses therouter.push
method to navigate to the specified URL, which includes the prompt ID as a query parameter.handleDelete
: ThehandleDelete
function is declared but left empty. You can add the necessary logic to delete a prompt when this function is invoked.- Rendering: The
Profile
component is rendered with the appropriate props. Thename
prop is set to "My", indicating that it's the user's own profile page. Thedesc
prop provides a welcome message. Thedata
prop is set to theposts
state, which contains the user's prompts. ThehandleEdit
andhandleDelete
functions are passed as props to theProfile
component.
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
: TheuseRouter
hook from thenext/navigation
module is imported to allow for programmatic navigation.useSearchParams
: TheuseSearchParams
hook is imported to access the query parameters in the URL.promptId
: ThepromptId
is obtained from the query parameters using theuseSearchParams
hook.getPromptDetails
: ThegetPromptDetails
function is modified to fetch the details of the prompt based on thepromptId
. The retrieved data is used to populate thepost
state with the prompt details.updatePrompt
: TheupdatePrompt
function is modified to send a PATCH request to update the prompt. The request includes the updated prompt details from thepost
state.- Rendering: The
Form
component is rendered with the appropriate props. Thetype
prop is set to "Edit" to indicate that it's an edit form. Thepost
state is passed as thepost
prop to pre-fill the form fields with the existing prompt details. ThesetPost
function is passed as thesetPost
prop to update thepost
state. Thesubmitting
state is passed as thesubmitting
prop to manage the form submission status. TheupdatePrompt
function is passed as thehandleSubmit
prop 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
: ThehandleDelete
function 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 theposts
state to update the UI.- Rendering: The
Profile
component is rendered with the appropriate props. Thename
prop is set to "My" to indicate that it's the user's profile. Thedesc
prop provides a welcome message. Thedata
prop is set to theposts
state to display the user's posts. ThehandleEdit
andhandleDelete
functions 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!