JB logo

Command Palette

Search for a command to run...

yOUTUBE
Blog
PreviousNext

Creating a Dashboard in Next.js - Complete Step-by-Step Guide

Learn how to implement a fully functional dashboard in your Next.js application with charts, data tables, and responsive design.

Adding a Dashboard to a Next.js Project

This guide will walk you through creating a modern, responsive dashboard in your Next.js application.

Prerequisites

  • Next.js 15+ project with App Router

  • Basic knowledge of React and Next.js

  • Node.js installed on your system

  • You need to create a dashboard page group, if you havent

app/(dashboard)/dashboard/ ├── layout.tsx ├── page.tsx ├── components/ │ ├── Sidebar.tsx │ ├── Header.tsx │ ├── StatsGrid.tsx │ ├── Chart.tsx │ └── DataTable.tsx └── styles/ └── dashboard.module.css

Step 1: Create Dashboard Layout

import Navbar from "@/components/dashboard/Navbar";
import Sidebar from "@/components/dashboard/Sidebar";
import { GridBackground } from "@/components/reusable-ui/grid-background";
import { authOptions } from "@/config/auth";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import React, { ReactNode } from "react";
 
export default async function DashboardLayout({
  children,
}: {
  children: ReactNode;
}) {
  const session = await getServerSession(authOptions);
  const userRole = session?.user.role;
  if (!session) {
    redirect("/login");
  }
  return (
    <div className="flex min-h-screen">
      {/* Fixed Sidebar */}
      <aside className="fixed left-0 top-0 z-40 hidden h-screen md:block">
        <Sidebar />
      </aside>
 
      {/* Main Content */}
      <main className="flex-1 md:ml-[220px] lg:ml-[280px]">
        <Navbar session={session} />
        <div className="bg-gray-50">{children}</div>
      </main>
    </div>
  );
}

Step 2 : Create the Navbar.tsx Component

"use client";
import React, { useState, useEffect } from "react";
import Link from "next/link";
import {
  Bell,
  Search,
  LogOut,
  User,
  Settings,
  HelpCircle,
  ChevronDown,
  Check,
  CheckCheck,
  FileCheck,
} from "lucide-react";
import { Button } from "@/components/ui/button";
 
