DEV Community

Cover image for TMP: GClient Learner Platform – Phase 1: Building a Dynamic Navbar with Next.js, TypeScript, and Tailwind CSS (Update 3)
Makafui
Makafui

Posted on

TMP: GClient Learner Platform – Phase 1: Building a Dynamic Navbar with Next.js, TypeScript, and Tailwind CSS (Update 3)

Let's dive right into the code :



"use client";


import { useState, useEffect, useRef } from "react";
import LoginClient from "../LearnerRegFlow/LoginClient";
import SignupClient from "../LearnerRegFlow/SignupClient";
import ResetPassword from "../LearnerRegFlow/ResetPassword";
import Link from "next/link";
import Image from "next/image";
import { LogIn, ChevronDown } from "lucide-react";


const Navbar = () => {
  const [showLogin, setShowLogin] = useState(false);
  const [showSignup, setShowSignup] = useState(false);
  const [showResetPassword, setShowResetPassword] = useState(false);
  const [user, setUser] = useState<{ name: string; email: string } | null>(null);
  const [dropdownOpen, setDropdownOpen] = useState(false);


  useEffect(() => {
    // Load user from localStorage
    const storedUser = localStorage.getItem("user");
    if (storedUser) {
      setUser(JSON.parse(storedUser));
    }
  }, []);


  const handleCloseModals = () => {
    setShowLogin(false);
    setShowSignup(false);
    setShowResetPassword(false);
  };


  const handleForgotPassword = () => {
    setShowLogin(false);
    setShowSignup(false);
    setShowResetPassword(true);
  };



  const handleSwitchToSignup = () => {
    setShowLogin(false);
    setShowResetPassword(false);
    setShowSignup(true);
  };




  const handleSwitchToLogin = () => {
    setShowSignup(false);
    setShowResetPassword(false);
    setShowLogin(true);
  };



  const handleLoginSuccess = (userData: { name: string; email: string }) => {
    setUser(userData);
  };


  const handleLogout = () => {
    localStorage.removeItem("token");
    localStorage.removeItem("user");
    setUser(null);
    setDropdownOpen(false);
  };


  return (
    <nav className="flex justify-between items-center px-xl py-4 bg-white font-sans mx-auto rounded box-border width-full">
      <div className="flex items-center gap-6 space-y-2 m-2">
        <Link href="#" className="link">
          <Image className="max-h-8" src="/Azubi-Logo.svg" alt="logo" width={100} height={100} />
        </Link>
        <Link href="/" className="link m-0 block pb-2 text-black text-[16px] font-inter">Home</Link>
        <Link href="/courses" className="link m-0 block pb-2 text-black text-[16px] font-inter">Courses</Link>
      </div>


      <div className="relative">
        {user ? (
          // Logged-in UI (Profile Dropdown)
          <div className="relative flex items-center  cursor-pointer" onClick={() => setDropdownOpen(!dropdownOpen)}>
            <div className="w-10 h-10 rounded-full bg-hero-bg flex items-center justify-center mr-[16px] text-white text-lg font-semibold">
            {user?.name
              ? user.name
                  .split(" ") // Split by space into words
                  .map((word) => word.charAt(0).toUpperCase()) // Get first letter of each word
                  .slice(0, 2) // Take only first two words (if available)
                  .join("") // Combine letters
              : "U"}

            </div>
            <span className="text-black font-medium">{user.name}</span>
            <ChevronDown className="text-black ml-[48px]" />

            {/* Dropdown Menu */}
            {dropdownOpen && (
              <div className="absolute top-[60px] right-0 mt-2 w-48 bg-white border border-gray-300 rounded-md shadow-md z-20">
                <nav className="w-full text-left px-4 py-2 hover:bg-white text-black">
                  <Link href="/" className="block py-2 text-black text-[16px] font-inter hover:text-hero-bg transition-colors duration-200">Portal</Link>
                  <Link
                    href="/"
                    onClick={handleLogout}
                    className="block py-2 text-black text-[16px] font-inter hover:text-hero-bg transition-colors duration-200"
                  >
                    Logout
                  </Link>
                </nav>
              </div>


            )}
          </div>
        ) : (
          // Login Button
          <button
            className="link bg-transparent text-blue-700 py-3 px-6 border border-blue-700 rounded-md flex items-center gap-3 text-base font-medium transition-colors duration-300 ease-in-out hover:bg-hero-bg hover:text-white font-inter"
            onClick={() => setShowLogin(true)}
          >
            <span className="font-inter">Login</span>
            <LogIn />
          </button>
        )}


        {/* LOGIN / SIGNUP / RESET PASSWORD MODALS */}
        {showLogin || showSignup || showResetPassword ? (
          <div className="absolute top-[70px] right-[0px] z-10">
            {showLogin && (
              <LoginClient
                onClose={handleCloseModals}
                onForgotPassword={handleForgotPassword}
                onSignup={handleSwitchToSignup}
                onLoginSuccess={handleLoginSuccess}
              />
            )}
            {showSignup && (
              <SignupClient
                onClose={handleCloseModals}
                onLogin={handleSwitchToLogin}
              />
            )}
            {showResetPassword && (
  <ResetPassword onClose={handleCloseModals} onSignup={handleSwitchToSignup} />
)}




          </div>
        ) : null}
      </div>
    </nav>
  );
};


