Building a Dynamic Navbar with Next.js 14 and GitHub OAuth

Building a Dynamic Navbar with Next.js 14 and GitHub OAuth

A Guide to Blending Server-Side Rendering (SSR) and Client-Side Rendering (CSR) with Next Auth

ยท

5 min read

This is the navbar that we'll be creating today. Here's how it's designed:

  • Always display the logo on the left

  • Display the 'Create Post' button towards the right. This route will be protected by middleware and accessible to only authenticated users.

  • The right-most section will be dynamic based on whether there is a user session or not.
    - In case of no user session, the 'Login with Github' button will be displayed.
    - In case of an active user session, the profile picture of the user shall be displayed. Clicking on the picture should open up a menu which displays the user's name and a few other relevant routes in the application.

Prerequisites:

  • A Next.js project setup using the App router

  • NextAuth configuration for Github OAuth ( I referred to this tutorial to get this set up)

We begin by creating the Navbar component and adding the static elements to it:

//app/ui/navbar.tsx
import Link from "next/link";
import Image from "next/image";

export default async function NavBar() {

  return (
    <nav className="flex items-center justify-between py-4 px-16 bg-white fixed w-full">
      <Link href="/">
        <Image src="/logo-color.png" width={50} height={60} alt="Logo" />
      </Link>
      <div className="flex items-center justify-center gap-4">
        <Link
          href="/new"
          className="rounded-md bg-indigo-600 px-4 py-3.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
        >
          Create Post
        </Link>
      </div>
    </nav>
  );
}

You should add an image to display your app's logo in the /public folder and set the Image src equal to the file name for the above code to work.

Now, let's dive into creating the dynamic components of the navbar: the Login button and the Navbar menu.

Here's how I created the Login button:

//app/ui/login-button.tsx
"use client";

import { signIn } from "next-auth/react";

const LoginButton = () => {
  return (
    <button
      className="bg-slate-600 px-4 py-3.5 text-sm font-semibold shadow-sm text-white hover:bg-slate-500 rounded-md flex items-center justify-center gap-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-600"
      onClick={() => signIn("github", { callbackUrl: "/" })}
      type="button"
    >
      <svg
        className="w-5 h-5"
        fill="currentColor"
        viewBox="0 0 24 24"
        aria-hidden="true"
      >
        <path
          fillRule="evenodd"
          d="M12 2C6.477 2 2 6.477 2 12c0 4.418 2.865 8.166 6.839 9.489.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.155-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.111-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.026A9.564 9.564 0 0112 6.838c.85.004 1.705.115 2.504.337 1.909-1.295 2.747-1.026 2.747-1.026.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.841-2.337 4.687-4.564 4.934.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .269.18.579.688.481A10.001 10.001 0 0022 12c0-5.523-4.477-10-10-10z"
          clipRule="evenodd"
        />
      </svg>
      Login With GitHub
    </button>
  );
};

export default LoginButton;

I added "use client" on the top of this file to make this a client component since we're using the onClick event handler. I also specified the callbackUrl to tell NextAuth where to redirect the user once they are logged in.
The <svg> is used to create the Github logo on the button.

Next, we will write the code for the Navbar menu that is only visible once the user logs in. This will also be a client component as the menu will toggle based on user interaction.

"use client";

import { useState, useRef, useEffect } from "react";
import { FaUser } from "react-icons/fa";
import Link from "next/link";

interface User {
  name?: string | null;
  email?: string | null;
  image?: string | null;
}

interface NavBarMenuProps {
  user: User;
}

export default function NavBarMenu({ user }: NavBarMenuProps) {
  const [isMenuOpen, setIsMenuOpen] = useState(false);
  const menuRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const handler = (e: MouseEvent) => {
      if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
        setIsMenuOpen(false);
        console.log(menuRef.current);
      }
    };

    document.addEventListener("mousedown", handler);

    return () => {
      document.removeEventListener("mousedown", handler);
    };
  });

  const menuItems = [
    { name: "Dashboard", href: "/dashboard" },
    { name: "Bookmarks", href: "/bookmarks" },
    { name: "Sign out", href: "/signout" },
  ];

  const imageDisplay = user?.image ? (
    <img
      src={user?.image}
      alt="Profile pic"
      className="h-10 w-10 rounded-full border-2 border-gray-600 focus:border-white"
    />
  ) : (
    <div className="h-10 w-10 rounded-full border-2 border-gray-600 focus:border-white flex items-center justify-center">
      <FaUser className="text-xl" />
    </div>
  );

  return (
    <div className="relative" ref={menuRef}>
      <button
        onClick={() => setIsMenuOpen(!isMenuOpen)}
        className="flex items-center justify-center"
      >
        {imageDisplay}
      </button>
      {isMenuOpen && (
        <div className="absolute right-0 mt-2 py-2 w-48 bg-white rounded-md shadow-xl z-20">
          <div className="block px-4 py-2 text-sm text-gray-700 border-b">
            {user?.name}
          </div>
          {menuItems.map((item) => (
            <Link
              key={item.name}
              href={item.href}
              className="block px-4 py-2 text-sm text-gray-700 hover:bg-indigo-100 hover:underline"
            >
              {item.name}
            </Link>
          ))}
        </div>
      )}
    </div>
  );
}

In the above code, first a button is rendered that will display the user's Github profile picture if it's available, otherwise it will display a user icon imported from the react-icons/fa library. Clicking on the button toggles the isMenuOpen state. We conditionally render the user's name and all the menu items if the isMenuOpen state is set to true.

I also added additonal code to close the menu if the user clicks anywhere else on the screen other than the menu div to make it a smoother experience.

Now, the only thing remaining is to conditionally render the Login or the Menu button based on user session. We can add these lines to the navbar component to achieve this:

//app/ui/navbar.tsx

//other imports
import { getServerSession } from "next-auth/next";
import { options } from "@/app/api/auth/[...nextauth]/options";
import LoginButton from "./login-button";
import NavBarMenu from "./navbar-menu";

export default async function NavBar() {
  const session = await getServerSession(options);

  return (
    <nav className="flex items-center justify-between py-4 px-16 bg-white fixed w-full">
      // ...
        <div className="flex items-center justify-center gap-4">
            // ...
            {!session ? (
              <LoginButton />
            ) : (
              <>{session?.user && <NavBarMenu user={session.user} />}</>
            )}
      </div>
    </nav>
  );
}

We could have made the navbar component a client component as well but since it requires session data, it would mean shipping extra code to the client which isn't the most optimal way of doing things.

Note that since the navbar is a server component, any changes you make to its child components might not be visible on hot reload and you might have to do a build for the changes to take place on the browser while developing locally.

And that's it, your beautiful dynamic Navbar is ready! ๐ŸŽ‰

Say hi to me on Twitter and LinkedIn. :)