import { Input } from "@/components/ui/input";
import { Session } from "next-auth";
import { signOut } from "next-auth/react";
import { useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
 
import { NotificationType } from "@prisma/client";
 
import { formatDistanceToNow } from "date-fns";
import {
  getNotifications,
  markAllNotificationsAsRead,
  markNotificationAsRead,
} from "@/actions/notifactions";
 
export default function Navbar({ session }: { session: Session }) {
  const router = useRouter();
  const role = session.user.roles[0]?.roleName;
  const [profileOpen, setProfileOpen] = useState(false);
  const [notificationsOpen, setNotificationsOpen] = useState(false);
  const [notifications, setNotifications] = useState<any[]>([]);
  const [loading, setLoading] = useState(false);
 
  useEffect(() => {
    if (notificationsOpen) {
      fetchNotifications();
    }
  }, [notificationsOpen]);
 
  const fetchNotifications = async () => {
    setLoading(true);
    try {
      const response = await getNotifications({ limit: 5 });
      if (response.success && response.data) {
        setNotifications(response.data);
      }
    } catch (error) {
      console.error("Failed to fetch notifications:", error);
    } finally {
      setLoading(false);
    }
  };
 
  async function handleLogout() {
    try {
      await signOut();
      router.push("/login");
    } catch (error) {
      console.log(error);
    }
  }
 
  const handleMenuItemClick = (path: string) => {
    setProfileOpen(false);
    router.push(path);
  };
 
  const handleMarkAsRead = async (id: string) => {
    try {
      const response = await markNotificationAsRead(id);
      if (response.success) {
        fetchNotifications();
      }
    } catch (error) {
      console.error("Failed to mark notification as read:", error);
    }
  };
 
  const handleMarkAllAsRead = async () => {
    try {
      const response = await markAllNotificationsAsRead();
      if (response.success) {
        fetchNotifications();
      }
    } catch (error) {
      console.error("Failed to mark all notifications as read:", error);
    }
  };
 
  const handleViewAllNotifications = () => {
    setNotificationsOpen(false);
    router.push("/dashboard/notifications");
  };
 
  // Get unread notifications count
  const unreadCount = notifications.filter((n) => !n.isRead).length;
 
  // Get notification icon based on type
  const getNotificationIcon = (type: NotificationType) => {
    switch (type) {
      case "DOCUMENT_ALERT":
        return <Bell className="h-4 w-4 text-blue-500" />;
      case "STATUS_CHANGE":
        return <FileCheck className="h-4 w-4 text-green-500" />;
      case "TASK_ASSIGNED":
        return <User className="h-4 w-4 text-orange-500" />;
      case "DEADLINE_APPROACHING":
        return <Bell className="h-4 w-4 text-red-500" />;
      default:
        return <Bell className="h-4 w-4" />;
    }
  };
 
  return (
    <header className="bg-background/95 sticky top-0 z-30 flex h-16 w-full items-center justify-between border-b px-2 backdrop-blur-sm lg:px-6">
      {/* Center - Search (only on desktop) */}
      <div className="mx-4 hidden w-full max-w-md md:block">
        <div className="relative">
          <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
            <Search className="text-muted-foreground h-4 w-4" />
          </div>
          <Input
            type="text"
            placeholder="Search shipments, documents, clients..."
            className="bg-muted/30 w-full pl-10 text-xs"
          />
        </div>
      </div>
 
      {/* Right side - Notifications and User Menu */}
      <div className="flex items-center space-x-4">
        {/* Notifications */}
        <div className="relative">
          <Button
            variant="ghost"
            size="icon"
            className="rounded-full"
            onClick={() => setNotificationsOpen(!notificationsOpen)}
          >
            <Bell className="text-2xl" />
            {unreadCount > 0 && (
              <span className="bg-destructive absolute right-0 top-0 flex h-5 w-5 -translate-y-1/3 translate-x-1/3 transform items-center justify-center rounded-full text-xs text-white">
                {unreadCount}
              </span>
            )}
            <span className="sr-only">Notifications</span>
          </Button>
 
          {notificationsOpen && (
            <div className="bg-background absolute right-0 z-50 mt-2 w-80 rounded-md shadow-lg ring-1 ring-black ring-opacity-5">
              <div className="flex items-center justify-between border-b p-3">
                <h3 className="text-sm font-medium">Notifications</h3>
                {unreadCount > 0 && (
                  <Button
                    variant="ghost"
                    size="sm"
                    className="flex items-center text-xs"
                    onClick={handleMarkAllAsRead}
                  >
                    <CheckCheck className="mr-1 h-3 w-3" />
                    Mark all as read
                  </Button>
                )}
              </div>
              <div className="max-h-96 overflow-y-auto">
                {loading ? (
                  <div className="flex items-center justify-center p-4">
                    <div className="border-primary h-6 w-6 animate-spin rounded-full border-b-2"></div>
                  </div>
                ) : notifications.length > 0 ? (
                  <div>
                    {notifications.map((notification) => (
                      <div
                        key={notification.id}
                        className={cn(
                          "hover:bg-muted/50 flex cursor-pointer border-b p-3 transition-colors",
                          !notification.isRead &&
                            "bg-blue-50 dark:bg-blue-900/10"
                        )}
                      >
                        <div className="mr-3 mt-0.5">
                          {getNotificationIcon(notification.type)}
                        </div>
                        <div className="flex-1">
                          <div className="flex items-start justify-between">
                            <h4 className="text-sm font-medium">
                              {notification.title}
                            </h4>
                            <div className="flex items-center">
                              {!notification.isRead && (
                                <Button
                                  variant="ghost"
                                  size="sm"
                                  className="h-6 w-6 p-0"
                                  onClick={() =>
                                    handleMarkAsRead(notification.id)
                                  }
                                >
                                  <Check className="h-3 w-3" />
                                </Button>
                              )}
                            </div>
                          </div>
                          <p className="text-muted-foreground text-xs">
                            {notification.message}
                          </p>
                          <span className="text-muted-foreground mt-1 block text-xs">
                            {formatDistanceToNow(
                              new Date(notification.timestamp),
                              { addSuffix: true }
                            )}
                          </span>
                        </div>
                      </div>
                    ))}
                  </div>
                ) : (
                  <p className="text-muted-foreground p-4 text-center text-sm">
                    No notifications
                  </p>
                )}
              </div>
              <div className="border-t p-2">
                <Button
                  variant="ghost"
                  size="sm"
                  className="w-full text-xs"
                  onClick={handleViewAllNotifications}
                >
                  View all notifications
                </Button>
              </div>
            </div>
          )}
        </div>
 
        {/* User Menu */}
        <div className="relative">
          <Button
            variant="ghost"
            className="flex items-center space-x-2 rounded-full"
            onClick={() => setProfileOpen(!profileOpen)}
          >
            <div className="bg-primary text-primary-foreground flex h-8 w-8 items-center justify-center rounded-full font-medium">
              {session.user.name?.charAt(0)}
            </div>
            <div className="hidden flex-col items-start md:flex">
              <span className="text-sm font-medium">{session.user.name}</span>
              <span className="text-muted-foreground text-xs capitalize">
                {role}
              </span>
            </div>
            <ChevronDown
              className={`text-muted-foreground hidden h-4 w-4 transition-transform duration-200 md:block ${
                profileOpen ? "rotate-180 transform" : ""
              }`}
            />
          </Button>
 
          {/* Profile Dropdown */}
          {profileOpen && (
            <div className="bg-background absolute right-0 z-50 mt-2 w-56 rounded-md shadow-lg ring-1 ring-black ring-opacity-5">
              <div className="border-b px-4 py-3">
                <p className="text-sm font-medium">{session.user.name}</p>
                <p className="text-muted-foreground text-xs">
                  {session.user.email}
                </p>
              </div>
              <div className="py-1">
                <Button
                  variant="ghost"
                  onClick={() => handleMenuItemClick("/dashboard/settings")}
                  className="text-foreground flex w-full items-center justify-start px-4 py-2 text-sm font-normal"
                >
                  <User className="mr-3 h-4 w-4" />
                  Profile
                </Button>
                <Button
                  variant="ghost"
                  onClick={() => handleMenuItemClick("/dashboard/settings")}
                  className="text-foreground flex w-full items-center justify-start px-4 py-2 text-sm font-normal"
                >
                  <Settings className="mr-3 h-4 w-4" />
                  Settings
                </Button>
                <Button
                  variant="ghost"
                  onClick={() => {
                    window.open("https://wa.me/1234567890", "_blank");
                  }}
                  className="text-foreground flex w-full items-center justify-start px-4 py-2 text-sm font-normal"
                >
                  <HelpCircle className="mr-3 h-4 w-4" />
                  Help & Support
                </Button>
              </div>
              <div className="border-t py-1">
                <Button
                  variant="ghost"
                  onClick={handleLogout}
                  className="text-destructive hover:bg-destructive/10 flex w-full items-center justify-start px-4 py-2 text-sm font-normal"
                >
                  <LogOut className="mr-3 h-4 w-4" />
                  Logout
                </Button>
              </div>
            </div>
          )}
        </div>
      </div>
    </header>
  );
}

Step 3 : Create the Sidebar Component

"use client";
 
import type React from "react";
import { useState, useEffect } from "react";
import { Package, ChevronLeft, ChevronRight, LogOut } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useSession } from "next-auth/react";
import { navItems } from "@/config/sidebar";
 