export default Navbar;

Enter fullscreen mode Exit fullscreen mode

I know this is a lot to wrap your head around but stay with me or it's not maybe it's part of the day-to-day life of a developer to go through this amount of code. Well, this is my thinking process when building the Navbar components.
This is the relative path to the component src\app\components\LearnerPage\Navbar.tsx. you can get my folder structure for my project in the previous post, ie Update 2

This file defines a navigation bar component for my Next.js application, handling user authentication and navigation.
Here's a detailed explanation:

Navbar Component

1. "use client";

This directive at the beginning of the file indicates that this component is a client-side component. In Next.js, components are server-side rendered by default. "use client" specifies that this component, and any components imported into it, will be rendered in the user's browser. This is necessary because this component uses React hooks like useState and useEffect, which are client-side functionalities.

  • Server-Side Rendering (SSR): The server sends a complete web page to your browser, so it loads quickly and is easier for search engines to understand.
  • Client-Side Rendering (CSR): The server sends basic instructions to your browser, which then builds the web page itself. This can make the first load slower, but moving between pages can be faster after that.

React hooks (e.g., useState, useEffect) are tools that manage things like data changes and side effects, and they work only in the browser environment.


2. Import Statements

import { useState, useEffect, useRef } from "react";
import LoginClient from "../LearnerRegFlow/LoginClient";
import SignupClient from "../LearnerRegFlow/SignupClient";
import ResetPassword from "../LearnerRegFlow/ResetPassword";
import Link from "next/link";
import Image from "next/image";
import { LogIn, ChevronDown } from "lucide-react";

Enter fullscreen mode Exit fullscreen mode


typescript

useState, useEffect, useRef from "react": These are React hooks:

useState: Allows you to add state variables to functional components. State variables are used to manage data that can change and trigger re-renders of the component when they do.

  • State Variables: These are like special containers for data in your component. They can hold any kind of data, such as numbers, text, or objects.
  • Manage Data Changes: When the data inside these state variables changes, React knows something has changed in the component.
  • Trigger Re-renders: When the data changes, React automatically updates the component to show the new data. This process is called re-rendering.

useEffect: Lets you perform side effects in functional components. Side effects are operations that interact with the outside world, like data fetching, DOM manipulation, or timers. In this case, it's used to load user data from local storage.

useRef: While imported, useRef is not actually used in the provided code snippet. It's typically used to directly interact with DOM elements or to hold mutable values that don't trigger re-renders.

  • Interacting with DOM Elements: It allows you to directly interact with elements on the page (like inputs, buttons, etc.) without causing the component to re-render. This is useful for things like focusing an input or measuring an element's size.
  • Holding Mutable Values: You can use useRef to store values that might change but don’t need to trigger a re-render. For example, you might use it to keep track of a timer, or to store a previous value for comparison.

