• Что бы вступить в ряды "Принятый кодер" Вам нужно:
    Написать 10 полезных сообщений или тем и Получить 10 симпатий.
    Для того кто не хочет терять время,может пожертвовать средства для поддержки сервеса, и вступить в ряды VIP на месяц, дополнительная информация в лс.

  • Пользаватели которые будут спамить, уходят в бан без предупреждения. Спам сообщения определяется администрацией и модератором.

  • Гость, Что бы Вы хотели увидеть на нашем Форуме? Изложить свои идеи и пожелания по улучшению форума Вы можете поделиться с нами здесь. ----> Перейдите сюда
  • Все пользователи не прошедшие проверку электронной почты будут заблокированы. Все вопросы с разблокировкой обращайтесь по адресу электронной почте : info@guardianelinks.com . Не пришло сообщение о проверке или о сбросе также сообщите нам.

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

Lomanu4 Оффлайн

Lomanu4

Команда форума
Администратор
Регистрация
1 Мар 2015
Сообщения
1,481
Баллы
155

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

of what you will be building.

In case you get stuck, you can visit the

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

, 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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

. 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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

in your browser. It should have been

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

, 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:


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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

, click Get Started and select the stack you are using, which is Next.js in our case. Next, you have to initialize

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

by running the command below in your terminal:

npx shadcn@latest init

Press y to proceed when prompted to install

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

. 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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

, 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="

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

"
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="

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

"
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="

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

"
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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

, 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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

. To get started, visit the

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

website, scroll down and click GET STARTED. On the left pane, click Installation to get the installation guide. We first have to install

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

. Open your terminal and run the command below:

npm install recharts

After the installation, go back to the

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

. 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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

website and make sure the

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

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 = '

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

. From the examples in the

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

website, we will use

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

. The example code for the

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

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 = '

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



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.


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

 
Вверх Снизу