export const Sidebar: React.FC = () => {
  const [collapsed, setCollapsed] = useState(false);
  const pathname = usePathname();
  const { data: session } = useSession();
 
  useEffect(() => {
    document.documentElement.style.setProperty(
      "--sidebar-width",
      collapsed ? "64px" : "256px"
    );
  }, [collapsed]);
 
  const handleToggle = () => {
    setCollapsed(!collapsed);
  };
 
  // Correct isActive logic to distinguish sub-paths
  const isActive = (path: string) => {
    if (path === "/dashboard") {
      return pathname === "/dashboard"; // Only active when exactly /dashboard
    }
    return pathname.startsWith(path); // Active for other nested paths
  };
 
  // Filter navigation items based on user permissions
  const filteredNavItems = navItems.filter((item) => {
    if (!session?.user?.permissions) return false;
    return session.user.permissions.includes(item.permission);
  });
 
  return (
    <aside
      className={`fixed left-0 top-0 z-50 flex h-full flex-col bg-[#0F2557] text-white shadow-xl transition-all duration-300 ${
        collapsed ? "w-16" : "w-64"
      }`}
    >
      {/* Header */}
      <div
        className={`border-b border-blue-600/20 p-4 ${collapsed ? "px-2" : "px-4"}`}
      >
        <div
          className={`flex ${collapsed ? "justify-center" : "justify-between"} items-center`}
        >
          {!collapsed && (
            <div className="flex items-center">
              <div className="mr-3 rounded-lg bg-blue-500 p-2">
                <Package size={20} className="text-white" />
              </div>
              <h1 className="text-xl font-bold text-white">TrakIT</h1>
            </div>
          )}
          {collapsed && (
            <div className="rounded-lg bg-blue-500 p-2">
              <Package size={20} className="text-white" />
            </div>
          )}
          <button
            onClick={handleToggle}
            className="rounded-full p-1.5 text-blue-200 transition-colors duration-200 hover:bg-blue-600/50 hover:text-white"
            title={collapsed ? "Expand sidebar" : "Collapse sidebar"}
          >
            {collapsed ? <ChevronRight size={18} /> : <ChevronLeft size={18} />}
          </button>
        </div>
      </div>
 
      {/* Navigation - Now using filtered nav items based on permissions */}
      <nav className="flex-1 py-2">
        <div className={`space-y-1 ${collapsed ? "px-2" : "px-3"}`}>
          {filteredNavItems.map((item) => (
            <Link
              key={item.path}
              href={item.path}
              className={`group flex items-center ${
                collapsed ? "justify-center px-2" : "justify-start px-3"
              } relative rounded-lg py-3 transition-all duration-200 ${
                isActive(item.path)
                  ? "border-l-4 border-blue-300 bg-gradient-to-r from-blue-600 to-blue-700 text-white shadow-lg shadow-blue-500/30"
                  : "text-blue-100 hover:bg-blue-700/50 hover:text-white hover:shadow-md"
              }`}
              title={collapsed ? item.name : undefined}
            >
              <span className={`flex-shrink-0 ${!collapsed ? "mr-3" : ""}`}>
                {item.icon}
              </span>
              {!collapsed && (
                <span className="text-sm font-medium">{item.name}</span>
              )}
              {/* Active indicator for collapsed state */}
              {isActive(item.path) && collapsed && (
                <div className="absolute bottom-0 right-0 top-0 w-1 rounded-l-full bg-blue-300"></div>
              )}
              {/* Active indicator for expanded state */}
              {isActive(item.path) && !collapsed && (
                <div className="ml-auto h-2 w-2 animate-pulse rounded-full bg-blue-200"></div>
              )}
            </Link>
          ))}
        </div>
      </nav>
 
      {/* Footer - Logout */}
      <div
        className={`mt-auto border-t border-blue-600/20 p-4 ${collapsed ? "px-2" : "px-4"}`}
      >
        <div className="space-y-2">
          <button
            className={`group flex items-center text-sm ${
              collapsed ? "justify-center px-2" : "justify-start px-3"
            } w-full rounded-lg py-3 text-gray-200 transition-all duration-200 hover:border-red-400/40 hover:bg-red-500/20 hover:text-red-100`}
            title={collapsed ? "Logout" : undefined}
          >
            <LogOut
              size={20}
              className={`${!collapsed ? "mr-3" : ""} transition-transform group-hover:scale-110`}
            />
            {!collapsed && <span className="font-medium">Logout</span>}
          </button>
        </div>
      </div>
    </aside>
  );
};
 