LoginClient from "../LearnerRegFlow/LoginClient": Imports a component named LoginClient. This component is responsible for rendering the login modal or form. It's located in the ../LearnerRegFlow/LoginClient path, a directory structure within the Next.js project.

SignupClient from "../LearnerRegFlow/SignupClient": Imports a component named SignupClient, for the signup modal or form, located in the same directory.

ResetPassword from "../LearnerRegFlow/ResetPassword": Imports the ResetPassword component, for handling the password reset functionality, also from the same directory.

Link from "next/link": Imports the Link component from Next.js. This component is used for client-side navigation between routes in your Next.js application, providing better performance than traditional tags.

Image from "next/image": Imports the Image component from Next.js. This component is used for optimized image rendering, including features like lazy loading and responsive sizing.

{ LogIn, ChevronDown } from "lucide-react": Imports two icons, LogIn and ChevronDown, from the lucide-react icon library. These are likely used for visual elements in the navigation bar.

3. Component Definition:

const Navbar = () => { ... }
Enter fullscreen mode Exit fullscreen mode

This line defines a functional React component named Navbar. Functional components are a common way to create UI elements in React.

4. State Variables:

const [showLogin, setShowLogin] = useState(false);
const [showSignup, setShowSignup] = useState(false);
const [showResetPassword, setShowResetPassword] = useState(false);
const [user, setUser] = useState<{ name: string; email: string } | null>(null);
const [dropdownOpen, setDropdownOpen] = useState(false);
Enter fullscreen mode Exit fullscreen mode
  • showLogin, setShowLogin: A state variable to control the visibility of the login modal. showLogin holds a boolean value (initially false), and setShowLogin is a function to update this value. When showLogin is true, the login modal will be displayed.
  • showSignup, setShowSignup: Similar to showLogin, this controls the visibility of the signup modal.
  • showResetPassword, setShowResetPassword: Controls the visibility of the reset password modal.
  • user, setUser: Stores the logged-in user's information. The type useState<{ name: string; email: string } | null>(null) specifies that user can hold either an object with name and email properties (both strings) or null (if no user is logged in). It's initialized to null.
  • dropdownOpen, setDropdownOpen: Controls whether the user profile dropdown menu is open or closed. It's a boolean state, initially false.

5. useEffect Hook:

useEffect(() => {
    // Load user from localStorage
    const storedUser = localStorage.getItem("user");
    if (storedUser) {
        setUser(JSON.parse(storedUser));
    }
}, []);
Enter fullscreen mode Exit fullscreen mode
  • Runs once after the component mounts (because of the empty dependency array []).
  • localStorage.getItem("user"): Retrieves user data from the browser's localStorage using the key "user".
  • if (storedUser): Checks if user data was found in localStorage.
  • setUser(JSON.parse(storedUser)): Converts stored JSON string into an object and updates user state.

6. Modal Handlers:

const handleCloseModals = () => {
    setShowLogin(false);
    setShowSignup(false);
    setShowResetPassword(false);
};

const handleForgotPassword = () => {
    setShowLogin(false);
    setShowSignup(false);
    setShowResetPassword(true);
};

const handleSwitchToSignup = () => {
    setShowLogin(false);
    setShowResetPassword(false);
    setShowSignup(true);
};

const handleSwitchToLogin = () => {
    setShowSignup(false);
    setShowResetPassword(false);
    setShowLogin(true);
};
Enter fullscreen mode Exit fullscreen mode
  • handleCloseModals: Closes all modals (login, signup, reset password).
  • handleForgotPassword: Closes login and signup modals and opens the reset password modal.
  • handleSwitchToSignup: Closes login and reset password modals and opens the signup modal.
  • handleSwitchToLogin: Closes signup and reset password modals and opens the login modal.

