DEV Community

Anyars Yussif
Anyars Yussif

Posted on

Build the UI of an Admin Dashboard for a Pharmacy Management Application using Next.js

Image description

In this tutorial, I will guide you to build the UI of an Admin dashboard for a Pharmacy Management Application using Next.js. By the end of this lesson, you will have better understanding of:

  • Next.js file based routing.
  • how to add tailwind css classes.
  • how to add light and dark mode with a toggle.
  • how to use third party libraries like shadcn, and recharts.

Watch a sneak peak of what you will be building.

In case you get stuck, you can visit the GitHub repository for reference and please, give it a star!

You could use any code editor to follow along, but for this tutorial, I recommend Visual Studio Code. I assume you are an intermediate developer. So, I would skip explaining some basic concepts.

Project Setup

To start, create a folder on your computer, open up your code editor, and drag the folder you created unto your code editor. Open the terminal and run the command below to spin up your Next.js application:

npx create-next-app@latest ./

Make sure to add the ./. Running the command without it would require you to specify the path for your application during the installation. The ./ would ensure that the app is installed in your current directory.

Accept all the default options till the setup is complete.

Once the app is installed, take note of the files and folders created. We will be spending most of our time in the app directory with little to do in the other files. Delete the public folder, open this link, download, extract the content and copy the public folder into the root directory.

Open the app/globals.css file, delete the content and add a default-font. This means, that font would be applied throughout the application. Update it like below:

@import "tailwindcss";

body {
  font-family: "Inter", sans-serif;
}
Enter fullscreen mode Exit fullscreen mode

Open the app/page.tsx file and delete the entire content. Type rafce and select the first option from the intellisense to spin up a new functional component. If that doesn’t work for you, it means you don’t have the ES7+ snippets installed. Go to your extensions and search for ES7+. Install ES7+ React/Redux/React-Native snippets. Back to the page.tsx, typing rafce and selecting the first option should spin up the functional component.

Now, let’s check and see if our app and the tailwind css are working by running npm run dev in the terminal. Open a browser and visit http://localhost:3000. You should see the text page on the interface.

Let’s give the div a className of text-red-500. If the text color changes to red, it means tailwind CSS is working and we can dive right in.

Routing

Let’s take a look at routing. The layout.tsx is the entry point of our application and the page.tsx gets rendered through the layout.tsx. That is the {children} you see in the layout.tsx file.

Any information added directly in the layout.tsx automatically appears on all pages in the app. Inside the layout.tsx, lets update the bottom code like below. We are adding the word “Amazing” below {children}:

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
        Amazing
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

You should see the word “Amazing” appear below page on the browser. Now you can remove the “Amazing” word from the layout.tsx file.

We are going to create our own route groups since the app will involve navigating from one route to another. Firstly, lets delete the app/page.tsx file.

Create a new folder inside the app directory and name it (root). A folder with a name inside parenthesis is used to group routes without affecting the url structure. Inside the (root) folder, create a file and name it layout.tsx and update it like below:

export default function MainLayout({
    children,
  }: Readonly<{
    children: React.ReactNode;
  }>) {
    return (
      <div>
        {children}
      </div>
    );
  }
Enter fullscreen mode Exit fullscreen mode

Every route group must have its own layout.tsx file which will be responsible for rendering the pages within that group. Notice that, this layout.tsx differs from the RootLayout. It only renders {children} within a div tag.

Still inside the (root) folder, create another folder and name it dashboard. Inside the dashboard, create a page.tsx file, run rafce to spin up a functional component and update it like below:

const Dashboard = () => {
  return (
    <div>Dashboard</div>
  )
}

export default Dashboard
Enter fullscreen mode Exit fullscreen mode

When a folder’s name is placed in parenthesis, it means you could navigate to the routes inside that folder without involving the name in parenthesis. Now, visit http://localhost:3000/dashboard in your browser. It should have been http://localhost:3000/(root)/dashboard, but the (root) is skipped.

You should see the word “Dashboard” on the browser.

Lets create additional routes inside the (root) folder like below:

Image description

After creating the folders, create a page.tsx file in each folder. Now run rafce in each page.tsx and create functional components inside them. Name each component with the name of it’s folder. Example, when I run rafce inside medicines-list, I will rename the component like below:

const MedicinesList = () => {
  return (
    <div>MedicinesList</div>
  )
}

export default MedicinesList
Enter fullscreen mode Exit fullscreen mode

Running rafce inside purchase-orders, I will rename like this:

const PurchaseOrders = () => {
  return (
    <div>PurchaseOrders</div>
  )
}

export default PurchaseOrders
Enter fullscreen mode Exit fullscreen mode

Now with the folders and files created, we could navigate through them. In the browser url, navigate to the following pages:

If you could see Customers, MedicinesList, Prescriptions, POS in each case on the browser, it means you have successfully implemented routing in Next.js. Congrats!

Challenge yourself! In your browser, update the url to display the contents of:

  • Purchase Orders
  • Stock Alerts
  • Suppliers
  • Reports
  • Invoices
  • Returns
  • Try creating routes for Profile, Settings, and Sign In.

The pages inside the (root) directory are rendered through the layout.tsx file inside the (root) directory. The {children} within the div represent the pages. Therefore, if we add any text or component directly inside the layout.tsx file, that text or component would be rendered alongside any page being rendered through {children}.

Considering that our app will have a Navbar and a Menu bar which are expected to be visible irrespective of the page being rendered, we could add the Navbar and Menu bar directly inside the MainLayout.tsx.

From the sneak peak video, you could see that the dashboard has two sections; left and right. The left side contains a logo, the app name and the side menu. The right side contains the Navbar and the pages.

Firstly, let’s give the parent container in the MainLayout.tsx a full height by giving it a className of h-screen, make it a flex box, give it a background color, and make the text white. Update the MainLayout.tsx like below:

export default function MainLayout({
    children,
  }: Readonly<{
    children: React.ReactNode;
  }>) {
    return (
      <div className='h-screen flex bg-gray-950 text-white'>
        {children}
      </div>
    );
  }
Enter fullscreen mode Exit fullscreen mode

Let’s add two divs to separate the left and right sides. The left side div will take 14% of the screen and the right side will take 86%. Note that the {children} is part of the right side div. Update the MainLayout.tsx as below:

export default function MainLayout({
    children,
  }: Readonly<{
    children: React.ReactNode;
  }>) {
    return (
      <div className='h-screen flex bg-gray-950 text-white'>
        <div className='w-[14%] bg-gray-800'>Left</div>
        <div className='w-[86%]'>
          Right
        {children}
        </div>
      </div>
    );
  }
Enter fullscreen mode Exit fullscreen mode

Note that the left div has a width of 14% and the right side has a width of 86%. Let’s give the left div some padding and a right border. Update the MainLayout.tsx as below:

export default function MainLayout({
    children,
  }: Readonly<{
    children: React.ReactNode;
  }>) {
    return (
      <div className='h-screen flex bg-gray-950 text-white'>
        <div className='w-[14%] bg-gray-800 p-4 border-r border-gray-400'>Left</div>
        <div className='w-[86%]'>
          Right
        {children}
        </div>
      </div>
    );
  }
Enter fullscreen mode Exit fullscreen mode

At the top of the left side div, let’s a add a Link imported from next/link which will contain the logo and the name of the app. Update the MainLayout.tsx as below:

import Image from "next/image";
import Link from "next/link";

export default function MainLayout({
    children,
  }: Readonly<{
    children: React.ReactNode;
  }>) {
    return (
      <div className='h-screen flex bg-gray-950 text-white'>
        <div className='w-[14%] bg-gray-800 p-4 border-r border-gray-400'>
          <Link
            href="/dashboard"
            className="flex items-center justify-center gap-2"
          >
            <Image src="/logo.png" alt="Logo" width={32} height={32} />
            <span className="font-bold">
              Point of Care Pharmacy
            </span>
          </Link>
        </div>
        <div className='w-[86%]'>
          Right
        {children}
        </div>
      </div>
    );
  }
Enter fullscreen mode Exit fullscreen mode

I added a Link which redirects to the dashboard and contains an image and a span with the name of the app. I made the link a flex box with centered contents and a gap of 2. The logo is given width and height of 32. The text in the span is boldened.

Since the app would be mobile responsive, let’s add some responsiveness to the classNames.

Firstly, the left side div will be 14% initially, then 8% on md screens, 16% on lg screens, and 14% on xl screens. Also, the content of the link are centered but would be justified to the start on lg screens. The span will initially be hidden but would be visible on lg screens. Update the MainLayout.tsx as below:

import Image from "next/image";
import Link from "next/link";

export default function MainLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <div className="h-screen flex bg-gray-950 text-white">
      <div className="w-[14%] md:w-[8%] lg:w-[16%] xl:w-[14%] bg-gray-800 p-4 border-r border-gray-400">
        <Link
          href="/"
          className="flex items-center justify-center lg:justify-start gap-2"
        >
          <Image src="/logo.png" alt="Logo" width={32} height={32} />
          <span className="hidden lg:block font-bold">
            Point of Care Pharmacy
          </span>
        </Link>
      </div>
      <div className="w-[86%]">
        Right
        {children}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Below the link, let’s add the side menu items. We will create that in a separate component and import it into the MainLayout.tsx below the link.

Create a folder in the root directory and name it components. Inside the components folder, create a file and name it SideMenu.tsx. Inside the SideMenu.tsx file, run rafce to spin up a functional component. It should look like below:

const SideMenu = () => {
  return (
    <div>SideMenu</div>
  )
}

export default SideMenu
Enter fullscreen mode Exit fullscreen mode

Import the SideMenu component into the MainLayout.tsx below the link like below:

import SideMenu from "@/components/SideMenu";
import Image from "next/image";
import Link from "next/link";

export default function MainLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <div className="h-screen flex bg-gray-950 text-white">
      <div className="w-[14%] md:w-[8%] lg:w-[16%] xl:w-[14%] bg-gray-800 p-4 border-r border-gray-400">
        <Link
          href="/"
          className="flex items-center justify-center lg:justify-start gap-2"
        >
          <Image src="/logo.png" alt="Logo" width={32} height={32} />
          <span className="hidden lg:block font-bold">
            Point of Care Pharmacy
          </span>
        </Link>

        <SideMenu />
      </div>
      <div className="w-[86%]">
        Right
        {children}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now, let’s open the SideMenu.tsx file and implement it.

Side Menu

First, we have to create a file which will contain the menu items. Then we can import the file into the SideMenu.tsx and map through them to display the menu items.

Create a folder in the root directory and name it constants. Inside the constants folder, create a file and name it menu.ts.

According to the routes we created, we are supposed to have a list of menu items consisting of Dashboard, Customers, Invoices, Medicines List, POS, Prescriptions, Purchase Orders, Reports, Returns, Settings, Staff Management, Stock Alerts, and Suppliers. Let’s put the menu items under two separate titles; MENU, and OTHER. So, create an array containing those titles like below:

const menuItems = [
    {
        title: "MENU",
        items: [],
    },
    {
        title: "OTHER",
        items: [],
    },
 ];
Enter fullscreen mode Exit fullscreen mode

Each title object will contain an array of items beneath them like below:

export const menuItems = [
  {
    title: "MENU",
    items: [
      {
        icon: "/dashboard.png",
        label: "Dashboard",
        href: "/dashboard",
      },
      {
        icon: "/customer.png",
        label: "Customers",
        href: "customers",
      },
      {
        icon: "/medicine_list.png",
        label: "Medicines List",
        href: "/medicines-list",
      },
      {
        icon: "/purchase_orders.png",
        label: "Purchase Orders",
        href: "/purchase-orders",
      },
      {
        icon: "/stock_alert.png",
        label: "Stock Alerts",
        href: "/stock-alerts",
      },
      {
        icon: "/suppliers.png",
        label: "Suppliers",
        href: "/suppliers",
      },
      {
        icon: "/prescription.png",
        label: "Prescriptions",
        href: "/prescriptions",
      },
      {
        icon: "/report.png",
        label: "Reports",
        href: "/reports",
      },
      {
        icon: "/invoice.png",
        label: "Invoices",
        href: "invoices",
      },
      {
        icon: "/pos.png",
        label: "POS",
        href: "/pos",
      },
      {
        icon: "/return.png",
        label: "Returns",
        href: "/returns",
      },
      {
        icon: "/staff_management.png",
        label: "Staff Management",
        href: "/staff-management",
      },
    ],
  },
  {
    title: "OTHER",
    items: [
      {
        icon: "/profile.png",
        label: "Profile",
        href: "/profile",
      },
      {
        icon: "/settings.png",
        label: "Settings",
        href: "/settings",
      },
      {
        icon: "/logout.png",
        label: "Logout",
        href: "/sign-in",
      },
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

The export keyword added at the top allows the menuItems to be imported inside any component in the app. Now we can import the menuItems into the sideMenu.tsx and map through them like below:

import { menuItems } from "@/constants/menu";
import Image from "next/image";
import Link from "next/link";
import React from "react";

const SideMenu = () => {
  return (
    <div className="mt-4 text-sm">
      {menuItems.map((item, i) => (
        <div key={i}>
          <span className="">{item.title}</span>
        </div>
      ))}
    </div>
  );
};

export default SideMenu;
Enter fullscreen mode Exit fullscreen mode

Quite a lot happened here. First, I added some classNames to the container div to give some margin at the top and made the texts small.

Then I mapped through the menuItems taking the item and it’s index i. For each item, I instantly returned a div and gave it a key of the index i. Inside the div, I rendered a span into which I rendered the item.title.

I mentioned the word instantly. There are two types of returns; normal return and instant return.

Below is a normal return:

{menuItems.map((item, i) => {
  return (
    <div key={i}>
      <span className="">{item.title}</span>
    </div>
  )
})}
Enter fullscreen mode Exit fullscreen mode

In a normal return, a set of curly braces {} follows the => and you will have to use return (). You then render the contents inside the () of the return.

Below is an instant return:

{menuItems.map((item, i) => (
  <div key={i}>
    <span className="">{item.title}</span>
  </div>
))}
Enter fullscreen mode Exit fullscreen mode

In an instant return, a set of parenthesis () follows the => and you can directly render the contents without the need for any return ().

Now back to the course, let’s make the outer div a flex box, arranged in a column with a gap of 2. The span will initially be hidden but show on lg screens. It will also have a text color, light font and a margin top and bottom of 4, like below:

import { menuItems } from "@/constants/menu";
import Image from "next/image";
import Link from "next/link";
import React from "react";

const SideMenu = () => {
  return (
    <div className="mt-4 text-sm">
      {menuItems.map((item, i) => (
        <div key={i} className="flex flex-col gap-2">
            <span className="hidden lg:block text-gray-400 font-light my-4">
              {item.title}
            </span>
        </div>
      ))}
    </div>
  );
};

export default SideMenu;
Enter fullscreen mode Exit fullscreen mode

Remember that the menuItems file we created is an array with nested array of items. Therefore, each item contains an array of items. So we have to map through item.items to get the items beneath the titles in the menu, like below:

import { menuItems } from "@/constants/menu";
import Image from "next/image";
import Link from "next/link";
import React from "react";

const SideMenu = () => {
  return (
    <div className="mt-4 text-sm">
      {menuItems.map((item, i) => (
        <div key={i} className="flex flex-col gap-2">
            <span className="hidden lg:block text-gray-400 font-light my-4">
              {item.title}
            </span>

          {item.items.map((subItem, j) => {
            return (
              <Link href={subItem.href} key={j}>
                <Image
                  src={subItem.icon}
                  alt={subItem.label}
                  width={20}
                  height={20}
                />
                <span>{subItem.label}</span>
              </Link>
            );
          })}
        </div>
      ))}
    </div>
  );
};

export default SideMenu;
Enter fullscreen mode Exit fullscreen mode

Note that I used a normal return in the second map. I will then give the Link, and span classNames like below:

import { menuItems } from "@/constants/menu";
import Image from "next/image";
import Link from "next/link";
import React from "react";

const SideMenu = () => {
  return (
    <div className="mt-4 text-sm">
      {menuItems.map((item, i) => (
        <div key={i} className="flex flex-col gap-2">
            <span className="hidden lg:block text-gray-400 font-light my-4">
              {item.title}
            </span>

          {item.items.map((subItem, j) => {
            return (
              <Link
                href={subItem.href}
                key={j}
                className="flex items-center justify-center lg:justify-start gap-4 text-gray-500 md:px-2 rounded-md hover:bg-purple-500 w-full"
              >
                <Image
                  src={subItem.icon}
                  alt={subItem.label}
                  width={20}
                  height={20}
                  className='invert my-4'
                />
                <span className="hidden lg:block text-gray-400 font-light my-4">
                  {subItem.label}
                </span>
              </Link>
            );
          })}
        </div>
      ))}
    </div>
  );
};

export default SideMenu;
Enter fullscreen mode Exit fullscreen mode

At this point, you could realize that the menu items are overflowing. To fix this, add overflow-y-scroll h-[90vh] classNames to the outer div of the SideMenu.tsx. After adding the overflow-y-scroll, you will see an ugly looking scroll bar. Open your globals.css file and add the code below:

.no-scrollbar::-webkit-scrollbar {
  display: none;
}

.no-scrollbar {
  -ms-overflow-style: none; /* IE and Edge */
  scrollbar-width: none; /* Firefox */
}
Enter fullscreen mode Exit fullscreen mode

Then add no-scrollbar className to the outer div of the SideMenu.tsx. The scrollbar should disappear. Below is the updated SideMenu.tsx:

import { menuItems } from "@/constants/menu";
import Image from "next/image";
import Link from "next/link";
import React from "react";

const SideMenu = () => {
  return (
    <div className="mt-4 text-sm overflow-y-scroll h-[90vh] no-scrollbar">
      {menuItems.map((item, i) => (
        <div key={i} className="flex flex-col gap-2">
            <span className="hidden lg:block text-gray-400 font-light my-4">
              {item.title}
            </span>

          {item.items.map((subItem, j) => {
            return (
              <Link
                href={subItem.href}
                key={j}
                className="flex items-center justify-center lg:justify-start gap-4 text-gray-500 md:px-2 rounded-md hover:bg-purple-500 w-full"
              >
                <Image
                  src={subItem.icon}
                  alt={subItem.label}
                  width={20}
                  height={20}
                  className='invert my-4'
                />
                <span className="hidden lg:block text-gray-400 font-light my-4">
                  {subItem.label}
                </span>
              </Link>
            );
          })}
        </div>
      ))}
    </div>
  );
};

export default SideMenu;
Enter fullscreen mode Exit fullscreen mode

At this point, clicking on the menu items should navigate you to their respective pages. Hovering over them should also give you a nice background hover effect. If you were able to implement routes for the Profile, Settings, and Sign In pages, you should be able to navigate to them as well. Great job for coming this far! Give yourself a tap in the back!

Menu Items Background Color

When I click on a menu item, I want the background color of that item to be highlighted. to achieve that, I will make use of usePathname() hook coming from next/navigation. It allows you to grab the current URL’s pathname.

At the extreme top of the SideMenu component, let’s declare a constant, name it pathname and assign it to the usePathname() hook, like below:

const SideMenu = () => {
  const pathname = usePathname();

  return (
    <div className="mt-4 text-sm overflow-y-scroll h-[90vh] no-scrollbar">
      {menuItems.map((item, i) => (
        <div key={i} className="flex flex-col gap-2">
            <span className="hidden lg:block text-gray-400 font-light my-4">
              {item.title}
            </span>
Enter fullscreen mode Exit fullscreen mode

Be sure to import { usePathname } from “next/navigation”. Now, inside our map function, we have to declare an isActive constant to check the active state, like below:

import { menuItems } from "@/constants/menu";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import React from "react";

const SideMenu = () => {
  const pathname = usePathname();

  return (
    <div className="mt-4 text-sm overflow-y-scroll h-[90vh] no-scrollbar">
      {menuItems.map((item, i) => (
          <div key={i} className="flex flex-col gap-2">
              <span className="hidden lg:block text-gray-400 font-light my-4">
                {item.title}
              </span>

            {item.items.map((subItem, j) => {
              const isActive =
              (pathname.includes(subItem.href.toLowerCase()) &&
              subItem.href.length > 1) ||
              pathname === subItem.href.toLowerCase();

              return (
                <Link
                  href={subItem.href}
                  key={j}
                  className="flex items-center justify-center lg:justify-start gap-4 text-gray-500 md:px-2 rounded-md hover:bg-purple-500 w-full"
                >
                  <Image
                    src={subItem.icon}
                    alt={subItem.label}
                    width={20}
                    height={20}
                    className="invert my-4"
                  />
                  <span className="hidden lg:block text-gray-400 font-light my-4">
                    {subItem.label}
                  </span>
                </Link>
              );
            })}
          </div>
      ))}
    </div>
  );
};

export default SideMenu;
Enter fullscreen mode Exit fullscreen mode

You could notice I added this code:

const isActive =
  (pathname.includes(subItem.href.toLowerCase()) &&
            subItem.href.length > 1) ||
          pathname === subItem.href.toLowerCase();
Enter fullscreen mode Exit fullscreen mode

It is basically checking if the subItem.href exists and is contained in the pathname (url path) or if the pathname is equal to the subItem.href. Of course, it converts them to lowercase before the check. If it contains the subItem.href, then it is active. Otherwise, it is inactive.

With that, we could update the classNames of the Link to update the background color based on if the link is active or otherwise, like below:

"use client";

import { menuItems } from "@/constants/menu";
import { cn } from "@/lib/utils";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import React from "react";

const SideMenu = () => {
  const pathname = usePathname();

  return (
    <div className="mt-4 text-sm overflow-y-scroll h-[90vh] no-scrollbar">
      {menuItems.map((item, i) => (
          <div key={i} className="flex flex-col gap-2">
              <span className="hidden lg:block text-gray-400 font-light my-4">
                {item.title}
              </span>

            {item.items.map((subItem, j) => {
              const isActive =
              (pathname.includes(subItem.href.toLowerCase()) &&
              subItem.href.length > 1) ||
              pathname === subItem.href.toLowerCase();

              return (
                <Link
                  href={subItem.href}
                  key={j}
                  className={cn(isActive ? "bg-purple-500" : "","flex items-center justify-center lg:justify-start gap-4 text-gray-500 md:px-2 rounded-md hover:bg-purple-500 w-full")}
                >
                  <Image
                    src={subItem.icon}
                    alt={subItem.label}
                    width={20}
                    height={20}
                    className="invert my-4"
                  />
                  <span className="hidden lg:block text-gray-400 font-light my-4">
                    {subItem.label}
                  </span>
                </Link>
              );
            })}
          </div>
        ))}
    </div>
  );
};

export default SideMenu;
Enter fullscreen mode Exit fullscreen mode

A series of things are happening here as well. Firstly, we are using a hook (usePathname), so our component has to be a client component. You will have to add a “use client” directive at the top of the file.

Secondly, I am using a cn function inside the className of the Link. The cn is a utility function that allows to merge classNames. You could see that, I used a ternary operator isActive ? “bg-purple-500” : “” to dynamically apply the background color, then I added other tailwind classes after a comma. that is the power of the cn function. The background color is applied only if it is active.

To be able to use the cn function, you first have to install tailwind-merge and clsx. So, open your terminal and run npm i tailwind-merge clsx.

After, create a folder in the root directory and name it lib. Inside the lib folder, create a file and name it utils.ts. Update the utils.ts file like below:

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
Enter fullscreen mode Exit fullscreen mode

Now, when you click on a menu item, it will have a background color. Great job!

If you could remember, the menu item descriptions are initially hidden and only visible on lg screens. See the span below:

<span className="hidden lg:block text-gray-400 font-light my-4">
  {subItem.label}
</span>
Enter fullscreen mode Exit fullscreen mode

The challenge would be that if the user is using a device with a screen size smaller than lg, it is only the menu icons they will see and it will be difficult for them to identify the icons by just looking at them. Therefore, it would be helpful if the user could get the name of the menu through a tooltip when they hover over the icon. To achieve that, we are using shadcn tooltip.

Shadcn Tooltip

Since this is a text tutorial, we can’t go through the shadcn documentation together. I will therefore tell you the things we need to do and the code we need to copy from the documentations to use in our application.

You first have to visit shadcn, click Get Started and select the stack you are using, which is Next.js in our case. Next, you have to initialize shadcn by running the command below in your terminal:

npx shadcn@latest init

Press y to proceed when prompted to install shadcn. Choose slate as the base color (just my choice though!).

Select Use — legacy-peer-deps. This is to help resolve package versions compatibility issues.

Next, we have to search for the particular component we want to use, which is Tooltip. So, on the shadcn website, press control + K or command + K and type tooltip. You would be guided to install it using the command below:

npx shadcn@latest add tooltip

make sure to select Use — legacy-peer-deps and proceed. You will notice that a folder named ui is created inside your components folder. Inside the ui folder, you will see a file named tooltip.tsx. You will not have to do anything inside that file anyway. I just wanted you to know that it was created for you.

Next, we have to import the items below at the the top of our component:

import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip"
Enter fullscreen mode Exit fullscreen mode

We have now come to where the magic happens! The only code block we need to implement the tooltip functionality from shadcn is below:

<TooltipProvider>
  <Tooltip>
    <TooltipTrigger>Hover</TooltipTrigger>
    <TooltipContent>
      <p>Add to library</p>
    </TooltipContent>
  </Tooltip>
</TooltipProvider>
Enter fullscreen mode Exit fullscreen mode

The only thing we need to do here is to specify the item we want to trigger the tooltip on, and also specify the text we want to display as the tooltip.

In our case, it is the menu icon we want to use to trigger the tooltip and when the user hovers over the menu icon, the label for that item is displayed as the tooltip. So we will update our code like below:

"use client";

import { menuItems } from "@/constants/menu";
import { cn } from "@/lib/utils";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import React from "react";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";

const SideMenu = () => {
  const pathname = usePathname();

  return (
    <div className="mt-4 text-sm overflow-y-scroll h-[90vh] no-scrollbar">
      {menuItems.map((item, i) => (
        <div key={i} className="flex flex-col gap-2">
          <span className="hidden lg:block text-gray-400 font-light my-4">
            {item.title}
          </span>

          {item.items.map((subItem, j) => {
            const isActive =
              (pathname.includes(subItem.href.toLowerCase()) &&
                subItem.href.length > 1) ||
              pathname === subItem.href.toLowerCase();

            return (
              <Link
                href={subItem.href}
                key={j}
                className={cn(
                  isActive ? "bg-purple-500" : "",
                  "flex items-center justify-center lg:justify-start gap-4 text-gray-500 md:px-2 rounded-md hover:bg-purple-500 w-full"
                )}
              >
                <TooltipProvider>
                  <Tooltip>
                    <TooltipTrigger>
                      <Image
                        src={subItem.icon}
                        alt={subItem.label}
                        width={20}
                        height={20}
                        className="invert my-4"
                      />
                    </TooltipTrigger>
                    <TooltipContent>
                      <p>{subItem.label}</p>
                    </TooltipContent>
                  </Tooltip>
                </TooltipProvider>

                <span className="hidden lg:block text-gray-400 font-light my-4">
                  {subItem.label}
                </span>
              </Link>
            );
          })}
        </div>
      ))}
    </div>
  );
};

export default SideMenu;
Enter fullscreen mode Exit fullscreen mode

I basically pasted the code where we are rendering the image. I replaced the text “Hover” with the image tag. I replaced the “Add to library” text in the p tag with {subItem.label}. Now, when you hover over the menu icons, you should see the tooltips appear. Well-done for coming this far!

Now that we are done with the SideMenu.tsx, lets implement the Navbar.

Navbar

Inside the components folder, create a new file and name it Navbar.tsx. Run rafce to spin up the functional component.

Let’s import and render the Navbar inside the MainLayout (the layout.tsx file inside the (root) folder), like below:

import Navbar from "@/components/Navbar";
import SideMenu from "@/components/SideMenu";
import Image from "next/image";
import Link from "next/link";

export default function MainLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <div className="h-screen flex bg-gray-950 text-white">
      <div className="w-[14%] md:w-[8%] lg:w-[16%] xl:w-[14%] bg-gray-800 p-4 border-r border-gray-400">
        <Link
          href="/dashboard"
          className="flex items-center justify-center lg:justify-start gap-2"
        >
          <Image src="/logo.png" alt="Logo" width={32} height={32} className='invert'/>
          <span className="hidden lg:block font-bold">
            Point of Care Pharmacy
          </span>
        </Link>

        <SideMenu />
      </div>
      <div className="w-[86%]">
        <Navbar />
        {children}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now, le’s open the Navbar and start implementing it.

From the sneak peak, the navbar contains two sections; a search field on the left and other items on the right. So, the parent container will be a flex box with a space between them like below:

const Navbar = () => {
  return (
    <div className='flex items-center justify-between p-4 fixed w-[86%] md:w-[92%] lg:w-[84%] xl:w-[86%] border-b border-gray-400 z-10'>

    </div>
  )
}

export default Navbar
Enter fullscreen mode Exit fullscreen mode

I also added some padding and widths for various screen sizes. I added a bottom border and a z-index of 10 to make sure it appears above other items during scrolling. I also made it fixed with a background color.

Since the Navbar contains two sections, we have to have two divs inside. For now, let’s create the divs with the texts “Left” and “Right” inside of each.

const Navbar = () => {
  return (
    <div className='flex items-center justify-between p-4 bg-gray-800 fixed w-[86%] md:w-[92%] lg:w-[84%] xl:w-[86%] border-b border-gray-400 z-10'>
        <div>Left</div>

        <div>Right</div>
    </div>
  )
}

export default Navbar
Enter fullscreen mode Exit fullscreen mode

You will notice that, the other component that was showing alongside the Navbar is now hidden. It is actually behind the Navbar. Remember I told you that, the {childre} in the layout.tsx represent the pages being rendered. Since the {children} are now falling behind the Navbar, let’s give the {children} some margin-top to push it down. Update the MainLayout like below:

import Navbar from "@/components/Navbar";
import SideMenu from "@/components/SideMenu";
import Image from "next/image";
import Link from "next/link";

export default function MainLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <div className="h-screen flex bg-gray-950 text-white">
      <div className="w-[14%] md:w-[8%] lg:w-[16%] xl:w-[14%] bg-gray-800 p-4 border-r border-gray-400">
        <Link
          href="/dashboard"
          className="flex items-center justify-center lg:justify-start gap-2"
        >
          <Image
            src="/logo.png"
            alt="Logo"
            width={32}
            height={32}
            className="invert"
          />
          <span className="hidden lg:block font-bold">
            Point of Care Pharmacy
          </span>
        </Link>

        <SideMenu />
      </div>
      <div className="w-[86%]">
        <Navbar />

        <div className="mt-16 p-4">{children}</div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

I just kept the {children} inside a div and gave it a margin top of 16px and a padding of 4px.

Now back to the Navbar and implementing the left side, update the code like below:

const Navbar = () => {
  return (
    <div className='flex items-center justify-between p-4 bg-gray-800 fixed w-[86%] md:w-[92%] lg:w-[84%] xl:w-[86%] border-b border-gray-400 z-10'>
        <div className='hidden md:flex items-center gap-2 text-xs rounded-full ring-[1.5px] ring-grey-500 ring-light-500 px-2'>Left</div>

        <div>Right</div>
    </div>
  )
}

export default Navbar
Enter fullscreen mode Exit fullscreen mode

I just gave the div some classNames to make it hidden at the beginning and only visible on md screens and above. I also made it a flex box, rounded, with a ring around it. I also added some horizontal padding.

The div is supposed to have a search icon and a search input. So, update it like below:

const Navbar = () => {
  return (
    <div className="flex items-center justify-between p-4 bg-gray-800 fixed w-[86%] md:w-[92%] lg:w-[84%] xl:w-[86%] border-b border-gray-400 z-10">
      <div className="hidden md:flex items-center gap-2 text-xs rounded-full ring-[1.5px] ring-grey-500 ring-light-500 px-2">
        <Image src="/search.png" alt="Logo" width={14} height={14} />
        <input
          type="text"
          placeholder="Search..."
          className="w-[200px] p-2 bg-transparent outline-none"
        />
      </div>

      <div>Right</div>
    </div>
  );
};

export default Navbar;
Enter fullscreen mode Exit fullscreen mode

I added an image and a search input. The search input has a width of 200px, padding of 2, transparent background with no outline.

Now, let’s look at the right side of the Navbar. The right side contains a profile image, notifications icon with a counter, messages icon and a theme toggle. The items have to be flex with some gap between them. Update the code like below:

const Navbar = () => {
  return (
    <div className="flex items-center justify-between p-4 bg-gray-800 fixed w-[86%] md:w-[92%] lg:w-[84%] xl:w-[86%] border-b border-gray-400 z-10">
      <div className="hidden md:flex items-center gap-2 text-xs rounded-full ring-[1.5px] ring-grey-500 ring-light-500 px-2">
        <Image src="/search.png" alt="Logo" width={14} height={14} />
        <input
          type="text"
          placeholder="Search..."
          className="w-[200px] p-2 bg-transparent outline-none"
        />
      </div>

      <div className='flex items-center justify-end gap-6 w-full'>
        <Image
          src="/avatar.png"
          alt="Logo"
          width={36}
          height={36}
          className="rounded-full"
        />
      </div>
    </div>
  );
};

export default Navbar;
Enter fullscreen mode Exit fullscreen mode

After making the div flex with gap between the items, I added a user avatar and made it rounded. Don’t forget to import Image from "next/image".

To the left of the user avatar, there is a full name and a user role beneath it. That should be another flex div. Adding that will look like this:

import Image from "next/image";
import React from "react";

const Navbar = () => {
  return (
    <div className="flex items-center justify-between p-4 bg-gray-800 fixed w-[86%] md:w-[92%] lg:w-[84%] xl:w-[86%] border-b border-gray-400 z-10">
      <div className="hidden md:flex items-center gap-2 text-xs rounded-full ring-[1.5px] ring-grey-500 ring-light-500 px-2">
        <Image src="/search.png" alt="Logo" width={14} height={14} />
        <input
          type="text"
          placeholder="Search..."
          className="w-[200px] p-2 bg-transparent outline-none"
        />
      </div>

      <div className="flex items-center justify-end gap-6 w-full">
        <div className="flex flex-col">
          <span className="text-xs leading-3 font-medium">John Doe</span>
          <span className="text-[10px] text-gray-500 text-right">
            Admin
          </span>
        </div>

        <Image
          src="/avatar.png"
          alt="Logo"
          width={36}
          height={36}
          className="rounded-full"
        />
      </div>
    </div>
  );
};

export default Navbar;
Enter fullscreen mode Exit fullscreen mode

I just added a flex div with a column direction above the avatar. Then I added two spans; one for the full name and the other for the user role.

Next is the notifications icon. Since it has a counter, it also has to be inside another div. So, above the div containing the full name and role, add another div with classNames like below:

import Image from "next/image";
import React from "react";

const Navbar = () => {
  return (
    <div className="flex items-center justify-between p-4 bg-gray-800 fixed w-[86%] md:w-[92%] lg:w-[84%] xl:w-[86%] border-b border-gray-400 z-10">
      <div className="hidden md:flex items-center gap-2 text-xs rounded-full ring-[1.5px] ring-grey-500 ring-light-500 px-2">
        <Image src="/search.png" alt="Logo" width={14} height={14} />
        <input
          type="text"
          placeholder="Search..."
          className="w-[200px] p-2 bg-transparent outline-none"
        />
      </div>

      <div className="flex items-center justify-end gap-6 w-full">
        <div className="flex items-center justify-center cursor-pointer">
          <Image
            src="/notifications.png"
            alt="notification"
            width={20}
            height={20}
            className="invert"
          />
        </div>

        <div className="flex flex-col">
          <span className="text-xs leading-3 font-medium">John Doe</span>
          <span className="text-[10px] text-gray-500 text-right">Admin</span>
        </div>

        <Image
          src="/avatar.png"
          alt="Logo"
          width={36}
          height={36}
          className="rounded-full"
        />
      </div>
    </div>
  );
};

export default Navbar;
Enter fullscreen mode Exit fullscreen mode

Here, I added a flex div above the names with an image within it.

To add the counter, add another div right below the notifications image like below:

import Image from "next/image";
import React from "react";

const Navbar = () => {
  return (
    <div className="flex items-center justify-between p-4 bg-gray-800 fixed w-[86%] md:w-[92%] lg:w-[84%] xl:w-[86%] border-b border-gray-400 z-10">
      <div className="hidden md:flex items-center gap-2 text-xs rounded-full ring-[1.5px] ring-grey-500 ring-light-500 px-2">
        <Image src="/search.png" alt="Logo" width={14} height={14} />
        <input
          type="text"
          placeholder="Search..."
          className="w-[200px] p-2 bg-transparent outline-none"
        />
      </div>

      <div className="flex items-center justify-end gap-6 w-full">
        <div className="flex items-center justify-center cursor-pointer">
          <Image
            src="/notifications.png"
            alt="notification"
            width={20}
            height={20}
            className="invert"
          />

          <div className="bg-purple-500 text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
            <span className="">0</span>
          </div>
        </div>

        <div className="flex flex-col">
          <span className="text-xs leading-3 font-medium">John Doe</span>
          <span className="text-[10px] text-gray-500 text-right">Admin</span>
        </div>

        <Image
          src="/avatar.png"
          alt="Logo"
          width={36}
          height={36}
          className="rounded-full"
        />
      </div>
    </div>
  );
};

export default Navbar;
Enter fullscreen mode Exit fullscreen mode

Here I added a div below the notifications image with a span that renders 0 within. I gave the div a width and height and made it flex, centered, and rounded. I also gave it a background color.

Once you do this, you will notice that the image and the 0 are side by side each other, but the 0 is supposed to be at the top-right corner of the image. To achieve this, set their parent div as relative and the div containing the span as absolute like below:

<div className="relative flex items-center justify-center cursor-pointer">
   <Image
     src="/notifications.png"
     alt="notification"
     width={20}
     height={20}
     className="invert"
   />

   <div className="absolute bg-purple-500 text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
      <span className="">0</span>
   </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Now you can use positioning to move the span around. I found these values to work the best: -top-3 -right-3. Adding the values:

<div className="relative flex items-center justify-center cursor-pointer">
  <Image
    src="/notifications.png"
    alt="notification"
    width={20}
    height={20}
    className="invert"
  />

   <div className="absolute -top-3 -right-3 bg-purple-500 text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
      <span className="">0</span>
   </div>
 </div>
Enter fullscreen mode Exit fullscreen mode

We can now add the message icon above the notification parent. Add an image tag above the notification parent div like below:

<div className="flex items-center justify-end gap-6 w-full">
        <Image
          src="/message.png"
          alt="Logo"
          width={20}
          height={20}
          className="invert"
        />

        <div className="relative flex items-center justify-center cursor-pointer">
          <Image
            src="/notifications.png"
            alt="notification"
            width={20}
            height={20}
            className="invert"
          />

          <div className="absolute -top-3 -right-3 bg-purple-500 text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
            <span className="">0</span>
          </div>
        </div>

        <div className="flex flex-col">
          <span className="text-xs leading-3 font-medium">John Doe</span>
          <span className="text-[10px] text-gray-500 text-right">Admin</span>
        </div>

        <Image
          src="/avatar.png"
          alt="Logo"
          width={36}
          height={36}
          className="rounded-full"
        />
      </div>
Enter fullscreen mode Exit fullscreen mode

Theme and Theme Toggler

Next, let’s add the theme toggler. Before we do that, lets install next-themes. So, open your terminal and run the following command:

npm i next-themes

Now, let’s add a div with classNames above the message icon like below:

<div className="flex items-center justify-end gap-6 w-full">
        <div className="flex items-center">
        </div>

        <Image
          src="/message.png"
          alt="Logo"
          width={20}
          height={20}
          className="invert"
        />

        <div className="relative flex items-center justify-center cursor-pointer">
          <Image
            src="/notifications.png"
            alt="notification"
            width={20}
            height={20}
            className="invert"
          />

          <div className="absolute -top-3 -right-3 bg-purple-500 text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
            <span className="">0</span>
          </div>
        </div>

        <div className="flex flex-col">
          <span className="text-xs leading-3 font-medium">John Doe</span>
          <span className="text-[10px] text-gray-500 text-right">Admin</span>
        </div>

        <Image
          src="/avatar.png"
          alt="Logo"
          width={36}
          height={36}
          className="rounded-full"
        />
      </div>
Enter fullscreen mode Exit fullscreen mode

Inside that div, let’s add a checkbox input with an associated label like below:

<div className="flex items-center justify-end gap-6 w-full">
        <div className="flex items-center">
          <input
            type="checkbox"
            className="checkbox"
            id="checkbox"
          />

          <label
            htmlFor="checkbox"
            className="label"
          >
          </label>
        </div>

        <Image
          src="/message.png"
          alt="Logo"
          width={20}
          height={20}
          className="invert"
        />

        <div className="relative flex items-center justify-center cursor-pointer">
          <Image
            src="/notifications.png"
            alt="notification"
            width={20}
            height={20}
            className="invert"
          />

          <div className="absolute -top-3 -right-3 bg-purple-500 text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
            <span className="">0</span>
          </div>
        </div>

        <div className="flex flex-col">
          <span className="text-xs leading-3 font-medium">John Doe</span>
          <span className="text-[10px] text-gray-500 text-right">Admin</span>
        </div>

        <Image
          src="/avatar.png"
          alt="Logo"
          width={36}
          height={36}
          className="rounded-full"
        />
      </div>
Enter fullscreen mode Exit fullscreen mode

The checkbox input and label have been given classNames of checkbox and label respectively. The code below shows what each className does. Update your globals.css with the code below:

.checkbox {
  opacity: 0;
  position: absolute;
}

.label {
  transform: scale(1.5);
}
Enter fullscreen mode Exit fullscreen mode

Inside the label, let’s add two font awesome icons like below:

<div className="flex items-center justify-end gap-6 w-full">
        <div className="flex items-center">
          <input
            type="checkbox"
            className="checkbox"
            id="checkbox"
          />

          <label
            htmlFor="checkbox"
            className="label"
          >
            <i className="fas fa-sun" />
            <i className="fas fa-moon" />
          </label>
        </div>

        <Image
          src="/message.png"
          alt="Logo"
          width={20}
          height={20}
          className="invert"
        />

        <div className="relative flex items-center justify-center cursor-pointer">
          <Image
            src="/notifications.png"
            alt="notification"
            width={20}
            height={20}
            className="invert"
          />

          <div className="absolute -top-3 -right-3 bg-purple-500 text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
            <span className="">0</span>
          </div>
        </div>

        <div className="flex flex-col">
          <span className="text-xs leading-3 font-medium">John Doe</span>
          <span className="text-[10px] text-gray-500 text-right">Admin</span>
        </div>

        <Image
          src="/avatar.png"
          alt="Logo"
          width={36}
          height={36}
          className="rounded-full"
        />
      </div>
Enter fullscreen mode Exit fullscreen mode

The icons added are the sun and moon. The icons have also been given classNames of fa-sun and fa-moon. Let’s update the globals.css with the code below:

.fa-moon {
  color: pink;
  font-size: 9px;
}

.fa-sun {
  color: yellow;
  font-size: 9px;
}
Enter fullscreen mode Exit fullscreen mode

This just adds colors and sizes to the icons. let’s add some styles to the label like below:

<div className="flex items-center justify-end gap-6 w-full">
        <div className="flex items-center">
          <input
            type="checkbox"
            className="checkbox"
            id="checkbox"
          />

          <label
            htmlFor="checkbox"
            className="flex justify-between w-8 h-4 bg-black rounded-2xl p-1 relative label"
          >
            <i className="fas fa-sun" />
            <i className="fas fa-moon" />
          </label>
        </div>

        <Image
          src="/message.png"
          alt="Logo"
          width={20}
          height={20}
          className="invert"
        />

        <div className="relative flex items-center justify-center cursor-pointer">
Enter fullscreen mode Exit fullscreen mode

The classNames make the items inside the label flex, give it some width and height, make it rounded, and relative, because there is going to be an absolute self-closing div inside. Let’s add the absolute div which is basically a ball like below:

<div className="flex items-center justify-end gap-6 w-full">
        <div className="flex items-center">
          <input
            type="checkbox"
            className="checkbox"
            id="checkbox"
          />

          <label
            htmlFor="checkbox"
            className="flex justify-between w-8 h-4 bg-black rounded-2xl p-1 relative label"
          >
            <i className="fas fa-sun" />
            <i className="fas fa-moon" />

            <div className="w-3 h-3 absolute bg-yellow-600 rounded-full ball" />
          </label>
        </div>

        <Image
          src="/message.png"
          alt="Logo"
          width={20}
          height={20}
          className="invert"
        />

        <div className="relative flex items-center justify-center cursor-pointer">
Enter fullscreen mode Exit fullscreen mode

You could see that the added div has a className of ball. let’s update the globals.css with the ball class like below:

.ball {
  top: 2px;
  left: 2px;
  transition: transform 0.2s linear;
}

.checkbox:checked + .label .ball {
  transform: translateX(16px);
}
Enter fullscreen mode Exit fullscreen mode

This positions the ball and gives it some transition effect. The second code targets the element after the label. It also makes the ball move left and right when clicked.

We can’t see anything yet because we are not importing the fontawesome icons yet. Let’s open the RootLayout. That is app/layout.tsx and let's add the code below right below {children} but above the body closing tag:

<Script
  src="https://kit.fontawesome.com/a6d38f6541.js"
  crossOrigin="anonymous"
/>
Enter fullscreen mode Exit fullscreen mode

You will have to import Script from “next/script”. You should now see the ball. When you click, the ball switches from the sun to the moon, but nothing is happening. That is because we are not using the next-themes we installed yet. Let’s call the hook below at the top of the component like below:

"use client";

import { useTheme } from "next-themes";
import Image from "next/image";
import React from "react";

const Navbar = () => {
  const { theme, setTheme } = useTheme();

  return (
    <div className="flex items-center justify-between p-4 bg-gray-800 fixed w-[86%] md:w-[92%] lg:w-[84%] xl:w-[86%] border-b border-gray-400 z-10">
      <div className="hidden md:flex items-center gap-2 text-xs rounded-full ring-[1.5px] ring-grey-500 ring-light-500 px-2">
        <Image src="/search.png" alt="Logo" width={14} height={14} />
        <input
          type="text"
          placeholder="Search..."
          className="w-[200px] p-2 bg-transparent outline-none"
        />
      </div>

      <div className="flex items-center justify-end gap-6 w-full">
        <div className="flex items-center">
          <input
            type="checkbox"
            className="checkbox"
            id="checkbox"
Enter fullscreen mode Exit fullscreen mode

Make sure to import { useTheme } from “next-themes”. Next, let’s add an onChange property to the checkbox input we created like below:

"use client";

import { useTheme } from "next-themes";
import Image from "next/image";
import React from "react";

const Navbar = () => {
  const { theme, setTheme } = useTheme();

  return (
    <div className="flex items-center justify-between p-4 bg-gray-800 fixed w-[86%] md:w-[92%] lg:w-[84%] xl:w-[86%] border-b border-gray-400 z-10">
      <div className="hidden md:flex items-center gap-2 text-xs rounded-full ring-[1.5px] ring-grey-500 ring-light-500 px-2">
        <Image src="/search.png" alt="Logo" width={14} height={14} />
        <input
          type="text"
          placeholder="Search..."
          className="w-[200px] p-2 bg-transparent outline-none"
        />
      </div>

      <div className="flex items-center justify-end gap-6 w-full">
        <div className="flex items-center">
          <input
            type="checkbox"
            className="checkbox"
            id="checkbox"
            onChange={() =>
              setTheme(theme === "light" ? "dark" : "light")
            }
          />
Enter fullscreen mode Exit fullscreen mode

The onChnage property uses a ternary operator to set the theme based on the previous theme.

Nothing will happen now because we have not included the ThemeProvider in our RootLayout. Update the RootLayout (app/layout.tsx) like below:

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}

          <Script
            src="https://kit.fontawesome.com/a6d38f6541.js"
            crossOrigin="anonymous"
          />
        </ThemeProvider>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, we are wrapping the {children} and the Script tag with ThemeProvider. Be sure to import { ThemeProvider } from “next-themes”.

You will obviously face hydration error at this point and it is expected. That simply means what is rendered on the server differs from what is rendered on the client. To fix the hydration issue, we have to suppress it in the RootLayout. Update the RootLayout (app/layout.tsx) like below:

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html suppressHydrationWarning lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}

          <Script
            src="https://kit.fontawesome.com/a6d38f6541.js"
            crossOrigin="anonymous"
          />
        </ThemeProvider>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, we update the html tag to include the suppressHydrationWarning attribute.

Now that we have successfully doe all the configurations for the theme, let’s add some styles to the pages based on the theme. First, let’s open the MainLayout. That is app/(root)/layout.tsx:

export default function MainLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <div className="h-screen flex bg-gray-950 text-white">
      <div className="w-[14%] md:w-[8%] lg:w-[16%] xl:w-[14%] bg-gray-800 p-4 border-r border-gray-400">
        <Link
          href="/dashboard"
          className="flex items-center justify-center lg:justify-start gap-2"
        >
          <Image
            src="/logo.png"
            alt="Logo"
            width={32}
            height={32}
            className="invert"
          />
          <span className="hidden lg:block font-bold">
            Point of Care Pharmacy
          </span>
        </Link>
Enter fullscreen mode Exit fullscreen mode

You could see that we are adding some default background colors. In addition to those background colors, let’s add some background colors with the dark: condition like below:

return (
    <div className="h-screen flex bg-gray-100 dark:bg-gray-950 text-white">
      <div className="w-[14%] md:w-[8%] lg:w-[16%] xl:w-[14%] bg-gray-200 dark:bg-gray-800 p-4 border-r border-gray-900 dark:border-gray-400">
Enter fullscreen mode Exit fullscreen mode

In the top div, we are giving a default background color of bg-gray-100, but when it is in the dark mode, the background color should be dark:bg-gray-950. In the second div, we are giving a background color of bg-gray-200, but when in dark mode, it should be dark:bg-gray-800. We are also giving border color of border-gray-900, but when in the dark mode, the border color should be dark:border-gray-400.

Lets update the color for the span as well:

<span className="hidden lg:block font-bold text-black dark:text-white">
  Point of Care Pharmacy
</span>
Enter fullscreen mode Exit fullscreen mode

Here, we are saying the the text color should be black, but it should be white when in dark mode.

Let’s open the SideMenu.tsx and update the color like below:

"use client";

import { menuItems } from "@/constants/menu";
import { cn } from "@/lib/utils";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import React from "react";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";

const SideMenu = () => {
  const pathname = usePathname();

  return (
    <div className="mt-4 text-sm overflow-y-scroll h-[90vh] no-scrollbar">
      {menuItems.map((item, i) => (
        <div key={i} className="flex flex-col gap-2">
          <span className="hidden lg:block text-black dark:text-gray-400 font-light my-4">
            {item.title}
          </span>

          {item.items.map((subItem, j) => {
            const isActive =
              (pathname.includes(subItem.href.toLowerCase()) &&
                subItem.href.length > 1) ||
              pathname === subItem.href.toLowerCase();

            return (
              <Link
                href={subItem.href}
                key={j}
                className={cn(
                  isActive ? "bg-purple-500" : "",
                  "flex items-center justify-center lg:justify-start gap-4 text-gray-500 py-2 md:px-2 rounded-md hover:bg-purple-500 w-full"
                )}
              >
                <TooltipProvider>
                  <Tooltip>
                    <TooltipTrigger>
                      <Image
                        src={subItem.icon}
                        alt={subItem.label}
                        width={20}
                        height={20}
                        className="invert(1) dark:invert cursor-pointer my-4"
                      />
                    </TooltipTrigger>
                    <TooltipContent>
                      <p>{subItem.label}</p>
                    </TooltipContent>
                  </Tooltip>
                </TooltipProvider>

                <span className="hidden lg:block text-black dark:text-gray-400 font-light my-4">
                  {subItem.label}
                </span>
              </Link>
            );
          })}
        </div>
      ))}
    </div>
  );
};

export default SideMenu;
Enter fullscreen mode Exit fullscreen mode

I updated the colors for the two spans for both light and dark mode. I also updated the className for the image to invert the colors based on light and dark mode. In the light mode, invert(1), in dark mode, dark:invert.

Let’s update the colors in the Navbar like below:

"use client";

import { useTheme } from "next-themes";
import Image from "next/image";
import React from "react";

const Navbar = () => {
  const { theme, setTheme } = useTheme();

  return (
    <div className="flex items-center justify-between p-4 bg-gray-200 dark:bg-gray-800 fixed w-[86%] md:w-[92%] lg:w-[84%] xl:w-[86%] border-b border-gray-400 z-10">
      <div className="hidden md:flex items-center gap-2 text-xs rounded-full ring-[1.5px] ring-gray-700 dark:ring-gray-500 ring-light-500 px-2">
        <Image src="/search.png" alt="Logo" width={14} height={14} />
        <input
          type="text"
          placeholder="Search..."
          className="w-[200px] p-2 bg-transparent outline-none text-black dark:text-white"
        />
      </div>

      <div className="flex items-center justify-end gap-6 w-full">
        <div className="flex items-center">
          <input
            type="checkbox"
            className="checkbox"
            id="checkbox"
            onChange={() =>
              setTheme(theme === "light" ? "dark" : "light")
            }
          />

          <label
            htmlFor="checkbox"
            className="flex justify-between w-8 h-4 bg-black rounded-2xl p-1 relative label"
          >
            <i className="fas fa-sun" />
            <i className="fas fa-moon" />

            <div className="w-3 h-3 absolute bg-yellow-600 rounded-full ball" />
          </label>
        </div>

        <Image
          src="/message.png"
          alt="Logo"
          width={20}
          height={20}
          className="invert(1) dark:invert"
        />

        <div className="relative flex items-center justify-center cursor-pointer">
          <Image
            src="/notifications.png"
            alt="notification"
            width={20}
            height={20}
            className="invert(1) dark:invert"
          />

          <div className="absolute -top-3 -right-3 bg-purple-500 text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
            <span className="">0</span>
          </div>
        </div>

        <div className="flex flex-col">
          <span className="text-xs leading-3 font-medium text-black dark:text-white">John Doe</span>
          <span className="text-[10px] text-gray-500 text-right">Admin</span>
        </div>

        <Image
          src="/avatar.png"
          alt="Logo"
          width={36}
          height={36}
          className="rounded-full"
        />
      </div>
    </div>
  );
};

export default Navbar;
Enter fullscreen mode Exit fullscreen mode

Now that you understand how to apply colors based on selected themes, carefully go though and update all colors with dark options. Images also have classNames of invert(1) dark:invert.

Finally, in the MainLayout, update the div containing {children} like below:

</div>
      <div className="w-[86%]">
        <Navbar />

        <div className="mt-16 p-4 text-black dark:text-white">{children}</div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

All texts will be black by default, but white when in dark mode.

We are now on the last part of this guide. As I stated earlier, this guide is to take you through building the UI of the admin dashboard, so we will not be building the other pages. I only created the pages to help you understand Next.js file based routing.

From the sneak peak, the admin dashboard has three sections; product cards, sales graph, and income and expenditure graph. Therefore, we will have three divs flexed vertically.

Let’s open app/(root)/dashboard/page.tsx and create the three divs like below:

const Dashboard = () => {
  return (
    <div className='h-screen flex gap-4 flex-col'>
      <div>Product Cards</div>

      <div>Sales Graph</div>

      <div>income and expenditure graph</div>
    </div>
  )
}

export default Dashboard
Enter fullscreen mode Exit fullscreen mode

The parent container has a full height and the items within it will be vertical with a gap between them. Let’s start with the product cards.

Product Cards

The product cards will have six cards within it horizontally, but they will wrap when the screen size reduces. So, the product cards will have a parent container like below:

const Dashboard = () => {
  return (
    <div className='h-screen flex gap-4 flex-col'>
      <div className="flex gap-4 justify-between flex-wrap">
        Product Cards
      </div>

      <div>Sales Graph</div>

      <div>income and expenditure graph</div>
    </div>
  )
}

export default Dashboard
Enter fullscreen mode Exit fullscreen mode

As I stated earlier, there will be six cards but we will create only one card component and re-use it for all the six. So, inside your components folder, create a new file and name it ProductCard.tsx. Run rafce inside to spin up the functional component. Next, import and render the product card six times inside the Dashboard component like below:

import ProductCard from "@/components/ProductCard"

const Dashboard = () => {
  return (
    <div className='h-screen flex gap-4 flex-col'>
      <div className="flex gap-4 justify-between flex-wrap">
        <ProductCard />
        <ProductCard />
        <ProductCard />
        <ProductCard />
        <ProductCard />
        <ProductCard />
      </div>

      <div>Sales Graph</div>

      <div>income and expenditure graph</div>
    </div>
  )
}

export default Dashboard
Enter fullscreen mode Exit fullscreen mode

Now, let’s open the product card and start implementing it.

const ProductCard = () => {
  return (
    <div>ProductCard</div>
  )
}

export default ProductCard
Enter fullscreen mode Exit fullscreen mode

The cards will have rounded edges with some padding within them. So, let’s update the parent div like below:

const ProductCard = () => {
  return (
    <div className='rounded-2xl odd:bg-purple-500 even:bg-yellow-400 p-4 flex-1 min-w-[130px]'>
        ProductCard
    </div>
  )
}

export default ProductCard
Enter fullscreen mode Exit fullscreen mode

Here, I made the edges rounded. I also gave them background colors based on their count index. Those with even number counts have a separate background against those with odd number counts. I also gave it flex-1 which means all the cards will occupy equal spaces with a minimum width of 130px.

Each card has three sections; a text at the top left and three dots on the right, the count of products, and then the type of products at the bottom. Let’s handle the top part. The top items are a span and an image flexed with space between like below:

import Image from "next/image"

const ProductCard = () => {
  return (
    <div className='rounded-2xl odd:bg-purple-500 even:bg-yellow-400 p-4 flex-1 min-w-[130px]'>
         <div className='flex justify-between items-center'>
            <span className='text-xs text-green-600 bg-white px-2 py-1 rounded-full'>05/2025</span>
            <Image src='/more.png' alt='More' width={20} height={20} className='cursor-pointer' />
         </div>
    </div>
  )
}

export default ProductCard
Enter fullscreen mode Exit fullscreen mode

I added a span with green extra small texts, a white background with padding on x and y axes an also made it rounded. I also added an image which displays the three dots on the right end.

Below that section is the count of products. Let’s add that using an h1 tag as follows:

import Image from "next/image"

const ProductCard = () => {
  return (
    <div className='rounded-2xl odd:bg-purple-500 even:bg-yellow-400 p-4 flex-1 min-w-[130px]'>
         <div className='flex justify-between items-center'>
            <span className='text-xs text-green-600 bg-white px-2 py-1 rounded-full'>05/2025</span>
            <Image src='/more.png' alt='More' width={20} height={20} className='cursor-pointer' />
        </div>

        <h1 className='text-2xl font-semibold my-4 text-black'>24,500</h1>
    </div>
  )
}

export default ProductCard
Enter fullscreen mode Exit fullscreen mode

Below the count is the type of product. Let’s add that using an h2 tag as follows:

import Image from "next/image"

const ProductCard = () => {
  return (
    <div className='rounded-2xl odd:bg-purple-500 even:bg-yellow-400 p-4 flex-1 min-w-[130px]'>
         <div className='flex justify-between items-center'>
            <span className='text-xs text-green-600 bg-white px-2 py-1 rounded-full'>05/2025</span>
            <Image src='/more.png' alt='More' width={20} height={20} className='cursor-pointer' />
        </div>

        <h1 className='text-2xl font-semibold my-4 text-black'>24,500</h1>

        <h2 className='capitalize text-sm font-medium text-gray-100'>tablets</h2>
    </div>
  )
}

export default ProductCard
Enter fullscreen mode Exit fullscreen mode

One special thing to note here is the capitalize className which capitalizes the first letter of the text.

Now, you will notice that all the cards are rendering the same content. Ideally, each card is supposed to display a different content. To get separate content for each card, we pass the contents as props to where we are calling the cards. We then receive those props through the card component and render them.

Let’s open the dashboard and pass different props to the cards like below:

import ProductCard from "@/components/ProductCard"

const Dashboard = () => {
  return (
    <div className='h-screen flex gap-4 flex-col'>
      <div className="flex gap-4 justify-between flex-wrap">
        <ProductCard type='tablets' count={15210} />
        <ProductCard type='syrups' count={9510} />
        <ProductCard type='capsules' count={17542} />
        <ProductCard type='vials' count={13524} />
        <ProductCard type='others' count={7210} />
        <ProductCard type='expired' count={125} />
      </div>

      <div>Sales Graph</div>

      <div>income and expenditure graph</div>
    </div>
  )
}

export default Dashboard
Enter fullscreen mode Exit fullscreen mode

Here, we are passing type and count which contains different values. We can now go into the ProductCard component and accept those props like below:

import Image from "next/image"

const ProductCard = ({ type, count }: { type: string; count: number }) => {
  return (
    <div className='rounded-2xl odd:bg-purple-500 even:bg-yellow-400 p-4 flex-1 min-w-[130px]'>
         <div className='flex justify-between items-center'>
            <span className='text-xs text-green-600 bg-white px-2 py-1 rounded-full'>05/2025</span>
            <Image src='/more.png' alt='More' width={20} height={20} className='cursor-pointer' />
        </div>

        <h1 className='text-2xl font-semibold my-4 text-black'>24,500</h1>

        <h2 className='capitalize text-sm font-medium text-gray-100'>tablets</h2>
    </div>
  )
}

export default ProductCard
Enter fullscreen mode Exit fullscreen mode

We are accepting {type, count} where type is a string and count is a number. Remember we are using TypeScript so we have to specify the types for the props we are receiving.

We can now render those props into the component like below:

import Image from "next/image"

const ProductCard = ({ type, count }: { type: string; count: number }) => {
  return (
    <div className='rounded-2xl odd:bg-purple-500 even:bg-yellow-400 p-4 flex-1 min-w-[130px]'>
         <div className='flex justify-between items-center'>
            <span className='text-xs text-green-600 bg-white px-2 py-1 rounded-full'>05/2025</span>
            <Image src='/more.png' alt='More' width={20} height={20} className='cursor-pointer' />
        </div>

        <h1 className='text-2xl font-semibold my-4 text-black'>{count}</h1>

        <h2 className='capitalize text-sm font-medium text-gray-100'>{type}</h2>
    </div>
  )
}

export default ProductCard
Enter fullscreen mode Exit fullscreen mode

Each card now renders different information. Notice however, that the count figures don’t look appealing without comma separators since they are numbers. We could add the comma separators using the toLocaleString() function like below:

import Image from "next/image"

const ProductCard = ({ type, count }: { type: string; count: number }) => {
  return (
    <div className='rounded-2xl odd:bg-purple-500 even:bg-yellow-400 p-4 flex-1 min-w-[130px]'>
         <div className='flex justify-between items-center'>
            <span className='text-xs text-green-600 bg-white px-2 py-1 rounded-full'>05/2025</span>
            <Image src='/more.png' alt='More' width={20} height={20} className='cursor-pointer' />
        </div>

        <h1 className='text-2xl font-semibold my-4 text-black'>{count.toLocaleString()}</h1>

        <h2 className='capitalize text-sm font-medium text-gray-100'>{type}</h2>
    </div>
  )
}

export default ProductCard
Enter fullscreen mode Exit fullscreen mode

Also, I want a card to move upwards when I hover over it and only come down when the cursor leaves it.

Let’s update the parent container with this classNames: hover:-translate-y-2 transition-transform duration-300. This simply moves the card upwards with a duration of 300 milliseconds.

We are now done with the product cards. Let’s handle the sales graph.

Sales Graph

To implement the sales graph, we will use recharts. To get started, visit the recharts website, scroll down and click GET STARTED. On the left pane, click Installation to get the installation guide. We first have to install recharts. Open your terminal and run the command below:

npm install recharts

After the installation, go back to the recharts websites and click Examples. You will find a whole lot of chart types on the left pane. That is where we will be selecting the type of graphs we would be using.

For the Sales Graph, we would be using SimpleBarChart. Let’s create a separate component for the graph and import it into the Dashboard component.

Create a new file in your components folder and name it SalesGraph.tsx. Run rafce and spin up the functional component. Import it into the Dashboard like below:

import ProductCard from "@/components/ProductCard"
import SalesGraph from "@/components/SalesGraph"

const Dashboard = () => {
  return (
    <div className='h-screen flex gap-4 flex-col'>
      <div className="flex gap-4 justify-between flex-wrap">
        <ProductCard type='tablets' count={15210} />
        <ProductCard type='syrups' count={9510} />
        <ProductCard type='capsules' count={17542} />
        <ProductCard type='vials' count={13524} />
        <ProductCard type='others' count={7210} />
        <ProductCard type='expired' count={125} />
      </div>

        <SalesGraph />

      <div>income and expenditure graph</div>
    </div>
  )
}

export default Dashboard
Enter fullscreen mode Exit fullscreen mode

Let’s open the SalesGraph.tsx and start implementing it.

At the top of the graph is a title and ellipsis(3 dots) at both ends, so they would be flex items with a space between them like below:

import Image from "next/image";

const SalesGraph = () => {
  return (
    <div className="w-full bg-white dark:bg-gray-800 shadow-md rounded-lg p-4 flex flex-col gap-4">
      <div className="flex items-center justify-between">
        <h1 className="text-lg font-semibold dark:text-white text-gray-500">
          Sales
        </h1>
        <Image
          src="/more.png"
          alt="More"
          width={20}
          height={20}
          className="cursor-pointer"
        />
      </div>
    </div>
  );
};

export default SalesGraph;
Enter fullscreen mode Exit fullscreen mode

The title is the h1 and the ellipsis is the image. I placed them inside a div because I wanted to set them to flex. I have given the parent container (the first div) a full width. On light mode, the background will be white, but it will be bg-gray-800 on dark mode. I gave it a shadow, made the edges rounded, gave some padding, and made the contents flex column with a gap 0f 4px between them.

Now, if you switch between the light and dark mode, you will notice that the ellipsis at the right end corner is clearly visible on dark mode, but its almost hidden in light mode. To fix that, we could dynamically change the image source (src) based on the selected them. We could achieve that using the useTheme() hook from next-themes. We call it like so:

const { theme } = useTheme()

Then we can use the theme in the image src to dynamically change the source like below:

src={theme === "dark" ? "/more.png" : "/moreDark.png"}

We are using a ternary operator to check if the theme is dark, we set the image src to more.png, else the src would be moreDark.png. With this understanding, let’s update the code with those two lines of code like below:

"use client";

import { useTheme } from "next-themes";
import Image from "next/image";

const SalesGraph = () => {
    const {theme} = useTheme();

  return (
    <div className="w-full bg-white dark:bg-gray-800 shadow-md rounded-lg p-4 flex flex-col gap-4">
      <div className="flex items-center justify-between">
        <h1 className="text-lg font-semibold dark:text-white text-gray-500">
          Sales
        </h1>
        <Image
          src={theme === "dark" ? "/more.png" : "/moreDark.png"}
          alt="More"
          width={20}
          height={20}
          className="cursor-pointer"
        />
      </div>
    </div>
  );
};

export default SalesGraph;
Enter fullscreen mode Exit fullscreen mode

Note that since we are using a hook, we have to turn the component into a client component, so make sure to add the “use client” directive at the top.

Now we can add the graph. Let’s go back to the recharts website and make sure the SimpleBarChart is selected under examples. The code for the example graph on the right side is like below:

import React, { PureComponent } from 'react';
import { BarChart, Bar, Rectangle, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';

const data = [
  {
    name: 'Page A',
    uv: 4000,
    pv: 2400,
    amt: 2400,
  },
  {
    name: 'Page B',
    uv: 3000,
    pv: 1398,
    amt: 2210,
  },
  {
    name: 'Page C',
    uv: 2000,
    pv: 9800,
    amt: 2290,
  },
  {
    name: 'Page D',
    uv: 2780,
    pv: 3908,
    amt: 2000,
  },
  {
    name: 'Page E',
    uv: 1890,
    pv: 4800,
    amt: 2181,
  },
  {
    name: 'Page F',
    uv: 2390,
    pv: 3800,
    amt: 2500,
  },
  {
    name: 'Page G',
    uv: 3490,
    pv: 4300,
    amt: 2100,
  },
];

export default class Example extends PureComponent {
  static demoUrl = 'https://codesandbox.io/p/sandbox/simple-bar-chart-72d7y5';

  render() {
    return (
      <ResponsiveContainer width="100%" height="100%">
        <BarChart
          width={500}
          height={300}
          data={data}
          margin={{
            top: 5,
            right: 30,
            left: 20,
            bottom: 5,
          }}
        >
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="name" />
          <YAxis />
          <Tooltip />
          <Legend />
          <Bar dataKey="pv" fill="#8884d8" activeBar={<Rectangle fill="pink" stroke="blue" />} />
          <Bar dataKey="uv" fill="#82ca9d" activeBar={<Rectangle fill="gold" stroke="purple" />} />
        </BarChart>
      </ResponsiveContainer>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

We will not need everything in the code example. First we will need the data array. So, lets copy the data array and update our code like below:

"use client";

import { useTheme } from "next-themes";
import Image from "next/image";

const data = [
  {
    name: 'Page A',
    uv: 4000,
    pv: 2400,
    amt: 2400,
  },
  {
    name: 'Page B',
    uv: 3000,
    pv: 1398,
    amt: 2210,
  },
  {
    name: 'Page C',
    uv: 2000,
    pv: 9800,
    amt: 2290,
  },
  {
    name: 'Page D',
    uv: 2780,
    pv: 3908,
    amt: 2000,
  },
  {
    name: 'Page E',
    uv: 1890,
    pv: 4800,
    amt: 2181,
  },
  {
    name: 'Page F',
    uv: 2390,
    pv: 3800,
    amt: 2500,
  },
  {
    name: 'Page G',
    uv: 3490,
    pv: 4300,
    amt: 2100,
  },
];

const SalesGraph = () => {
  const { theme } = useTheme();

  return (
    <div className="w-full bg-white dark:bg-gray-800 shadow-md rounded-lg p-4 flex flex-col gap-4">
      <div className="flex items-center justify-between">
        <h1 className="text-lg font-semibold dark:text-white text-gray-500">
          Sales
        </h1>
        <Image
          src={theme === "dark" ? "/more.png" : "/moreDark.png"}
          alt="More"
          width={20}
          height={20}
          className="cursor-pointer"
        />
      </div>
    </div>
  );
};

export default SalesGraph;
Enter fullscreen mode Exit fullscreen mode

It is a weekly sales graph we are displaying. So, instead of the name in the array being “Page A”, “Page B”, “Page C” etc, let’s update and limit it to the sales and days of the week like below:

"use client";

import { useTheme } from "next-themes";
import Image from "next/image";

const data = [
    {
      name: 'Sun',
      sale: 4000,
    },
    {
      name: 'Mon',
      sale: 3000,
    },
    {
      name: 'Tue',
      sale: 2000,
    },
    {
      name: 'Wed',
      sale: 2780,
    },
    {
      name: 'Thu',
      sale: 1890,
    },
    {
      name: 'Fri',
      sale: 2390,
    },
    {
      name: 'Sat',
      sale: 3490,
    },
  ];

const SalesGraph = () => {
  const { theme } = useTheme();

  return (
    <div className="w-full bg-white dark:bg-gray-800 shadow-md rounded-lg p-4 flex flex-col gap-4">
      <div className="flex items-center justify-between">
        <h1 className="text-lg font-semibold dark:text-white text-gray-500">
          Sales
        </h1>
        <Image
          src={theme === "dark" ? "/more.png" : "/moreDark.png"}
          alt="More"
          width={20}
          height={20}
          className="cursor-pointer"
        />
      </div>
    </div>
  );
};

export default SalesGraph;
Enter fullscreen mode Exit fullscreen mode

The next thing we will need from the recharts code example is the ResponsiveContainer block. So, let's copy that and update our code like below:

"use client";

import { Tooltip } from "@radix-ui/react-tooltip";
import { useTheme } from "next-themes";
import Image from "next/image";
import { Bar, BarChart, CartesianGrid, Legend, Rectangle, ResponsiveContainer, XAxis, YAxis } from "recharts";

const data = [
    {
      name: 'Sun',
      sale: 4000,
    },
    {
      name: 'Mon',
      sale: 3000,
    },
    {
      name: 'Tue',
      sale: 2000,
    },
    {
      name: 'Wed',
      sale: 2780,
    },
    {
      name: 'Thu',
      sale: 1890,
    },
    {
      name: 'Fri',
      sale: 2390,
    },
    {
      name: 'Sat',
      sale: 3490,
    },
  ];

const SalesGraph = () => {
  const { theme } = useTheme();

  return (
    <div className="w-full bg-white dark:bg-gray-800 shadow-md rounded-lg p-4 flex flex-col gap-4">
      <div className="flex items-center justify-between">
        <h1 className="text-lg font-semibold dark:text-white text-gray-500">
          Sales
        </h1>
        <Image
          src={theme === "dark" ? "/more.png" : "/moreDark.png"}
          alt="More"
          width={20}
          height={20}
          className="cursor-pointer"
          suppressHydrationWarning
        />
      </div>

      <ResponsiveContainer width="100%" height="100%">
        <BarChart
          width={500}
          height={300}
          data={data}
          margin={{
            top: 5,
            right: 30,
            left: 20,
            bottom: 5,
          }}
        >
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="name" />
          <YAxis />
          <Tooltip />
          <Legend />
          <Bar dataKey="pv" fill="#8884d8" activeBar={<Rectangle fill="pink" stroke="blue" />} />
          <Bar dataKey="uv" fill="#82ca9d" activeBar={<Rectangle fill="gold" stroke="purple" />} />
        </BarChart>
      </ResponsiveContainer>
    </div>
  );
};

export default SalesGraph;
Enter fullscreen mode Exit fullscreen mode

Make sure to add all the imports at the top. You will be faced with a hydration error and it would be referring to the img. I don't know why that is so, but let's add suppressHydrationWarning to the Image tag like below to get rid of the error:

"use client";

import { Tooltip } from "@radix-ui/react-tooltip";
import { useTheme } from "next-themes";
import Image from "next/image";
import { Bar, BarChart, CartesianGrid, Legend, Rectangle, ResponsiveContainer, XAxis, YAxis } from "recharts";

const data = [
    {
      name: 'Sun',
      sale: 4000,
    },
    {
      name: 'Mon',
      sale: 3000,
    },
    {
      name: 'Tue',
      sale: 2000,
    },
    {
      name: 'Wed',
      sale: 2780,
    },
    {
      name: 'Thu',
      sale: 1890,
    },
    {
      name: 'Fri',
      sale: 2390,
    },
    {
      name: 'Sat',
      sale: 3490,
    },
  ];

const SalesGraph = () => {
  const { theme } = useTheme();

  return (
    <div className="w-full bg-white dark:bg-gray-800 shadow-md rounded-lg p-4 flex flex-col gap-4">
      <div className="flex items-center justify-between">
        <h1 className="text-lg font-semibold dark:text-white text-gray-500">
          Sales
        </h1>
        <Image
          src={theme === "dark" ? "/more.png" : "/moreDark.png"}
          alt="More"
          width={20}
          height={20}
          className="cursor-pointer"
          suppressHydrationWarning
        />
      </div>

      <ResponsiveContainer width="100%" height="100%">
        <BarChart
          width={500}
          height={300}
          data={data}
          margin={{
            top: 5,
            right: 30,
            left: 20,
            bottom: 5,
          }}
        >
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="name" />
          <YAxis />
          <Tooltip />
          <Legend />
          <Bar dataKey="pv" fill="#8884d8" activeBar={<Rectangle fill="pink" stroke="blue" />} />
          <Bar dataKey="uv" fill="#82ca9d" activeBar={<Rectangle fill="gold" stroke="purple" />} />
        </BarChart>
      </ResponsiveContainer>
    </div>
  );
};

export default SalesGraph;
Enter fullscreen mode Exit fullscreen mode

We currently cannot see anything because we have not given the parent container any height. So, let's give the main div a height of h-[350px] like below:

"use client";

import { Tooltip } from "@radix-ui/react-tooltip";
import { useTheme } from "next-themes";
import Image from "next/image";
import { Bar, BarChart, CartesianGrid, Legend, Rectangle, ResponsiveContainer, XAxis, YAxis } from "recharts";

const data = [
    {
      name: 'Sun',
      sale: 4000,
    },
    {
      name: 'Mon',
      sale: 3000,
    },
    {
      name: 'Tue',
      sale: 2000,
    },
    {
      name: 'Wed',
      sale: 2780,
    },
    {
      name: 'Thu',
      sale: 1890,
    },
    {
      name: 'Fri',
      sale: 2390,
    },
    {
      name: 'Sat',
      sale: 3490,
    },
  ];

const SalesGraph = () => {
  const { theme } = useTheme();

  return (
    <div className="w-full bg-white dark:bg-gray-800 shadow-md rounded-lg p-4 flex flex-col gap-4 h-[350px]">
      <div className="flex items-center justify-between">
        <h1 className="text-lg font-semibold dark:text-white text-gray-500">
          Sales
        </h1>
        <Image
          src={theme === "dark" ? "/more.png" : "/moreDark.png"}
          alt="More"
          width={20}
          height={20}
          className="cursor-pointer"
          suppressHydrationWarning
        />
      </div>

      <ResponsiveContainer width="100%" height="100%">
        <BarChart
          width={500}
          height={300}
          data={data}
          margin={{
            top: 5,
            right: 30,
            left: 20,
            bottom: 5,
          }}
        >
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="name" />
          <YAxis />
          {/* <Tooltip /> */}
          <Legend />
          <Bar dataKey="pv" fill="#8884d8" activeBar={<Rectangle fill="pink" stroke="blue" />} />
          <Bar dataKey="uv" fill="#82ca9d" activeBar={<Rectangle fill="gold" stroke="purple" />} />
        </BarChart>
      </ResponsiveContainer>
    </div>
  );
};

export default SalesGraph;
Enter fullscreen mode Exit fullscreen mode

The example code we copied contains two bars for each data record, but we need only a single bar for each record, so let's remove one bar tag:

<Bar dataKey="uv" fill="#82ca9d" activeBar={<Rectangle fill="gold" stroke="purple" />} />

We don’t need the margin inside the BarChart tag, so, let’s remove that and replaced it with barSize={20}. That will increase the thickness of the bar:

<BarChart
   width={500}
   height={300}
   data={data}
   barSize={20}
>
Enter fullscreen mode Exit fullscreen mode

Also, note that in the data array, we removed the uv and pv that were in the examples and included a sale attribute. Let’s change the dataKey in the remaining Bar tag to “sale”:

<Bar dataKey="sale" fill="#8884d8" activeBar={<Rectangle fill="pink" stroke="blue" />} />

In the CartesianGrid, let’s remove the vertical lines (vertical={false}) and add a color for the horizontal grid (stroke=”#ddd”)

<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#ddd" />

In the XAxis, let’s remove the axis line (axisLine={false}), give a color to the tick (tick={{ fill: “gray”}}) and disable the tickLine (tickLine={false}). The tick is the small vertical line above each week day name that connects to the x-axis.

<XAxis dataKey="name" axisLine={false} tick={{ fill: "gray"}} tickLine={false} />

In the YAxis, let’s remove the axis line (axisLine={false}), give a color to the tick (tick={{ fill: “gray”}}) and disable the tickLine (tickLine={false}).

<YAxis axisLine={false} tick={{ fill: "gray"}} tickLine={false} />

In the Tooltip, let’s apply some styles; borderRadius, borderColor, backgroundColor, and color using the contentStyle property.

<Tooltip contentStyle={{ borderRadius: "10px", borderColor: "lightgray", backgroundColor: "white", color: "black" }} />

In the Legend, let’s align it to the left (align=”left”), move it to the top (verticalAlign=”top”), and give it some Wrapper styles for padding (wrapperStyle={{ paddingTop: “20px”, paddingBottom: “40px”}}).

<Legend align="left" verticalAlign="top" wrapperStyle={{ paddingTop: "20px", paddingBottom: "40px"}} />

In the Bar tag, let’s change the fill color to “#8B5CF6”, change the legend type to a circle (legendType=”circle”), make the top edges of the bars rounded (radius={[10, 10, 0, 0]}).

<Bar
   dataKey="sale"
   fill="#C3EBFA"
   activeBar={<Rectangle fill="pink" stroke="blue" />}
   legendType="circle"
   radius={[10, 10, 0, 0]}
/>
Enter fullscreen mode Exit fullscreen mode

After applying all the modifications above, the final SalesGraph.tsx should look like below:

"use client";

import { useTheme } from "next-themes";
import Image from "next/image";
import {
  Bar,
  BarChart,
  CartesianGrid,
  Legend,
  Rectangle,
  ResponsiveContainer,
  XAxis,
  YAxis,
  Tooltip
} from "recharts";

const data = [
  {
    name: "Sun",
    sale: 4000,
  },
  {
    name: "Mon",
    sale: 3000,
  },
  {
    name: "Tue",
    sale: 2000,
  },
  {
    name: "Wed",
    sale: 2780,
  },
  {
    name: "Thu",
    sale: 1890,
  },
  {
    name: "Fri",
    sale: 2390,
  },
  {
    name: "Sat",
    sale: 3490,
  },
];

const SalesGraph = () => {
  const { theme } = useTheme();

  return (
    <div className="w-full bg-white dark:bg-gray-800 shadow-md rounded-lg p-4 flex flex-col gap-4 h-[350px]">
      <div className="flex items-center justify-between">
        <h1 className="text-lg font-semibold dark:text-white text-gray-500">
          Sales
        </h1>
        <Image
          src={theme === "dark" ? "/more.png" : "/moreDark.png"}
          alt="More"
          width={20}
          height={20}
          className="cursor-pointer"
          suppressHydrationWarning
        />
      </div>

      <ResponsiveContainer width="100%" height="100%">
        <BarChart width={500} height={300} data={data} barSize={20}>
          <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#ddd" />
          <XAxis
            dataKey="name"
            axisLine={false}
            tick={{ fill: "gray" }}
            tickLine={false}
          />
          <YAxis />
          <Tooltip contentStyle={{ borderRadius: "10px", borderColor: "lightgray", backgroundColor: "gray", color: "white" }} />
          <Legend align="left" verticalAlign="top" wrapperStyle={{ paddingTop: "20px", paddingBottom: "40px"}} />
          <Bar
            dataKey="sale"
            fill="#8B5CF6"
            activeBar={<Rectangle fill="pink" stroke="blue" />}
            legendType="circle"
            radius={[10, 10, 0, 0]}
          />
        </BarChart>
      </ResponsiveContainer>
    </div>
  );
};

export default SalesGraph;
Enter fullscreen mode Exit fullscreen mode

We are done with the Sale Graph. Now let’s handle the Income and Expenditure Graph.

Income & Expenditure Graph

To implement the Income and Expenditure Graph, let’s create another file inside the components folder and name it IncomeExpediture.tsx. Run rafce and spin up the functional component, then import it into the dashboard/page.tsx like below:

import IncomeExpediture from "@/components/IncomeExpediture"
import ProductCard from "@/components/ProductCard"
import SalesGraph from "@/components/SalesGraph"

const Dashboard = () => {
  return (
    <div className='h-screen flex gap-4 flex-col'>
      <div className="flex gap-4 justify-between flex-wrap">
        <ProductCard type='tablets' count={15210} />
        <ProductCard type='syrups' count={9510} />
        <ProductCard type='capsules' count={17542} />
        <ProductCard type='vials' count={13524} />
        <ProductCard type='others' count={7210} />
        <ProductCard type='expired' count={125} />
      </div>

      <SalesGraph />

      <IncomeExpediture />
    </div>
  )
}

export default Dashboard
Enter fullscreen mode Exit fullscreen mode

Let’s open the IncomeExpediture.tsx file and start implementing it.

At the top of the graph is a title and ellipsis(3 dots) at both ends, so they would be flex items with a space between them like below:

"use client";

import { useTheme } from "next-themes";
import Image from "next/image";

const IncomeExpediture = () => {
    const { theme } = useTheme();

  return (
    <div className="w-full bg-white dark:bg-gray-800 shadow-md rounded-lg p-4 flex flex-col gap-4 h-[350px]">
      <div className="flex items-center justify-between">
        <h1 className="text-lg font-semibold dark:text-white text-gray-500">
          Income and Expenditure
        </h1>
        <Image
          src={theme === "dark" ? "/more.png" : "/moreDark.png"}
          alt="More"
          width={20}
          height={20}
          className="cursor-pointer"
          suppressHydrationWarning
        />
      </div>
    </div>
  );
};

export default IncomeExpediture;
Enter fullscreen mode Exit fullscreen mode

We are doing the same thing we did with the Sales Graph. I gave a full width to the parent div, gave background colors based on selected theme, gave some shadow, made the edges rounded, gave some padding, made it a flex container with a height of 350px. I then added the heading and the ellipsis image.

Like the previous graph, we will make use of recharts. From the examples in the recharts website, we will use SimpleLineChart. The example code for the SimpleLineChart is like below:

import React, { PureComponent } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';

const data = [
  {
    name: 'Page A',
    uv: 4000,
    pv: 2400,
    amt: 2400,
  },
  {
    name: 'Page B',
    uv: 3000,
    pv: 1398,
    amt: 2210,
  },
  {
    name: 'Page C',
    uv: 2000,
    pv: 9800,
    amt: 2290,
  },
  {
    name: 'Page D',
    uv: 2780,
    pv: 3908,
    amt: 2000,
  },
  {
    name: 'Page E',
    uv: 1890,
    pv: 4800,
    amt: 2181,
  },
  {
    name: 'Page F',
    uv: 2390,
    pv: 3800,
    amt: 2500,
  },
  {
    name: 'Page G',
    uv: 3490,
    pv: 4300,
    amt: 2100,
  },
];

export default class Example extends PureComponent {
  static demoUrl = 'https://codesandbox.io/p/sandbox/line-chart-width-xaxis-padding-8v7952';

  render() {
    return (
      <ResponsiveContainer width="100%" height="100%">
        <LineChart
          width={500}
          height={300}
          data={data}
          margin={{
            top: 5,
            right: 30,
            left: 20,
            bottom: 5,
          }}
        >
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="name" />
          <YAxis />
          <Tooltip />
          <Legend />
          <Line type="monotone" dataKey="pv" stroke="#8884d8" activeDot={{ r: 8 }} />
          <Line type="monotone" dataKey="uv" stroke="#82ca9d" />
        </LineChart>
      </ResponsiveContainer>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

First, let’s copy the data array and update our component like below:

"use client";

import { useTheme } from "next-themes";
import Image from "next/image";

const data = [
    {
      name: 'Page A',
      uv: 4000,
      pv: 2400,
      amt: 2400,
    },
    {
      name: 'Page B',
      uv: 3000,
      pv: 1398,
      amt: 2210,
    },
    {
      name: 'Page C',
      uv: 2000,
      pv: 9800,
      amt: 2290,
    },
    {
      name: 'Page D',
      uv: 2780,
      pv: 3908,
      amt: 2000,
    },
    {
      name: 'Page E',
      uv: 1890,
      pv: 4800,
      amt: 2181,
    },
    {
      name: 'Page F',
      uv: 2390,
      pv: 3800,
      amt: 2500,
    },
    {
      name: 'Page G',
      uv: 3490,
      pv: 4300,
      amt: 2100,
    },
  ];

const IncomeExpediture = () => {
    const { theme } = useTheme();

  return (
    <div className="w-full bg-white dark:bg-gray-800 shadow-md rounded-lg p-4 flex flex-col gap-4 h-[350px]">
      <div className="flex items-center justify-between">
        <h1 className="text-lg font-semibold dark:text-white text-gray-500">
          Income and Expenditure
        </h1>
        <Image
          src={theme === "dark" ? "/more.png" : "/moreDark.png"}
          alt="More"
          width={20}
          height={20}
          className="cursor-pointer"
          suppressHydrationWarning
        />
      </div>
    </div>
  );
};

export default IncomeExpediture;
Enter fullscreen mode Exit fullscreen mode

We need income and expenditure data for the half year, so let’s modify the data array to suit our use case like below:

"use client";

import { useTheme } from "next-themes";
import Image from "next/image";

const data = [
    {
      name: 'Jan',
      income: 4000,
      expenditure: 2400,
    },
    {
      name: 'Feb',
      income: 3000,
      expenditure: 1398,
    },
    {
      name: 'Mar',
      income: 2000,
      expenditure: 9800,
    },
    {
      name: 'April',
      income: 2780,
      expenditure: 3908,
    },
    {
      name: 'May',
      income: 1890,
      expenditure: 4800,
    },
    {
      name: 'Jun',
      income: 2390,
      expenditure: 3800,
    },
  ];

const IncomeExpediture = () => {
    const { theme } = useTheme();

  return (
    <div className="w-full bg-white dark:bg-gray-800 shadow-md rounded-lg p-4 flex flex-col gap-4 h-[350px]">
      <div className="flex items-center justify-between">
        <h1 className="text-lg font-semibold dark:text-white text-gray-500">
          Income and Expenditure
        </h1>
        <Image
          src={theme === "dark" ? "/more.png" : "/moreDark.png"}
          alt="More"
          width={20}
          height={20}
          className="cursor-pointer"
          suppressHydrationWarning
        />
      </div>
    </div>
  );
};

export default IncomeExpediture;
Enter fullscreen mode Exit fullscreen mode

Next, let’s copy the responsive container code and update our component like below:

"use client";

import { useTheme } from "next-themes";
import Image from "next/image";
import {
  CartesianGrid,
  Legend,
  Line,
  LineChart,
  ResponsiveContainer,
  Tooltip,
  XAxis,
  YAxis,
} from "recharts";

const data = [
  {
    name: "Jan",
    income: 4000,
    expenditure: 2400,
  },
  {
    name: "Feb",
    income: 3000,
    expenditure: 1398,
  },
  {
    name: "Mar",
    income: 2000,
    expenditure: 9800,
  },
  {
    name: "April",
    income: 2780,
    expenditure: 3908,
  },
  {
    name: "May",
    income: 1890,
    expenditure: 4800,
  },
  {
    name: "Jun",
    income: 2390,
    expenditure: 3800,
  },
];

const IncomeExpediture = () => {
  const { theme } = useTheme();

  return (
    <div className="w-full bg-white dark:bg-gray-800 shadow-md rounded-lg p-4 flex flex-col gap-4 h-[350px]">
      <div className="flex items-center justify-between">
        <h1 className="text-lg font-semibold dark:text-white text-gray-500">
          Income and Expenditure
        </h1>
        <Image
          src={theme === "dark" ? "/more.png" : "/moreDark.png"}
          alt="More"
          width={20}
          height={20}
          className="cursor-pointer"
          suppressHydrationWarning
        />
      </div>

      <ResponsiveContainer width="100%" height="100%">
        <LineChart
          width={500}
          height={300}
          data={data}
          margin={{
            top: 5,
            right: 30,
            left: 20,
            bottom: 5,
          }}
        >
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="name" />
          <YAxis />
          <Tooltip />
          <Legend />
          <Line
            type="monotone"
            dataKey="pv"
            stroke="#8884d8"
            activeDot={{ r: 8 }}
          />
          <Line type="monotone" dataKey="uv" stroke="#82ca9d" />
        </LineChart>
      </ResponsiveContainer>
    </div>
  );
};

export default IncomeExpediture;
Enter fullscreen mode Exit fullscreen mode

Be sure to add all the imports from recharts.

The graph is not properly showing because we changed the data attributes in the data array but same is not reflecting in the ResponsiveContainer block. Let’s make some modifications.

In the CartesianGrid, let’s hide the vertical grid (vertical={false}) and change the color for the horizontal grid (stroke=”#ddd”).

<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#ddd" />

In the XAxis, let’s hide the axis line (axisLine={false}), make the tick gray (tick={{ fill: “gray”}}), hide the tick line (tickLine={false}), and give a margin of 10 to the tick (tickMargin={10}). The tick is the tiny vertical line above each month name connecting to the x-axis.

<XAxis dataKey="name" axisLine={false} tick={{ fill: "gray"}} tickLine={false} tickMargin={10} />

In the YAxis, let’s hide the axis line (axisLine={false}), make the tick gray (tick={{ fill: “gray”}}), hide the tick line (tickLine={false}), and give a margin of 20 to the tick (tickMargin={20}).

<YAxis axisLine={false} tick={{ fill: "gray"}} tickLine={false} tickMargin={20} />

In the Tooltip, let’s apply some styles; borderRadius, borderColor, backgroundColor, and color using the contentStyle property.

<Tooltip contentStyle={{ borderRadius: "10px", borderColor: "lightgray", backgroundColor: "white", color: "black" }} />

In the Legend, let’s align it to the center (align=”center”), set it to the top (verticalAlign=”top”), and some padding top and bottom using the wrapperStyle (wrapperStyle={{ paddingTop: “10px”, paddingBottom: “30px”}}).

<Legend align="center" verticalAlign="top" wrapperStyle={{ paddingTop: "10px", paddingBottom: "30px"}} />

In the first Line, let’s set the dataKey to income and the stroke to “#8B5CF6”. Add a strokeWidth of 5 (strokeWidth={5}). This only makes the graph line thicker.

<Line
   type="monotone"
   dataKey="income"
   stroke="#8B5CF6"
   activeDot={{ r: 8 }}
   strokeWidth={5}
/>
Enter fullscreen mode Exit fullscreen mode

In the other Line, set the dataKey to expenditure and the stroke to “#FACC15”. Add a strokeWidth of 5 (strokeWidth={5}).

<Line type="monotone" dataKey="expenditure" stroke="#FACC15" strokeWidth={5} />

That’s it for this tutorial and thank you very much for following along.

Challenge yourself and try displaying three cards inside the purchase orders component. The cards should display the count of products and their status; Pending, Fulfilled and Cancelled.

I hope you have learned a lot from this guide. If you have any corrections, or suggestions, leave that in the comment section.

Top comments (0)