export default Sidebar;

Step 4 : Create the Sidebar-Config file in the config folder

import {
  Truck,
  FileText,
  BarChart3,
  Users,
  Settings,
  Package,
  Anchor,
  PlaneLanding,
  MessageCircle,
  MessageCircleCode,
  Shield,
  UserCheck,
} from "lucide-react";
import type { JSX } from "react";
 
export interface ISidebarLink {
  name: string;
  path: string;
  icon: JSX.Element;
  permission: string; // Required permission to view this item
  roles?: string[]; // Keep for backward compatibility
}
 
export const navItems: ISidebarLink[] = [
  {
    name: "Dashboard",
    path: "/dashboard",
    icon: <BarChart3 size={20} />,
    permission: "dashboard.read",
    roles: ["ADMIN", "STAFF", "AGENT", "USER"],
  },
  {
    name: "Customers",
    path: "/dashboard/customers",
    icon: <UserCheck size={20} />,
    permission: "customers.read",
    roles: ["ADMIN", "STAFF", "AGENT"],
  },
  {
    name: "Shipments",
    path: "/dashboard/shipments-trakit",
    icon: <Package size={20} />,
    permission: "shipments.read",
    roles: ["ADMIN", "STAFF", "AGENT"],
  },
  {
    name: "Sea Freight",
    path: "/dashboard/sea-freights",
    icon: <Anchor size={20} />,
    permission: "sea_freight.read",
    roles: ["ADMIN", "STAFF", "AGENT"],
  },
  {
    name: "Air Freight",
    path: "/dashboard/air-freight",
    icon: <PlaneLanding size={20} />,
    permission: "air_freight.read",
    roles: ["ADMIN", "STAFF", "AGENT"],
  },
  {
    name: "Documents",
    path: "/dashboard/documents",
    icon: <FileText size={20} />,
    permission: "documents.read",
    roles: ["ADMIN", "STAFF", "AGENT"],
  },
  {
    name: "Tracking",
    path: "/dashboard/tracking",
    icon: <Truck size={20} />,
    permission: "tracking.read",
    roles: ["ADMIN", "STAFF", "AGENT", "USER"],
  },
  {
    name: "Alert Panel",
    path: "/dashboard/panel",
    icon: <MessageCircleCode size={20} />,
    permission: "alert-panel.read",
    roles: ["ADMIN"],
  },
  {
    name: "Notifications",
    path: "/dashboard/notifications",
    icon: <MessageCircle size={20} />,
    permission: "notifications.read",
    roles: ["ADMIN", "STAFF", "AGENT", "USER"],
  },
  {
    name: "Users",
    path: "/dashboard/users",
    icon: <Users size={20} />,
    permission: "users.read",
    roles: ["ADMIN"],
  },
  {
    name: "Roles",
    path: "/dashboard/roles",
    icon: <Shield size={20} />,
    permission: "roles.read",
    roles: ["ADMIN"],
  },
  {
    name: "Settings",
    path: "/dashboard/settings",
    icon: <Settings size={20} />,
    permission: "settings.read",
    roles: ["ADMIN"],
  },
];