7. Authentication Handlers:

const handleLoginSuccess = (userData: { name: string; email: string }) => {
    setUser(userData);
};

const handleLogout = () => {
    localStorage.removeItem("token");
    localStorage.removeItem("user");
    setUser(null);
    setDropdownOpen(false);
};
Enter fullscreen mode Exit fullscreen mode
  • handleLoginSuccess(userData): Updates user state with received user data (name and email).
  • handleLogout():
    • Removes "token" and "user" from localStorage.
    • Sets user state back to null.
    • Closes the dropdown menu.

8. JSX Structure (the return statement):

return (
    <nav className="flex justify-between items-center px-xl py-4 bg-white font-sans mx-auto rounded box-border width-full">
      <div className="flex items-center gap-6 space-y-2 m-2">
        <Link href="#" className="link">
          <Image className="max-h-8" src="/Azubi-Logo.svg" alt="logo" width={100} height={100} />
        </Link>
        <Link href="/" className="link m-0 block pb-2 text-black text-[16px] font-inter">Home</Link>
        <Link href="/courses" className="link m-0 block pb-2 text-black text-[16px] font-inter">Courses</Link>
      </div>


      <div className="relative">
        {user ? (
          // Logged-in UI (Profile Dropdown)
          <div className="relative flex items-center  cursor-pointer" onClick={() => setDropdownOpen(!dropdownOpen)}>
            <div className="w-10 h-10 rounded-full bg-hero-bg flex items-center justify-center mr-[16px] text-white text-lg font-semibold">
            {user?.name
              ? user.name
                  .split(" ") // Split by space into words
                  .map((word) => word.charAt(0).toUpperCase()) // Get first letter of each word
                  .slice(0, 2) // Take only first two words (if available)
                  .join("") // Combine letters
              : "U"}

            </div>
            <span className="text-black font-medium">{user.name}</span>
            <ChevronDown className="text-black ml-[48px]" />

            {/* Dropdown Menu */}
            {dropdownOpen && (
              <div className="absolute top-[60px] right-0 mt-2 w-48 bg-white border border-gray-300 rounded-md shadow-md z-20">
                <nav className="w-full text-left px-4 py-2 hover:bg-white text-black">
                  <Link href="/" className="block py-2 text-black text-[16px] font-inter hover:text-hero-bg transition-colors duration-200">Portal</Link>
                  <Link
                    href="/"
                    onClick={handleLogout}
                    className="block py-2 text-black text-[16px] font-inter hover:text-hero-bg transition-colors duration-200"
                  >
                    Logout
                  </Link>
                </nav>
              </div>


            )}
          </div>
        ) : (
          // Login Button
          <button
            className="link bg-transparent text-blue-700 py-3 px-6 border border-blue-700 rounded-md flex items-center gap-3 text-base font-medium transition-colors duration-300 ease-in-out hover:bg-hero-bg hover:text-white font-inter"
            onClick={() => setShowLogin(true)}
          >
            <span className="font-inter">Login</span>
            <LogIn />
          </button>
        )}


        {/* LOGIN / SIGNUP / RESET PASSWORD MODALS */}
        {showLogin || showSignup || showResetPassword ? (
          <div className="absolute top-[70px] right-[0px] z-10">
            {showLogin && (
              <LoginClient
                onClose={handleCloseModals}
                onForgotPassword={handleForgotPassword}
                onSignup={handleSwitchToSignup}
                onLoginSuccess={handleLoginSuccess}
              />
            )}
            {showSignup && (
              <SignupClient
                onClose={handleCloseModals}
                onLogin={handleSwitchToLogin}
              />
            )}
            {showResetPassword && (
  <ResetPassword onClose={handleCloseModals} onSignup={handleSwitchToSignup} />
)}




          </div>
        ) : null}
      </div>
    </nav>
  );
};


export default Navbar;

Enter fullscreen mode Exit fullscreen mode

<nav> Element

