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;
}
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>
);
}
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>
);
}
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
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:
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
Running rafce inside purchase-orders, I will rename like this:
const PurchaseOrders = () => {
return (
<div>PurchaseOrders</div>
)
}
export default PurchaseOrders
Now with the folders and files created, we could navigate through them. In the browser url, navigate to the following pages:
- http://localhost:3000/customers
- http://localhost:3000/medicines-list
- http://localhost:3000/prescriptions
- http://localhost:3000/pos
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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
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>
);
}
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: [],
},
];
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",
},
],
},
];
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;
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>
)
})}
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>
))}
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;
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;
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;
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 */
}
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;
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>
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;
You could notice I added this code:
const isActive =
(pathname.includes(subItem.href.toLowerCase()) &&
subItem.href.length > 1) ||
pathname === subItem.href.toLowerCase();
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;
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))
}
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>
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"
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>
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;
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>
);
}
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
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
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>
);
}
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
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;
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;
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;
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;
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;
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>
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>
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>
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>
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>
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);
}
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>
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;
}
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">
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">
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);
}
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"
/>
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"
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")
}
/>
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>
);
}
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>
);
}
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>
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">
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>
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;
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;
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>
);
}
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
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
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
Now, let’s open the product card and start implementing it.
const ProductCard = () => {
return (
<div>ProductCard</div>
)
}
export default ProductCard
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
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
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
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
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
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
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
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
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
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;
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;
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>
);
}
}
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;
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;
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;
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;
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;
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}
>
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]}
/>
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;
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
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;
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>
);
}
}
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;
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;
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;
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}
/>
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)