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:

  1. /** @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.
  2. module.exports = { ... }: This line exports an object containing the Tailwind CSS configuration.
  3. 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}".
  4. theme: { ... }: This property allows you to customize the default theme provided by Tailwind CSS. Inside the extend object, you can add or modify theme values. In this example, the theme is being extended to include customizations for fonts and colors.
  5. 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"].
  6. 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".
  7. plugins: []: This property allows you to enable or configure plugins for Tailwind CSS. In this case, the plugins 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:

  1. import "@/styles/globals.css";: This imports a global CSS file named globals.css using the @ alias. This file likely contains global styles that will be applied throughout the application.
  2. import Nav from "@/components/nav";: This imports a component named Nav from the @/components/nav module. It suggests that there is a nav.js or nav.jsx file in the components directory, which exports the Nav component.
  3. import Provider from "@components/Provider";: This imports a component named Provider from the @components/Provider module. It suggests that there is a Provider.js or Provider.jsx file in the components directory, which exports the Provider component.
  4. export const metadata = { ... }: This exports an object named metadata that contains metadata related to the layout or the application in general. The metadata object includes properties such as title and description with their respective values.
  5. const RootLayout = ({ children }) => { ... }: This defines a functional component named RootLayout that represents the layout for the application. It receives the children prop, which represents the nested components or elements within the layout.
  6. <html lang="en">: This is an HTML tag indicating the root of the HTML document. The lang attribute specifies the language of the document as English.
  7. <body suppressHydrationWarning={true}>: This is the HTML body tag where the content of the page resides. The suppressHydrationWarning attribute is set to true, which can be used to suppress hydration warnings in certain scenarios when server-side rendering (SSR) is involved.
  8. <div className="main">: This is a div element with the CSS class name "main". It likely represents the main content area of the page.
  9. <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.
  10. <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.
  11. <Nav />: This is a self-closing component tag that renders the Nav component imported earlier. It represents a navigation component for the layout.
  12. {children}: This is a special placeholder where the content of the page or component using this layout will be rendered. The children prop represents the nested components or elements within the layout.
  13. </body>: Closing tag for the body element.
  14. </html>: Closing tag for the HTML element.
  15. export default RootLayout;: This exports the RootLayout 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.

  1. "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:

  1. 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.
  2. 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.
  3. <section className="w-full flex-col flex-center"> ... </section>: This JSX code represents a <section> element with the CSS classes w-full, flex-col, and flex-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.
  4. <h1 className="head_text text-center"> ... </h1>: This JSX code represents an <h1> element with the CSS classes head_text and text-center. The text within this element is "Discover and Share", which will be displayed as the main heading of the section.
  5. <br className="max-md:hidden" />: This JSX code represents a line break element (<br>). It has the CSS class max-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.
  6. <span className="orange_gradient text-center">AI-Powered Prompts </span>: This JSX code represents a <span> element with the CSS classes orange_gradient and text-center. The text within this element is "AI-Powered Prompts", which will be displayed as a part of the heading. The orange_gradient class likely applies a CSS style to create an orange gradient effect on this text.
  7. <p className="text-center desc"> ... </p>: This JSX code represents a <p> element with the CSS classes text-center and desc. 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".
  8. 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:

  1. use client;: This indicates NextJS that this component will be rendered on client side.
  2. 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 and useEffect from "react": These are hooks provided by React for managing component state and performing side effects respectively.
    • signIn, signOut, useSession, and getProviders from "next-auth/react": These are authentication-related hooks and functions provided by the NextAuth library for managing user sessions and authentication providers.
  3. The component function Nav is declared using the arrow function syntax.
  4. Inside the component, there are several variables declared using the useState hook:
    • isUserLoggedIn: This variable is currently set to true as a placeholder. It likely represents the state of whether a user is logged in or not.
    • providers: This variable is initialized as null and is used to store the authentication providers available for sign-in. It will be populated asynchronously using the setProviders function.
    • toggleDropdown: This variable represents the state of whether the mobile dropdown menu is open or closed. It is initially set to false.
  5. The useEffect hook is used to fetch the authentication providers when the component is mounted. Inside the effect, the getProviders function is called asynchronously to retrieve the available providers. The result is then set using the setProviders function. The effect runs only once when the component is mounted, as indicated by the empty dependency array [].
  6. 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 class flex-between, w-full, pt-3, and mb-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 classes sm:flex and hidden. 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 classes sm:hidden, flex, and relative. 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 and signOut 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.
  7. 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:

  1. 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.
  2. The console.log() statement outputs an object containing the values of process.env.GOOGLE_CLIENT_ID and process.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.
  3. The NextAuth function is invoked to create a server-side authentication configuration. The function takes an object as its argument with various configuration options.
  4. 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 the clientId and clientSecret values obtained from the environment variables.
  5. 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 takes profile 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 the try block.
  6. The handler variable is assigned the result of the NextAuth function, which is the authentication configuration object.
  7. The authentication configuration object is exported twice using the GET and POST properties. This allows the Next.js API route to handle both GET and POST requests using the same authentication configuration.
    • GET exports the handler configuration object for GET requests.
    • POST exports the same handler 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:

  1. The import statement at the beginning of the code imports the mongoose module, which is the MongoDB ODM library for Node.js.
  2. The variable isConnected is declared and initialized to false. This variable is used to track whether the database connection has been established or not.
  3. The connectToDB function is declared as an asynchronous function.
  4. The mongoose.set() method is called to set the "strictQuery" option to true. 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.
  5. The function checks the isConnected variable. If it is true, 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.
  6. If the isConnected variable is false, indicating that the connection has not been established, the function attempts to connect to the MongoDB database.
  7. Inside a try-catch block, the mongoose.connect() method is called with the process.env.MONGODB_URI value. This value is expected to contain the URI of the MongoDB database, which is retrieved from the environment variables.
  8. The mongoose.connect() method accepts an options object as the second argument, where the dbName, useNewUrlParser, and useUnifiedTopology options are specified.
    • dbName specifies the name of the database to connect to.
    • useNewUrlParser is set to true to use the new MongoDB connection string parser.
    • useUnifiedTopology is set to true to use the new MongoDB Server Discovery and Monitoring engine.
  9. If the connection is established successfully, the isConnected variable is set to true.
  10. 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.
  11. 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:

  1. 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.
  2. The handler variable is declared and assigned the result of the NextAuth function, which is the authentication configuration object.
  3. The authentication configuration object is created by invoking the NextAuth function and passing an object as its argument.
  4. 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 the clientId and clientSecret values obtained from the environment variables.
  5. 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 the session 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 to session.user.id, and the modified session object is returned.
    • The signIn callback is an async function that receives the profile object as a parameter. Inside the function, a database connection is established by calling the connectToDB 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 the profile object. Finally, the function returns true to indicate successful sign-in.
  6. The authentication configuration object is exported twice using the GET and POST properties. This allows the Next.js API route to handle both GET and POST requests using the same authentication configuration.
    • GET exports the handler configuration object for GET requests.
    • POST exports the same handler 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:

  1. 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.
  2. The UserSchema variable is declared and assigned a new instance of the Schema class. This schema defines the structure and validation rules for a user document.
    • The email field is defined as a string with the unique option set to true to ensure email uniqueness. It is also marked as required 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 as required.
    • The image field is defined as a string and does not have any validation rules.
  3. The User variable is declared and assigned the existing "User" model if it exists in the models object. If not, the model function is called to create a new model named "User" based on the UserSchema.
  4. 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:

  1. The /** @type {import('next').NextConfig} */ comment at the beginning of the code is a type annotation comment that provides type information for the nextConfig object. It indicates that the object conforms to the NextConfig type from the Next.js library.
  2. The nextConfig object is declared and initialized with configuration options for the Next.js application.
  3. The experimental property within the nextConfig object specifies experimental features for Next.js.
    • appDir is set to true to enable the experimental support for a separate app 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.
  4. The images property within the nextConfig object configures the image domains that Next.js should allow when using the next/image component. The domains array includes the domain "lh3.googleusercontent.com", indicating that images from this domain should be allowed and optimized by Next.js.
  5. The webpack function within the nextConfig 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 the config object is spread using the spread operator to retain the default experimental features.
    • The topLevelAwait experimental feature is enabled by setting it to true, allowing the usage of top-level await in the application code.
  6. The webpack function returns the modified config object.
  7. The module.exports statement exports the nextConfig 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:

  1. The line "use client"; is a comment indicating that the code should be executed on the client-side.
  2. 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 and useEffect are imported from "react" and are used for managing component state and performing side effects.
    • signIn, signOut, useSession, and getProviders are imported from "next-auth/react" and are NextAuth-specific hooks and functions for managing authentication.
  3. The Nav component is defined as a functional component.
  4. The useSession hook is used to fetch the session data, and the resulting session object is destructured from data property. The session object contains information about the authenticated user.
  5. Two state variables, providers and toggleDropdown, are declared using the useState hook. The providers state variable is used to store the authentication providers available, and the toggleDropdown state variable is used to toggle the visibility of the mobile navigation dropdown menu.
  6. The useEffect hook is used to fetch the available authentication providers when the component mounts. It calls the getProviders function asynchronously and sets the retrieved providers in the providers state variable.
  7. The return statement renders the JSX code representing the navigation bar.
  8. The navigation bar consists of two sections: desktop navigation and mobile navigation.
  9. 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 the Image 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 from providers. These buttons allow the user to sign in with the respective provider.
  10. 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 from providers.
  1. 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:

  1. The line "use client"; is a comment indicating that the code should be executed on the client-side.
  2. 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.
  3. The CreatePrompt component is defined as a functional component.
  4. The useRouter hook is used to access the router object, which provides methods for navigation.
  5. The useSession hook is used to fetch the session data, and the resulting session object is destructured from data property. The session object contains information about the authenticated user.
  6. Two state variables, submitting and post, are declared using the useState hook. The submitting state variable is used to track whether the form is currently being submitted, and the post state variable is used to store the form data (prompt and tag).
  7. 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 to true 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 using router.push("/").
    • If an error occurs, it logs the error to the console.
    • Finally, it sets the submitting state variable back to false to indicate that the form submission is complete.
  8. The return statement renders a Form component with the following props:
    • type is set to "Create" to indicate that it is a form for creating a prompt.
    • post and setPost are used to pass the post state variable and its setter function to the Form component.
    • submitting is used to indicate whether the form is currently being submitted.
    • handleSubmit is set to the createPrompt function to handle the form submission.
  9. 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:

  1. The Link component is imported from "next/link" to create links within the application.
  2. 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.
  3. 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, the onSubmit event is set to the handleSubmit 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 the post 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.
  4. 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:

  1. The Schema, model, and models 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.
  2. The PromptSchema is defined using the Schema 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 the ObjectId 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.
  3. The Prompt model is defined using the models.Prompt or model("Prompt", PromptSchema) syntax.
    • The models.Prompt part checks if the "Prompt" model is already registered in the models object. If it exists, it assigns the existing model to the Prompt variable. If not, it proceeds to the next part.
    • The model("Prompt", PromptSchema) part creates a new model named "Prompt" using the model function from Mongoose. It uses the PromptSchema defined earlier as the schema for the model.
  4. 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:

  1. The connectToDB function is imported from the "@utils/database" module. This function is responsible for establishing a connection to the MongoDB database.
  2. The Prompt model is imported from the "@models/prompt" module. This model represents the Mongoose model for the "Prompt" data in MongoDB.
  3. The POST function is defined as an asynchronous function that takes req (request) and res (response) as parameters.
  4. Inside the function, the request body is destructured to extract the userId, prompt, and tag values.
  5. The function then attempts to create a new prompt using the Prompt.create method.
    • The Prompt.create method creates a new instance of the Prompt model with the provided data.
    • The creator, prompt, and tag 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.
  6. After creating the new prompt, the newPrompt.save() method is called to save the prompt to the database.
  7. 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.
  8. 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:

  1. The code begins by importing the necessary dependencies, including the React hooks useState and useEffect, and the PromptCard component.
  2. The PromptCardList component is defined as a separate component that takes two props: data and handleTagClick. It renders a list of PromptCard components based on the data array.
  3. The PromptCardList component iterates over the data array using the map method and renders a PromptCard component for each item in the array. The key prop is set to the _id of the post, and the post and handleTagClick props are passed to each PromptCard component.
  4. The Feed component is defined as a functional component.
  5. Inside the Feed component, two state variables are declared using the useState hook: searchText and posts.
  6. The handleSearchChange function is defined to handle changes in the search input. It updates the searchText state variable based on the value entered in the input field.
  7. 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 the data variable.
    • The setPosts function is called to update the posts state variable with the fetched data.
  8. 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 the posts data as the data prop and an empty function as the handleTagClick prop.
    • The component is wrapped in a <section> element with the class name "feed".
  9. 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:

  1. The connectToDB function is imported from the "@utils/database" module. This function is responsible for establishing a connection to the MongoDB database.
  2. The Prompt model is imported from the "@models/prompt" module. This model represents the Mongoose model for the "Prompt" data in MongoDB.
  3. The GET function is defined as an asynchronous function that takes a request parameter.
  4. Inside the function, the database connection is established using the connectToDB function.
  5. 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.
  6. The await keyword is used to wait for the asynchronous operation of fetching prompts to complete.
  7. 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.
  8. 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:

  1. The necessary dependencies are imported, including useState, Image from Next.js, useSession from next-auth/react, and useRouter and usePathname from next/navigation.
  2. The "PromptCard" component is defined as a functional component that takes several props: post, handleTagClick, handleEdit, and handleDelete. These props represent the prompt data and various event handlers.
  3. Inside the component, the useState hook is used to define a state variable called copied, which will keep track of whether the prompt text has been copied to the clipboard.
  4. The handleCopy function is defined to handle the copy action. It sets the copied state to the prompt text, uses the navigator.clipboard.writeText method to write the prompt text to the clipboard, and resets the copied state after 3 seconds.
  5. 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.
  6. 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:

  1. The necessary dependencies are imported, including useState, useEffect from React, useSession from next-auth/react, and useRouter from next/navigation.
  2. The "MyProfile" component is defined as a functional component.
  3. Inside the component, the useSession hook is used to retrieve the session data, specifically the data property.
  4. The useState hook is used to define a state variable called posts, which will hold the user's posts.
  5. 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 using setPosts.
  6. The handleEdit and handleDelete functions are defined. These functions can be used as event handlers to edit or delete a post.
  7. The MyProfile component renders the Profile component.
    • The Profile component receives props such as name, desc, data, handleEdit, and handleDelete.
    • 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 the posts state variable, which contains the user's posts.
    • The handleEdit and handleDelete functions are passed as props to allow editing and deleting of posts.
  8. 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:

  1. The connectToDB function is imported from the @utils/database module. This function establishes a connection to the database.
  2. The Prompt model is imported from the @models/prompt module. This model represents the prompts collection in the database.
  3. The GET function accepts two parameters: request and params. request represents the incoming request object, and params contains the URL parameters.
  4. Inside the GET function, there is a try-catch block to handle any potential errors.
  5. The await connectToDB() statement ensures that the database connection is established before proceeding with the database operation.
  6. The Prompt.find({ creator: params.id }) query is used to find all prompts where the creator field matches the id value provided in the URL parameters. This query searches for prompts created by a specific user.
  7. The populate("creator") method is used to populate the creator field with the corresponding user data. This allows retrieving the complete user object associated with each prompt.
  8. The retrieved prompts are stored in the prompts variable.
  9. The function returns a Response object with a status of 200 (OK) and the JSON stringified prompts as the response body.
  10. 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:

  1. The PromptCard component is imported.
  2. The Profile component is defined as a functional component that accepts several props: name, desc, data, handleEdit, and handleDelete.
  3. Inside the component, there is a JSX structure that represents the profile section.
  4. The name prop is used to display the name in the heading of the profile section.
  5. The desc prop is used to display the description text in the paragraph below the heading.
  6. The data prop is an array of prompts associated with the user. It is mapped over using the map function to generate a list of PromptCard components.
  7. 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.
  8. The rendered list of PromptCard components is enclosed in a <div> element with the class prompt_layout.
  9. 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:

  1. The useRouter and usePathname hooks from the next/navigation package are imported.
  2. The useRouter hook is used to access the current router instance, and the usePathname hook is used to get the current pathname.
  3. The PromptCard component now includes additional functionality based on the user's session and the current pathname.
  4. The useSession hook is used to access the session data.
  5. The router variable is assigned the router instance using the useRouter hook.
  6. The pathName variable is assigned the current pathname using the usePathname hook.
  7. 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.
  8. 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 and handleEdit).
  9. 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:

  1. GET: This handler is used to retrieve a single prompt by its ID. It first connects to the database using the connectToDB function. Then it retrieves the prompt from the database using Prompt.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 using populate("creator"). Finally, it returns a response with the prompt data and a status of 200 (OK).
  2. 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 using request.json(). Then it connects to the database using connectToDB. Next, it retrieves the existing prompt from the database using Prompt.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 using existingPrompt.save() and returns a response with the updated prompt data and a status of 200 (OK).
  3. DELETE: This handler is used to delete a prompt by its ID. It first connects to the database using connectToDB. Then it finds and removes the prompt from the database using Prompt.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:

  1. useRouter: The useRouter hook from the next/navigation module is imported to allow for programmatic navigation.
  2. handleEdit: The handleEdit function is modified to navigate to the "Update Prompt" page when invoked. It uses the router.push method to navigate to the specified URL, which includes the prompt ID as a query parameter.
  3. handleDelete: The handleDelete function is declared but left empty. You can add the necessary logic to delete a prompt when this function is invoked.
  4. Rendering: The Profile component is rendered with the appropriate props. The name prop is set to "My", indicating that it's the user's own profile page. The desc prop provides a welcome message. The data prop is set to the posts state, which contains the user's prompts. The handleEdit and handleDelete functions are passed as props to the Profile 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:

  1. useRouter: The useRouter hook from the next/navigation module is imported to allow for programmatic navigation.
  2. useSearchParams: The useSearchParams hook is imported to access the query parameters in the URL.
  3. promptId: The promptId is obtained from the query parameters using the useSearchParams hook.
  4. getPromptDetails: The getPromptDetails function is modified to fetch the details of the prompt based on the promptId. The retrieved data is used to populate the post state with the prompt details.
  5. updatePrompt: The updatePrompt function is modified to send a PATCH request to update the prompt. The request includes the updated prompt details from the post state.
  6. Rendering: The Form component is rendered with the appropriate props. The type prop is set to "Edit" to indicate that it's an edit form. The post state is passed as the post prop to pre-fill the form fields with the existing prompt details. The setPost function is passed as the setPost prop to update the post state. The submitting state is passed as the submitting prop to manage the form submission status. The updatePrompt function is passed as the handleSubmit 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:

  1. handleDelete: The handleDelete 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 the posts state to update the UI.
  2. Rendering: The Profile component is rendered with the appropriate props. The name prop is set to "My" to indicate that it's the user's profile. The desc prop provides a welcome message. The data prop is set to the posts state to display the user's posts. The handleEdit and handleDelete 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!

Popular Posts

Perform CRUD (Create, Read, Update, Delete) Operations using PHP, MySQL and MAMP : Part 4. My First Angular-15 Ionic-7 App

Visualize Your Data: Showcasing Interactive Charts for Numerical Data using Charts JS Library (Part 23) in Your Angular-15 Ionic-7 App

How to Build a Unit Converter for Your Baking Needs with HTML, CSS & Vanilla JavaScript