The root element represents the navigation bar. It uses Tailwind CSS classes for styling (e.g., flex, justify-between, items-center, bg-white).


First <div> (Logo and Nav Links)

<div className="flex items-center gap-6 space-y-2 m-2">
    <Link href="#"> ... </Link>
    <Image ... src="/Azubi-Logo.svg" ... />
    <Link href="/">Home</Link>
    <Link href="/courses">Courses</Link>
</div>
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • className="flex items-center gap-6 space-y-2 m-2": Uses flexbox to arrange items horizontally, with spacing and margins.
  • <Link href="#"> ... </Link>: A Next.js Link component for the logo. href="#" might be a placeholder, and you might want to change it to the homepage path (/).
  • <Image ... src="/Azubi-Logo.svg" ... />: Displays the logo image using the Next.js Image component, referencing an image file in the public directory (/Azubi-Logo.svg).
  • Two more <Link> components for "Home" and "Courses" navigation links, pointing to the root path (/) and /courses path, respectively.

Second <div> (User Authentication and Dropdown)

<div className="relative">
    {user ? (
        <div className="relative flex items-center cursor-pointer" onClick={() => setDropdownOpen(!dropdownOpen)}>
            <div className="w-10 h-10 rounded-full bg-hero-bg flex items-center justify-center mr-[16px] text-white text-lg font-semibold">
                {user?.name
                    ? user.name
                        .split(" ")
                        .map((word) => word.charAt(0).toUpperCase())
                        .slice(0, 2)
                        .join("")
                    : "U"}
            </div>
            <span className="text-black font-medium">{user.name}</span>
            <ChevronDown className="text-black ml-[48px]" />
        </div>
    ) : (
        <button className="link bg-transparent" onClick={() => setShowLogin(true)}>
            <span className="font-inter">Login</span>
            <LogIn />
        </button>
    )}
</div>
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • className="relative": Sets the positioning context for absolute positioning of the dropdown menu later.
  • Conditional Rendering ({user ? (...) : (...) }):
    • If a user is logged in (user is truthy), it displays the logged-in UI.
    • If no user is logged in, it displays the logged-out UI.

Logged-in UI (user ? (...)):

  • <div className="relative flex items-center cursor-pointer" onClick={() => setDropdownOpen(!dropdownOpen)}>: A clickable div that toggles the dropdownOpen state when clicked, opening or closing the dropdown menu.
  • Profile Icon <div>:

    • className="w-10 h-10 rounded-full bg-hero-bg flex items-center justify-center mr-[16px] text-white text-lg font-semibold": Styles the profile icon container (circular, colored background, etc.).
    • User Initial Logic:
    {user?.name
        ? user.name
            .split(" ") // Split by space into words
            .map((word) => word.charAt(0).toUpperCase()) // Get first letter of each word
            .slice(0, 2) // Take only first two words (if available)
            .join("") // Combine letters
        : "U"}
    

    This code extracts the first letter of the first two words of the user's name to display as initials in the profile icon. If there's no name, it defaults to "U".

  • <span className="text-black font-medium">{user.name}</span>: Displays the user's name next to the icon.

  • <ChevronDown className="text-black ml-[48px]" />: Displays a chevron down icon, indicating a dropdown menu.

Logged-out UI (: (...)):

  • <button ... onClick={() => setShowLogin(true)}>: A button element styled as a link.
    • className="link bg-transparent ...": Tailwind CSS classes for styling the button.
    • onClick={() => setShowLogin(true)}: When clicked, it sets showLogin to true, opening the login modal.
    • <span className="font-inter">Login</span>: Text "Login" for the button.
    • <LogIn />: The LogIn icon from lucide-react.

Dropdown Menu ({dropdownOpen && (...)})