Step 5 : Modify the Sidebar filter function accordingly

const filteredNavItems = navItems.filter((item) => {
  if (!session?.user?.roles) return false;
  return session.user.roles.includes(role);
});

Update your global for styles to use primary and secondary colors

:root {
  --radius: 0.625rem;
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);
  /* Updated primary to your dark blue #0C2D4A */
  --primary: oklch(0.25 0.08 240);
  --primary-foreground: oklch(0.985 0 0);
  /* Updated secondary to your yellow #FFD300 */
  --secondary: oklch(0.85 0.15 85);
  --secondary-foreground: oklch(0.15 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  /* Updated accent to complement your brand colors */
  --accent: oklch(0.95 0.02 85);
  --accent-foreground: oklch(0.25 0.08 240);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.25 0.08 240);
  --chart-1: oklch(0.25 0.08 240);
  --chart-2: oklch(0.85 0.15 85);
  --chart-3: oklch(0.35 0.12 240);
  --chart-4: oklch(0.75 0.12 85);
  --chart-5: oklch(0.45 0.15 240);
  --sidebar: oklch(0.985 0 0);
  --sidebar-foreground: oklch(0.145 0 0);
  --sidebar-primary: oklch(0.25 0.08 240);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.97 0 0);
  --sidebar-accent-foreground: oklch(0.25 0.08 240);
  --sidebar-border: oklch(0.922 0 0);
  --sidebar-ring: oklch(0.708 0 0);
}
 
.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.205 0 0);
  --popover-foreground: oklch(0.985 0 0);
  /* Updated dark mode primary to lighter version of your blue */
  --primary: oklch(0.45 0.12 240);
  --primary-foreground: oklch(0.985 0 0);
  /* Updated dark mode secondary to slightly muted yellow */
  --secondary: oklch(0.75 0.12 85);
  --secondary-foreground: oklch(0.15 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  /* Updated dark mode accent */
  --accent: oklch(0.35 0.08 240);
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.704 0.191 22.216);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.45 0.12 240);
  --chart-1: oklch(0.45 0.12 240);
  --chart-2: oklch(0.75 0.12 85);
  --chart-3: oklch(0.55 0.15 240);
  --chart-4: oklch(0.65 0.1 85);
  --chart-5: oklch(0.35 0.1 240);
  --sidebar: oklch(0.205 0 0);
  --sidebar-foreground: oklch(0.985 0 0);
  --sidebar-primary: oklch(0.45 0.12 240);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.269 0 0);
  --sidebar-accent-foreground: oklch(0.985 0 0);
  --sidebar-border: oklch(1 0 0 / 10%);
  --sidebar-ring: oklch(0.556 0 0);
}
/* CONVERT HEX TO OKLH : https://openreplay.com/tools/hex-to-oklch/ */