{dropdownOpen && (
    <div className="absolute top-[60px] right-0 mt-2 w-48 bg-white border border-gray-300 rounded-md shadow-md z-20">
        <nav className="...">
            <Link href="/">Portal</Link>
            <Link href="/" onClick={handleLogout}>Logout</Link>
        </nav>
    </div>
)}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Conditionally rendered only when dropdownOpen is true.
  • className="absolute top-[60px] right-0 mt-2 w-48 bg-white border border-gray-300 rounded-md shadow-md z-20": Styles and positions the dropdown menu absolutely, appearing below and to the right of the profile icon.
  • <nav className="..."> ... </nav>: Contains the dropdown menu items.
    • <Link href="/">Portal</Link>: A link to a "Portal" page.
    • <Link href="/" onClick={handleLogout}>Logout</Link>: A link that, when clicked, calls the handleLogout function to log the user out.

Modal Rendering ({showLogin || showSignup || showResetPassword ? (...) : null})

{showLogin || showSignup || showResetPassword ? (
    <div className="absolute top-[70px] right-[0px] z-10">
        {showLogin && <LoginClient onClose={handleCloseModals} onForgotPassword={handleForgotPassword} onSignup={handleSwitchToSignup} onLoginSuccess={handleLoginSuccess} />}
        {showSignup && <SignupClient onClose={handleCloseModals} onLogin={handleSwitchToLogin} />}
        {showResetPassword && <ResetPassword onClose={handleCloseModals} onSignup={handleSwitchToSignup} />}
    </div>
) : null}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Conditionally renders the modal components only if showLogin, showSignup, or showResetPassword is true.
  • className="absolute top-[70px] right-[0px] z-10": Styles and positions the modal container absolutely, appearing below the navigation bar on the right side.
  • {showLogin && <LoginClient ... />}: Renders the LoginClient component if showLogin is true.
    • onClose={handleCloseModals}: Passes the handleCloseModals function as a prop to LoginClient, allowing the login modal to be closed from within LoginClient.
    • onForgotPassword={handleForgotPassword}: Passes handleForgotPassword to allow the login modal to trigger the reset password modal.
    • onSignup={handleSwitchToSignup}: Passes handleSwitchToSignup to allow switching to the signup modal from the login modal.
    • onLoginSuccess={handleLoginSuccess}: Passes handleLoginSuccess to be called when login is successful, updating the user state in the Navbar.
  • {showSignup && <SignupClient ... />}: Renders the SignupClient component if showSignup is true.
    • onClose={handleCloseModals}: Passes handleCloseModals to close the signup modal.
    • onLogin={handleSwitchToLogin}: Passes handleSwitchToLogin to allow switching to the login modal from the signup modal.
  • {showResetPassword && <ResetPassword ... />}: Renders the ResetPassword component if showResetPassword is true.
    • onClose={handleCloseModals}: Passes handleCloseModals to close the reset password modal.
    • onSignup={handleSwitchToSignup}: Passes handleSwitchToSignup to allow switching to the signup modal from the reset password modal.

Conclusion

Building a Navbar component in a Next.js application might seem like a daunting task at first, especially when you’re juggling multiple states, modals, and user authentication flows. However, breaking it down into smaller, manageable pieces—like handling state, rendering conditional UI, and managing user interactions—makes the process much more approachable.

This Navbar component is a great example of how to create a dynamic and user-friendly navigation bar that adapts to the user's authentication status. By leveraging React hooks like useState and useEffect, along with Next.js features like Link and Image, we’ve built a robust and scalable solution. The use of Tailwind CSS for styling ensures that the component is both responsive and visually appealing.

As developers, we often encounter complex requirements, but with a clear plan and a step-by-step approach, even the most intricate components can be built efficiently. Whether you're a beginner or an experienced developer, understanding how to structure and implement such components is a valuable skill that will serve you well in your projects.

So, the next time you’re faced with a challenging task, remember to break it down, tackle one piece at a time, and don’t hesitate to refer back to examples like this one. Happy coding! 🚀


P.S. If you found this breakdown helpful, feel free to share it with others or leave a comment below. And don’t forget to check out the previous post for the folder structure and more insights into this project!


Top comments (0)